2014年5月17日土曜日

Samba4 で ActiveDirectory / Apache で Windows 統合認証

はじめに

ユーザアカウントをそれぞれのシステムで別々に管理するのでなく
ActiveDirectory で一元的に管理したいというのはある。
例えば HTTP 認証で保護されたページにアクセスする時の ユーザID / パスワードなど。
もうちょっと欲張るとドメインにログインしている状態であれば
その ユーザID / パスワード でもって自動的に認証処理がパスできればなお素晴らしい!
まぁそういう訳で Windows 統合認証 / Windows Desktop SSO 認証になります。
Apache であればモジュールを追加すると試せそうだ。
ActiveDirectory については Samba4 を使います。
ではでは Getting Started!!

環境

Primary Domain Controller

OS ContOS 6.5(CentOS-6.5-x86_64-minimal.iso)
Samba 4.1.7
Bind 9.8.2
NTP 4.2.6p5
レルム MYDOMAIN.LOCAL
ドメイン MYDOMAIN
Host pdc
IP 192.168.0.10
Gateway 192.168.0.1
DNS 127.0.0.1


Web Server

OS ContOS 6.5(CentOS-6.5-x86_64-minimal.iso)
VMware Player 6.0.2
Apache 2.2.15
Host web01
IP 192.168.0.110
Gateway 192.168.0.1
DNS 192.168.0.10
FQDN web01.mydomain.local


Windows クライアント

OS Windows 8.1
レルム MYDOMAIN.LOCAL
ドメイン MYDOMAIN
Host win01
IP 192.168.0.150
Gateway 192.168.0.1
DNS 192.168.0.10


Primary Domain Controller の構築

わたしも blog にまとめているのでそちらを参考に構築してみてください。
『Samba4 で Active Directory / SerNet Repo でセットアップ』
『Samba4 で ActiveDirectory / インストール編 - わたしもやってみた - 』
『Samba4 で ActiveDirectory / PDC編 - わたしもやってみた - 』


Web Server の構築

とりあえず SELinux と iptables は止めておこう。
/etc/selinux/config
...
SELINUX=disabled
...
service iptables stop
service ip6tables stop
chkconfig iptables off
chkconfig ip6tables off


ネットワークの設定は次のような感じ。
/etc/sysconfig/network-scripts/ifcfg-eth0
...
ONBOOT=yes
BOOTPROTO=static
IPADDR=192.168.0.110
NETMASK=255.255.255.0
GATEWAY=192.168.0.1
NETWORK=192.168.0.0
BROADCAST=192.168.0.255
...
/etc/sysconfig/network
NETWORKING=yes
NETWORKING_IPV6=no
HOSTNAME=web01
DNS は PDC のものを利用する。
/etc/resolv.conf
domain mydomain.local
nameserver 192.168.0.10

必要なモジュールをインストールしていく。
Apache は当然として Samba AD とアクセスするために Kerberos 関連のものと
テストページ用に php もインストールする。
yum -y install httpd krb5-workstation mod_auth_kerb php
サービスの起動設定をしておく。
chkconfig httpd on

ここで再起動します。
reboot

PDC にログインして Web Server の 正引き DNS の設定をします。
一応確認もしておく。
ssh root@pdc
samba-tool dns add pdc mydomain.local web01 A 192.168.0.110
dig web01.mydomain.local
exit


Windows クライアントの設定

とりあえず MYDOMAIN.LOCAL ドメインへの参加くらいでしょうか?
ドメイン参加しているのであれば DNS は PDC に設定されていると思うので DNS の設定は 大丈夫だと思う。
もし hosts ファイルに Web Server の IP を追加するなら以下のような感じ。
C:\Windows\System32\drivers\etc\hosts
...
192.168.0.110  web01.mydomain.local web01

ここまでできたら Web ブラウザで「http://web01.mydomain.local」を開いて Apache が動いているか確認する。


Samba AD のユーザで Basic 認証

手始めに Apache から Samba AD にアクセスできているかの確認も兼ねて
Basic 認証の設定をしてみようと思います。
雰囲気としては、.htpasswd ファイルの代わりに Samba AD のユーザデータを使う感じだと思います。
では Web Server の設定をしていきます。
httpd.conf に追加してもよいのだけれども mod_auth_kerb をインストールすると
auth_kerb.conf が生成されていると思うのでこちらを編集します。
/etc/httpd/conf.d/auth_kerb.conf
<location "/">
  AuthName "AD authentication"
  AuthBasicProvider ldap
  AuthType Basic
  AuthLDAPGroupAttribute member
  AuthLDAPGroupAttributeIsDN On
  AuthLDAPURL ldap://pdc:389/cn=Users,dc=mydomain,dc=local?sAMAccountName?sub?(objectClass=*)
  AuthLDAPBindDN cn=administrator,cn=Users,dc=mydomain,dc=local
  AuthLDAPBindPassword passwd-123
  require valid-user 
</Location>
Apache から Samba AD にアクセスするユーザを administrator にしています。
パスワードは passwd-123 にしています。

確認用のテストページを以下の内容で作成します。
/var/www/html/index.php
<html>
  <head>
    <title>Samba AD User</title>
  </head>
  <body>
    <h2>Samba AD User</h2>
    <pre>
      USER : <?php echo $_SERVER['PHP_AUTH_USER']; ?>
      PASS : <?php echo $_SERVER['PHP_AUTH_PW']; ?>
    </pre>
  </body>
</html>

ここで再起動します。
reboot

再起動が終わったら Windows で Web ブラウザから「http://web01.mydomain.local」を開いてみます。
Basic 認証 のダイアログが表示されると思います。
適当な Samba AD ユーザを入力して「OK」ボタンを押します。
すると画面に入力したユーザIDとパスワードが表示されていると思います。
パスワードまでが表示されてしまっているのは Basic 認証 なんで仕方ないのでしょう。
今は Basic 認証 の確認なので Windows はドメインログインでなくローカルログインでも
検証できると思います。
また、Web Server もドメイン参加していなくても大丈夫そうです。


Windows Desktop SSO 認証の設定

さて、いよいよ本題の Windows 統合認証の設定に入りたいと思います。
まず、PDC 側での作業から始めます。
Web Server の DNS の正引きの設定に加えて逆引きの設定も必要だとのことなので
PDC の DNS に逆引きゾーンを追加して Web Server の 逆引き設定をする。
ssh root@pdc
samba-tool dns zonecreate pdc 0.168.192.in-addr.arpa
samba-tool dns add pdc 0.168.192.in-addr.arpa 110 PTR web01.mydomain.local
わたしのPDCの場合 administrator@MYDOMAIN.LOCAL のパスワードは passwd-123 になります。
DNS の正引き、逆引き設定ができているか確認します。
dig web01.mydomain.local 
dig -x 192.168.0.110

次に Web Server のための Service Principal を追加します。
どのユーザでも構わないと思うのですが
Service Principal 追加時に関連付けするユーザが必要なので
ここでは sso ユーザを作成しています。
そして作成した Service Principal を keytab ファイルに出力します。
cd /root
samba-tool user create --random-password sso
samba-tool user setexpiry --noexpiry sso
samba-tool spn add HTTP/web01.mydomain.local sso
samba-tool domain exportkeytab httpd.keytab --principal=HTTP/web01.mydomain.local  
出力した keytab ファイルを Web Server に転送して
Web Server に戻ります。
scp httpd.keytab root@192.168.0.110:/etc/httpd/conf/httpd.keytab
rm httpd.keytab
exit

