SPECIALIST

多様な専門性を持つNRIデジタル社員のコラム、インタビューやインサイトをご紹介します。

BACK

AWSにおけるOIDC認証認可構成の選択肢を探る

今回は「認証認可」に関するテーマについて触れたいと思います。
どのようなシステムにも基本的には「利用者」がいます。そのため、Webアプリケーションなどのアプリケーションを構築する際、必ず必要になってくるのは「認証認可の仕組み」です。本記事ではAWSクラウドでWebサイトやWebAPIを構築する上で、実現可能なOIDC認証認可の方式や構成にどのような選択肢があるのか」を探ってみたいと思います。

認証認可方式

まず、そもそも認証認可とはなんでしょうか?「認証」と「認可」は似て非なるものであり、有名な認証認可プラットフォームである「okta」の公式ページでは以下のように定義されています。

認証

ユーザーが本人の主張どおりの人物であることを検証する行為です。これは、セキュリティプロセスの最初のステップです。

認可

特定のリソースまたは機能にアクセスする許可をユーザーに付与するプロセスです。この用語は、アクセス制御やクライアント特権と同じ意味でしばしば使用されます。

つまり、認証とは「その人が誰かを証明する」こと、認可とは「その人に利用する権限があるかを確認する」ことと言えます。

okta公式ページより抜粋

そして、その認証認可をシステムで実現するための主な方式には「SAML(Security Assertion Markup Language)」や「OAuth 2.0」、またOAuth 2.0を拡張(認証機能を追加)したOIDC(OpenID Connect 以下OIDC)があります。この中で、Webアプリケーションやエンタープライズ環境において、近年デファクト化しつつある認証認可方式が「OIDC」であり、非常にセキュアな認証認可が実現可能となっています。

OIDC概要

OIDCは、OAuth 2.0の上に構築された認証認可プロトコルであり、ユーザーのID情報を安全に取得・共有するために利用されます。OAuth 2.0が「認可(Authorization)」を目的とするのに対し、OIDCはそれに加えて「認証(Authentication)」も扱います。
OIDCの代表的な認証フローである「認可コードフロー」と各ロールの概要は以下のようなものとなります。

ロール 主な役割 具定例 備考
End-User 認証される本人。ブラウザやアプリを通じてサービスを利用する。 サイトにログインしようとするユーザー 認証結果はトークン(IDトークン)として他のシステムに伝わる。
RP(Relying Party) OPを信頼してユーザーの認証情報を利用するOIDCクライアントアプリケーション。 フロントアプリケーション, モバイルアプリケーション 認証を委託する側。OPからトークンを受け取り、ユーザー識別や有効期限確認などを実行する。
OP(OpenID Provider) ユーザーを認証し、IDトークンやアクセストークンを発行する認証サーバ。 Keycloak, Amazon Cognito, Okta(Auth0) etc.. 認証を提供する側。ユーザーのID情報を管理し、認証後に各種トークンを発行する。
Resource Server アクセストークンを検証して保護されたリソースを提供するサーバ。 API Gateway、APIアプリケーション etc.. 業務処理のためのAPI、アプリケーションを含む。

OIDCには本記事で取り扱う「認可コードフロー」以外にも複数の認証フローがサポートされております。その他どのようなものがあるかは以下ページなどを参照してください。
認証フローと認可フロー

フローには記載されてませんが、ユーザー認証とは別にRPとOPとの間ではクライアントシークレットなどでシステム間認証がバックエンドで行われています。また、「リフレッシュトークン」というトークンを使用して、アクセストークンの有効期限をリフレッシュするようなことも行われます。

AWSでのOIDC認証認可の実現方式

さて、このOIDCの構成をAWS上で実現しようとした場合、どのような選択肢があるでしょうか?本記事では実機検証を通していくつかのパターンとそのメリットデメリットを確認していきたいと思います。

まず、AWSにおいて「認証認可」という言葉で真っ先に辿り着くサービスは「Amazon Cognito(以下Cognito)」でしょう。まずOPにCognitoを使用するパターンについて確認したいと思います。

本記事の検証ではリソースサーバの確認まではせず、認可コードフローによる「UserInfoエンドポイント」からのユーザークレーム取得までの実装とします。

Cognitoの概要は以下のAWS Black Beltで確認できますが、掲載時期が古い(2020年)のため注意してください。
https://pages.awscloud.com/rs/112-TZM-766/images/20200630_AWS_BlackBelt_Amazon_Cognito_ver2.pdf

パターン①

