エッジで OAuth の認証プロセスをよりシンプルに

認証とは複雑で危険なプロセスでもありますが、必要不可欠なものです。アプリケーションのほとんどで認証手続きが必要なため、ほぼすべてのエンドユーザーリクエストの前提条件になっています。Web アプリケーションの認証プロセスは、エンドユーザーの近くで行われると同時に、システムの他の部分からは隔離されていることが理想的です。またセキュリティ担当者によって実装・管理され、統合しやすいことが重要です。

Fastly の Compute@Edge に実装されている高速かつ安全で自律的な分散型の OAuth の認証プロセスは、まさに理想的です。今回は、その仕組みについてご説明します。

simplified flow diagram

まずは認証と認可の違いですが、認証 (authentication) とは自分が誰であるかを証明することであり、認可 (authorization) は誰に何の権限を与えるかという判断です。では、まずユーザー ID を確立する方法に注目しましょう。ユーザー ID を確立するには、OAuth 2.0 や OpenID Connect を使うのが一般的です。undefinedundefined

エッジに届いたリクエストは、特定のユーザーに関連付けられるものと、匿名または無効なものとを区別する必要があります。匿名のリクエストは、OAuth 認可コードのフローを辿って処理され、無効なリクエストは拒否されます。その結果、認証されたユーザーからのリクエストのみが、アプリケーションのオリジンサーバーに送信されます。

以下は、今回構築するプロセスの詳細なフローです。

flow diagram

では、順番に見ていきましょう。

  1. ユーザーは、保護されているリソースへのリクエストを行いますが、セッション Cookie を持っていません。

  2. そこで Fastly はエッジで以下を生成します。

    1. ユーザーが行おうとしていたアクション (/articles/kittens の読み込み) をエンコードする、一意の推測不可能な state パラメーター。

    2. code verifier と呼ばれる、暗号化されたランダムな文字列。

    3. code verifier から導出された code challenge 値。 

    4. state 値nonce 値 (リプレイ攻撃を軽減するために使用される一意の値) をエンコードする、シークレットを用いて認証された期限付きトークン。

    (a) と (b) は、後で検証できるように Cookie に保存します。(c) と (d) は、認可サーバーへの次のリクエストに含まれます。

  3. Fastly は認可 URL をビルドし、ID プロバイダー (IdP) が使用する認可サーバーにユーザーをリダイレクトします。

  4. ユーザーは IdP でのログイン手続きを直接完了させます。ログイン処理の結果を含むコールバック URL へのリクエストを受信するまで、Fastly はこれには関与しません。IdP は、ログイン後のコールバックに認可コードstate 値 (先ほど作成した期限付きトークンと一致するもの) を含めます。 

  5. エッジサービスは、IdP が返した state トークンを認証し、保存している state 値がそのサブジェクトクレームと一致することを確認します。

  6. Fastly は IdP に直接接続し、認可コード (1回のみ使用可) と code verifier を以下のセキュリティトークンと交換します。

    1. アクセストークン : ユーザーの代わりに特定の操作を実行する権限を表すキー

    2. ID トークン : ユーザーのプロファイル情報が含まれるトークン

  7. Fastly は、Cookie に保存されているセキュリティトークンと共に、エンドユーザーを元のリクエスト URL (/articles/kittens) へリダイレクトします。

  8. ユーザーがリダイレクトされたリクエスト、またはセキュリティトークンを伴うリクエストを行うと、Fastly が両トークンの整合性、有効性、およびクレームを検証します。トークンに問題ない場合は、リクエストをオリジンにプロキシします。

さて、このプロセスを構築するには、まず IdP が必要です。 

ID プロバイダー (IdP)取得

独自の ID サービスを利用することも可能ですが、OAuth 2.0 OpenID Connect (OIDC)準拠したプロバイダーであれば何でも構いません。以下の手順で IdP をセットアップします。

  1. まず、アプリケーションを IdP に登録します。Client_id と認可サーバーundefinedのアドレスを忘れないようにしてください。

  2. サーバーに関連付けられた OpenID Connect Discovery メタデータのローカルコピーを保存します。これは、認可サーバーのドメインの /.well-known/openid-configuration にあります。例えば、こちらが Google提供しているものです。

  3. JSON Web Key Set (JWKS) メタデータのローカルコピーを保存します。これは、先ほどダウンロードした Discovery メタデータドキュメント内の jwks_uri プロパティ下にあります。