Web Server に戻ってきたら 転送した keytab ファイルの権限を変更します。
一応、keytab ファイルの中身も確認しておきます。
chown root:apache /etc/httpd/conf/httpd.keytab
chmod 640 /etc/httpd/conf/httpd.keytab
klist -ke /etc/httpd/conf/httpd.keytab

次は Kerberos の設定をします。
mv /etc/krb5.conf /etc/krb5.conf.original
以下のような内容で krb5.conf を作成します。
/etc/krb5.conf
[logging]
  default = FILE:/var/log/krb5libs.log
  kdc = FILE:/var/log/krb5kdc.log
  admin_server = FILE:/var/log/kadmind.log

[libdefaults]
  default_realm = MYDOMAIN.LOCAL
  dns_lookup_realm = false
  dns_lookup_kdc = false
  ticket_lifetime = 24h
  renew_lifetime = 7d
  forwardable = true

[realms]
  MYDOMAIN.LOCAL = {
    kdc = pdc.mydomain.local
    admin_server = pdc.mydomain.local
  }

[domain_realm]
  .mydomain.local = MYDOMAIN.LOCAL
  mydomain.local = MYDOMAIN.LOCAL
Kerberos チケットのクリアと初期化、そして確認をします。
kdestroy
kinit administrator@MYDOMAIN.LOCAL
klist

その次に Apache の設定をします。
auth_kerb.conf を以下のような内容に編集します。
/etc/httpd/conf.d/auth_kerb.conf
<location "/">
  AuthType Kerberos
  AuthName "Network Login"
  KrbMethodNegotiate On
  KrbMethodK5Passwd On
  KrbAuthRealms MYDOMAIN.LOCAL
  require valid-user
  Krb5KeyTab /etc/httpd/conf/httpd.keytab
  KrbLocalUserMapping On 
</Location>
Apache を再起動します。
service httpd restart
確認用のテストページは、先ほどの Basic 認証で作成したものをそのまま使います。
これで Web Server の設定は終わりです。

Windows にドメインログインします。
確認用のテストページを開く前に Windows のブラウザの設定をします。
IE の場合
  「ツール」/「インターネット オプション」を開く。
  「セキュリティ」タブを選択する。
      「ローカル イントラネット」を選択する。
        「サイト」ボタンを押す。
          「ローカル イントラネット」ダイアログの「詳細設定」ボタンを押す。
            「http://web01.mydomain.local」を追加する。
        「レベルのカスタマイズ」ボタンを押す。
            「イントラネット ゾーンでのみ自動的にログインする」にチェックを入れる。
  「詳細設定」タブを選択する。
      「統合 Windows 認証を使用する」にチェックを入れる。
Firefox の場合
  アドレスバーに「about:config」と入力して以下のプロパティを設定する。
      network.negotiate-auth.delegation-uris    web01.mydomain.local
      network.negotiate-auth.trusted-uris       web01.mydomain.local
Web ブラウザから「http://web01.mydomain.local」を開いてみます。
HTTP 認証 のダイアログなしに画面にドメインログインしているユーザIDが
表示されていれば OK です。
Basic 認証の時と違ってパスワードは表示されていないと思います。
Windows はドメインログインしていないとうまくいかないと思うけど
Web Server はドメイン参加していなくても問題なさそうです。


おわりに

内々で使うちょっとしたサイトでユーザ認証がほしいという時には使えそうだ。
ユーザが所属するドメインのグループを条件にアクセス制限もかけられそうだ。
ガッツリ作りこむような Web アプリには厳しそうだ。
HTTP 認証のダイアログでなく独自のログイン画面がほしいだろうし
ユーザID 以外の ActiveDirectory に登録された情報もほしいところだ。
参考URL
https://wiki.samba.org/index.php/Samba4/beyond
https://community.zarafa.com/pg/blog/read/18332/zarafa-outlook-amp-webaccess-sso-with-samba4
http://int128.hatenablog.com/entry/20130829/1377768762
http://passing.breeze.cc/mt/archives/2008/12/active-directory-apache-window.html

2014年5月14日水曜日

Samba4 で Active Directory / SerNet Repo でセットアップ

はじめに

Samba4.1 がリリースされていた。
またセットアップしてみようかなと Samba Wiki をぶらぶらしていると
Samba4 の yum リポジトリを公開しているサイトを見つけた。
おぉ!これは助かる!!
という訳で今回は、yum で Samba4 をしてみようと思います。
ではでは Getting Started!

yum リポジトリの設定

これがないと始まらない。
EnterpriseSamba.com というところが Samba4 yum リポジトリを公開しているようだ。
利用するためにはユーザ登録が必要になる。
このサイトの Samba のページに移動して register のリンクから  Sign up を行う。
メールアドレスの確認やなんやかんやあって一通りユーザ登録が終わったら Login してみる。
ログインしたページをよ~く見ると username と accesskey が表示されているので
これをしっかりメモっておく。
さて準備が整ったところで yum リポジトリの設定をします。
まず yum リポジトリを取得します。
以下のコマンドの USERNAME:ACCESSKEY の部分を
先ほどメモった username と accesskey に置き換えて実行します。
cd /etc/yum.repos.d
wget https://USERNAME:ACCESSKEY@download.sernet.de/packages/samba/4.1/centos/6/sernet-samba-4.1.repo
今度は、取ってきた yum リポジトリをエディタで開いて baseurl と gpgkey の
USERNAME:ACCESSKEY の部分を先ほどメモった username と accesskey に置き換えて保存します。
/etc/yum.repos.d/sernet-samba-4.1.repo
[sernet-samba-4.1]
name=SerNet Samba 4.1 Packages (centos-6)
type=rpm-md
baseurl=https://USERNAME:ACCESSKEY@download.sernet.de/packages/samba/4.1/centos/6/
gpgcheck=1
gpgkey=https://USERNAME:ACCESSKEY@download.sernet.de/packages/samba/4.1/centos/6/repodata/repomd.xml.key
enabled=1
最後に gpg build key の設定をして終わりです。
cd /root
wget http://ftp.sernet.de/pub/sernet-build-key-1.1-4.noarch.rpm
rpm -i sernet-build-key-1.1-4.noarch.rpm


インストール

といっても yum を実行するだけです。
yum install -y sernet-samba sernet-samba-ad  sernet-samba-client
/var/lib/samba ディレクトリにデータファイルがインストールされていると思います。
smb.conf は /etc/samba ディレクトリに出力されるようです。
ただ、サービス名が sernet-samba-ad、sernet-samba-smbd、sernet-samba-nmbd、sernet-samba-winbindd
となりソースからインストールした時と異なるのがちょっと微妙かなぁ。

PDC の構築

せっかくなので Primary Domain Controller も構築してみたいと思います。
環境

OS ContOS 6.5(CentOS-6.5-x86_64-minimal.iso)
Samba 4.1.7
Bind 9.8.2
NTP 4.2.6p5
レルム MYDOMAIN.LOCAL
ドメイン MYDOMAIN
Host pdc
IP 192.168.0.10
Gateway 192.168.0.1
DNS 127.0.0.1

Samba 以外の設定でハマりたくないので SELinux と iptables は止めておく。
/etc/selinux/config
...
SELINUX=disabled
...
service iptables stop
service ip6tables stop
chkconfig iptables off
chkconfig ip6tables off

