第二回は、第一回でご紹介した構成(図1)の構築方法をご紹介します。
図1:構成
認可サーバ(Keycloak)の構築
まずは認可サーバを構築します。本連載では、Keycloak 9.0.3を使用します。インストール先ホストのOSはCentOS 7.8とします。
Keycloakのインストール
Keycloakをインストールします。Keycloakのインストールは、zipファイルをダウンロードして解凍するのみと、非常に簡単です。
まずは事前準備として、Java Development Kitをインストールします。本連載では、OpenJDK 8を使用します。
次に以下のサイトから、KeycloakのStandalone server distributionをダウンロードします。
https://www.keycloak.org/archive/downloads-9.0.3.html
ダウンロードしたzipファイルを解凍します。
リスト2:Keycloackのzipファイルを解凍
1 | $ unzip keycloak-9.0.3.zip |
これでインストールは完了です。
Keycloakの起動
インストールしたKeycloakを起動します。まず、起動する前に、Keycloakの管理者アカウントを作成します。ここではユーザ名を「admin」とします。
リスト3:管理者アカウントの作成
2 | $ ./bin/add-user-keycloak.sh -u admin |
次にKeycloakを起動します。localhost以外からも、管理コンソールにアクセスできるようにするために、「-b」オプションを指定します。
リスト4:Keycloakの起動
1 | $ ./bin/standalone.sh -b 0.0.0.0 |
以下のようなログが出力されれば、起動成功です。Keycloakのログは、keycloak-9.0.3/standalone/log/server.logで確認できます。
リスト5:Keycloak起動の成功をログで確認
1 | 13:54:22,252 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 9.0.3 (WildFly Core 10.0.3.Final) started in 14765ms - Started 590 of 885 services (601 services are lazy, passive or on-demand) |
http://<hostname>:8080/auth/にアクセスすると、管理コンソールのWelcome画面が表示されます(図2)。
図2:Welcome画面
各種リソースの準備
最低限必要なリソースを準備します。最低限必要なリソースとしては、以下があります。
- レルム:Keycloak独自の名前空間。クライアントやユーザの入れ物として、最初に作成する必要がある。テナントやサービスの単位で作成することが多い
- クライアント:OAuth 2.0(RFC6749)の登場人物における、Resource ServerおよびClientをクライアントとして設定する。識別子としてクライアントIDを持つ。ここでは以下の2つのクライアントを設定する
- 外部アプリ用のクライアント:OAuth 2.0のClientに相当する。Keycloakに対してトークンを要求する
- APIゲートウェイ用のクライアント:OAuth 2.0のResource Serverに相当する。Keycloakに対してトークンイントロスペクションを要求する。その時のクライアント認証のためにResource ServerもKeycloakのクライアントとして設定する
- ユーザ:外部アプリを使うエンドユーザ。APIサーバに自らのリソース※1を持つ
※1:例えば、銀行が提供するAPIサーバであれば「口座残高」、SNSが提供するAPIサーバであれば、「投稿履歴」など。
管理コンソールで上記リソースを作っていきます。Welcome画面(図2)にて、[Administration Console]をクリックすると、管理コンソールのログイン画面が表示されます(図3)。
図3:ログイン画面
作成したKeycloakの管理者アカウントのユーザ名とパスワードを入力し、[Log In]ボタンをクリックします。MasterレルムのRealm SettingsタブのGeneralタブが表示されれば、ログイン成功です(図4)。
図4:Realm SettingsタブのGeneralタブ
まずはレルムを作成します。左ペインの上部[Master]にカーソルを合わせると表示される[Add realm]ボタンをクリックすると、Add realm画面が表示されますので(図5)、レルム名(ここでは「sample_service」とします)を入力し、[Create]ボタンをクリックします。
図5:Add realm画面
次に外部アプリ用のクライアントを作成します。Clientsタブの[Create]ボタンをクリックすると、Add Client画面が表示されますので(図6)、クライアントID(ここでは「sample_application」とします)を入力し、[Save]ボタンをクリックします。クライアントIDは、RFC6749で定義されているClient Identifierと同義です。
図6:Add Client画面
外部アプリ用のクライアントの設定を行っていきます。ここでは簡単のため、外部アプリへのトークンの発行方法は、RFC6749で定義されているOAuth 2.0の認可フローのうち、Resource Owner Password Credentials Grant※2を使うこととします。Settingsタブにて、以下の通り設定します(表1、図7)。
表1:外部アプリ用のクライアントの設定項目
設定項目名 | 説明 | 設定値 |
Access Type | Client Typesと類義。「confidential」を設定したクライアントには、トークン発行時にクライアントシークレットなどの秘密情報を用いたクライアント認証が要求される | confidential |
Standard Flow Enabled | Authorization Code Grantの有効性。KeycloakではStandard Flowと呼ぶ | OFF |
Direct Access Grants Enabled | Resource Owner Password Credentials Grantの有効性。KeycloakではDirect Access Grantと呼ぶ | ON |
※2:Resource Owner Password Credentials Grantは、OAuth 2.0のセキュリティベストプラクティス(https://tools.ietf.org/html/draft-ietf-oauth-security-topics-15)では推奨されていません。OAuth 2.0のセキュリティベストプラクティスに則ったシステム堅牢化方法は第三回以降で説明します。
図7:ClientsタブのSettingsタブ
次にAPIゲートウェイ用のクライアントを作成します。ここではクライアントIDを「sample_api_gateway」とします。Settingsタブにて、以下の通り設定します(表2、図8)。
表2:APIゲートウェイ用のクライアントの設定項目
設定項目名 | 説明 | 設定値 |
Access Type | Client Typesと類義。「bearer-only」を設定したクライアントは、トークン発行はできないが、トークンイントロスペクションはできる | bearer-only |
図8:ClientsタブのSettingsタブ
次にユーザを作成します。Usersタブの[Add user]ボタンをクリックして表示されるAdd user画面(図9)で、ユーザ名(ここでは「sample_user」とします)を入力し、[Save]ボタンをクリックします。
図9:Add user画面
ユーザのパスワードの設定を行っていきます。パスワードの設定は、Credentialsタブにて設定します(図10)。ここでは設定したパスワードを永続的なパスワードにするために、TemporaryをOFFにしています※3。
※3:TemporaryをONにすると、トークン発行が"Invalid user credentials"で失敗します。
図10:UsersタブのCredentialsタブ
以上でリソースの準備は完了です。
トークンの発行 - Keycloakの動作確認
Keycloakを用いて、トークンを発行してみましょう。RFC6749に則って、Keycloakのトークンエンドポイントをコールします。Resource Owner Password Credentials Grantですので、grant_typeに「password」を指定します。また、client_secretに指定するクライアントシークレット(client secret)は、ClientsタブのCredentialsタブより確認します。
以下のようなレスポンスが返ってくれば、トークン発行は成功です。アクセストークン、リフレッシュトークン、IDトークンの3種類のトークンが発行されます。
リスト7:トークン発行に成功した際のレスポンスの例
02 | "access_token": "<access token>", |
04 | "refresh_expires_in": 1800, |
05 | "refresh_token": "<refresh token>", |
06 | "token_type": "bearer", |
07 | "id_token": "<id token>", |
08 | "not-before-policy": 0, |
09 | "session_state": "75760e1a-853a-4e9c-94b7-7fe0cab77b6e", |
10 | "scope": "openid profile email" |
アクセストークンの中身を見てみます。Keycloakが発行するアクセストークンは、JWT(JSON Web Token)の形式をとっており、base64urlエンコードされたHeader、Payload、Signatureが、「.」で結合しています。
リスト8:アクセストークンの形式
1 | base64url(Header).base64url(Payload).base64url(Signature) |
試しに、Payloadの部分をbase64urlデコードしてみます。デフォルトでは以下のような情報が入っています。
リスト9:Payloadをbase64urlデコードしてみた結果の例
04 | "jti": "c5d29c74-d37f-4d9f-8ebc-bd56495900f7", |
07 | "sub": "f8d84f8c-7237-414b-8c24-42deb0237eea", |
09 | "azp": "sample_application", |
10 | "session_state": "e16a38f4-5205-4ed7-80cf-f1c1b6bb7be4", |
22 | "manage-account-links", |
27 | "scope": "openid profile email", |
28 | "email_verified": false, |
29 | "preferred_username": "sample_user" |
APIゲートウェイ(NGINX)の構築
次にAPIゲートウェイを構築します。本連載では、NGINX 1.18.0を使用します。インストール先ホストのOSはCentOS 7.8とします。
NGINXのインストール
NGINXをインストールします。まずはyumリポジトリ(/etc/yum.repos.d/nginx.repo)を作成します。
yumコマンドでNGINXをインストールします。
リスト11:NGINXのインストール
1 | $ sudo yum install nginx |
バージョンを確認します。
リスト12:インストールしたNGINXのバージョン確認
2 | nginx version: nginx/1.18.0 |
これでインストールは完了です。
NGINXの起動
インストールしたNGINXを起動します。
リスト13:NGINXの起動
1 | $ sudo systemctl start nginx |
http://<hostname>にアクセスすると、NGINXのWelcome画面が表示されます(図11)。
図11:NGINXのWelcome画面
リバースプロキシの設定
NGINXにリバースプロキシの設定をし、NGINX経由でAPIをコールできるようにします。まずは構成定義ファイル(/etc/nginx/conf.d/sample.conf)を作成します。ここでは8008ポートを使うこととします。
リスト14:リバースプロキシの設定
03 | ignore_invalid_headers off; |
06 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
07 | proxy_set_header X-Real-IP $remote_addr; |
08 | proxy_set_header Connection close; |
構成定義ファイルをリロードします。
APIのコール - リバースプロキシの動作確認
NGINX経由でAPIをコールしてみましょう。APIサーバには、「Hello!」と返すメソッド「/echo」が実装されているものとします。
リスト16:リバースプロキシの動作確認
1 | $ curl http://<APIゲートウェイhostname>:8008/echo |
NGINX経由でのAPIコールに成功しました。
トークンイントロスペクションの設定
NGINXにトークンイントロスペクションの設定をし、APIコール時にアクセストークンの有効性をチェックできるようにします(図12)。
図12:トークンイントロスペクション
まずはNGINX JavaScript Moduleをインストールします。
リスト17:JavaScript Moduleのインストール
1 | $ sudo yum install nginx-module-njs |
nginx.conf(/etc/nginx/nginx.conf)のトップレベルのディレクティブに、NGINX JavaScript Moduleをロードする設定を書きます。
リスト18:JavaScript Moduleの設定を追加
1 | load_module modules/ngx_http_js_module.so; |
2 | load_module modules/ngx_stream_js_module.so; |
作成した構成定義ファイル(/etc/nginx/conf.d/sample.conf)に、トークンイントロスペクションの設定を書きます。
リスト19:トークンイントロスペクションの設定を追加
03 | map $http_authorization $access_token { |
11 | auth_request /_oauth2_token_introspection; |
15 | location = /_oauth2_token_introspection { |
17 | js_content introspectAccessToken; |
20 | location = /_oauth2_send_request { |
23 | proxy_set_header Content-Type "application/x-www-form-urlencoded"; |
24 | proxy_set_body "token=$access_token&token_hint=access_token&client_id=sample_api_gateway&client_secret=<client secret>"; |
25 | proxy_pass http://<認可サーバhostname>:8080/auth/realms/sample_service/protocol/openid-connect/token/introspect; |
トークンイントロスペクションのロジックを、oauth2.js(/etc/nginx/oauth2.js)に書きます。
リスト20:ロジックの記述を追加
01 | function introspectAccessToken(r) { |
02 | r.subrequest("/_oauth2_send_request", |
04 | if (reply.status == 200) { |
05 | var response = JSON.parse(reply.responseBody); |
06 | if (response.active == true) { |
構成定義ファイルをリロードします。
APIのコール - トークンイントロスペクションの動作確認
NGINX経由でAPIをコールし、トークンイントロスペクションの動作を確認してみましょう。まずはアクセストークンを付与せずにAPIをコールしてみます。アクセストークンを付与せずにAPIをコールすると、403 Forbiddenが返ってきます。
リスト22:アクセストークンなしだと「403 Forbidden」が返される
1 | $ curl http://<APIゲートウェイhostname>:8008/echo |
3 | <head><title>403 Forbidden</title></head> |
5 | <center><h1>403 Forbidden</h1></center> |
6 | <hr><center>nginx/1.18.0</center> |
次に、先ほど発行したアクセストークンを付与してAPIをコールしてみます。しかし先ほど発行したアクセストークンはすでに有効期限(デフォルトでは5分)が切れていたため、やはり403 Forbiddenが返ってきます。
リスト23:有効期限切れのアクセストークンでも「403 Forbidden」となる
1 | $ curl http://<APIゲートウェイhostname>:8008/echo -H "Authorization: Bearer <access token>" |
3 | <head><title>403 Forbidden</title></head> |
5 | <center><h1>403 Forbidden</h1></center> |
6 | <hr><center>nginx/1.18.0</center> |
新しいアクセストークンを発行し、そのアクセストークンを付与してAPIをコールしてみます。
リスト24:APIコールに成功
1 | $ curl http://<APIゲートウェイhostname>:8008/echo -H "Authorization: Bearer <access token>" |
APIコールに成功しました。
次回は、OAuth 2.0のセキュリティベストプラクティスに則って、今回構築したシステムをより堅牢にしていきましょう。