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