Cognitoを使って学ぶAuthorization Code Flow

Cognitoを使って学ぶAuthorization Code Flow

2024-12-30

この記事ではCognitoおよびNode.jsを使って認証・認可を実施するクライアントアプリの開発を通して、OAuth/OIDCのAuthorization Code Flowを学習します。

対象読者としては次のような方を想定しています。

  • OAuthやOIDCを少し勉強したけど、うっすらとしかわかっていない人
  • 基本的なWebの仕組み(HTTPリクエスト・レスポンス)がわかっている人

目次

  • この記事で登場する用語集
  • Authorization Code Flow
  • Node.jsを使ってAuthorization Code Flowで認証・認可を実施する
    • 環境構築
    • ルートエンドポイント
    • ログインエンドポイント(/login)
    • コールバックエンドポイント(/callback)
    • 動作確認
  • まとめ

この記事で登場する用語集

この記事で登場する用語集です。うっすらわかっていれば良いです。

なおCognitoのユーザープールの作成方法はこの記事では触れません。あらかじめ適当なユーザープールを作成し、アプリケーションクライアントを作成していただく必要があります(いつか記事にするかもしれません)。

  • Cognito: AWSでユーザー認証を利用するためのサービスです
  • OAuth/OIDC: 認証・認可をする際のプロトコルです
  • Authorization Code Flow: OAuth/OIDCでいくつかあるフローのうち、代表的なフローの一つです

Authorization Code Flow

そもそもAuthorization Code Flowとはなんなのか簡単に説明します。

登場人物は次のとおりです。

  1. ユーザー: クライアントアプリに権限・認証情報を与えたい人
  2. クライアントアプリ: ユーザー情報・権限を使いたいサービス
  3. IdP: ユーザー情報の管理や権限管理を行っているサーバー

上記登場人物が、以下のようなフローを使って、認証・認可を行うのがAuthorization Code Flowです。

  1. ユーザーがクライアントアプリの認証・認可を必要とするページにアクセスしようとする
  2. クライアントアプリはユーザー情報を取得するために、ユーザーをIdPにリダイレクトする
    • リダイレクト時には以下の情報を付与します
      • 認証後クライアントアプリの戻ってきて欲しい場所(redirect uri)
      • IdPから返却して欲しいレスポンスタイプ
        • Authorization Code Flowではレスポンスタイプにcodeを指定します
      • クライアントID
    • 他にも付与できる・した方が良いものはありますが、ここでは割愛しています
  3. IdPがユーザーの確認を行う(一旦ログイン画面が表示されると理解でOK)
  4. ユーザーの確認が完了後、IdPは一時的な認可コードを付与して、ユーザーをクライアントアプリにリダイレクトする
    • この時リダイレクト先となるのが、2で指定したredirect uriとなる
    • 2でレスポンスタイプにcodeを指定することで、一時的な認可コードが発行される
  5. クライアントアプリは受け取った認可コードをIdPのトークンエンドポイントに提示する
  6. IdPは提示された認可コードの確認をして、問題なければアクセストークン・IDトークンを発行し、クライアントアプリに提供する
  7. クライアントアプリは提供されたアクセストークン・IDトークンを使って、権限やユーザー情報を利用する

Node.jsを使ってAuthorization Code Flowで認証・認可を実施する

ここからはNode.jsのExpressを使ってAuthorization Code Flowで認証・認可を行うWebアプリケーションを開発します。

今回開発するWebアプリケーションには3つのエンドポイントを用意します。

  1. ルートエンドポイント(/)
    • index.htmlを表示する
    • index.htmlにログインボタンを設置
  2. ログインエンドポイント(/login)
    • index.htmlのログインボタンを押下時に遷移
    • Cognitoの認可エンドポイントにユーザーをリダイレクトする
  3. コールバックエンドポイント(/callback)
    • Cognitoで認証後、戻ってくるエンドポイント
    • Cognitoで発行された認可コードをアクセストークンに変換する

なお完成したアプリケーションはこちらのGitHubリポジトリに格納しています。

環境構築

まずはExpressサーバーを起動するための環境構築を行います。

筆者は次の環境で実施しております。

  • macOS Sonoma 14.2.1(Apple M2 Pro)
  • Node v22.12.0
  • npm v10.9.0

適当なディレクトリを作って、作業を開始します。

mkdir learn-authorization-code-flow && cd learn-authorization-code-flow

次にNodeの環境と必要なライブラリのインストールを行います。

npm init -y
npm install express dotenv querystring