次に、IdP と通信する Compute@Edge サービスを Fastly 上で作成します。

Compute@Edge サービスの作成

このプロジェクトに必要なものはすべて GitHubリポジトリにまとめましたが、それ以外にも Compute@Edge サービスの利用が可能な Fastly アカウントが必要になります。まだインストールしていない場合は、Compute@Edge ウェルカムガイドに従い、Fastly CLI と Rust ツールチェーンをローカルマシンにインストールしてください。準備が整ったら、早速コーディングを始めましょう。

  1. まず、リポジトリをクローンします。
    git clone https://github.com/fastly/compute-rust-auth

  2. undefinedREADME に記載されている指示に従います。

おめでとうございます!これで Compute@Edge サービスをデプロイできました。統合を完了するには、ご利用の ID プロバイダーが、ログイン後にユーザーを Compute@Edge サービスに誘導できるか確認してください。

IdPリンクする

https://{some-funky-words}.edgecompute.app/callback を、IdP のアプリケーション設定で、許可されたコールバック URL リストに追加します。これにより、認可サーバーはユーザーを Fastly がホストするWebサイトに送り返すことができます。

では、早速ブラウザでアプリケーションを開いてみましょう。

Auth0 screenshot

このサンプルアプリケーションでは、あらゆるパスへのアクセスに認証が必要になります。すでに認証されている場合、Fastly が通常通りリクエストをオリジンにプロキシします。より高度なことも実行可能ですが、それについては後日のブログ記事で詳しくご説明します。では次に、この基本的な統合の仕組みについて見てみましょう。

Compute@Edge との統合

Fastly Rust SDK用いて Rust で書かれた Compute@Edge プログラムの main 関数は、リクエストのエントリーポイントとして機能します。通常、main 関数は Request 構造体を受け取り、Response 構造体を返します。

まず main 関数では、ユーザーが IdP から戻り、セッションを初期化する準備ができていることを示すコールバックパスがあるかどうか確認します。このパスは常に阻止される必要があり、バックエンドにプロキシされることはありません。

if req.get_url_str().starts_with(&redirect_uri) {
// ... snip: Validate state and code_verifier ...
// ... snip: Check authorization code with identity provider ...
// ... snip: Identity provider returns access & ID tokens ...
Ok(responses::temporary_redirect(
        original_req,
        cookies::session("access_token", &auth.access_token),
        cookies::session("id_token", &auth.id_token),
        cookies::expired("code_verifier"),
        cookies::expired("state"),
    ))
}

IdP からのレスポンスは、認可コードと PKCE を使い (先ほどの code verifier と challenge の出番です)、access_tokenid_tokenID を返します。

  • このアクセストークンはベアラートークンです。つまり、持参人がユーザーに代わって認可済みリソースにアクセスする権限があることを意味します。 

  • ID トークンは、ユーザーの ID 情報をエンコードした JSON Web Token (JWT) です。

将来のリクエスト認証に使用するため、これらを Cookie に保存し、認証プロセスの途中で使用した Cookie は破棄します (先ほどの state パラメーターと code verifier)。そして、ユーザーを最初にリクエストした URL へとリダイレクトします。

ユーザーがコールバックパス上にいないことが確認できた場合は、リクエストに付随する Cookie をチェックして、アクティブセッションがあるかどうか判断します (アクセストークンと ID トークンによって定義されます)。

let cookie = cookies::parse(req.get_header_str("cookie").unwrap_or(""));
if let (Some(access_token), Some(id_token)) = (cookie.get("access_token"), cookie.get("id_token")) {
    // snip: ... validation logic ...
    req.set_header("access-token", access_token);
    req.set_header("id-token", id_token);
    return Ok(req.send("backend")?);
}

