この記事はHolmes Advent Calendar 2019の最終日です。
はじめに
最近はOAuth2.0認可サーバーを自前で作るよりもCognitoやKeycloakなどのシングルサインオン基盤を利用する方が良いみたいです。メインで利用しているフレームワークもメンテナンスモード入りしてEOLを待つだけの状態のようです。まずは手っ取り早くKeycloakのDockerでClient Credentials Flowを試してみる事にしました。
Keycloak
Docker起動
docker run -d -p 18080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin --name keycloak jboss/keycloak
ブラウザでhttp://localhost:18080/
にアクセスすると難なく立ち上がっている事を確認できました。便利。
アクセストークンの取得
Docker起動時に指定したadminユーザーのID・パスワードを使用してトークンを取得してみます。
リクエスト
curl -X POST -H "Content-Type:application/x-www-form-urlencoded" --data "client_id=admin-cli&username=admin&password=admin&grant_type=password" http://localhost:18080/auth/realms/master/protocol/openid-connect/token
レスポンス
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjOU56bnlEUklKQUVGa0U0M3lGM2xyVFNYSWprRGZHdmFsR2JLMGx3WkMwIn0.eyJqdGkiOiI0NWMyOTU0Yi1hZTVjLTRjZmEtYjNjMy0zYzZmZTEyYzAxOTciLCJleHAiOjE1NzcxNjcxNDcsIm5iZiI6MCwiaWF0IjoxNTc3MTY3MDg3LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjE4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsInN1YiI6ImRkNWEyYzExLWFlYWYtNGYyMy1iN2M4LTA0YmVjNzBlN2U4NSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFkbWluLWNsaSIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjRkZTJkZDc0LWNjMWEtNDM1OS1iOTI3LTVmMzcwN2ZmZDI3NyIsImFjciI6IjEiLCJzY29wZSI6InByb2ZpbGUgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIn0.G5wUrtQXPHKkEAzDUJ67-xpFsd6ORmhLR_MVatZmFvwabOkzFTcqahNOcqOuSPJirnFlFXctLY8u44XI_ydVTJ5as60Ey-vCI8t_G94N6L2cKnAx-7fXV39bfbK1hUbCOJv9AKsEThVGv9yD4XUR6Wwp9bRLdH_RCVY_91K3xMqTvJunK9qqsMgN7pDG6P6FEzruHUleewCZZr7u1HHrGPuJvaBBulhdpJSVA30jqAh0FSN04C1KOTT20HsR1E6TGLbfr5n81VR6_4YM5gRFbNScEL8PIlRj4qnvh8BmIPi5Cn8H_juB0_2wQppULPHf5pD_vHBX6KkI60diKIBZgQ",
"expires_in": 60,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjNWNlYWM2MS02ZTI3LTQ5ODMtODQxYi0yZDBlZWQxZWQ1MmYifQ.eyJqdGkiOiI5MmNmYTExNi00MjJlLTRjNTgtYWU2Ni1mNDVlZjk1NWUwYzciLCJleHAiOjE1NzcxNjg4ODcsIm5iZiI6MCwiaWF0IjoxNTc3MTY3MDg3LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjE4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MTgwODAvYXV0aC9yZWFsbXMvbWFzdGVyIiwic3ViIjoiZGQ1YTJjMTEtYWVhZi00ZjIzLWI3YzgtMDRiZWM3MGU3ZTg1IiwidHlwIjoiUmVmcmVzaCIsImF6cCI6ImFkbWluLWNsaSIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjRkZTJkZDc0LWNjMWEtNDM1OS1iOTI3LTVmMzcwN2ZmZDI3NyIsInNjb3BlIjoicHJvZmlsZSBlbWFpbCJ9.sTu79Wt6U_tJYaYGqwSELddG3_-CmpEfVrmp3HC1eh4",
"token_type": "bearer",
"not-before-policy": 0,
"session_state": "4de2dd74-cc1a-4359-b927-5f3707ffd277",
"scope": "profile email"
}
無事に管理クライアントのアクセストークンを取得する事ができました。
クライアントの登録
取得したTokenをヘッダーに指定してアプリケーションを登録してみます。当たり前ですが、クライアントIDは被ると怒られます。"serviceAccountsEnabled":"true"
を指定しないとトークンの取得でエラーがレスポンスされます。
リクエスト
curl -X POST -H "Content-Type:application/json" -H "Authorization: Bearer eyJ..." -d \
'{"clientId":"hoge", "serviceAccountsEnabled":"true", ...}' \
"http://localhost:18080/auth/realms/master/clients-registrations/default"
レスポンス
{"id":"70c0e1fa-a769-48b6-bc34-1336056e1cbf","clientId":"hoge","surrogateAuthRequired":false,"enabled":true,"clientAuthenticatorType":"client-secret","secret":"b10c3e8c-3e4e-4bd8-a363-be18337b5251","registrationAccessToken":"eyJ...","redirectUris":[],"webOrigins":[],"notBefore":0,"bearerOnly":false,"consentRequired":false,"standardFlowEnabled":true,"implicitFlowEnabled":false,"directAccessGrantsEnabled":false,"serviceAccountsEnabled":true,"publicClient":false,"frontchannelLogout":false,"protocol":"openid-connect","attributes":{},"authenticationFlowBindingOverrides":{},"fullScopeAllowed":true,"nodeReRegistrationTimeout":-1,"defaultClientScopes":["web-origins","role_list","roles","profile","email"],"optionalClientScopes":["address","phone","offline_access","microprofile-jwt"]}
どうやら登録されたようです。
登録したクライアントでトークンを取得
クライアントの秘密キーは先ほどクライアントを登録した時のレスポンスに含まれているようです。秘密キーは/auth/admin/realms/master/clients/{id}/client-secret
で問い合わせて取得する事もできます。
リクエスト
curl -X POST "http://localhost:18080/auth/realms/master/protocol/openid-connect/token" \
--data "grant_type=client_credentials&client_secret=b10c3e8c-3e4e-4bd8-a363-be18337b5251&client_id=hoge"
取得される結果の形式は同じなので省略します。
アクセストークンの検証
アクセストークンはJson Web Token(JWT)形式になっています。
Base64(Header).Base64(Payload).Base64(Signature)
手っ取り早く検証するにはjwt.ioが便利です。
JWT(Nodejs)を使ってデコード
npm install -g jwt-cli
jwt eyJh...(省略)
✻ Header
{
"alg": "RS256",
"typ": "JWT",
"kid": "c9NznyDRIJAEFkE43yF3lrTSXIjkDfGvalGbK0lwZC0"
}
✻ Payload
{
"jti": "f43a9b37-8cb7-4757-bd7c-ac3ddbbd019f",
"exp": 1577233302,
"nbf": 0,
"iat": 1577233242,
"iss": "http://localhost:18080/auth/realms/master",
"sub": "dd5a2c11-aeaf-4f23-b7c8-04bec70e7e85",
"typ": "Bearer",
"azp": "admin-cli",
"auth_time": 0,
"session_state": "360d1400-64f3-4bf2-bc57-40be0a2d8dc8",
"acr": "1",
"scope": "profile email",
"email_verified": false,
"preferred_username": "admin"
}
Issued At: 1577233242 2019/12/25 9:20:42
Not Before: 0 1970/1/1 9:00:00
Expiration Time: 1577233302 2019/12/25 9:21:42
✻ Signature FKL7thl_B0zRgqXfGw-TtXFtPPIYOgZNPIEbPVazNptHzCZo2rrLtI-2WpdO4LMsKsoSDpLoU8KI16kLFuRjPqyoD1ZvKVvsxqHXr4dGvp9SxUcKVWeJj7s6FzbLhyhGhbt8IOpDwMosxHlz_ijokjljXrfmFSMOWlmlzeUmpZiOFI_ZQvdq8qDN6J7FZRuDSqXtezLnkc9jlEE9B5ILbSJLkA31Mf4rnEv07gP44ceVVOMvDzpQvNJZc4sPK4Gw3bmtIfi6upduofk9pPl7TrWUH5-saaMnE1ky02pJ00AtGHqceWpe4hrwz7grQFPXP-JPKB5k7sggoL1HLJ97BA
トークンの中身を取り出す事ができました。
署名の検証
署名の検証をするにはKeycloakから事前に公開鍵を取得しておく必要があります。
curl "http://localhost:18080/auth/realms/master/protocol/openid-connect/certs"
opensslを使って検証できると思いますが面倒なので、x5c:[]の中身を公開鍵の形にしてjwt.ioの公開鍵欄に貼り付けて検証します。
署名が正しい事が検証できました。
最後に
如何でしたでしょうか。認可フローをKeycloakを使って簡単に試す事ができました。実際のサービスで使用する場合はCognitoを使うんだろうなぁと思いつつ、また機会があれば投稿してみたいと思います。