🍪

HttpOnly属性が有効なCookieに保存したトークンをAPIGateway認証に使いたい

2023/05/11に公開

久しぶりの投稿です。HttpOnly 属性が有効な Cookie に保存したトークン(Cognito が発行したもの)を API Gateway の認証に使おうと思っていたときに当初考えていた方法ではいかなかったので、メモがきとして残します。

きっかけ

下記記事の関連記事になります。

https://zenn.dev/lilac31/articles/c0f216d29ac2b9

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;
};

おわりに

短いですが、終わりです。これから動作検証していこうと思います。

脚注
  1. https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html#api-gateway-lambda-authorizer-lambda-function-create ↩︎

Discussion