はじめに
私は、手を動かしながらOAuth2/OIDC認可コードフローを学びたいと思い、この記事を書きました。本記事ではAmazon Cognitoを使ってOAuth2/OIDCの認可フローを学ぶハンズオンです。使用するのはCurlだけで、アプリケーションコードの準備は不要です。
目次
登場人物は4人
1. クライアント(フロントエンド)
Webアプリや、モバイルアプリなど、ユーザーの目に触れる画面を指します。今回は画面がないので、curlコマンドなどで代用します。
2. 認可エンドポイント(API)
ユーザーの入力したIDやPasswordを検証し、認証が成功した場合に認可コードを発行します。この時点ではログインに成功していません。
https://[Cognitoのドメイン名].auth.ap-northeast-1.amazoncognito.com/oauth2/authorize
3. トークンエンドポイント(API)
認可エンドポイントから発行されたコードを検証し、コードが妥当であると判断した場合に、アクセストークンなどを発行します。この時点でログイン成功となります。
https://[Cognitoのドメイン名].auth.ap-northeast-1.amazoncognito.com/oauth2/token
4. リソースエンドポイント(Webアプリなど)
Railsなどのアプリケーションサーバーだと考えてください。ログイン中のユーザーのみが閲覧できるデータを返却します。
https://[Cognitoのドメイン名].auth.ap-northeast-1.amazoncognito.com/oauth2/userInfo
認可コードフローの概要
本記事で説明する認可の流れは下記のとおりです。
ログインのフロー
- クライアントがID/Passwordなどのログイン情報を使って認可エンドポイントにリクエストを送信します
- 認可エンドポイントはログイン情報が正しい場合、クライアントが指定するURLにリダイレクト(注意)します
- クライアントは取得した認可コードを使って、トークンエンドポイントにアクセストークンを要求します
- トークンエンドポイントは認可コードが正しい場合、クライアントにアクセストークンを返却します
注意) 2.でリダイレクトしているため、対策しないと中間者攻撃のリスクがあります。認可コードフローの脆弱性の対策を本記事の終盤に記載しています。
ログイン後のフロー
- クライアントは、リクエスト時に毎回アクセストークンを含めます。アクセストークンはAuthorization ヘッダーに含めます
- リソースサーバーはクライアントから渡されたアクセストークンを検証し、正しいユーザーの場合、要求されたリソース(情報)を返却します
詳細な手順
ここからは、Cognitoとcurlコマンドを使ったログインの具体的な手順をご説明いたします。
STEP0: AWSマネジメントコンソールでCognito環境をサクッとつくる
動作確認のためのテスト用のCognito環境をつくります。詳しい方法はこちらの記事で紹介しています。
STEP1: 認可エンドポイントから認可コードを取得する
AWSのマネジメントコンソールの「アプリケーションクライアント」の欄で、「ホストされたUIを表示」ボタンを右クリックし、URLをコピーします。URLのクエリパラメータの一部は、STEP2で使いまわすことができるため、URLはコピーしておいてください。
コピーしたURLをブラウザに貼り付けて実行します。
# URLの例
https://cog210.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize
?client_id=4q73grdculbv1dgvfdm1hvm1k0
&response_type=code
&scope=email+openid
&redirect_uri=http%3A%2F%2Flocalhost%3A8000
ブラウザでHosted UIのログイン画面に遷移し、ここでID/Passを入力します。
Sign in
に成功すると、ブラウザのURLに code=XXXX
といったクエリがあるので、codeの値をメモします。
http://localhost:8000/?code=8b67009c-21a3-4fb4-bbfa-1aef69e91865
^^^^^
※ Hosted UIを使わずcurlコマンドだけで完結する方法もあるようです。
STEP2: 認可コードを使ってアクセストークンを取得する
認可コードを使ってログインの完了まで進みます。curlのフォーマットは下記のようになります。設定時に考慮が必要な箇所(1)~(4)に関して補足します。
- リクエストのフォーマット
curl -X POST \
--url https:/[Cognitoのドメイン名].auth.ap-northeast-1.amazoncognito.com/oauth2/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic [クライアントID: シークレットをBase64エンコードした値]' \ ... (1)
--data-urlencode 'grant_type=authorization_code' \ ... (2)
--data-urlencode 'code=[STEP1で取得したコードの値]' \ ... (2)
--data-urlencode 'client_id=[クライアントID]' \ ... (4)
--data-urlencode 'redirect_uri=[リダイレクト先のURL]' ... (5)
(1) Authorizationヘッダーで使用するためのBase64エンコードした値を作る
こちらは下記のステップで生成します
-
マネジメントコンソールでクライアントID,クライアントシークレットをコピーし、下記のような平文を作ります
4q73grdculbv1dgvfdm1hvm1k0:1br05lqh0g3lqg7igno2huqtaudo2om8ncenmct3se5n1bke3ocr
- 上記の平文をBase64エンコードします。(下記のようなエンコードサイトを使うと楽です)
-
エンコード後に下記のような文字列になるため、こちらをコピーしてCurlコマンドの(1)に貼り付けます
NHE3M2dyZGN1bGJ2MWRndmZkbTFodm0xazA6MWJyMDVscWgwZzNscWc3aWdubzJodXF0YXVkbzJvbThuY2VubWN0M3NlNW4xYmtlM29jcg==
(2) 認可エンドポイントのコードを使う
- grant_type=authorization_code
- code=[STEP1で取得した値]
code=
は一度使うと二回目以降のリクエストではエラーになります。STEP2でエラーが発生した場合は、STEP1からやり直してください
(3) クライアントID
AWSマネジメントコンソールのクライアントIDをコピーして貼り付けます。
(4) リダイレクトURL
AWSマネジメントコンソールの許可されているコールバックURLをコピーして貼り付けます。
※ 注意) URLは厳密に指定してください。http://localhost:8000/
のように/
の有無でもエラーになります
ここまでの入力例
(1)~(4)の補足を踏まえた入力例とレスポンス例を示します。JSONが得られてたらログイン成功となります。
- 入力例
curl -X POST \
--url https://cog210.auth.ap-northeast-1.amazoncognito.com/oauth2/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic NHE3M2dyZGN1bGJ2MWRndmZkbTFodm0xazA6MWJyMDVscWgwZzNscWc3aWdubzJodXF0YXVkbzJvbThuY2VubWN0M3NlNW4xYmtlM29jcg==' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=c536edd5-1e38-470a-8886-9e9b120d9bb0' \
--data-urlencode 'client_id=1kv1qaigl5k64vhi667cqj50km' \
--data-urlencode 'redirect_uri=http://localhost:8000'
- レスポンス例
{
"id_token":"eyJraWQiO...",
"access_token":"eyJraWQiOiJ...", // ← この値をコピーして、STEP3で使用します。
"refresh_token":"eyJjdHkiOiJKV1...",
"expires_in":3600,
"token_type":"Bearer"
}
※ なお、リソースサーバーの認可にaccess_tokenではなくid_tokenを利用すると、OpenIdConnect準拠となります。本記事ではaccss_tokenを使用しているため、厳密にはOAuth2認可フローとなります。
STEP3: リソースエンドポイントでアクセストークンを復元する
リソースエンドポイントは自前のWebアプリケーションになることが一般的です。今回はアプリケーションサーバーの代わりに下記のエンドポイントを叩きます。
- リクエストのフォーマット
curl -H "Authorization: Bearer [アクセストークン]" https://[Cognitoのドメイン名].auth.ap-northeast-1.amazoncognito.com/oauth2/userInfo
アクセストークンには、STEP2で得られたaccess_token
の値を入力します。
- 入力例
curl -H "Authorization: Bearer eyJraWQiOi..." https://cog210.auth.ap-northeast-1.amazoncognito.com/oauth2/userInfo
- レスポンスの例
{
"sub":"b7e49ad8-80f1-7010-21cb-ad6d2b42ef65",
"email_verified":"true",
"email":"test-user@example.com",
"username":"test-user"
}
上記のように "sub"や"email"が表示されれば成功です!
セキュリティを向上させるために
ここまで説明した認可フローでは、下記の理由によりアクセストークンを乗っ取られるリスクがあります。
- 認可コード付与時にリダイレクトされる(CSRF)
- 認可リクエストを再送して不正に認可コードを得る(再生攻撃)
- 認可エンドポイントとトークンエンドポイントが異なるため、本当にログインユーザー自身が認可コードを発行したのか分からない (認可コード横取り)
これらの対策としてstate, nonce, code_challengeの3つのパラメータを付与、検証することが推奨されています。
パラメータ | 誰が | 何を検証する? | 対策する攻撃 |
---|---|---|---|
state | クライアント | 認可コードは自分自身が要求したものか? | CSRF |
nonce | クライアント | アクセストークンは自分自身が要求したものか? | 再生攻撃 |
code_challenge | トークンエンドポイント | 認可コードは正規のクライアントが要求したものか? | 認可コード横取り |
図にすると下記のようになります。
②③④の各ステップにおいて、①の認証開始時のクライアントが別の攻撃者にすり替わっていないことをチェックするために、3つのパラメータがあると考えると理解しやすいです。
- state
- ②の要求が、①と同一のクライアントによる要求であるか検証する
- code_challenge
- ③の要求が、①と同一のクライアントによる要求であるか検証する
- nonce
- ④の要求が、①と同一のクライアントによる要求であるか検証する
フローを詳細に知りたい方はこちらの記事を参考にしてください。
Cognitoを使って3つのパラメータを触ってみる。
stateパラメータの使用
STEP1のURLの末尾に&state=
を追加します。この状態でログインすると、ログイン後のリダイレクト先のURLにstate文字列が追加されます。リダイレクト後のクライアントはURLのstate文字列とリダイレクト前にsession_storageに保存したstate文字列を比較して、一致しているかを検証します。一致しない場合、クライアントはログインフローを中止します。なお、state文字列はクライアントが任意に決めて良いですが、推測性、再現性が低い文字列にしてください。
- STEP1: 認可リクエストURL
https://cog210.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize?
... &state=aaabbbcccddd ← stateクエリを追加
- STEP1: リダイレクト先のURL
code_challengeの使用
code_verifier, code_challenge_method, code_challenge の3つのパラメータがあります。
これらの関係は、入力、関数、出力の関係になっています。
code_verifier(入力) -> code_challenge_method(関数) -> code_challenge(出力)
- STEP1の認可リクエストURLの末尾に
&code_challenge_method=S256
,&code_challenge=(出力)
を追加します - STEP2のトークンリクエストURLの末尾に
&code_verifier=(入力)
を追加します
これらを用いて、トークンエンドポイントは①のcode_challenge(出力)が④で渡されたcode_verifier(入力)により生成された値と一致するか検証します。一致しない場合はトークンの払い出しを拒否します。
なお、クライアント側でもcode_verifier(入力)からcode_challgenge(出力)を生成する必要があります。詳しく知りたい方は、こちらの記事をご覧ください
nonceパラメータの使用
STEP1のURLの末尾に&nonce=
を追加します。この状態でログインフローのSTEP3まで完了させます。
トークンエンドポイントのレスポンスのうち、id_tokenの文字列eyJraWQi000000...
をコピーします。コピーした文字列を下記のjwt.ioに貼り付けて復号します。
(※ jwtって誰でも復号できる文字列なんです。鍵も不要です。)
{
"id_token":"eyJraWQi000000...", // ← この値をコピーして下記のサイトに貼り付けます。
"access_token":"eyJraWQiOiJ...",
"refresh_token":"eyJjdHkiOiJKV1...",
"expires_in":3600,
"token_type":"Bearer"
}
- jwt復号後の文字列
{
"at_hash": "xxxxxxjXHl5Isl3Wpw",
"sub": "xxxxxxxxx21cb-ad6d2b42ef65",
"email_verified": true,
"iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_mfpfQrTFU",
"cognito:username": "test-user",
"nonce": "nonce0nonce", // <--STEP1でクエリパラメータに指定した文字列
}
クライアントはjwtを復号した後のnonce文字列と、STEP1でsession_storageに保存したnonce文字列を比較して、一致しているかを検証します。一致しない場合、クライアントは受領したアクセストークンを破棄します。
なお、nonce文字列はクライアントが任意に決めて良いですが、推測性、再現性が低い文字列にしてください。
まとめ
本記事では、手を動かしながらOIDC認可フローを学ぶためのステップを記述しました。またセキュリティ向上のための3つのパラメータもご紹介しました。本記事がOIDC認可フローについての理解の手助けになれば幸いです。
参考記事