ネットワークの設定は次のような感じ。
/etc/sysconfig/network-scripts/ifcfg-eth0
...
ONBOOT=yes
BOOTPROTO=static
IPADDR=192.168.0.10
NETMASK=255.255.255.0
GATEWAY=192.168.0.1
NETWORK=192.168.0.0
BROADCAST=192.168.0.255
...
/etc/sysconfig/network
NETWORKING=yes
NETWORKING_IPV6=no
HOSTNAME=pdc
/etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.0.10 pdc.mydomain.local pdc

Samba のサービスのなかで PDC で使わないものを止めておきます。
chkconfig sernet-samba-ad on
chkconfig sernet-samba-smbd off
chkconfig sernet-samba-nmbd off
chkconfig sernet-samba-winbindd off

このあたりで再起動してみる。
reboot

セットアップに必要なライブラリは一通り yum でインストールされているのだけど
Bind と NTP がインストールされていないので追加します。
Kerbors のコマンド類もインストールされていないのでそれも追加します。
NTP も signed ntp が サポートされたものが yum で提供されているようなので
合わせてインストールします。
yum install -y bind ntp krb5-workstation
サービスの起動設定をしておく。
chkconfig named on
chkconfig ntpd on

では、ドメインコントローラーの設定します。
まず、Samba が Active Directory モードで起動するように sernet-samba ファイルを編集します。
/etc/default/sernet-samba
...
SAMBA_START_MODE="ad"
...
smb.conf などドメインコントローラーに必要なファイル一式を出力します。
samba-tool domain provision \
  --use-rfc2307 \
  --function-level=2008_R2 \
  --use-ntvfs \
  --realm=MYDOMAIN.LOCAL \
  --domain=MYDOMAIN \
  --server-role=dc \
  --dns-backend=BIND9_DLZ \
  --adminpass 'passwd-123'
--use-ntvfs
--function-level=2008_R2 にした場合 --use-ntvfs オプションも必要になるみたいです。
出力された smb.conf をチェックする。
testparm
smb.conf は以下のような感じ。
/etc/samba/smb.conf
[global]
 workgroup = MYDOMAIN
 realm = MYDOMAIN.LOCAL
 netbios name = PDC
 server role = active directory domain controller
 server services = rpc, nbt, wrepl, ldap, cldap, kdc, drepl, winbind, ntp_signd, kcc, dnsupdate, smb
 dcerpc endpoint servers = epmapper, wkssvc, rpcecho, samr, netlogon, lsarpc, spoolss, drsuapi, dssetup, unixinfo, browser, eventlog6, backupkey, dnsserver, winreg, srvsvc
 idmap_ldb:use rfc2307 = yes
 
 winbind use default domain = yes
 template shell = /bin/bash

[netlogon]
 path = /var/lib/samba/sysvol/fummy.local/scripts
 read only = No

[sysvol]
 path = /var/lib/samba/sysvol
 read only = No

Kerberos の設定ファイルが出力されているので差し替える。

mv /etc/krb5.conf /etc/krb5.conf.original
cp /var/lib/samba/private/krb5.conf /etc/krb5.conf

krb5.conf は以下のような感じ。

/etc/krb5.conf
[libdefaults]
  default_realm = MYDOMAIN.LOCAL
  dns_lookup_realm = false
  dns_lookup_kdc = true

Bind の設定は次のような感じ。
ソースからインストールした時と変わらないのだけど
ファイル Path が違っているのでそのあたりを確認する。
/etc/named.conf
options {
  ...
  listen-on port 53 { any; };
  ...
  allow-query     { any; };
  ...
  tkey-gssapi-keytab "/var/lib/samba/private/dns.keytab";
};
...
include "/var/lib/samba/private/named.conf";
dns.keytab の権限の設定をする。
chgrp named /var/lib/samba/private/
chgrp named /var/lib/samba/private/named.conf
chmod g+r /var/lib/samba/private/dns.keytab
chown -R named:named /var/lib/samba/private/dns
/etc/rndc.key を生成する。
rndc-confgen -a -r /dev/urandom
resolv.conf の設定をする。
/etc/resolv.conf
domain mydomain.local
nameserver 127.0.0.1

Samba4、Bindを再起動する。

service sernet-samba-ad start
service named restart

ここで NTP の設定もしておこう。
ここもソースからインストールした時と変わらないのだけど
ntp_signd ディレクトリの Path が違っているのでそのあたりを確認する。
/etc/ntp.conf
server 127.127.1.0
fudge 127.127.1.0 stratum 10
server 0.pool.ntp.org iburst prefer
server 1.pool.ntp.org iburst prefer
driftfile /var/lib/ntp/ntp.drift
logfile /var/log/ntp
ntpsigndsocket /var/lib/samba/ntp_signd
restrict default kod nomodify notrap nopeer mssntp
restrict 127.0.0.1
restrict 0.pool.ntp.org mask 255.255.255.255 nomodify notrap nopeer noquery
restrict 1.pool.ntp.org mask 255.255.255.255 nomodify notrap nopeer noquery
NTP との連動ために ntp_signd ディレクトリの権限を変更する。
時刻合せした後、NTP を起動する。
chmod 750 /var/lib/samba/ntp_signd
ntpdate ntp.nict.jp
service ntpd start

DNS を更新する。
samba_dnsupdate --verbose --all-names
更新した DNS の設定を確認する。
not found: 3(NXDOMAIN) とかが返ってこないことを確認する。
host -t SRV _ldap._tcp.mydomain.local.
host -t SRV _kerberos._udp.mydomain.local.
host -t A pdc.mydomain.local.

Kerberos のチケットを一旦クリアした後、初期化と確認をします。
administrator@MYDOMAIN.LOCAL のパスワードは今回の場合、passwd-123 になります。
kdestroy
kinit administrator@MYDOMAIN.LOCAL
klist

とりあえず以下のコマンドで ログインできれば設定は OK だ。
smbclient //localhost/netlogon -U administrator
以下のようにして administrator のパスワードの期限を無効にしておく方が検証には便利かも?
samba-tool user setexpiry --noexpiry administrator

ドメインユーザを追加してログインしてみる。
Windows からだとこの状態でもログインできるのだけれども 
CentOS からだと、もうすこし設定が必要だ。
Winbind を使ってドメインユーザがログインできるように PAM の設定する。
LANG=en_US.UTF-8 authconfig --enablewinbindauth --update
LANG=en_US.UTF-8 authconfig --enablemkhomedir --update
authconfig について
authconfig コマンドで Winbind の PAM 設定をすると
「サービス winbind に関する情報の読み込み中にエラーが発生しました: 
そのようなファイルやディレクトリはありません
winbind: 認識されていないサービスです。
error reading information on service winbind: No such file or directory
winbind: unrecognized service」
というようなエラーメッセージが表示されると思います。
原因はきっと winbind のサービス名が sernet-samba-winbindd になっているからだと思います。
なので sernet-samba-winbindd のサービス名を winbind に変更して authconfig を実行してみると
今度は「sernet-samba ファイルの SAMBA_START_MODE の設定を ad から classic に変更してほしい」的な
警告が出ます。
エラーメッセージや警告は出るものの PAM 設定自体はできている。
また、Samba AD にした場合、Winbind そのものを使う訳でもない。
この辺りは PAM 設定でなく yum のパッケージング(winbind の起動スクリプト?)の問題だと思う。
エラーメッセージや警告が気になるからといって PAM を手動で設定するのはちょっと…
またいつの日にかこの問題が解消されることを期待しましょう。
nsswitch.conf を以下のように編集して
/etc/nsswitch.conf
... 
passwd:     compat winbind
shadow:     files
group:      compat winbind
...
ドメインユーザを追加したら
samba-tool user add user01
ドメインログイン!!!
ssh user01@pdc
administrator でドメインログイン
やっぱり administrator は ssh で 自身の PDC に CentOS からドメインログインできなかった。
普通に追加したドメインユーザであればログインはできるのに。
まぁこれはこんなもんなんだろう。