SPA + OP(Cognito)
まず、フロントのアーキテクチャをSingle-Page Application(SPA)にする場合のパターンです。本記事のSPAはReactで実装します。このパターンではReactのSPAがRPの役割を担う形となります。

Cognito認証後に「UserInfoエンドポイント」へアクセスしデータを取得するサンプルを作成します。Cognitoのユーザープール作成後、以下の通りSPAを選択してアプリケーションクライアントを作成します。

SPAはクライアントシークレットを安全に保持できないため、クライアントシークレットは作成されません。後述するPKCEによってSPAとのセキュリティを担保します。

SPA側のサンプルコードは「Quick Setupガイド」に記載されております。

今回は「UserInfoエンドポイント」から情報取得するようにしたいので、上記コードをベースに「UserInfoエンドポイント」から情報を取得(ユーザー名とEメールを取得)するように修正します。

App.js

import { useAuth } from "react-oidc-context";
import { useEffect, useState } from "react";

const COGNITO_DOMAIN = "https://ap-northeast-[cognito_domain].auth.ap-northeast-1.amazoncognito.com"

function App() {
    const auth = useAuth();
    const [userInfo, setUserInfo] = useState(null);
    const [userInfoError, setUserInfoError] = useState(null);

    useEffect(() => {
        const fetchUserInfo = async () => {
            if (auth.isAuthenticated && auth.user?.access_token) {
                try {
                    const response = await fetch(
                        COGNITO_DOMAIN + "/oauth2/userInfo",
                        {
                            headers: {
                                Authorization: `Bearer ${auth.user.access_token}`,
                            },
                        }
                    );
                    if (!response.ok) {
                        throw new Error("ユーザー情報の取得に失敗しました");
                    }
                    const data = await response.json();
                    setUserInfo(data);
                } catch (error) {
                    setUserInfoError(error.message);
                }
            } else {
                setUserInfo(null);
                setUserInfoError(null);
            }
        };
        fetchUserInfo();
    }, [auth.isAuthenticated, auth.user?.access_token]);

    const signOutRedirect = () => {
        const clientId = [clientId];
        const logoutUri = "<logout uri>";
        window.location.href = COGNITO_DOMAIN + `/logout?client_id=${clientId}&logout_uri=${encodeURIComponent(logoutUri)}`;
    };

    if (auth.isLoading) {
        return <div>Loading...</div>;
    }

    if (auth.error) {
        return <div>Encountering error... {auth.error.message}</div>;
    }

    if (auth.isAuthenticated) {
        return (
            <div>
                <h2>ユーザー情報(userinfoエンドポイント)</h2>
                {userInfoError && <div style={{color: "red"}}>{userInfoError}</div>}
                {userInfo ? (
                    <ul>
                        <li>
                            <b>ユーザー名</b>: <span>{userInfo.username}</span>
                        </li>
                        <li>
                            <b>Eメール</b>: <span>{userInfo.email}</span>
                        </li>
                    </ul>
                ) : (
                    <div>ユーザー情報を取得中...</div>
                )}
            </div>
        );
    }

    return (
        <div>
            <button onClick={() => auth.signinRedirect()}>Sign in</button>
            <button onClick={() => signOutRedirect()}>Sign out</button>
        </div>
    );
}

export default App;

ログイン認証用のユーザーも作成しておきます。

上記SPAサンプルコードを起動し、初期画面を表示させます。

「Sign in」を押下し、ログイン画面からログインします。

「UserInfoエンドポイント」から情報を取得できることを確認しました。

なお、クライアントをこのようなSPAやモバイルアプリケーションにて構築する場合、シークレット情報を安全に保持できないため、セキュリティを考慮すると認可コードの横取り対策である「PKCE (Proof Key for Code Exchange RFC 7636)」の実装が必要となります。PKCE は、認可コードを盗まれても、攻撃者がトークンを取れないようにする仕組みとなりますが、CognitoはそのPKCEに対応しております。ブラウザのトレースなどを見ると、以下のようにPKCEの連携が行われていることが確認できました。

認可エンドポイントアクセス時に「code_challenge」を付加

トークンエンドポイントアクセス時に「code_verifier」を送信

パターン②

通常Web + OP(Cognito)
次にフロントのアーキテクチャを通常のWebアプリケーション(サーバサイドレンダリング)にした場合です。このパターンではRPをサーバサイドのアプリケーションで実装することになります。今回はJavaの認証認可フレームワークとして実績のある「Spring Security」を使用します。

ユーザープールのアプリケーションクライアントは「従来のウェブアプリケーション」で作成します。

SPAと異なりクライアントシークレットをサーバ側で保持できるため、こちらはクライアントシークレットを使用したクライアント認証となります。

