Auth0を利用してOAuth 2.0のPKCEを理解する
はじめに
みなさま。はじめまして、Auth0社の筒井です。ソリューションアーキテクト・テクニカルアカウントマネージャーとして主にAuth0のEnterprise版をご契約頂いたお客様に対して技術支援を行っています。今回はゲストブロガーとして投稿します。
Auth0を利用してPKCEを一緒に試しつつ、理解していきましょう。PKCEはすでに様々なところで詳細に説明されているので、ご存知の方も多いと思います。 本記事ではPKCEを実際に試してみて、理解できることをゴールに書いていきます。
さっそくですが、PKCEって何でしょうか?
PKCEは、Proof Key for Code Exchangeの略で、呼び方はピクシーと呼びます。RFC 7636として公開されています。公開されたのは、2015年の9月で、もう5年弱ほど経っていますが、IETFのセキュリティベストプラクティスのドラフト(現在は14版)にも記載されており、理解しておくべき項目の一つです。PKCEは認可コードの横取り攻撃の対策として定義されています。
認可コードフローおさらい
PKCEは認可コードフローのフロー自体を変えるものではなく、認可コードの横取り攻撃の対策を追加するものです。 まず、認可コードフローを見ていきましょう。認可コードフローは、
- 認可サーバーに認可リクエストを投げて、リダイレクトを通して認可コードを受け取る
- 受け取った認可コードを認可サーバーに投げて、アクセストークンを取得する
という2段階のフローから成り立っています。
上記のフローでセキュリティに問題があるケースがあります。もし、悪意あるアプリケーションが認可コードを横取りできた場合、悪意あるアプリケーションは奪い取った認可コードを利用してアクセストークンを取得することが可能になるというものです。
PKCEはこの認可コード横取り攻撃を防ぎます。そのため、現在Auth0ではiOS/AndroidのモバイルアプリではPKCEは必須、SPAでは推奨としています。詳しくは、「Which OAuth 2.0 Flow Should I Use?」のページに記載してあります。
PKCEの具体的な内容
PKCEを実装すると、認可コードが悪意あるアプリケーションに奪われたとしても、それだけでは悪意あるアプリケーションはアクセストークンを取得することができません。認可コード送信元を検証する仕組みが入っているからです。
PKCEのキーとなるのがcode_verifier
と呼ばれるものです。アクセストークンリクエスト時にこのcode_verifier
を利用して検証を行います。
認可コードフローの流れをシンプルにした下図を見ていきましょう。(下図は、RFC7636 section-1.1 Protocol Flowを日本語化したもの)
図の中にあるt(code_verifier)
は、tはtransformation(変換)のことで、code_verifier
を変換することを指しています。変換された値は、code_challenge
と呼ばれます。
t_mはtransformation method(変換方法)は、code_challenge_method
と呼ばれます。code_verifier
を変換する方法が指定されています。上図の流れを一つずつ見ていきましょう。
- (A) 認可リクエストの際に
t(code_verifier)
(code_challenge)とt_m
(code_challenge_method)を認可サーバに送信する。認可サーバーはcode_challenge
とcode_challenge_method
を後ほど検証に利用するため保存する - (B) 認可コードをクライアントに返す
- (C) トークンリクエストの際に
code_verifier
を送信する。認可サーバーは受け取ったcode_verifier
を保存しておいたcode_challenge_method
で変換し、code_challenge
と同一になるかチェックする - (D) チェックして問題なければ、アクセストークンを返す
実際のシーケンスは以下のようになります。
code_challenge_method
は、plain
とS256
の2種類あります。plain
は「変換なし」を意味し、code_verifier
とcode_challenge
が同じ値となります。認可リクエスト自体が悪意あるクライアントに対して漏れてしまうケースについては、plain
の場合は悪意あるクライアントの攻撃を防ぐことができません。code_challenge
が漏れることがcode_verifier
が漏れることと同義になるからです。
もう一つのS256
はSHA256ハッシュ化したものをbase64URLエンコード化する手法です。悪意あるクライアントに対して認可リクエストが漏れてしまったとしても、ばれてしまうのはcode_challenge
になります。認可コードの有効期限内にcode_challenge
からcode_verifier
を導き出すことは現実的に困難なため、悪意あるクライアントがアクセストークンを取得することはできないということになります。そのため、基本的にS256
を利用することが必須です。
なお、RFC6749 section-4.1.2に認可コードの有効期限は10分間と推奨という記載があります。
code_challenge
、code_challenge_method
、code_verifier
と3つの値が出てきて、「どれがどれだったかな??」と少し混乱するかもしれませんが、code_verifier
がPKCEの中心であることを覚えておくと、理解の助けになると思います。
PKCEを試してみよう
では、これから認可コードフロー+PKCEでアクセストークンを取得してみましょう。 ターミナル上でコマンドを実行して試していきます。コマンドの実行には、nodejsとhttpieを利用するので前もってインストールが必要です。 Mac OS Xの場合はHomebrewを使って簡単にインストールできます。
brew install node brew install httpie
なお、わかりやすさ優先で記事をシンプルにしたいため、具体的なサンプルアプリやAPIは作成せずに、ターミナルからコマンドを打って確認していきます。 アクセストークンの取得までが今回のゴールです。以下のような流れになります。
- Auth0のアプリケーションの設定
- Auth0のAPIの設定
- code_verifier, code_challengeの生成
- 認可コードの取得
- アクセストークンの取得
1. Auth0のアプリケーションの作成と設定
Auth0のダッシュボードにある左側のメニューより、「Applications」を選択します。 次に「CREATE APPLICATION」を押下して、アプリケーションの設定作成を最初に行います。
次に「Name」にPKCE Demoと入力、「Choose an application type」はNativeを選択し、「CREATE」を押下します。
これでアプリケーションの設定が作成されました。次に、「Settings」を選択します。
「Allowed Callback URLs」にhttps://localhost/callback
と入力します。
作成したアプリケーションのclient_idは後ほど利用します。
2. Auth0のAPIの設定
Auth0のダッシュボードにある左側のメニューより、「API」を選択します。 次に、「CREATE API」を押下して、APIの設定作成を最初に行います。
次に、「Name」にPKCE Demo APIと入力し、「Identifier」はhttps://pkce-demo/
を入力します。そして「CREATE」を押下します。
これでAPIの設定が作成されました。
3. code_verifier, code_challengeの生成
S256のcode_challengeの生成方法はこちらのAuth0のページにサンプルコードがあり、本記事ではJavascriptのコードをそのまま用います。
S256はSHA256でハッシュ化したものをbase64URLエンコードするアルゴリズムです。以下のコードをgen_code_challenge.jsというファイル名で保存します。
var crypto = require('crypto'); function base64URLEncode(str) { return str.toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } var verifier = base64URLEncode(crypto.randomBytes(32)); function sha256(buffer) { return crypto.createHash('sha256').update(buffer).digest(); } var challenge = base64URLEncode(sha256(verifier)); console.log("verifier: " + verifier); console.log("challenge: " + challenge);
以下のコマンドで実行できます。
node gen_code_challenge.js
実際の出力例です。今回はこれをそのまま用います。
verifier: iBW92Zt20bo744UMDJc5UlMgvQPKBqMZmQc3lPajAZY challenge: qfXJcChpQ_tVurEUQF3saqGKR33RZrj0F5nxwdHaEFA
4. 認可コードの取得
code_verifierとcode_challengeが生成できました。 GET /authorizeのエンドポイントにアクセスし、認可コードを取得しましょう。
以下のコマンドを実行し、認可リクエストを投げます。
- テナント名は、ご自身のものに置き換えてください
- client_idは「1. Auth0のアプリケーションの作成と設定」で作成したclient_idに置き換えてください
テナント名はダッシュボードの右上に表示されています。
http https://{自分のテナント名}.auth0.com/authorize \ response_type==code \ client_id=={自分のclient_id} \ audience==https://pkce-demo/ \ state==43b43b94-756d-11ea-bc55-0242ac130003 \ redirect_uri==https://localhost/callback \ code_challenge_method==S256 \ code_challenge==qfXJcChpQ_tVurEUQF3saqGKR33RZrj0F5nxwdHaEFA
実際の出力は以下のようになり、リダイレクトされます。
HTTP/1.1 302 Found 〜〜中略〜〜 Location: /login?state=g6Fo2SBlbUEwd29HTVUtWEdKRS1rVExMYzQ3MlRFYjhxTURfVKN0aWTZIFBnS3NUSFRvcXpHVjZfaFZ5NDdndmZZY08zRllpMXBOo2NpZNkgOUttVzl1bU1RN2lHbDZrZXVVVVpINFdONTdZdG5hWlc&client=9KmW9umMQ7iGl6keuUUZH4WN57YtnaZW&protocol=oauth2&response_type=code&audience=https%3A%2F%2Fpkce-demo%2F&redirect_uri=https%3A%2F%2Flocalhost%2Fcallback&code_challenge_method=S256&code_challenge=qfXJcChpQ_tVurEUQF3saqGKR33RZrj0F5nxwdHaEFA 〜〜後略〜〜
リダイレクト先をそのままブラウザで開きます。
https://{自分のテナント名}.auth0.com/login?state=g6Fo2SBlbUEwd29HTVUtWEdKRS1rVExMYzQ3MlRFYjhxTURfVKN0aWTZIFBnS3NUSFRvcXpHVjZfaFZ5NDdndmZZY08zRllpMXBOo2NpZNkgOUttVzl1bU1RN2lHbDZrZXVVVVpINFdONTdZdG5hWlc&client=9KmW9umMQ7iGl6keuUUZH4WN57YtnaZW&protocol=oauth2&response_type=code&audience=https%3A%2F%2Fpkce-demo%2F&redirect_uri=https%3A%2F%2Flocalhost%2Fcallback&code_challenge_method=S256&code_challenge=qfXJcChpQ_tVurEUQF3saqGKR33RZrj0F5nxwdHaEFA
ブラウザで開くと認証表示されます。今回は「Sign in with Google」を押下し、手元のGoogleアカウントで認証を進めました。(「Sign Up」からユーザー登録を進める形でも問題ありません。)認証後、コンセント画面が表示されるので、アクセスを許可します。
なお、コンセント画面をスキップすることも可能です。詳細はこちらのページにあるAllow Skipping User Consentを参照してください。
認証後、https://localhost/callback
にリダイレクトされます。もちろん、サーバーを立てていないので、ブラウザ上ではエラーになります。そこは気にせず、必要な値はクエリパラメータに含まれているcode
なので、それをコピペします。このcodeが認可コードとなります。今回はbB4EFKpYGLgefIw-
という値になっていました。
5. アクセストークンの取得
認可コードが取得できたので、これでアクセストークンを取得する準備ができました。では、POST /oauth/tokenのエンドポイントにアクセスして、アクセストークンを取得しましょう。
以下のコマンドを実行し、トークンリクエストを投げます。
http --form POST https://{自分のテナント名}.auth0.com/oauth/token \ grant_type=authorization_code \ client_id={自分のclient_id} \ code_verifier=iBW92Zt20bo744UMDJc5UlMgvQPKBqMZmQc3lPajAZY \ code=bB4EFKpYGLgefIw- \ redirect_uri=https://localhost/callback
実際の出力は以下のようになりました。アクセストークンが含まれていることが確認できましたね!
HTTP/1.1 200 OK 〜〜中略〜〜 { "access_token": "eyJhbGciO 〜〜中略〜〜 igemMqHUsRxo3w", "expires_in": 86400, "id_token": "eyJhbGciOiUzI1 〜〜中略〜〜 fLWbECXf28Q", "scope": "openid profile email", "token_type": "Bearer" }
(補足:scopeに"openid profile email"が含まれていますが、scope未指定の場合は、Auth0はこれらのscopeを自動的に付与します。詳しくはこちらのAuth0のページに記載があります)
まとめ
実際に試してみることでPKCEの理解深まりましたでしょうか? みなさまの理解に少しでも役にたてたらとても嬉しいです。
なお、Auth0が用意しているSDKは標準でPKCEの対応が含まれているため、実は開発者はPKCEを意識しなくても対応できるようになっています。Auth0を利用してアプリケーション開発される場合には、是非ご利用ください。
- Auth0 SDK for Single Page Applications
- Android Java toolkit for Auth0 API
- Swift toolkit for Auth0 API
なお、上記のSDKはGithub上でオープンソースとして公開されています。 興味ある方は実装も確認してみてください。学びになると思います。