Member Server の構築

Member Server も構築してみたいと思います。
先ほど構築した PDC にドメイン参加する形になります。
環境

OS ContOS 6.5(CentOS-6.5-x86_64-minimal.iso)
VMware Player 6.0.2
Samba 4.1.7
Bind 9.8.2
NTP 4.2.6p5
レルム MYDOMAIN.LOCAL
ドメイン MYDOMAIN
Host mbr01
IP 192.168.0.100
Gateway 192.168.0.1
DNS 192.168.0.10


PDC の時と同様に SELinux と iptables は一旦止めておいた方がハマらないと思う…
ネットワークの設定は次のような感じ。
/etc/sysconfig/network-scripts/ifcfg-eth0
...
ONBOOT=yes
BOOTPROTO=static
IPADDR=192.168.0.100
NETMASK=255.255.255.0
GATEWAY=192.168.0.1
NETWORK=192.168.0.0
BROADCAST=192.168.0.255
...
/etc/sysconfig/network
NETWORKING=yes
NETWORKING_IPV6=no
HOSTNAME=mbr01
/etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.0.100 mbr01.mydomain.local mbr01
DNS は PDC のものを利用しようと思うので DNS を PDC に設定する。
/etc/resolv.conf
domain mydomain.local
nameserver 192.168.0.10
ipv6 を無効化しておきます。
以下のような内容で disable-ipv6.conf を作成します。
/etc/modprobe.d/disable-ipv6.conf
options ipv6 disable=1
ipv6 の無効化
ドメイン参加時に
「print_kdc_line: can't resolve name for kdc with non-default port [::]. Error 
名前またはサービスが不明です」的なエラーメッセージは出るものの 
ipv6 を無効にしなくてもドメイン参加はできているようです。
Samba のサービスのなかで Member Server で使わないものを止めておきます。
chkconfig sernet-samba-ad off
chkconfig sernet-samba-smbd off
chkconfig sernet-samba-nmbd off
chkconfig sernet-samba-winbindd on

ここで一度、再起動する。
reboot

追加で必要なライブラリをインストールする。
yum install -y ntp krb5-workstation
サービスの起動設定をしておく。
chkconfig ntpd on
今回の Member Server では Bind を使わないのでインストールしている場合は OFF っておいてください。

まずは、Kerberos の設定から行います。
mv /etc/krb5.conf /etc/krb5.conf.original
以下のような内容で krb5.conf を作成します。
/etc/krb5.conf
[logging]
  default = FILE:/var/log/krb5libs.log
  kdc = FILE:/var/log/krb5kdc.log
  admin_server = FILE:/var/log/kadmind.log

[libdefaults]
  default_realm = MYDOMAIN.LOCAL
  dns_lookup_realm = true
  dns_lookup_kdc = true
  ticket_lifetime = 24h
  forwardable = yes

[appdefaults]
  pam = {
    debug = false
    ticket_lifetime = 36000
    renew_lifetime = 36000
    forwardable = true
    krb4_convert = false
  }
Kerberos のチケットをクリアしてから初期化と確認をします。
administrator@MYDOMAIN.LOCAL のパスワードは今回の場合、passwd-123 になります。
kdestroy
kinit administrator@MYDOMAIN.LOCAL
klist

では Member Server の設定をします。
きっと Winbind を使うことになりそうなので
Samba が Classic モードで起動するように sernet-samba ファイルを編集します。
/etc/default/sernet-samba
...
SAMBA_START_MODE="classic"
...
smb.conf は以下のような感じ。
/etc/samba/smb.conf
[global]
  netbios name = MBR01
  workgroup = MYDOMAIN
  security = ads
  realm = MYDOMAIN.LOCAL
  encrypt passwords = yes

  idmap config *:backend = tdb
  idmap config *:range = 70001-80000
  idmap config MYDOMAIN:default = yes
  idmap config MYDOMAIN:backend = rid
  idmap config MYDOMAIN:range = 10000 - 19999

  winbind nss info = rfc2307
  winbind trusted domains only = no
  winbind use default domain = yes
  winbind enum users  = yes
  winbind enum groups = yes

  template shell = /bin/bash
ドメインに参加します。
net ads join -U administrator
DNS を確認しておきます。
dig mbr01.mydomain.local
samba-tool について
Setup_a_Samba_AD_Member_Server によると 
samba-tool コマンドを使った Member Server のドメイン参加は、まだ未対応のようで
現段階では、traditional な方法でのセットアップになるみたいだ。
以下のように smb.conf など Member Server に必要なファイル一式を出力するとか
samba-tool domain provision \
  --realm=MYDOMAIN.LOCAL \
  --domain=MYDOMAIN \
  --server-role=member \
  --dns-backend=NONE \
  --adminpass 'passwd-123'
以下のようにしてドメイン参加もできないみたいだ。
samba-tool domain join mydomain.local MEMBER \
  -U administrator \
  --password=passwd-123 \
  --realm=mydomain.local
NTP の設定もしておこう。
/etc/ntp.conf
server 127.127.1.0
fudge  127.127.1.0 stratum 10

server pdc.mydomain.local iburst prefer

driftfile /var/lib/ntp/ntp.drift
logfile   /var/log/ntp

restrict default ignore
restrict 127.0.0.1
restrict pdc.mydomain.local mask 255.255.255.255 nomodify notrap nopeer noquery

設定が終わったら時刻合せをして Winbind、NTP を起動する。
ntpdate ntp.nict.jp
service ntpd start
service sernet-samba-winbindd start

ドメインユーザを追加してログインできるか確かめてみる。
Winbind の PAM を設定することになるのだけど
この状態で authconfig を使うとサービス名が違うたら無いたらふぅたらいわれる。
別に設定はできるので問題ないと思うのだが
うるさいと感じる場合はサービス名を変更してしまう。
service sernet-samba-winbindd stop
cd /etc/rc.d/init.d
cp sernet-samba-winbindd winbind
chkconfig --del sernet-samba-winbindd
chkconfig --add winbind
chkconfig winbind on
chkconfig --list
service winbind start
静になったところで Winbind の PAM の設定する。
LANG=en_US.UTF-8 authconfig --enablewinbindauth --update
LANG=en_US.UTF-8 authconfig --enablemkhomedir --update
nsswitch.conf を以下のように編集したら
/etc/nsswitch.conf
... 
passwd:     compat winbind
shadow:     files
group:      compat winbind
...
あとは PDC 側でドメインユーザを追加したら
ssh root@pdc
samba-tool user add user02
ログインして Member Server に戻ってこれればとりあえず OK だ。
ssh user02@mbr01


おわりに

