OAuth を使ってエッジで認証プロセスを構築
認証は、エッジコンピューティングの用途の中でも最も有効なユースケースの一つです。できるだけユーザーに近い場所でユーザーをいち早く判別することで、強力なカスタマイズと迅速なレスポンスが可能になります。また、さまざまな方法で認証スキームをエッジで適用することができます。
前回の記事では、エッジで OAuth を実行し、現在のユーザーのセキュリティトークンにアクセスするためのリファレンス実装を公開し、解説しました。今回は、異なる4つのユースケースを具体的に取り上げ、新しい認証ゲートウェイのメリットを活用する方法についてご紹介します。
ペイウォールやその他の高度な承認判断
Web サイトによっては、エッジでは利用できない複雑なデータを使って承認の可否を判断をすることがあります。ペイウォールがその好例です。ユーザーがコンテンツを「購入」するのに十分なクレジットを有しているかどうかを確認する必要があっても、ユーザーの ID トークンの情報には現在の残高が含まれていません。
これは、ユーザーの id_token
から得られる関連情報を、追加の HTTP ヘッダーとしてオリジンに渡すための優れたユースケースです。オリジンはそのデータを利用して、アクセス権を付与すべきかどうか判断できるようになります。
以下に、id_token
データを複数の HTTP リクエストヘッダーに分散させる方法をご紹介します。
// Define a struct that groups together the pieces of data we care about.
#[derive(serde::Serialize, serde::Deserialize)]
struct IdTokenClaims {
uuid: String,
email: String,
country: String,
}
// Validate the ID token, and destructure the claims we defined earlier.
match validate_token_rs256::<IdTokenClaims>(id_token, &settings) {
Ok(claims) => {
// Here, claims.custom is an instance of IdTokenClaims.
req.set_header("Fastly-Auth-Uuid", claims.custom.uuid);
req.set_header("Fastly-Auth-Email", claims.custom.email);
req.set_header("Fastly-Auth-Country", claims.custom.country);
}
_ => {
return Ok(responses::unauthorized("ID token invalid."));
}
}
まず、オリジンがアクセス権の判断のために特定のプロファイルデータを使用する場合、オリジンは Vary ヘッダーで応答し、Fastly に同じプロファイル状態のユーザーに対してのみレスポンスをキャッシュするよう指示します。
Vary: Fastly-Auth-Uuid
Vary ヘッダーの使用については、以前からたびたびお伝えしていますが (2014年には同僚の Doc が、最近では Andrew が記事を投稿しています)、Vary ヘッダーは強力なメカニズムで、うまく使えばエッジでのキャッシュパフォーマンスを大幅に向上させることができます。
静的コンテンツへのアクセスをきめ細かくコントロール
Amazon S3 や Google Cloud Storage のような静的なバケットにコンテンツが保存されているとしましょう。一部のユーザーだけがこのようなコンテンツにアクセスできるようにするには、ちょっとした工夫が必要です。Fastly のサーバーレスコンピューティング環境でコードをビルド、テスト、デプロイするのに使用される Compute@Edge が、バケットプロバイダーからの静的コンテンツを配信する方法については以前ご紹介しました。そこで今回は、コンテンツにタグ付けされた情報と認証データをエッジで利用してアクセス権の判断を行うようにしてみましょう。
Fastly-Require-Country
HTTP レスポンスヘッダーを静的オブジェクトに追加します。エッジアプリケーションで、リクエストされたオブジェクトをオリジンから読み込む際に上記のヘッダーを読み取ります。
ユーザーの
id_token
のデータと値を比較します。値が一致しない場合はコンテンツレスポンスを破棄し、代わりに 403 Forbidden レスポンスを生成します。
前述の例で定義したのと同じ IdTokenClaims
の構造体を使って、以下にその方法をご紹介します。
match validate_token_rs256::<IdTokenClaims>(id_token, &settings) {
Ok(claims) => {
let beresp = req.send("backend")?;
if claims.custom.country != beresp.get_header_str("fastly-require-country").unwrap()
{
return Ok(Response::from_status(StatusCode::FORBIDDEN));
}
return Ok(beresp);
}
// ...
}
段階的な承認によるアクセス権の昇格
例えば、Google を ID プロバイダーとして使用するイベントチケットの発券アプリを運用していると仮定しましょう。ユーザーがイベントをブックマークしようとするだけであれば、ユーザーの身元を知るだけで十分です。しかし、ユーザーが Google カレンダーにイベントを追加したい場合は、ユーザーのカレンダーに書き込むためのアクセスを得るために追加のスコープが必要になります。その後、ユーザーが予約をしようとする際、ユーザーのウォレットや決済サービスへのアクセスが必要になりますが、それにはまた別のスコープが必要になるかもしれません。
最初の承認プロセスで複数のスコープをリクエストすることは理にかなっているかもしれません。特に、多数のユーザーが行うアクティビティに必要なスコープや、機密性の低いスコープについてはその通りと言えるでしょう。一方、特に許可されていない限り、オリジンに実行させたくないプロセス (例えば決済など) もあるでしょう。最小特権の原則を適用するためにもその方が賢明です。
このような場合、オリジンは現在のセッションの access_token
を使用して、ID プロバイダーに段階的な承認をリクエストすることができます。
エッジアプリでリクエストに
Fastly-Access-Token
ヘッダーを追加し、オリジンがアクセストークンを確認して使用できるようにします。オリジンでは、アクセストークンを使用して、例えば決済の開始などを、ID プロバイダーに直接リクエストします。
スコープが不十分なために ID プロバイダーがリクエストを拒否した場合は....
オリジンは、
Fastly-Required-Scopes
ヘッダーを含む403レスポンスを Fastly に返します。Fastly は新しい承認フローを開始し、ユーザーのトークンをアップグレードして新しいスコープを許可し、通常通りコールバックを処理してユーザーのセッションを新しいものに置き換えます。
最終的に、ユーザーはアップグレードされた承認を必要とする URL にリダイレクトされ、オリジンは新しいアクセストークンを使用して ID プロバイダーへのリクエストを成功させます。
エッジでは、Fastly-Required-Scopes
ヘッダーを持つ403レスポンスを認識し、新しいフローをトリガーするだけで済みます。
// First, let’s make the configuration object mutable.
let mut settings = Config::load();
let beresp = req.send("backend")?;
if beresp.get_status() == fastly::http::StatusCode::FORBIDDEN {
if let Some(incremental_scopes) = beresp.get_header_str("fastly-required-scopes") {
// Append the incremental scopes to the original settings.
settings.config.scope.push(' ');
settings.config.scope.push_str(incremental_scopes);
} else {
return Ok(beresp);
}
}
不正ユーザーのブロック
さまざまな理由から、ある時点でユーザーをアプリからブロックする必要が生じる場合があります。ここにはトレードオフの関係があります。長期間有効なセッショントークンは効率的です。しかし、あるユーザーをブロックしたいのに、そのユーザーのセッショントークンがまだ2週間も有効であることに気付き、しかもトークンをキャンセルする方法が無いという状況は避けたいものです。逆に、すべての操作の前に各ユーザーのセッションをチェックするとなると動作が遅くなり、エッジでコンテンツをまったくキャッシュできなくなる可能性があります。
そこでエッジで OAuth を使用すると、リクエストごとに ID プロバイダーで access_token
を検証し (ID プロバイダーがこうしたリクエストにグローバルに対応するように最適化されていれば、通常は非常に高速に処理できます)、キャッシュされたコンテンツを使用してリクエストに応えることができます。これにより、ID プロバイダーでユーザーのアクセスを取り消すと、そのユーザーは直ちにブロックされます。
今回のアプリの例では、各リクエストごとにリアルタイムに検証を実行するコールを ID プロバイダーに対して行うよう設定されています。
let mut userinfo_res = Request::get(settings.openid_configuration.userinfo_endpoint)
.with_header(AUTHORIZATION, format!("Bearer {}", access_token))
.send("idp")?;
if userinfo_res.get_status().is_client_error() {
return Ok(responses::unauthorized(userinfo_res.take_body()));
}
これにより、リアルタイムのセッション検証の問題が ID プロバイダーに委ねられますか、その結果、プロバイダーは各リクエストに最小限のレイテンシを加えることになります。これはトレードオフの関係にあるので、セッションの取り消しの遅延が多少増えることになってもレイテンシを減らすことを選ぶのであれば、Fastly によってエッジで ID プロバイダーのレスポンスを短期間キャッシュすることができます。
また Fastly では、キャッシュされたコンテンツを約150ミリ秒でグローバルにパージすることが可能です。従って、ID プロバイダーでのセッションの取り消しと Fastly API へのパージリクエストの送信を連係させることで、双方のメリットを活かし、可能な限りキャッシュを使用してセッションを検証しつつ、数秒でセッションの取り消しを行うことが可能になります。
創造性を発揮しましょう!
Web アプリケーションのセキュリティにおいては、独力でやるというアプローチは一般的に良くないと考えられており、「自分で暗号を作る」というのは、エンジニアが思いつくアイディアの中で最も恐ろしいものと言えるかもしれません。
しかし、セキュリティの分野によっては堅牢で効果が実証済みのツールに任せるのが最適な場合があるものの、実際のところ、アプリをコントロールするために認証・認可データをどのように使用するかは、極めて自由に決定することができます。ここではいくつかのアイデアをご紹介しましたが、ユーザーの権利やアクセス許可を判断する仕組みの一部として、これらの要素を組み合わせる方法は無数にあります。
興味深いアイデアをお持ちでしたら、お気軽に @fastly 宛てにツイートして教えてください!