
[!] この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
昨今、魅力的なWebサービスの増加に伴い、利用するサービスが増えていることかと思います。ただ同時に、管理するアカウントも増えてしまい、ID/パスワードを忘れてしまった経験は誰しも一度はあるでしょう。そうした問題もあり、近年、サービス間でIDを連携させる動きが活発になっています。「Googleでログイン」のような外部のアカウントでログインするボタンを目にしたこともあるのではないでしょうか。
今回はそんなID連携を実現するためのプロトコルであるOpenID Connectを紹介します。
ID連携とは
あるサービスと別のサービスとでアカウントを連携することを指します。 Googleを例にすると、連携先サービスであるGoogleがアカウント(ID/パスワード)管理し認証機能を提供します。連携元サービスは「Googleでログイン」のような認証を依頼する機能を用意し、ユーザが認証後にGoogleから認証情報を受け取ることで、アカウントを連携します。
ID連携には、次のようなメリットがあります。
- (利用者)サービス毎にID/パスワードを覚えなくてよい
- (開発者)認証機能周りの実装が減るため、開発コストが削減できる
- (サービス提供者)アカウント管理が不要なため、運用コストが削減できる
ID連携プロトコルとしては次の2つがメジャーです。
- OpenID Connect
- SAML 2.0
OpenID Connectとは
OpenID Connectでは、連携先サービスをOpenID Provider(OP)と呼び、連携元サービスをRelying Party(RP)と呼びます。
Relying Partyは認証をOpenID Providerに依頼しユーザはOpenID Provider上でID/パスワードなどによる認証を行います。認証に成功すると、依頼元であるRelying Partyが認証情報であるトークンを受け取れる仕組みです。通信にはJSONが利用されるため、RESTなWebサービスと親和性が高いのも特徴の1つです。
Google, LINE, Yahoo!等はOpenID Connectを使って自社のアカウントとID連携できるOpenID Providerを提供しています。事前設定は必要ですが、開発サービスから上記OpenID Providerを利用して認証することができます。また社内Active Directory(AD)とID連携するサービスとして、Active Directory Federation Services(ADFS)もあります。ADFSはOpenID Providerとして機能し、各種サービスに社内アカウントを連携する仕組みを提供します。
OpenID Connectは利用シーンに合わせて、複数の認証方法が用意されているため、実際にOpenID Connectを使う場合は、開発するサービスに合わせて認証方法を選択することになります。
この記事では、以下の認証方法についてまとめています。
- Authorization Codeフロー
- Device Authorization Codeフロー
事前準備
どの認証方法を使う場合でも事前にOpenID Provider上で設定が必要になります。設定できる項目はOpenID Provider毎に異なりますが、最低限、次の項目は設定が必要です。
- クライアント種別の設定
- confidential : サーバサイドアプリケーションなどクレデンシャル情報を秘匿できる場合に選択
- public : ネイティブアプリケーションなどクレデンシャル情報を秘匿できない場合に選択
- ClientId/ClientSecretの設定
- 選択した種別がconfidentialの場合のみClientSecretも設定する
- 認証成功時のリダイレクトURLの設定
- Device Authorization Codeの場合は不要
これらはこの後に説明する認証フローの中で利用されています。
また、OpenID ProviderはサポートするアルゴリズムやAPIの受け口である各種EndpointのURLが記載されたDiscoveryドキュメントを公開しています。例えば、Google Identityの場合は下記で公開されています。
Google Identity Discoveryドキュメント ⧉
Relying Partyは事前に、このDiscoveryドキュメントを取得しておき、認証フローの中で参照します。
Authorization Codeフロー
Authorization Codeフローは、主にサーバーサイドアプリケーションから利用される、もっとも一般的な認証方法です。ユーザ認証時に直接トークンを受け取るのではなく、Authorization Codeが発行され、そのAuthorization Codeを使ってトークンを取得します。トークン取得時にはクライアント認証が必要なため他のフローに比べて安全です。また、サーバサイドアプリケーションであれば、取得したトークンが漏洩する危険性も少ないため安全に利用すること可能です。
認証フロー
-
ユーザがブラウザでRelying Partyへアクセスする。
-
Relying Partyは認証のためにOpenID Providerのauthorization endpointへリダイレクト指示する。
ブラウザはリダイレクトを受けて、authorization endpointへアクセスする。
※EndpointはDiscoveryドキュメントを参照する。 -
OpenID Providerから認証画面が返され、ユーザは画面上で認証する。
例) ADFSでのログイン画面
IDとパスワードを入力してサインインする
-
OpenID Providerは認証に成功すると、Relying Partyへアクセスするようリダイレクト指示する。
リダイレクト先は事前にOpenID Providerへ設定しておいたURL。
ブラウザはリダイレクトを受けて、Relying Partyへアクセスする。 -
Relying Partyは認証成功時に発行されるcodeをリクエストから取得し、token endpointへリクエストする。成功するとトークンを取得する。
token endpointへのリクエストにはクライアント認証する必要がある。認証方法はいくつかあるが、client_secret_basic
の場合は、事前に登録しておいたClientId/ClientSecretをBasic認証に用いる。認証に成功すると、レスポンスから下記の各トークンを取得できる。- id token : 認証したユーザを確認/検証するために利用する
- access token : ユーザ情報を取得するために利用する
- refresh token : トークンの有効期限が切れた場合に再発行するために利用する
-
Relying PartyはIDトークンを検証し、問題ない場合は認証成功とする。
IDトークンの検証
-
IDトークン形式
IDトークンは下記のJWT(JSON Web Token)形式です。暗号化してJWE(JSON Web Encryption)として利用することも可能です。[ヘッダー].[ペイロード].[シグネチャ]- ヘッダー : 署名に利用したアルゴリズム等を格納している
- ペイロード : 認証情報を格納している。ペイロードの詳細はこちら ⧉を参照
- シグネチャ : ヘッダーとペイロードが改竄されていないかチェックするために利用する
各データはBase64 URLでエンコードされ、
.
(ドット)で連結されています。そのためIDトークンの中身を確認したい場合はBase64 URLデコードすれば確認出来ます。
例えば、IDトークンのヘッダーを、パディングしBase64 URLデコードすると以下のように確認できます。- デコード前
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJNUGt0a0Y2azI0dXFYRWNQcmFlaXNYQk9yWGFpRXk5UVRCNUppUnRnVkxjIn0- デコード後
{"alg":"RS256","typ" : "JWT","kid" : "MPktkF6k24uqXEcPraeisXBOrXaiEy9QTB5JiRtgVLc"} -
検証
- ヘッダーのチェック
algから改竄チェックするアルゴリズムを確認する。
※algをnoneに改竄してチェック自体をスキップさせる方法もあるので、noneは受け付けないなど対応するのがよい。 - 改竄チェック
ヘッダー指定のアルゴリズムで改竄チェックする。
RS256の場合は、シグネチャと公開鍵を利用して改竄をチェックする。
公開鍵はDiscoveryドキュメントのjwks_uriのURLにアクセスして取得する。
取得したキー情報からヘッダーのkidに一致する公開鍵を利用する。 - ペイロードのチェック
基本的にチェックできるパラメータは全てチェックするのがよい。
下記パラメータは必ず含まれているので確認する。- iss : IDトークンの発行者(OpenId Provider)として正しいことを確認する
- sub : 可能ならユーザ識別子が正しいことを確認する
- aud : 対象者(Relying Party)として正しいことを確認する
- exp : 有効期限が切れていないことを確認する
- iat : トークン発行時刻が現在時刻より前か確認する
- ヘッダーのチェック
-
-
[オプション] Relying Partyはユーザ属性などの情報が追加で必要な場合は、userinfo endpointから取得する。
UserInfo Request/Response ⧉ -
[オプション] Relying Partyはaccess tokenの有効期限が切れている場合などは、refresh tokenを利用してトークンの再取得をする。
Refresh Token Request/Response ⧉
Device Codeフロー
Device Authorization CodeフローはIoT製品やスマートテレビ等のブラウザを持たない環境や、コマンドラインからID連携で認証したいケースで利用します。Authorization Codeと違い、リダイレクト処理がなく、ブラウザを持つ端末上から認証することになります。OpenID ProviderによってはDevice Codeに対応していない場合もあります。Discoveryドキュメントのgrant_types_supported
にurn:ietf:params:oauth:grant-type:device_code
が記載されていれば対応しています。
認証フロー
-
ユーザがRelying Partyが用意しているインターフェースからアクセスする。
-
Relying Partyはコマンドラインからdevice authorization endpointへアクセスする。
リクエスト/レスポンス
-
リクエスト
curl --request POST --data "response_type=device_code" --data "client_id=[クライアントID]" --data "client_secret=[クライアントパスワード]" --data "scope=openid" "[device authorization endpoint]"- response_type :
device_code
を指定する - client_id : OpenID Providerに設定したClientIdを指定する
- client_secret : OpenID Providerに設定したClientSecretを指定する(Publicな場合は不要)
- scope :
openid
を指定。IDトークンに特殊な情報を含めたい場合はscopeに指定する
※
client_id
/client_secret
は他の方法(Basic認証など)で指定した場合は不要。 - response_type :
-
レスポンス
{"device_code":"[デバイスコード]","user_code":"[ユーザコード]","verification_uri":"[認証用URL]","verification_uri_complete":"[認証用URL]?user_code=[ユーザコード]","expires_in":600,"interval":5}- device_code : デバイス認証用のコード。手順4のDevice Access Tokenリクエストで利用する
- user_code : 手順5のブラウザ認証時に利用するコード
- verification_uri : 手順5でユーザが認証するためにアクセスするURL
- verification_uri_complete : verification_uriにuser_codeを加えたURL。アクセス時にユーザコードが入力された状態で表示できる
- expires_in : デバイスコードの有効期限
- interval : 手順4のDevice Access Tokenリクエストを受け付ける間隔
-
-
Relying Partyはレスポンスからアクセス先URL(verification_uri)を表示する。
Relying Party次第だが、例えば端末ディスプレイに表示するなどしてユーザに伝える。 -
Relying Partyは認証が完了しているか確認するためにtoken endpointへアクセスする。リクエストには手順2で取得したデバイスコードを利用する。
手順5で認証が完了するまではPendingがエラーとして返るので、認証に成功するか失敗するまでは一定間隔でポーリングする。リクエスト/レスポンス
-
リクエスト
curl --request POST --data "client_id=[クライアントID]" --data "client_secret=[クライアントパスワード]" --data "grant_type=urn:ietf:params:oauth:grant-type:device_code" --data "device_code=[デバイスコード]" "[token endpoint]"- client_id : OpenID Providerに設定したClientIdを指定する
- client_secret : OpenID Providerに設定したClientSecretを指定する(Publicな場合は不要)
- grant_type :
ietf:params:oauth:grant-type:device_code
を指定する - device_code : 手順2で取得したデバイスコードを指定する
※
client_id
/client_secret
は他の方法(Basic認証など)で指定した場合は不要。 -
レスポンス
認証成功時{"access_token":"[アクセストークン]","token_type":"Bearer" ,"refresh_token":"[リフレッシュトークン]","expires_in":300,"id_token":[IDトークン]}Authorization Codeと同様
Access Token Request/Response ⧉認証エラー時
{"error":"エラー種別","error_description":"エラー詳細"}- error
- authorization_pending : ブラウザからの認証待ち
- slow_down : ポーリング間隔が早い
- access_denied : ユーザが認証後の同意で許可しなかった
- expired_token : デバイスコードの有効期限切れ
- error
-
-
ユーザはブラウザを持つ端末から認証画面へアクセスする。
例えばスマホからURLを入力して認証画面から認証する。
手順6以降はAuthorization Codeと同様なため割愛します。
おわりに
Basic認証と比べると複雑で、開発コスト削減に繋がるか疑問に思われたかもしれません。多くの場合はライブラリが存在するので、利用すれば比較的簡単に実装できますが、それでもコストはそれなりに掛かります。ただ、それでもアカウント管理から開放されるメリットは大きいです。
ユーザのパスワードはサービス上に流れないので一見安全に思えますが、トークンが漏洩してしまうと、なりすまされてしまうので、リスクがなくなる訳ではありません。実装するにあたっては、下記のベストプラクティスな方法を是非参考にしてみてください。
OAuth2.0 Security BCP(draft) ⧉
JWT BCP ⧉
参考
- Authorization Codeフロー概要
https://openid.net/specs/openid-connect-core-1_0.html ⧉
https://datatracker.ietf.org/doc/html/rfc6749 ⧉ - Device Codeフロー概要
https://datatracker.ietf.org/doc/html/rfc8628 ⧉ - OAuth2.0 Securityベストプラクティス(draft)
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics ⧉ - JWTベストプラクティス
https://datatracker.ietf.org/doc/html/rfc8725 ⧉