微妙なところ(サービス名など)もあったが yum を使うと格段にセットアップが楽になった。
検証用には十分楽しめると思う。
しかし実運用となると MS AD を使いたいというのが本音か?!
参考URL
http://folgaizer.wordpress.com/2013/12/12/samba4-on-centos-6-4/
http://www.reddrop.net/howto_samba4_ActiveDirectory_Bind_DLZ
https://wiki.samba.org/index.php/Setup_a_Samba_AD_Member_Server
http://web.chaperone.jp/w/index.php?samba4%2Fuser

2014年4月23日水曜日

Spring Security で OAuth 2.0 Provider する

はじめに

ついに Windows 8 にした。
XP のサポートが終了したというのもあったのだけれども
Java 8 がインストールできなかったのが致命的だった。
.Net Framework 4.5 や PHP 5.5 このあたりが使えなくなったのは耐えていたのだけれど
この春、Java に見捨てられてもう我慢できなくなった。
Java 8 が使えたらきっと今も XP 使ってると思う。

で、OAuth
わたしにとって OAuth といえば Facebook なのですが
以前、Facebook のアプリを作る機会があり OAuth の認証処理に結構悩まされた。
リクエストが行ったり来たり、どのタイミングでが何が送られて来るのか?また何を送るのか?
と同時に向こう側の処理はどうなっているんだ!?
相手を知ればこちら側の処理がもっと明確に見えてくるのではと思ったりした。
そんな訳で今回は、Spring Security OAuth Provider を試してみようと思います。
現段階では、あまり詳しいドキュメントがない。
とりあえずサンプルがあるのでそれを参考にしつつソースを読むことにした。
なのでは、きっと何々だろう的なわたしの想像を元に進めていくことになる。
方針としては、
 ・アリものは最大限利用させて頂く
 ・データのやり取りを観察したいので InMemory のものでなくデータベースを使う
 ・リクエストのやり取りの中でデータが変わりそうにないものは InMemory でもよしとしよう
サンプルで作るアプリをしては2つ
1つは、OAuth Provider として my-sns
想定としては、Facebook とか Twitter になる。
まぁいえばあちら側のサイトになる。
そしてもう1つは、OAuth Client として my-app
これは、自前のサイト、こちら側になる。

Windows 8 にして心機一転、ビルドツールも Maven から Gradle に乗り換えました。
ではでは!Getting Started!! 

環境

OS Windows 8.1
データベース MySQL 5.6.16
アプリケーションサーバー Tomcat 7.0.53
Java JDK 1.8
フレームワーク Spring Framework 4.0.3.RELEASE
Spring Security 3.2.3.RELEASE
Spring Security OAuth 1.0.5.RELEASE
ビルドツール Gradle 1.11
※gradle コマンドが実行できるように設定しておきます。


プロジェクト構成

Cドライブ直下に「work」フォルダを作成し各ファイルを以下のように配置します。
ファイルは、すべて UTF-8 で保存します。

  • C:\work
    • studying-spring-security-oauth
      • my-app
        • src
          • main
            • webapp
              • WEB-INF
                • web.xml
              • index.jsp
        • build.gradle
      • my-sns
        • src
          • main
            • java
              • com
                • mydomain
                  • ApiController.java
            • resources
              • META-INF
                • spring
                  • applicationContext-oauth.xml
              • log4j.properties
            • webapp
              • WEB-INF
                • studying-oauth-servlet.xml
                • web.xml
        • build.gradle
    • build.gradle
    • settings.gradle


データベースとテーブルの作成

データベースユーザとして、パスワードなしのrootユーザが作成済みという前提で
何はともあれデータベースから

DROP DATABASE IF EXISTS studying_oauth;
CREATE DATABASE IF NOT EXISTS studying_oauth DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
USE studying_oauth;

では、メインの Access Token を保存するテーブルから作成します。
Schema に関しては、JdbcTokenStore(org.springframework.security.oauth2.provider.token パッケージ)を参考に逆算してみた。
恐らく token_id が PK になるだろう。
varchar は、とりあえず255文字あればいいでしょう。
user_name と refresh_token は使わない場合もあるので NULL OK にしてあとは強気に NOT NULL

DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
  `token_id`          varchar(255) NOT NULL,
  `token`             blob         NOT NULL,
  `authentication_id` varchar(255) NOT NULL,
  `user_name`         varchar(255) NULL,
  `client_id`         varchar(255) NOT NULL,
  `authentication`    blob         NOT NULL,
  `refresh_token`     varchar(255) NULL,
  PRIMARY KEY (`token_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

次に Refresh Token を保存するテーブルを作成します。
同様に JdbcTokenStore から Schema を逆算してみた。
ここは token_id が PK で問題ないでしょう。

DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
  `token_id`       varchar(255) NOT NULL,
  `token`          blob         NOT NULL,
  `authentication` blob         NOT NULL,
  PRIMARY KEY (`token_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

最後に Authorization Code Grant(認可コードグラント)で使用する Request Token を保存するテーブルを作成します。
Schema は、JdbcAuthorizationCodeServices(org.springframework.security.oauth2.provider.code パッケージ)から逆算した。
code が PK で問題ないでしょう。

DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
  `code`           varchar(255) NOT NULL,
  `authentication` blob         NOT NULL,
  PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


ServletとLoggerの設定

OAuth Provider あちら側 my-sns の設定から始めます。
my-sns/src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="3.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  xsi:schemaLocation="
    http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
  
  <display-name>studying-oauth</display-name>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath*:META-INF/spring/applicationContext*.xml</param-value>
  </context-param>
    
  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!-- Filter 設定 -->
  <filter>
    <filter-name>CorsFilter</filter-name>
    <filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
    <init-param>
      <param-name>cors.allowed.origins</param-name>
      <param-value>*</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>CorsFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  
  <servlet>
    <servlet-name>studying-oauth</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>studying-oauth</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  
  <session-config>
    <cookie-config>
      <http-only>true</http-only>
    </cookie-config>
    <tracking-mode>COOKIE</tracking-mode>
  </session-config>
</web-app>
  • Filter 設定

    クロスドメインで javascript から json データのやりとりをできるようにするための Filter を追加しています。
    Response ヘッダーに 「Access-Control-Allow-Origin: *」を出力しています。
    今回、Tomcat を my-sns と my-app で port を変えて2つ起動します。
    ドメインが同じでも Port が違うとクロスドメインと扱われるようです。
    Tomcat で用意されてるアリものの Filter を使わせてもらっています。


どの処理でどのクラスが呼び出されるか知りたいので
とりあえず springframework.security.oauth パッケージ以下にあるクラスの
デバッグログを出力するようにしています。
my-sns/src/main/resources/log4j.properties
log4j.rootLogger=error, stdout
 
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
 
# Print the date in ISO 8601 format
log4j.appender.stdout.layout.ConversionPattern=%d [%t] %-5p %c - %m%n
 
log4j.appender.R=org.apache.log4j.RollingFileAppender
log4j.appender.R.File=application.log
 
log4j.appender.R.MaxFileSize=100KB
# Keep one backup file
log4j.appender.R.MaxBackupIndex=1
 
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n

log4j.logger.org.springframework.security.oauth2=debug, stdout


Spring Security の設定

あちら側 my-sns の OAuth Provider としてのメインの設定をします。
この設定が今回のキモになります。
my-sns/src/main/resources/META-INF/spring/applicationContext-oauth.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans
  xmlns="http://www.springframework.org/schema/security" 
  xmlns:oauth="http://www.springframework.org/schema/security/oauth2" 
  xmlns:beans="http://www.springframework.org/schema/beans" 
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.2.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security.xsd
    http://www.springframework.org/schema/security/oauth2
    http://www.springframework.org/schema/security/spring-security-oauth2.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc.xsd">

  <!-- URL 設定① -->
  <http pattern="/members/**" create-session="stateless"
    entry-point-ref="myOauthAuthenticationEntryPoint"
    authentication-manager-ref="myOAuth2AuthenticationManager"
    access-decision-manager-ref="accessDecisionManager">
    <anonymous enabled="false" />
    <intercept-url pattern="/members/**" access="SCOPE_TRUST" />
    <custom-filter ref="myOauth2ProviderFilter" before="PRE_AUTH_FILTER" />
  </http>
  
  <!-- URL 設定② -->
  <http pattern="/oauth/token*" create-session="stateless" 
    entry-point-ref="myOauthAuthenticationEntryPoint"
    authentication-manager-ref="myClientAuthenticationManager">
    <custom-filter ref="myClientCredentialsTokenEndpointFilter" position="BASIC_AUTH_FILTER"/>
  </http>

  <!-- URL 設定③ -->
  <http pattern="/**"
    authentication-manager-ref="resourceOwner" 
    disable-url-rewriting="true">
    <intercept-url pattern="/oauth/authorize*" access="ROLE_USER" />
    <form-login />
  </http>


  <beans:bean
    id="myOauthAuthenticationEntryPoint"
    class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint" />

  <beans:bean
    id="myOAuth2AuthenticationManager"
    class="org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager">
    <beans:property name="tokenServices" ref="myTokenServices"/>
  </beans:bean>
  
  <beans:bean
    id="myTokenServices"
    class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
    <beans:property name="tokenStore" ref="myTokenStore" />
    <beans:property name="clientDetailsService" ref="myClientDetailsService" />
    <beans:property name="supportRefreshToken" value="true"/>
  </beans:bean>
  
  <!-- Access Token 設定 -->
  <beans:bean
    id="myTokenStore"
    class="org.springframework.security.oauth2.provider.token.JdbcTokenStore" >
    <beans:constructor-arg name="dataSource" ref="myDataSource"/>
  </beans:bean>
  
  <!-- 連携アプリ設定 -->
  <oauth:client-details-service
    id="myClientDetailsService" >
    <oauth:client
      client-id="my-auth"
      secret="pass123"
      scope="trust" 
      authorized-grant-types="authorization_code,implicit,password,client_credentials,refresh_token"
      authorities="ROLE_CLIENT"
      resource-ids="myResource" />
  </oauth:client-details-service>

  <!-- 権限設定 -->
  <beans:bean
    id="accessDecisionManager"
    class="org.springframework.security.access.vote.UnanimousBased">
    <beans:constructor-arg>
      <beans:list>
        <beans:bean class="org.springframework.security.oauth2.provider.vote.ScopeVoter" />
        <beans:bean class="org.springframework.security.access.vote.RoleVoter" />
        <beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
      </beans:list>
    </beans:constructor-arg>
  </beans:bean>

  <oauth:resource-server
    id="myOauth2ProviderFilter" resource-id="myResource" token-services-ref="myTokenServices" />


  <authentication-manager id="myClientAuthenticationManager">
    <authentication-provider user-service-ref="myClientDetailsUserService" />
  </authentication-manager>

  <beans:bean
    id="myClientDetailsUserService"
    class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
    <beans:constructor-arg ref="myClientDetailsService" />
  </beans:bean>
  
  <beans:bean
    id="myClientCredentialsTokenEndpointFilter"
    class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
    <beans:property name="authenticationManager" ref="myClientAuthenticationManager" />
    <beans:property name="authenticationEntryPoint" ref="myOauthAuthenticationEntryPoint" />
  </beans:bean>


  <!-- ユーザアカウント 設定 -->
  <authentication-manager id="resourceOwner">
    <authentication-provider>
      <user-service id="userDetailsService">
        <user name="user01" password="pass01" authorities="ROLE_USER" />
        <user name="user02" password="pass02" authorities="ROLE_USER" />
      </user-service>
    </authentication-provider>
  </authentication-manager>


  <!-- OAuth Provider 全体設定 -->
  <oauth:authorization-server
    client-details-service-ref="myClientDetailsService"
    token-services-ref="myTokenServices"
    user-approval-handler-ref="myUserApprovalHandler">
    <oauth:authorization-code authorization-code-services-ref="myAuthorizationCodeServices"/>
    <oauth:implicit />
    <oauth:refresh-token />
    <oauth:client-credentials />
    <oauth:password authentication-manager-ref="resourceOwner"/>
  </oauth:authorization-server>
  
  <beans:bean
    id="myUserApprovalHandler"
    class="org.springframework.security.oauth2.provider.approval.TokenServicesUserApprovalHandler">
    <beans:property name="tokenServices" ref="myTokenServices"/>
  </beans:bean>
   
  <!-- Request Token 設定 -->
  <beans:bean
    id="myAuthorizationCodeServices"
    class="org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices" >
    <beans:constructor-arg name="dataSource" ref="myDataSource"/>
  </beans:bean>


  <beans:bean
    id="oauthAccessDeniedHandler"
    class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler" />

  <beans:bean
    id="myDataSource"
    class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <beans:property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <beans:property name="url" value="jdbc:mysql://localhost:3306/studying_oauth"/>
    <beans:property name="username" value="root"/>
    <beans:property name="password" value=""/>
  </beans:bean>

  <context:component-scan base-package="com.mydomain" />
  <mvc:annotation-driven />

</beans:beans>
  • URL 設定①

    Access Token が必要な URL
    ここはセッション管理しないので 強気に stateless とした。

    URL 設定②

    Access Token を発行する URL
    この段階でどのユーザアカウントが Access Token を求めてきているか
    なにかしらの方法で分かるはずなのでここもセッションは stateless とした。

    URL 設定③

    あちら側 my-sns のログイン画面、アプリ承認画面を表示するURL
    Authorization Code Grant であれば Request Token を
    Implicit Grant であれば Access Token を発行する URL になる。
    ここはログイン画面などを使ってどのユーザアカウントであるか
    判別する必要があるだろうからセッション管理は通常どうりとした。

    Access Token 設定

    ここのデータのやりとりを観察したいのでデータベース用のもを使った。

    連携アプリ設定

    こちら側 my-app があちら側 my-sns の認証が必要な URL に
    アクセスするためのサイトのアカウント登録になる。
    Facebook、Twitter でいうアプリの登録になると思う。
    client-id、secret が アカウントIDとパスワードになる。
    ここは InMemory のものを使った。
    データベース用のものも用意されているので
    JdbcClientDetailsService(org.springframework.security.oauth2.provider パッケージ)
    あたりを解析すれば テーブル Schema が割り出せると思う。

    権限設定

    ScopeVoter によって Access Token の scope が Spring Security の Role に変換される。
    Access Token 取得時に Request パラメータで scope=trust というように指定された場合
    prefix 「SCOPE_」が付けられ 「SCOPE_TRUST」というような Role になる。

    ユーザアカウント設定

    あちら側 my-sns のユーザアカウントになる。
    ユーザアカウントは2つ、IDとパスワードはそれぞれ「user01/pass01」「user02/pass02」を登録した。
    ここも InMemory のものを使った。
    ここは Spring Security 本体でデータベース用のもが用意されている。

    OAuth Provider 全体設定

    OAuth の4つの Grant と Refresh Token が試せるように設定した。

    Request Token 設定

    このあたりもデータのやりとりを観察したいのでデータベース用のもを使った。


内容は空なのですが「アプリケーションコンテキスト名-servlet.xml」ファイルがないと
うまく動かないようなので一応作成しておきます。
my-sns/src/main/webapp/WEB-INF/studying-oauth-servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans
  xmlns="http://www.springframework.org/schema/security" 
  xmlns:oauth="http://www.springframework.org/schema/security/oauth2" 
  xmlns:beans="http://www.springframework.org/schema/beans" 
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security.xsd
    http://www.springframework.org/schema/security/oauth2
    http://www.springframework.org/schema/security/spring-security-oauth2.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc.xsd">
</beans:beans>


Controller クラスの作成

「/my-sns/members」でログインユーザ情報を json 形式で返すようにします。
my-sns/src/main/java/com/mydomain/ApiController.java
package com.mydomain.web;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;


@Controller
public class ApiController {

  @RequestMapping("/members")
  @ResponseBody
  public UserDetails members() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    return (UserDetails)auth.getPrincipal();
  }
}


