エッジでの認証処理をよりシンプルにする OAuth
認証とは複雑で危険なプロセスでもありますが、必要不可欠なものです。 アプリのほとんどにおいて認証処理が必要としているため、ほぼ全てのエンドユーザーリクエストの前提条件になっています。Web アプリの認証は、エンドユーザーの近くで行われると同時に、システムの他の部分からは隔離されていることが理想的です。またセキュリティー担当者によって実装・維持され、統合が簡単であることが重要です。
Fastly の Compute@Edge に実装されている高速、安全、自律的、そして分散されている OAuth サービスは、まさに理想的な認証です。今回は、その手順についてご説明します。
まずは認証と認可の違いですが、認証 (authentification) とは相手が誰であるかを証明することであり、認可 (authorization) は誰に何の権限を与えるかという判断です。では、まずユーザー ID を確立する方法に注目しましょう。ユーザー ID を確立するには、OAuth 2.0 や OpenID Connect を使うのが一般的です。
エッジに届いたリクエストは、特定のユーザーに関連づけられるものと、匿名または無効なものとを区別する必要があります。匿名のリクエストは、OAuth 認可コードのフローを辿って処理され、無効なリクエストは拒否されます。その結果、認証されたユーザーからのリクエストのみが、アプリケーションオリジンサーバーに進むことができます。
これが手順を詳しく表すフローです。
では、順番に見ていきましょう。
ユーザーは、保護されているリソースへのリクエストを行いますが、セッション Cookie を持っていません。
そこで Fastly はエッジで以下を生成します。
ユーザーが行おうとしていたアクション
/articles/kittens
の読み込み) をエンコードする、固有で推測不可能な state パラメーター。Code verifier と呼ばれる、暗号化されたランダムな文字列。
Code verifier から導出された code challenge 値。
State 値 と nonce 値 (リプレイ攻撃を軽減するために使用される固有の値) をエンコードする、シークレットを用いて認証された期限付きトークン。
(a) と (b) は、後で検証できるようにCookie に保存します。(c) と (d) は、認可サーバーへの次のリクエストに含まれます。
Fastly は認可 URL を構築し、ID プロバイダーが運行する認可サーバーにユーザーをリダイレクトします。
ユーザーは ID プロバイダー (IdP) でのログイン手続きを直接完了させます。 ログイン処理の結果を含むコールバック URL へのリクエストを受信するまで、Fastly はこれには関与しません。IdP は、ログイン後のコールバックに認可コードと state 値 (先ほど作成した期限付きトークンと一致するもの) を含め返します。
エッジサービスは、IdP からの state トークンを認証し、保存している state 値がそのサブジェクトクレームと一致することを確認します。
Fastly は ID プロバイダーに直接接続し、認可コード (1回のみ使用可) と code verifier を以下のセキュリティトークンと交換します。
アクセストークン : ユーザーの代わりに特定の操作を実行する権限を表すキー
ID トークン : ユーザーのプロファイル情報が含まれるトークン
Fastlyは、Cookie に保存されているセキュリティトークンと共に、エンドユーザーを元のリクエスト URL
/articles/kittens
へリダイレクトします。ユーザーがリダイレクトされたリクエスト、またはセキュリティトークンを伴うリクエストを行うと、Fastly が双方のトークンの整合性、有効性、およびクレームを検証します。トークンに問題ない場合は、リクエストをオリジンにプロキシします。
さて、この手順を構築するには、まずは ID プロバイダーが必要です。
ID プロバイダー (IdP) の取得
独自の ID サービスを利用することも可能ですが、OAuth 2.0 と OpenID Connect (OIDC) に準拠したプロバイダーであれば何でも構いません。以下の手順で IdP を設定していきます。
まず、アプリケーションを ID プロバイダーに登録します。
Client_id
と認可サーバーのアドレスを忘れないようにしてください。サーバーに関連付けられた OpenID Connect Discovery メタデータのローカルコピーを保存します。これは、認可サーバーのドメインの
/.well-known/openid-configuration
にあります。例えば、こちらが Google が提供しているものです。JSON Web Key Set (JWKS) メタデータのローカルコピーを保存します。これは、先ほどダウンロードした Discovery メタデータドキュメント内の
jwks_uri
プロパティ下にあります。
次に、IdP と通信する Compute@Edge サービスを Fastly 上で作成します。
Compute@Edge サービスの作成
このプロジェクトに必要なものはすべて GitHub のリポジトリにまとめましたが、それ以外にも Compute@Edge サービスを利用可能な Fastly アカウントが必要になります。まだインストールを行っていない場合は、Compute@Edge ウェルカムガイドに従い、Fastly CLI と Rust ツールチェーンをローカルマシンにインストールしてください。準備が整ったら、早速コーディングを始めましょう。
まず、リポジトリをクローンしてください :
git clone https://github.com/fastly/compute-rust-auth
README に記載されている指示に従ってください
おめでとうございます!これで Compute@Edge サービスのデプロイが完了しました。統合を完了するには、ご利用の ID プロバイダーが、ログイン後にユーザーを Compute@Edge サービスに誘導するように設定されているか確認してください。
ID プロバイダーをリンクする
https://{some-funky-words}.Edgecompute.app/callback
を、ID プロバイダーのアプリ設定内の許可されたコールバック URL リストに追加します。これにより、認可サーバーはユーザーを Fastly がホストする Web サイトに送り返すことができます。
では、早速ブラウザでアプリを開いてみましょう。

このサンプルアプリでは、パスにアクセスするために認証が必要になります。すでに認証されている場合、Fastly が通常どおりリクエストをオリジンにプロキシします。より野心的なことも実行可能ですが、それについては後日のブログ記事で詳しく説明したいと思います。今回の記事では、この基本的な統合の機能をご紹介します。
Compute@Edge との統合
Fastly Rust SDK を用いて Rust で書かれた Compute@Edge プログラムの main
関数は、リクエストのエントリーポイントとして機能しています。通常、main
関数は Request 構造体を受け取り、Response 構造体を返します。
まず main
関数では、ユーザーが ID プロバイダーから戻り、セッションを初期化する準備ができていることを示すコールバックパスを処理しているかどうか確認します。このパスは常に阻止される必要があり、バックエンドにプロキシされることはありません。
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"), ))}
ID プロバイダーからのレスポンスを受け取り、認可コードおよび PKCE を使い (先ほどの code verifier と challenge の出番です)、access_token
および id_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 をどのように使用するかによって他の方法もありますが、それについてはまた後ほど説明します。
最後に、ユーザーが認証されておらず、ログイン中でもない場合は、ユーザーを ID プロバイダーに送信してサインインプロセスを開始します。
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 を遠ざける良い例でもあります。分散システムにおけるスケーラビリティの課題は、state をより短期間で非同期にすることに関係しています。先日 Andrew が公開したエッジネイティブアプリについてのブログ記事も、併せてご覧ください。
皆さんがどのように Compute@Edge を活用していくのか、とても楽しみにしています。