OSSのAPI Gateway KongにOAuth2.0によるアクセス制御を導入する
はじめに
本記事では、OSSのAPI GatewayであるKongにOAuth2.0によるアクセス制御を追加する方法を紹介します。
OAuth2.0によるアクセス制御の導入には、Kong Gateway OAuth2 Pluginを利用します。
全体構成は以下のようになっています。特筆すべき点としては、Kongがリソースサーバーだけでなく認可サーバーとしての役割も担う点があげられます。
なお、本来であれば認可リクエストをKong Gateway OAuth2 Pluginが受け取れるPOSTに変換するコンポーネント (認可サーバーのフロントエンド相当) が必要になりますが、今回はクライアントから直接POSTでリクエストしている点に注意してください (後述)。
本記事の内容の大部分は以下のページを参考にしています。
また、上記のページ中にも環境構築手順の記載はありますが、より簡単に構築するためにDocker Composeで一通り環境構築するサンプルを作成していますのでご利用ください。 サンプルは以下のコマンドを実行することでコンポーネント一式が起動します。なお、以降の手順では、以下のコマンドを実行して環境が一通り立ち上がっていることを前提とします。$ git clone https://github.com/kg0r0/oauth-kong-example.git
$ cd oauth-kong-example
$ docker-compose up -d
※HTTPS通信がKongで終端しているなどの細かい違いはありますが、動作として問題ないと思います。もし不具合などあればIssueやPRをいただけますと幸いです。
Kong GatewayとAPIサーバーのセットアップ
本手順では、8001番ポートのKong Admin APIから設定をおこないます。なお、API Gatewayの後ろのAPIには8000番ポートからアクセスすることになります。
KongにプロキシするAPIを登録します。ここでは、step-on-api-server
としてAPIを登録しています。
$ curl -k -X POST -H "Content-Type: application/json" -d '{"name":"step-on-api-server", "url":"http://api:3000"}' https://localhost:8001/services
{
"retries": 5,
"port": 3000,
"protocol": "http",
"client_certificate": null,
"tls_verify_depth": null,
"created_at": 1629142483,
"updated_at": 1629142483,
"connect_timeout": 60000,
"write_timeout": 60000,
"read_timeout": 60000,
"tags": null,
"name": "step-on-api-server",
"path": null,
"tls_verify": null,
"ca_certificates": null,
"id": "89eebfb2-dc68-4300-b834-e66e435c18e7",
"host": "api"
}
次にKongを介してリクエストをAPIに転送するためのルーティング設定を追加します。ここでは、/stepon
というパスにアクセスした際に、先ほど登録したstep-on-api-server
が呼び出されるように設定しています。
$ curl -k -X POST -H "Content-Type: application/json" -d '{"name":"step-on-route", "service": {"name":"step-on-api-server"}, "paths": ["/stepon"]}' https://localhost:8001/routes
{
"hosts": null,
"paths": [
"/stepon"
],
"methods": null,
"sources": null,
"path_handling": "v0",
"request_buffering": true,
"response_buffering": true,
"updated_at": 1629142497,
"created_at": 1629142497,
"id": "7a70a6e5-63fd-4d10-a2a8-97318c301645",
"https_redirect_status_code": 426,
"service": {
"id": "89eebfb2-dc68-4300-b834-e66e435c18e7"
},
"regex_priority": 0,
"snis": null,
"strip_path": true,
"destinations": null,
"tags": null,
"headers": null,
"protocols": [
"http",
"https"
],
"preserve_host": false,
"name": "step-on-route"
}
ここまでで、KongとAPIの設定は終わりになります
ここで、認証済みユーザーを含むモックのヘッダを付与して状態でリクエストを送信して正常応答が得られるか試してみてください。
$ curl -k -H "mock-logged-in-as: clark" https://localhost:8000/stepon/stepcounts
[
{
"date": "2021-01-01",
"count": 2500
},
{
"date": "2021-01-02",
"count": 12000
},
{
"date": "2021-01-03",
"count": 9500
}
]
上記のヘッダは元の記事のサンプルAPIコードに合わせて付与しています。
Kong Gateway OAuth2.0 Pluginを利用した場合は、x-authenticated-userid
というヘッダでユーザーIDが送信されるようです。
その他にリソースサーバー側でKongから受け取ることができる情報についてはUpstream Headersをご覧ください。
OAuth2.0プラグインの有効化
8001番ポートのKong Admin APIからOAuth2.0に関連する設定をおこないます。
設定値に関する詳細はプラグインのページをご覧ください。
以下のリクエストを送信してOAuth2.0プラグインを有効化します。ここでは、主に許可するスコープの設定や認可コードフローの有効化などを行なっています。レスポンスに含まれるprovision_key
については後ほど利用するのでメモしておいてください。
$ curl -k -X POST -H "Content-Type: application/json" -d '{"name":"oauth2", "config": {"scopes":["user_profile", "biometric", "step_counts"], "mandatory_scope": true, "enable_authorization_code": true}, "protocols": ["https"]}' https://localhost:8001/services/step-on-api-server/plugins
{
"created_at": 1629143302,
"id": "f07fdaee-251c-41d6-8fb0-680cc70f7a9b",
"name": "oauth2",
"service": {
"id": "89eebfb2-dc68-4300-b834-e66e435c18e7"
},
"route": null,
"enabled": true,
"tags": null,
"protocols": [
"https"
],
"config": {
"enable_client_credentials": false,
"auth_header_name": "authorization",
"accept_http_if_already_terminated": false,
"scopes": [
"user_profile",
"biometric",
"step_counts"
],
"anonymous": null,
"provision_key": "KB8zmkuKDd8lzt3u8y4sYE47H9SFgOrJ",
"enable_authorization_code": true,
"reuse_refresh_token": false,
"token_expiration": 7200,
"refresh_token_ttl": 1209600,
"global_credentials": false,
"pkce": "lax",
"enable_password_grant": false,
"hide_credentials": false,
"mandatory_scope": true,
"enable_implicit_grant": false
},
"consumer": null
}
つづいてOAuth2.0のクライアント (API Consumer) の設定をします。
以下のように/consumers
というエンドポイントに対して登録するクライアントの名前を指定してリクエストを送信します。
$ curl -k -X POST -H "Content-Type: application/json" -d '{"username": "shoeflyshoe"}' https://localhost:8001/consumers
{
"username": "shoeflyshoe",
"id": "73411a66-15b6-4680-a44c-4f3daaa86b52",
"custom_id": null,
"tags": null,
"created_at": 1629143473
}
上記で登録したクライアント (API Consumer) の名前をパスに含め、ボディで登録するリダイレクトURIを指定してリクエストを送信することで、client_id
やclient_secret
が取得できます。
$ curl -k -X POST -H "Content-Type: application/json" -d '{"name": "Shoe Fly Shoe Customer Rewards", "redirect_uris": ["https://shoeflyshoe.store/oauth_return"]}' https://localhost:8001/consumers/shoeflyshoe/oauth2
{
"created_at": 1629143669,
"redirect_uris": [
"https://shoeflyshoe.store/oauth_return"
],
"name": "Shoe Fly Shoe Customer Rewards",
"client_id": "1wyDxCGUgq0ugdezJy5EN4iktJ5197B5",
"hash_secret": false,
"client_type": "confidential",
"tags": null,
"id": "8fe80872-a63b-49fe-a46a-0e3ecb7fd63a",
"consumer": {
"id": "73411a66-15b6-4680-a44c-4f3daaa86b52"
},
"client_secret": "UFqTO2mmpNrm82aJLeWocwi8akM92FIH"
}
ここまででClientの登録・設定は終了です。
次のステップでOAuthのフローを実行していきます。
OAuth2.0フローの実行
今回はクライアントをcurlコマンドで代替します。
また、既に設定したとおりOAuth2.0の認可コードフローに沿って実行します。
認可リクエスト
まず、以下の通り認可リクエスト相当のリクエストを送信します。
レスポンスとして認可コード付きのredirect_uri
が返ってくるので、User Agentをクライアントにリダイレクトさせます (認可レスポンス) 。
$ curl -k -X POST -H "Content-Type: application/json" -d '{"client_id": "1wyDxCGUgq0ugdezJy5EN4iktJ5197B5", "response_type": "code", "scope": "step_counts", "provision_key": "KB8zmkuKDd8lzt3u8y4sYE47H9SFgOrJ", "authenticated_userid": "clark", "redirect_url": "https://shoeflyshoe.store/oauth_return", "state": "xyz" }' https://localhost:8000/stepon/oauth2/authorize
{
"redirect_uri": "https://shoeflyshoe.store/oauth_return?code=HIbSOblERaBIVZxepeZEfqezzCrXRa9m&state=xyz"
}
上記のリクエストは冒頭の全体像の(2)にあたり、clarkというユーザーが認証され、クライアントが要求したscopeを許可した後のリクエストである点に注意してください。
また、OAuth2.0で定義されている認可リクエストに加えて、クライアント登録時に取得したprovision_key
という値が必要になります。
本来の利用方法として、こちらのような通常のGETによる認可リクエストをハンドリングするようなコンポーネントを用意し、Kongが受け取れる形式のPOSTリクエストに変換して送信します。
トークンリクエスト
つづいて、クライアントは受けと他認可レスポンスに含まれる認可コードを利用してトークンリクエストを送信します。
$ curl -k -X POST -H "Content-Type: application/json" -d '{"grant_type": "authorization_code", "code": "HIbSOblERaBIVZxepeZEfqezzCrXRa9m", "client_id": "1wyDxCGUgq0ugdezJy5EN4iktJ5197B5", "client_secret": "UFqTO2mmpNrm82aJLeWocwi8akM92FIH" }' https://localhost:8000/stepon/oauth2/token
{
"expires_in": 7200,
"access_token": "dMaYaT2uHOOi1YrSZ1NEb5agzwhtL7jY",
"token_type": "bearer",
"refresh_token": "Cxu5Oq55ZKoAv3E0S6Nx4QZbjrZYGfN0"
}
アクセストークンおよびリフレッシュトークンが取得できました。
保護されたリソースへのアクセス
クライアントは取得したアクセストークンを利用して、Kong Gateway経由でAPIにリクエストします。
$ curl -k -H "Authorization: Bearer dMaYaT2uHOOi1YrSZ1NEb5agzwhtL7jY" https://localhost:8000/stepon/stepcounts
[
{
"date": "2021-01-01",
"count": 2500
},
{
"date": "2021-01-02",
"count": 12000
},
{
"date": "2021-01-03",
"count": 9500
}
]
もちろん、誤ったアクセストークンを指定した場合はエラーとなることが確認できます。
$ curl -k -H "Authorization: Bearer dummy" https://localhost:8000/stepon/stepcounts
{
"error": "invalid_token",
"error_description": "The access token is invalid or has expired"
}
アクセストークンの更新
リフレッシュトークンについても、OAuth2.0の定義どおりKongにリクエストを送信することでアクセストークンの更新が可能になっています。
$ curl -k -X POST -H "Content-Type: application/json" -d '{"grant_type": "refresh_token", "refresh_token": "Cxu5Oq55ZKoAv3E0S6Nx4QZbjrZYGfN0", "client_id": "1wyDxCGUgq0ugdezJy5EN4iktJ5197B5", "client_secret": "UFqTO2mmpNrm82aJLeWocwi8akM92FIH" }' https://localhost:8000/stepon/oauth2/token
{
"expires_in": 7200,
"access_token": "aMpSs6BoXZMF60s4BUAOIjpN1L7uKvqg",
"token_type": "bearer",
"refresh_token": "CS8ODf7IVI6yNtI62FX5FLlGOaiaqZGj"
}
おわりに
既存の実装に依存することなくOAuth2.0によるAPIの保護を実現する方法の一つとして、OAuth2.0に対応したAPI Gatewayの導入が考えられます。
今回紹介したKong Gateway OAuth Pluginは認可サーバーとしても振る舞うため、OAuth2.0によるアクセス制御を簡単に導入できるかもしれません。
もちろんOAuth2.0 Introspection pluginを利用することでサードパーティーの認可サーバーにも対応できるそうなので、既に利用している認可サーバーがある場合でも導入は可能そうです。
Discussion