my-app の設定

こちら側 my-app の設定をします。
以下のような内容でOAuthのテストページを作成します。
my-app/src/main/webapp/index.jspx
<%@ page contentType="text/html; charset=UTF-8" %>
<html>
  <head>
    <title>OAuthのテスト</title>
  </head>
  <body>
    <h2>OAuthのテスト</h2>
  </body>
</html>

いらないかもしれないけど以下のような内容で web.xml も作成しておきます。
my-app/src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="3.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  xsi:schemaLocation="
    http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

  <session-config>
    <cookie-config>
      <http-only>true</http-only>
    </cookie-config>
    <tracking-mode>COOKIE</tracking-mode>
  </session-config>
</web-app>


ビルドスクリプトの作成

まずは Top プロジェクトのビルドスクリプトを以下の内容で作成します。
settings.gradle
include 'my-app', 'my-sns'
build.gradle
ext { 
  springVersion = '4.0.3.RELEASE'
  springSecurityVersion = '3.2.3.RELEASE'
  springSecurityOAuth2Version = '1.0.5.RELEASE'
  tomcatVersion = '7.0.53'
}

allprojects {
  repositories {
    mavenCentral()
  }
}

subprojects {
  apply plugin: 'war'
  apply plugin: 'tomcat'
  
  dependencies {
    compile "org.springframework:spring-context:${springVersion}"
    compile "org.springframework:spring-webmvc:${springVersion}"
    compile "org.springframework:spring-jdbc:${springVersion}"    
    
    compile "org.springframework.security:spring-security-web:${springSecurityVersion}"
    compile "org.springframework.security:spring-security-config:${springSecurityVersion}"
    compile "org.springframework.security:spring-security-taglibs:${springSecurityVersion}"   
    
    compile "org.springframework.security.oauth:spring-security-oauth2:${springSecurityOAuth2Version}"
    
    compile 'commons-dbcp:commons-dbcp:1.4'
    compile 'mysql:mysql-connector-java:5.1.30'
    compile 'log4j:log4j:1.2.17'
    
    tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}",
           "org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}"
    tomcat("org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}") {
           exclude group: 'org.eclipse.jdt.core.compiler', module: 'ecj'
    }
  }
  
  sourceCompatibility = '1.8'
  targetCompatibility = '1.8'
  [compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
}

buildscript {
  repositories {
    jcenter()
  }

  dependencies {
    classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:1.2.3'
  }
}

次は my-sns プロジェクトのビルドスクリプトを以下の内容で作成します。
my-sns/build.gradle
war {
  baseName = 'my-sns'
}

最後は my-app プロジェクトのビルドスクリプトを以下の内容で作成します。
Tomcat を2つ起動することになるので my-sns とは違う port を指定しています。
my-app/build.gradle
war {
  baseName = 'my-app'
}

tomcat {
  httpPort  = 15080
  httpsPort = 15443
  ajpPort   = 15009
  stopPort  = 15081
}


OAuth 認証を試す準備

とりあえずデータベースのデータを削除します。
DELETE FROM oauth_access_token;
DELETE FROM oauth_code;
DELETE FROM oauth_refresh_token;

そしてそれぞれの Tomcat を起動します。
まずは my-sns から
コマンドプロンプトを起動して以下を入力する。
cd /d C:\work\studying-spring-security-oauth
gradle :my-sns:clean :my-sns:tomcatRun

次は my-app 
もう一枚コマンドプロンプトを起動して以下を入力する。
cd /d C:\work\studying-spring-security-oauth
gradle :my-app:clean :my-app:tomcatRun


Authorization Code Grant を試す

手始めに一番一般的な方法、認可コードグラントで Access Token 取得をしてみます。

OAuthのテストに用意した my-app のページ「http://localhost:15080/my-app/」を開きます。
認可コードグラントの場合、Access Token の取得に Request Token が必要になってくるので
Request Token から取得してみます。
以下のURLをアドレスバーに入力して my-sns にアクセスします。
http://localhost:8080/my-sns/oauth/authorize?client_id=my-auth&scope=trust&response_type=code&redirect_uri=http://localhost:15080/my-app/
  • Request パラメータ にはアプリのID(client_id)は必要なのですがこの段階ではまだアプリのパスワード(client_secret)は不要です。

上記のURLにアクセスするとログイン画面が表示されると思います。
my-sns のユーザアカウント「user01/pass01」または「user02/pass02」でログインします。

ログインするとアプリの承認画面が表示されると思います。
「Authorize」ボタンを押してアプリを承認します。

今度は my-sns から my-app にリダイレクトされます。
この時 URL に ?code=XXXXX のような形で Request Token が送られてきているかと思います。
ここで oauth_code テーブルを見てみると送られてきた Request Token が登録されていると思います。

次に取得した Request Token から Access Token 取得をしてみます。
以下のURLをアドレスバーに入力して my-sns にアクセスします。
http://localhost:8080/my-sns/oauth/token?client_id=my-auth&client_secret=pass123&grant_type=authorization_code&redirect_uri=http://localhost:15080/my-app/&code=XXXXX
  • この段階で Request パラメータ にアプリのID(client_id)と共にアプリのパスワード(client_secret)が必要になります。
  • 最後の部分 code=XXXXX で取得した Request Token を指定します。

レスポンスデータとして Access Token が json 形式で画面に表示されると思います。
表示された access_token と refresh_token をメモしておきます。
ここで oauth_access_token テーブルと oauth_refresh_token テーブルを見てみると
Access Token と Refresh Token が登録されていると思います。
また一回使った Request Token でもう一度 Access Token 取得しようとすると
"error":"invalid_grant" とかになり Access Token が取得できないようになっていると思います。
また oauth_code テーブルを見てみると先ほどの Request Token 削除されていると思います。

これで Access Token を使って my-sns のユーザ認証が必要な情報にアクセスができるように
なっていると思います。
以下のURLをアドレスバーに入力して my-sns にアクセスしてログインユーザ情報が取得してみます。
http://localhost:8080/my-sns/members/?access_token=YYYYY
  • ?access_token=YYYYY で取得した Request Token を指定します。