実装するコードは同様に「Quick Setupガイド」に記載されております。Javaを選択すると実際の設定やコードはSpring Security(+Thymeleaf)となっています。上記SPAと同様の項目を取得するために以下のように実装しました。

Controller

・・・
@Controller
public class HelloController {

    @GetMapping("/hello")
    public String hello(Model model) {
        // get a successful user login
        OAuth2User user = ((OAuth2User) SecurityContextHolder
                .getContext().getAuthentication().getPrincipal());

        Object username = user.getAttributes().get("username");
        Object email = user.getAttributes().get("email");

        model.addAttribute("username", username);
        model.addAttribute("email", email);

        return "hello";

    }
}

hello.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Hello</title>
</head>
<body>
<h2>ユーザー情報(userinfoエンドポイント)</h2>
<ul>
    <li>
        <b>ユーザー名</b>: <span th:text="${username}"></span>
    </li>
    <li>
        <b>Eメール</b>: <span th:text="${email}"></span>
    </li>
</ul>
</body>
</html>

上記Springアプリケーションを起動し、アクセスしてみると、同様に「UserInfoエンドポイント」から情報を取得できることが確認できました。

ここまで、SPA及び通常のWebアプリケーションのユースケースで、OPをCognitoにしたパターンを確認してきました。特に問題なく動作させることができましたが、上記2パターンのような「自前のRP + Cognito」の構成には以下のようなデメリットがあると筆者は考えております。

① CognitoのOP機能不足

今回は簡単なサンプルでの確認だったので特に表面化してませんが、実際は専用のOPプロダクト(OktaやKeycloakなど)と比較して、OPとして機能やカスタマイズ性で劣っている部分があります。

  • ユーザークレームへの情報追加やカスタマイズに制限がある(他OPプロダクトは自由に追加やカスタマイズ可能)
  • リフレッシュトークンの有効期限が一律でありクライアントごとに設定できない
  • Hosted UIのカスタマイズが制限的(完全なフルカスタムは不可)

などです。そのため、OPとしてCognitoを利用するのであれば、仕様として要件充足するのかしっかりと確認する必要があります。

② RPの開発・運用管理コスト

RPを自前で構築することになるので、RPコードの実装が必要になります。本検証で使用したReactのOIDCライブラリやSpring Securityを利用すれば効率的に実装可能ですが、それでも利用するフレームワークの学習コストはかかりますし、そもそも構築したRPアプリケーション自体の運用管理が必要となります。

上記のようなデメリットをふまえ、次にCognitoをRPとして利用するパターンについても見ていきたいと思います。

パターン③

RP(Cognito)+ 3rd Party OP
CognitoをRPとして利用するパターンです。今回OPとしては「Keycloak」を使用していきます。

Keycloakを立ち上げ、RealmやClientの設定をしていきます。

Cognito側のユーザープールに外部OPとしてKeycloakを設定します。

アプリケーションクライアントの「ログインページ」のタブで「IDプロバイダー」に「Keycloak」を指定します。

フロントのコードはパターン①と同様Reactとし起動します。

「Sign in」を押下すると、Keycloakのログイン画面が表示されました。バックエンドでCognitoとKeycloakが正常に連携できていることが確認できました。

ログイン後、「UserInfoエンドポイント」の情報も正常に取得できました。

このような構成にすることで、Cognitoでは不足していた十分なOP機能を使用することができ、トークンの確認などRPの面倒な処理はCognitoに任せることができます。前述の通り、CognitoはOPの機能が不足している部分があるため、筆者としてはこのようにRPの役割で使用するのが良いのではないかと思っています。

ただ、この構成にも一つ大きなデメリットが存在します。それはコストです(※)。以下公式ページにCognitoのコストが記載されておりますが、CognitoはMAU(月間アクティブユーザー)単位の課金となります
料金 – Amazon Cognito | AWS

つまり月あたりの利用量ではなく、1度利用でも大量利用でも課金額は変わらず、ユニーク利用ユーザーが多いユースケースの場合コスト効率は高くありません。これはCognitoをOPとして利用する際も同様となりますが、加えてRPとしてCognitoを利用する場合(つまり外部OPと連携する場合)には、以下に記載の通り個別料金がかかります。

軽く試算(1ドル=160円計算)してみたところ、ユニークユーザーが多い場合のユースケースでは以下のようになりました。

ユースケースによっては高額な金額になりうるので、構成検討の際にはこのようなコスト部分にも着目していただければと思います。

※ 本件を調査した2025/8時点の、公式ページ上での料金及び為替相場を元に計算しています。今後変更される可能性はあります。

