HttpOnly属性が有効なCookieに保存したトークンをAPIGateway認証に使いたい
久しぶりの投稿です。HttpOnly 属性が有効な Cookie に保存したトークン(Cognito が発行したもの)を API Gateway の認証に使おうと思っていたときに当初考えていた方法ではいかなかったので、メモがきとして残します。
きっかけ
下記記事の関連記事になります。
API Gateway + Lambda の構成で、API へのアクセス制御を導入するとなったときは『Cognito オーソライザーがあるし、Cognito から発行されたトークン使って認証すればいいか』と考えていたものの、肝心のトークンは HttpOnly 属性が有効な Cookie に保存していたため、フロントエンドから API リクエストを投げる際に、ヘッダーにトークンを直に指定できないと気付く。
調べていると、Lambda オーソライザー[1]は柔軟に認証フローを組めそうなので、こちらでできないか模索してみたという経緯になります。
API Gateway に Lambda オーソライザーを作成
Lambda オーソライザーを作成します。(必要に応じて他項目は設定下さい)
まずは、トークンのソース
をCookie
にします。これにより、API Gateway に対して「リクエストのCookie
ヘッダーからトークンを取得する」という指示になります。
フロント側の API 呼び出し
今回、フロント側では、axios で API 呼び出しを行うとします。このとき、パラメータに{ withCredentials: true }
を指定します。これにより、ブラウザは現在のドメインに紐づいた Cookie をリクエストヘッダーに自動的に追加します。
await axios.post(url, param, { withCredentials: true });
そのため、このリクエストが API Gateway に到達した時点で、API Gateway はCookie
ヘッダーからトークンを取り出し、それを Lambda オーソライザーのevent.authorizationToken
として渡します。
Lambda オーソライザー側で、event.authorizationToken
を解析・検証することで、HttpOnly 属性が有効な Cookie に保存したトークンを認証に用いることができると思います。
Lambda オーソライザーの実装
いったん下記のように実装してみました。トークンを取り出す部分は環境によって実装が異なると思います。
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";
const region = process.env.AUTH_REGION;
const userPoolId = process.env.AUTH_USER_POOL_ID;
// Initialize jwksClient
const client = jwksClient({
jwksUri: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`,
});
export const handler = function (event, context, callback) {
let token;
const cookies = event.authorizationToken;
const cookieArray = cookies.split(";");
for (let i = 0; i < cookieArray.length; i++) {
const cookie = cookieArray[i].trim().split("=");
const cookieName = cookie[0];
const cookieValue = cookie[1];
if (cookieName == "idToken") {
token = cookieValue;
}
}
// Decode the JWT token
const decodedJwt = jwt.decode(token, { complete: true });
if (!decodedJwt) {
console.log("Not a valid JWT token");
callback("Unauthorized");
return;
}
// Get the key
client.getSigningKey(decodedJwt.header.kid, (err, key) => {
if (err) {
console.log("Error getting signing key: " + err.message);
callback("Unauthorized");
return;
}
// Verify the JWT token
jwt.verify(token, key.publicKey, { algorithms: ["RS256"] }, (err, decoded) => {
if (err) {
console.log("JWT token verification failed: " + err.message);
callback("Unauthorized");
return;
}
// JWT token is valid, now generate the policy
callback(null, generatePolicy(decoded.sub, "Allow", event.methodArn));
});
});
};
// Helper function to generate an IAM policy
var generatePolicy = function (principalId, effect, resource) {
var authResponse = {};
authResponse.principalId = principalId;
if (effect && resource) {
var policyDocument = {};
policyDocument.Version = "2012-10-17";
policyDocument.Statement = [];
var statementOne = {};
statementOne.Action = "execute-api:Invoke";
statementOne.Effect = effect;
statementOne.Resource = resource;
policyDocument.Statement[0] = statementOne;
authResponse.policyDocument = policyDocument;
}
// Optional output with custom properties of the String, Number or Boolean type.
authResponse.context = {
stringKey: "stringval",
numberKey: 123,
booleanKey: true,
};
return authResponse;
};
おわりに
短いですが、終わりです。これから動作検証していこうと思います。
Discussion