レスポンスデータとして ログインユーザ情報 が json 形式で画面に表示されると思います。
また、Request Token なしでアクセスしてみると 
An Authentication object was not found in the SecurityContext 的なデータが表示されると思います。


Refresh Token を試す

リフレッシュトークンも試してみたいと思います。
先ほどと同様に認可コードグラントを使います。

Access Token の有効期間は初期値で12時間になっている。
ここではそんなに待てないので1分にしてみる。
「applicationContext-oauth.xml」の myTokenServices に以下を追加する。
my-sns/src/main/resources/META-INF/spring/applicationContext-oauth.xml
  ...
  <beans:bean
    id="myTokenServices"
    class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
    ...
    <beans:property name="accessTokenValiditySeconds" value="60"/>
  </beans:bean>
  ...
OAuth 認証を試す準備で行ったデータベースの削除と my-sns の Tomcat を再起動してから
認可コードグラントの手順で Access Token と Refresh Token を取得します。
一応取得した Access Token で my-sns からログインユーザ情報が取得してみます。
それで待つこと1分以下のURLでログインユーザ情報が取得してみます。
http://localhost:8080/my-sns/members/?access_token=YYYYY
Access token expired: 的なデータが表示されると思います。
以下のURLをアドレスバーに入力して my-sns から Access Token を再取得してみます。
http://localhost:8080/my-sns/oauth/token?client_id=my-auth&client_secret=pass123&scope=trust&grant_type=refresh_token&refresh_token=ZZZZZ
  • Request パラメータ にアプリのID(client_id)とアプリのパスワード(client_secret)が必要になります。
  • ?refresh_token=ZZZZZ で取得した Refresh Token を指定します。

レスポンスデータとして Access Token が json 形式で画面に表示されると思います。
ここは手際よく再取得した Access Token で my-sns からログインユーザ情報を取得してみてください。


Implicit Grant を試す

インプリシットグラントで Access Token 取得をしてみます。
テストデータをリセットしたいのでOAuth 認証を試す準備で行った
データベースの削除と my-sns の Tomcat を再起動を行います。

OAuthのテストに用意した my-app のページ「http://localhost:15080/my-app/」を開きます。
以下のURLをアドレスバーに入力して my-sns にアクセスします。
http://localhost:8080/my-sns/oauth/authorize?client_id=my-auth&scope=trust&response_type=token&redirect_uri=http://localhost:15080/my-app/
  • Request パラメータ にアプリのID(client_id)は必要なのですがアプリのパスワード(client_secret)は不要なようです。

認可コードグラントの時と同様に my-sns のユーザアカウントのログイン、アプリの承認と進むのですが
インプリシットグラントの場合、Access Token の取得に Request Token がいらないので
直接 Access Token が指定した call back に指定した URL に送られてきます。

ここで注意が必要なのですが、?access_token=YYYYY の形ではなく #access_token=YYYYY で送られてくるので
通常の Request パラメータの取り出し方法では Access Token が取り出せない。
URL のハッシュ「#」以降を自力で解析して Access Token を取り出さないといけないようです。

また Access Token の取得にアプリのパスワードが不要なところも微妙だと思う。
さらに認可コードグラントのように Access Token をレスポンスデータでなく
call back URL の Request パラメータ(厳密には Request パラメータではない?)から
取得するところもどうかと思う。
call back URL といえど他の URL と変わらないので
悪意?のあるサイトが #access_token=YYYYY 的な Request パラメータで
関係ない(なりすまし用?) Access Token 直接送り付けてくることも考えられる。
とりあえず、こちら側のサイト my-app 側も送られてくる Request パラメータを無条件に信用するのでなく
何かしらのチェックは必要になるでしょう。


Resource Owner Password Credentials Grant を試す

リソースオーナーパスワードクレデンシャルグラントで Access Token 取得をしてみます。
まず試す前にテストデータのリセットを行います。
OAuth 認証を試す準備で行ったデータベースの削除と my-sns の Tomcat を再起動をします。

OAuthのテストに用意した my-app のページ「http://localhost:15080/my-app/」から
以下のURLをアドレスバーに入力して my-sns にアクセスします。
http://localhost:8080/my-sns/oauth/token?client_id=my-auth&client_secret=pass123&scope=trust&grant_type=password&username=user01&password=pass01
  • Request パラメータ username、password で my-sns のユーザアカウントのログインID、パスワードを指定します。
  • またアプリのIDとパスワードも必要になるようです。

ユーザアカウントのログイン画面が無ければアプリ承認画面もなく
レスポンスデータとして Access Token が json 形式で画面に表示されると思います。
まぁ username、password を Request パラメータで送っていくるのだから
あちら側 my-sns としてはログイン画面は必要ないのでしょう。

ここで微妙な仕様に気づくと思います。
あちら側 my-sns のユーザアカウントのログインID、パスワードを 
こちら側 my-app でどうやって知りえるのか?
たぶん、こちら側 my-app でログイン画面を用意して
あちら側 my-sns の ログインID、パスワード入力してもらい
こちら側 my-app で送られてきたログインID、パスワードを Request パラメータに付けて 
Access Token 取得 URL を呼び出すことになるのだろう。
本来 OAuth こちら側 my-app があちら側 my-sns のユーザアカウントのログインID、パスワードを知ることなく
あちら側 my-sns のユーザ認証の必要なデータにアクセスするということだと思う。
微妙な仕様だ。
アプリ承認画面に関してもきっとこちら側 my-app で用意する仕様なのだろう。


Client Credentials Grant を試す

クライアントクレデンシャルグラントで Access Token 取得をしてみます。
例によってテストデータのリセットのためにデータベースの削除と my-sns の Tomcat を再起動を行います。

OAuthのテストに用意した my-app のページ「http://localhost:15080/my-app/」から
以下のURLをアドレスバーに入力して my-sns にアクセスします。
http://localhost:8080/my-sns/oauth/token?client_id=my-auth&client_secret=pass123&scope=trust&grant_type=client_credentials
  • Request パラメータ には、アプリのIDとパスワードが必要になるようです。

先ほどのリソースオーナーパスワードクレデンシャルグラントと同様に
ユーザアカウントのログイン画面が無ければアプリ承認画面もなく
レスポンスデータとして Access Token が json 形式で画面に表示されると思います。

クライアントクレデンシャルグラントの場合、特定のユーザアカウントとという訳でなく
アプリのIDとパスワードに対して Access Token 発行されるような感じになるようです。
なのでこの場合 Access Token でログインユーザ情報が取得できないようです。
まぁこれも微妙といえば微妙な仕様だなぁ。


おわりに

触ってみて、なるほどと思うところとやっぱりそうなっていたのかと思うところがあった。
あたり前だけど発行した Token をユーザと紐づけてデータベースで管理する。
OAuth Provider の実装は、他にもいろいろある(java 以外を含めて)とあると思うけど
このあたりはどれも変わらないのだろう。

今回は、Access Token を Request パラメータで渡していたけど
Restful な API を目指すなら Basic認証、Digest認証と同じように
Request ヘッダーでやりとりするのが正しいようだ。
この辺は、Client 側の対応になりそうだ。
それは、また次回に。
次の Version の開発も着々と進んでそうなのでその時にでも。


参考URL
http://www.atmarkit.co.jp/ait/articles/1209/10/news105.html
http://penq.co.jp/blog/engineer/amazon-web-service/3113