それぞれのライブラリは次の用途です。

  • express: Webアプリケーションの構築
  • dotenv: 環境変数を利用
  • querystring: Getリクエスト時のクエリパラメータを設定するライブラリ

また環境変数を設定する.envファイルをルートディレクトリに作成します。

内容はそれぞれの環境に合わせて変更してください。

COGNITO_REGION=[your-cognito-region]
COGNITO_USER_POOL_ID=[your-cognito-user-pool-id]
COGNITO_CLIENT_ID=[your-cognit-client-id]
COGNITO_CLIENT_SECRET=[your-cognit-client-secret]
COGNITO_DOMAIN=[your-cognito-domain]
REDIRECT_URI=http://localhost:8000/callback

ルートエンドポイント

環境の準備ができたらルートエンドポイントとindex.htmlを作成します。

まずはルートディレクトリにindex.jsを作成し、内容を以下のようにします。

コードの説明はコメントで行っています。

require('dotenv').config(); // dotenvを使って環境変数をロード
const express = require('express');
const app = express();
const PORT = 8000;

// 静的ファイルの提供
app.use(express.static('public'));

// ルートエンドポイントでindex.htmlを返す
app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

// サーバーの起動
app.listen(PORT, () => {
  console.log(`Server is running at http://localhost:${PORT}`);
});

次にpublicディレクトリを作成し、その中にindex.htmlを格納します。

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Cognito Login</title>
</head>
<body>
  <h1>Authorization Code Flow</h1>
  <!-- /loginエンドポイントへ遷移するボタン -->
  <button onclick="location.href='/login'">Login with Cognito</button>
</body>
</html>

ログインエンドポイント(/login)

次にログインエンドポイントを作成します。

ログインエンドポイントは以下の役割を持っています。

  • index.htmlのログインボタンを押下時に遷移
  • Cognitoの認可エンドポイントにユーザーをリダイレクトする

Authorization Code Flowだと以下に該当します。

  1. クライアントアプリはユーザー情報を取得するために、ユーザーをIdPにリダイレクトする
    • リダイレクト時には以下の情報を付与します
      • 認証後クライアントアプリの戻ってきて欲しい場所(redirect uri)
      • IdPから返却して欲しいレスポンスタイプ
        • Authorization Code Flowではレスポンスタイプにcodeを指定します
      • クライアントID
    • 他にも付与できる・した方が良いものはありますが、ここでは割愛しています

index.jsに以下を追記します。

require('dotenv').config();
const express = require('express');
const querystring = require('querystring'); // 追記
const app = express();
const PORT = 8000;

// 追記:Cognitoの認可エンドポイント
const AUTH_URL = `${process.env.COGNITO_DOMAIN}/oauth2/authorize`;

// ... 省略

// 追記:ログインリクエスト
app.get('/login', (req, res) => {
  const loginUrl = `${AUTH_URL}?${querystring.stringify({
    response_type: 'code', // レスポンスタイプにcodeを指定して、認証成功後に認可コードを受け取る
    client_id: process.env.COGNITO_CLIENT_ID, // CognitoのクライアントIDを指定
    redirect_uri: process.env.REDIRECT_URI, // 認証成功後、IdP(Cognito)からリダイレクトしてもらう先を記載
    scope: 'openid profile', // スコープを指定
  })}`;

  res.redirect(loginUrl);
});

ログインエンドポイントの重要ポイントはloginUrlの構築部分です。

const loginUrl = `${AUTH_URL}?${querystring.stringify({
  response_type: 'code', // レスポンスタイプにcodeを指定して、認証成功後に認可コードを受け取る
  client_id: process.env.COGNITO_CLIENT_ID, // CognitoのクライアントIDを指定
  redirect_uri: process.env.REDIRECT_URI, // 認証成功後、IdP(Cognito)からリダイレクトしてもらう先を記載
  scope: 'openid profile', // スコープを指定
})}`;

上記で作成したURLにユーザーをリダイレクトし、ユーザーはIdP(Cognito)で認証を行います。

その際に指定しているクエリパラメータは以下の役割を持っています。

  • response_type
    • codeを指定することで、Authorization Code Flowで認証・認可を行うことをIdPに示します
      • codeを指定するとユーザーが認可した後リダイレクトURIで指定したエンドポイントに認可コードという一時的なコードが返却されます
      • リダイレクトURIでは認可コードをIdPのトークンエンドポイントに提示して、アクセストークンやIDトークンと交換します
  • client_id
    • IdPに対して認可を求めているクライアントアプリがなんなのか示します
  • redirect_uri
    • 認可コードを受け取るエンドポイントを指定します
    • リダイレクトURIはあらかじめIdP側で設定しておく必要があります
  • scope
    • クライアントアプリが求めるスコープを提示します
    • openidはOIDCをする上で必須となります