パターン④

RP(ALB)+ 3rd Party OP
前項まででCognitoを利用した構成を確認してきましたが、OP利用せよRP利用にせよ、少なからずデメリットがあることがわかりました。では、AWSにおいてCognitoを使った構成以外にOIDC認証認可方式を実現する方法がないかを確認した結果、もう一つの選択肢がありました。それはALBをRPとして利用する構成です。以下公式ページの通り、ALBのOIDC機能を使用することでALBをRPとして動作させることが可能です。
Application Load Balancer を使用してユーザーを認証する – エラスティックロードバランシング

OIDCの設定はALBのリスナールール設定にて実施します。(OPはパターン③で立ち上げたKeycloakを使用します)
認証をかけたい条件(URLなど)の「認証アクション」に対して「ユーザーを認証」をチェックし、構築したKeycloakの情報を入力します。

認証成功後は、ルーティング設定に基づきリクエストがルーティングされます。OPからの取得したトークンやユーザークレームなどの情報はドキュメントの「ユーザークレームのエンコードと署名の検証」にも記載の通り、ルーティング先のリソースへHTTPヘッダ情報として付与されます。今回は以下のようにHTTPヘッダから各種情報を取得し、レスポンスするLambaを作成し、ALBのターゲットグループとして登録します。

Lambdaコード(Python)

import base64
import json

def lambda_handler(event, context):
    
    headers = event.get("headers", {})

    access_token = headers.get('x-amzn-oidc-accesstoken')
    oidc_data = headers.get('x-amzn-oidc-data')

    token = {
        'access_token': {
            'header':
            json.loads(
                base64.b64decode(access_token.split('.')[0] +
                                 '==').decode('utf-8')),
            'payload':
            json.loads(
                base64.b64decode(access_token.split('.')[1] +
                                 '==').decode('utf-8'))
        }
    }

    data = {
        'oidc_data': {
            'header':
            json.loads(
                base64.b64decode(oidc_data.split('.')[0] +
                                 '==').decode('utf-8')),
            'payload':
            json.loads(
                base64.b64decode(oidc_data.split('.')[1] +
                                 '==').decode('utf-8'))
        }
    }

    header_oicd = {
        **token,
        **data, 'auth0': headers.get('X-Amzn-Oidc-Identity')
    }

    return {
        'statusCode': 200,
        "headers": {
            "Content-Type": "application/json; charset=utf-8"
        },
        'body': json.dumps(header_oicd)
    }

認証設定したALBのURLへアクセスすると、Keycloakの認可エンドポイントへリダイレクトされログイン画面が表示されます。

ログイン認証に成功すると、ルーティング先のLambdaへリエクエストが送信され、アクセストークンやユーザークレームなどの情報が取得できました。

アクセストークン
ユーザークレーム

以上、ALBをOIDCのRPとして機能することが確認できました。また、Cognitoの項で課題となったコストですが、ALBはCognitoと異なり従量課金となります。
料金 – Elastic Load Balancing | AWS

こちらも軽く試算(1ドル=160円計算)してみましたが、一人あたり100回/月利用した場合と比較していもCognitoのおよそ「1/20」程度のコストとなりました。

Cognitoと比べコスト上のメリットも非常に高く、導入も設定レベルで難しくないことが本調査・検証にて確認できました。
ただ、1点考慮すべき事項があります。ドキュメントの「認証フロー」でわかる通り、本方式ではALB独自発行のセキュアCookieベースでクライアントとやり取りすることになるので、認証に関わる情報は全てブラックボックスとなり、フロントで操作することは不可となります。そのため、一例としてドキュメントの「認証ログアウト」に記載の通り、ログアウト時はALB発行の認証Cookieをサーバサイドで失効させるなどの考慮が必要です。

以上、一部考慮事項はありますが、RPとしてノックアウトファクターとなるようなものではなく、今回調査・検証したかぎり、十分に実用性もありそうなため、上記コストメリットを鑑みると、是非選択肢の有力候補としてご検討いただけるのではないかと思います。

さいごに

利用ユーザー単位でのセキュアな認証認可が実現できるOIDCは、あらゆるシステムユースケースにおいて今後も引き続きデファクトとなる認証認可方式だと思います。本記事ではAWSにおけるOIDC構成の実現方法について、いくつかの選択肢を確認してきました。これら以外にも選択肢がないかは引き続き調査していきたいですが、いずれにせよシステム要件やユースケースに応じてどの構成にするかを選択していくことになると思います。本記事の情報が構成検討時の一助になれば幸いです。