有効なセッションが存在する場合は、req.send() がリクエストをアップストリームのオリジンに送信し、ダウンストリームのクライアントに送信する Response を返します。今回のサンプルサービスでは、アクセストークンと ID トークンをオリジンへのカスタム HTTP ヘッダーに設定しています。オリジンで ID をどのように使用するかによって他の方法もありますが、それについてはまた後ほどご説明します。

最後に、ユーザーが認証されておらず、ログイン手続きの最中でもない場合は、ユーザーを IdP に送信してサインインプロセスを開始します。

let authorize_req =
Request::get(settings.openid_configuration.authorization_endpoint)
  .with_query(&AuthCodePayload {
     client_id: &settings.config.client_id,
      code_challenge: &pkce.code_challenge,
      code_challenge_method: &settings.config.code_challenge_method,
      redirect_uri: &redirect_uri,
      response_type: "code",
      scope: &settings.config.scope,
      state: &state_and_nonce,
      nonce: &nonce,
  })
  .unwrap();
Ok(responses::temporary_redirect(
    authorize_req.get_url_str(),
    cookies::expired("access_token"),
    cookies::expired("id_token"),
    cookies::session("code_verifier", &pkce.code_verifier),
    cookies::session("state", &state),
))

ここでは Request::get を使ってリクエストを作成していますが、それを送信する代わりに、シリアル化された URL を抽出し、ユーザーをそちらにリダイレクトしています。また、後から検証できるように state パラメーターと code verifier を保存します。以前のコードでは承認されなかったことが明らかなため、アクセストークンと ID トークンは削除します。

以上が、認証ソリューションと受信リクエストの間で発生する3つのシナリオです。

最後に

認証を行った上で、ID データを使用して認可決定を行うという基本的なプロセスは、Web アプリケーションやネイティブアプリケーションの大半に適用されています。これをエッジで行うことで、開発者とエンドユーザーの両方に非常に大きなメリットをもたらします。

  • セキュリティの向上 : 認証プロセスは、すべてのバックエンドアプリケーションに適用される単一のユニバーサルな実装であるため、セキュリティが向上します。

  • メンテナンス性の向上 : コンポーネントが切り離されているため、メンテナンスが行いやすくなります。

  • プライバシーに配慮 : ユーザーデータを必要な場所に保管し、オリジンアプリケーションとのデータ共有を最小限に抑えることができるため、プライバシーへの配慮が高まります。

  • パフォーマンスの向上 : アクセス認証が必要なコンテンツをエッジでキャッシュすることができ、認証プロセスに関連するリクエストにエッジで直接応答できるため、パフォーマンスが向上します。

これはエッジコンピューティングの数多くあるユースケースの1つであり、コアインフラからステートを切り離す良い例でもあります。分散型システムにおけるスケーラビリティの課題は、state をより短期間で非同期にすることに関係しています。先日 Andrew が投稿したエッジネイティブのアプリケーションについてのブログ記事も、併せてご覧ください。

皆さんがどのように Compute@Edge を活用していくのか、とても楽しみにしています。

Dora Militaru
Developer Relations Engineer
Andrew Betts
Head of Developer Relations
投稿日

この記事は2分で読めます

興味がおありですか?
エキスパートへのお問い合わせ
この投稿を共有する
Dora Militaru
Developer Relations Engineer

Fastly で Developer Relations Engineer を務める Dora は、グローバル規模のニュースサイトの開発者として経験を積みました。また Dora はデータ保護チームやサイト信頼性エンジニアリングチームを指導した経験もあり、常に思いやりのある開発を心がけています。ロンドンのキッチンの片隅から、より信頼性の高い、よりスピーディで優れたインターネットの構築を支えています。

Andrew Betts
Head of Developer Relations

Andrew Betts は、Fastly の Head of Developer Relations として、世界各地の開発者と協力し、Web の高速化やセキュリティ、信頼性、使いやすさの向上に努めています。Fastly 入社前は、Web コンサルティング会社 (後に Financial Times により買収) を設立し、Financial Times の先駆的な HTML5 ベースの Web アプリケーションの開発を統括したほか、同紙のラボ部門の設立にも携わりました。また、W3C Technical Architecture Group (World Wide Web の開発を導く9名で構成される委員会) の選出メンバーでもあります。