他にも渡せる/渡した方が良いパラメータもあり、詳しくはCognito公式ドキュメントをご参照ください。

特にstateパラメータはCSRF攻撃を防ぐために必須のパラメータとなります。実際のプロジェクトで開発する際は設定・検証するようにしましょう。

コールバックエンドポイント(/callback)

最後にコールバックエンドポイントを作成します。

コールバックエンドポイントは以下の役割を持っています。

  • IdP(Cognito)で認証後、戻ってくるエンドポイント
  • IdP(Cognito)で発行された認可コードをアクセストークンに変換する

Authorization Code Flowだと以下に該当します。

  1. ユーザーの確認が完了後、IdPは一時的な認可コードを付与して、ユーザーをクライアントアプリにリダイレクトする
    • この時リダイレクト先となるのが、2で指定したredirect uriとなる
    • 2でレスポンスタイプにcodeを指定することで、一時的な認可コードが発行される
  2. クライアントアプリは受け取った認可コードをIdPのトークンエンドポイントに提示する
  3. IdPは提示された認可コードの確認をして、問題なければアクセストークン・IDトークンを発行し、クライアントアプリに提供する

index.js二以下の追記をします。

// index.js
// ... 省略

// Cognitoのエンドポイント
const AUTH_URL = `${process.env.COGNITO_DOMAIN}/oauth2/authorize`;
const TOKEN_URL = `${process.env.COGNITO_DOMAIN}/oauth2/token`; // 追記

// ...省略

// 追記:コールバック処理
app.get('/callback', async (req, res) => {

  // リダイレクト時にIdPから返された認可コードをクエリパラメータから取得
  const code = req.query.code;

  // 認可コードがない場合はエラーを返す
  if (!code) {
    return res.status(400).send('Authorization code is missing');
  } else {
    console.log(`Received code: ${code}`);
  }

  // 受け取った認可コードをIdPに送信してアクセストークンと交換する
  const response = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      client_id: process.env.COGNITO_CLIENT_ID,
      client_secret: process.env.COGNITO_CLIENT_SECRET,
      redirect_uri: process.env.REDIRECT_URI,
      code,
    }),
  });

  // 正常に交換できていない場合はエラーを返す
  if (!response.ok) {
    return res.status(500).send('Failed to exchange authorization code for access token');
  }

  // レスポンスからトークンを取得してログに出力
  const data = await response.json();
  console.log('%o', data);

  // ホーム画面にリダイレクトする
  res.redirect('http://localhost:8000/');

});

// ...省略

コールバックエンドポイントの重要ポイントはトークンエンドポイントへのリクエスト部分です

// 受け取った認可コードをIdPに送信してアクセストークンと交換する
const response = await fetch(TOKEN_URL, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    client_id: process.env.COGNITO_CLIENT_ID,
    client_secret: process.env.COGNITO_CLIENT_SECRET,
    redirect_uri: process.env.REDIRECT_URI,
    code,
  }),
});

IdPから受け取った認可コードを再度IdPに提示することで、アクセストークンやIDトークンを取得します。

今回のアプリではトークン取得後はログに表示していますが、実際のアプリケーションではこのトークンを使ってリソースサーバーへのアクセスやSSOといった機能を実現していくことになります。 トークンは重要な情報であるため、HTTPオンリーなクッキーで管理するなどサーバー側で安全に管理するようにしましょう。

const data = await response.json();
console.log('%o', data);

res.redirect('http://localhost:8000/');

動作確認

ではここまでの動作確認を行います。ターミナル等で以下コマンドを実行します。

node index.js

実行後ブラウザでhttp://localhost:8000 にアクセスするとログインボタンが表示されます。

ログインボタンを押すとCognitoのログイン画面が表示され、ログインに成功するとcallbackエンドポイントが実行され、アクセストークン・IDトークンがログに出力されるはずです。

まとめ

この記事ではAWS CognitoとNode.jsを利用してOAuth/OIDCのAuthorization Code Flowを実装しながら学習しました。

基本的なフローを実装することで、理解が深まったでしょうか?

OAuthやOIDCといった認証・認可に関する記事を今後も発信予定です。ぜひブックマーク等よろしくお願いいたします。この記事が役に立ったという方はSNSでの共有もお願いします!