ちょっと話題の記事

Auth0を利用してOAuth 2.0のPKCEを理解する

Auth0を利用してPKCEを一緒に試しつつ、理解していきましょう。本記事ではPKCEを実際に試してみて、理解できることをゴールに書いていきます。
2020.04.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

みなさま。はじめまして、Auth0社の筒井です。ソリューションアーキテクト・テクニカルアカウントマネージャーとして主にAuth0のEnterprise版をご契約頂いたお客様に対して技術支援を行っています。今回はゲストブロガーとして投稿します。

Auth0を利用してPKCEを一緒に試しつつ、理解していきましょう。PKCEはすでに様々なところで詳細に説明されているので、ご存知の方も多いと思います。 本記事ではPKCEを実際に試してみて、理解できることをゴールに書いていきます。

さっそくですが、PKCEって何でしょうか?

PKCEは、Proof Key for Code Exchangeの略で、呼び方はピクシーと呼びます。RFC 7636として公開されています。公開されたのは、2015年の9月で、もう5年弱ほど経っていますが、IETFのセキュリティベストプラクティスのドラフト(現在は14版)にも記載されており、理解しておくべき項目の一つです。PKCEは認可コードの横取り攻撃の対策として定義されています。

認可コードフローおさらい

PKCEは認可コードフローのフロー自体を変えるものではなく、認可コードの横取り攻撃の対策を追加するものです。 まず、認可コードフローを見ていきましょう。認可コードフローは、

  1. 認可サーバーに認可リクエストを投げて、リダイレクトを通して認可コードを受け取る
  2. 受け取った認可コードを認可サーバーに投げて、アクセストークンを取得する

という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_challengecode_challenge_methodを後ほど検証に利用するため保存する
  • (B) 認可コードをクライアントに返す
  • (C) トークンリクエストの際にcode_verifierを送信する。認可サーバーは受け取ったcode_verifierを保存しておいたcode_challenge_methodで変換し、code_challengeと同一になるかチェックする
  • (D) チェックして問題なければ、アクセストークンを返す

実際のシーケンスは以下のようになります。

code_challenge_methodは、plainS256の2種類あります。plainは「変換なし」を意味し、code_verifiercode_challengeが同じ値となります。認可リクエスト自体が悪意あるクライアントに対して漏れてしまうケースについては、plainの場合は悪意あるクライアントの攻撃を防ぐことができません。code_challengeが漏れることがcode_verifierが漏れることと同義になるからです。

もう一つのS256はSHA256ハッシュ化したものをbase64URLエンコード化する手法です。悪意あるクライアントに対して認可リクエストが漏れてしまったとしても、ばれてしまうのはcode_challengeになります。認可コードの有効期限内にcode_challengeからcode_verifierを導き出すことは現実的に困難なため、悪意あるクライアントがアクセストークンを取得することはできないということになります。そのため、基本的にS256を利用することが必須です。 なお、RFC6749 section-4.1.2に認可コードの有効期限は10分間と推奨という記載があります。

code_challengecode_challenge_methodcode_verifierと3つの値が出てきて、「どれがどれだったかな??」と少し混乱するかもしれませんが、code_verifierがPKCEの中心であることを覚えておくと、理解の助けになると思います。

PKCEを試してみよう

では、これから認可コードフロー+PKCEでアクセストークンを取得してみましょう。 ターミナル上でコマンドを実行して試していきます。コマンドの実行には、nodejsとhttpieを利用するので前もってインストールが必要です。 Mac OS Xの場合はHomebrewを使って簡単にインストールできます。

brew install node
brew install httpie

なお、わかりやすさ優先で記事をシンプルにしたいため、具体的なサンプルアプリやAPIは作成せずに、ターミナルからコマンドを打って確認していきます。 アクセストークンの取得までが今回のゴールです。以下のような流れになります。

  1. Auth0のアプリケーションの設定
  2. Auth0のAPIの設定
  3. code_verifier, code_challengeの生成
  4. 認可コードの取得
  5. アクセストークンの取得

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を利用してアプリケーション開発される場合には、是非ご利用ください。

なお、上記のSDKはGithub上でオープンソースとして公開されています。 興味ある方は実装も確認してみてください。学びになると思います。