LoginSignup
1
1

AWSのCognitoとAPIGatewayを連携させ、アプリクライアント別にAPIアクセスを管理してみた

Last updated at Posted at 2023-12-30

はじめに

API Gatewayは、各メソッドにオーソライザーと呼ばれる認証機能を設定することができます。
この設定により、APIGatewayへのリクエストの際に、IAMやCognito、Lambdaでの認証を必要とすることができます。

Cognitoを認証方法に選択する場合、Cognitoのアプリクライアントごとにメソッドに対する認可を個別設定するべきだと思います。

Cognitoではアプリクライアントごとにカスタムスコープを設定することができるため、アプリクライアントごとに認可スコープを設定し、API Gateway側でスコープを検証することができそうでした。

cap1.png

実際に設定し、NextJsとNextAuth.jsを使用し動作確認してみました。

参考:

1. API Gatewayリソースを作成する

API Gatewayでリソース、メソッドを作成します。
Cognitoオーソライザーはまだ設定できないので、特別な作業は行いません。

cap2.PNG

デフォルトでは「認可:None」になっていることが分かります。

2. Cognitoでユーザープール、リソースサーバー、アプリクライアントを作成する

ユーザープールの作成

ユーザープールを作成します。
特別な設定は不要ですが、今回はホストされたUIを使用するので、エンドポイント、ドメインの設定を行います。

cap3.PNG

ユーザーの作成

以下の記事を参考に、AWSCLIでユーザーを作成します。

cap14.PNG

リソースサーバーの設定

リソースサーバーを設定します。
リソースサーバー識別子は任意の値で良いらしいのですが、ステージURLを設定したところ、「/」を含むのがよくないのか、動作しなかったためAPI Gatewayのドメインのみを指定しています。

cap5.PNG

また、カスタムスコープに2つのスコープを設定しています。2つのアプリケーションクライアントにそれぞれ認可するスコープです。

アプリケーションクライアントの設定

アプリケーションクライアントを設定します。

cap6.PNG

NextAuthは認証にSRPの手法をとるので、SRP認証を許可します。

cap4.PNG

NextAuthは認証にクライアントシークレットを必要とするので、クライアントシークレットを発行するよう設定します。

cap7.PNG

アプリケーションクライアントへの認可を設定します。
NextAuthは認証後、コールバックURLとして「/api/auth/callback/${NextAuthで設定する任意のID}」を指定するので、「http://localhost:3000/api/auth/callback/${NextAuthで設定する任意のID}」を設定します。

cap8.PNG

認証もCognitoユーザープールで行うので、IdPはCognitoとします。
また、NextAuthはデフォルトでopenidスコープを受け取ろうとする(?)ため、OpenIDをスコープに設定します。(NextAuth以外は不要)
最後に、今回の検証の肝要な部分であるカスタムスコープを設定します。
「リソースサーバー識別子/リソースサーバーで設定したスコープ」の形式で選択できます。

以上の要領で、異なるカスタムスコープを持つ2つのアプリケーションクライアントを作成しました。

cap9.PNG

cap10.PNG

cap11.PNG

3. API GatewayリソースにCognitoオーソライザーを設定する

オーソライザーの作成

Cognitoを利用するオーソライザーを作成します。

cap13.PNG

メソッドの認証機能を設定

メソッドリクエストの設定画面から認証設定を行います。
「認可」にCognitoオーソライザーを指定し、「Authorization Scopes」に先ほど作成したカスタムスコープの片方を設定します。

cap12.PNG

4. NextAuth.jsで動作確認

npm install
npm install next-auth
npm install jwt-decode

Cognitoから取得するTokenの内容を確認するために、Tokenをデコードするライブラリもinstallしています。

認証APIを設定

今回記事のテーマ外なので詳細は明記しませんが、以下のような要領で、2つのCognitoアプリケーションクライアントを使用してログインできるよう設定します。

src/app/api/auth/[...nextauth]/route.tsx
import NextAuth from "next-auth"
import CognitoProvider from "next-auth/providers/cognito";
import { jwtDecode } from "jwt-decode";

const handler = NextAuth({
    providers: [
        CognitoProvider({
            // providers内で一意なidを設定する。
            // 設定したidはNextAuthの認証後のリダイレクトURLに使用される
            // 認証後のリダイレクトURLは「http://localhost:3000/api/auth/callback/${id}」
            // Cognitoアプリクライアント側でも、同じ値をコールバックURLとして設定しておくこと
            id: "cognito1",

            // https://cognito-idp.ap-northeast-1.amazonaws.com/${ユーザープールID(ap-northeast-1_XXXXXX)}
            issuer: process.env.COGNITO_ISSUER || "",

            // アプリケーションクライアントのクライアントID
            clientId: process.env.COGNITO_CLIENT_ID || "",

            // アプリケーションクライアントのクライアントシークレット
            clientSecret: process.env.COGNITO_CLIENT_SECRET || "",

            authorization: {
                // 任意のスコープを受け取る
                // 今回の検証ではopenidは不要だが、設定しないとNextAuthがエラーするため設定
                params: { scope: `openid ${process.env.RESOURCE_SERVER_ID}/sample-scope` },
            }
        }),
        CognitoProvider({
            id: "cognito2",
            issuer: process.env.COGNITO_ISSUER || "",
            clientId: process.env.COGNITO_CLIENT_ID2 || "",
            clientSecret: process.env.COGNITO_CLIENT_SECRET2 || "",
            authorization: {
                params: { scope: `openid ${process.env.RESOURCE_SERVER_ID}/not-sample-scope` },
            }
        })
    ],
    session: {
        strategy: "jwt",
    },
    callbacks: {
        // useSession()関数でCognitoから受け取ったTokenを参照するための設定
        async jwt({ token, account }) {
            if (account) {
                if (account["access_token"]) {
                    token.accessToken = account["access_token"]

                    // デバッグ用に検証
                    const decoded = jwtDecode(token.accessToken);
                    console.log("decoded", decoded);
                }
            }
            return token
        },
        async session({ session, token }) {
            if (token.accessToken) {
                session.user!.accessToken = token.accessToken
            }
            return session
        }
    },
})

export { handler as GET, handler as POST }

また、ログイン後、APIGatewayへリクエストを行うコードもテスト用に追加しています。

src/app/home/page.tsx
const onClick = async () => {
    const response = await fetch(`${process.env.NEXT_PUBLIC_API_GATEWAY_ENDPOINT}/example`, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Authorization": session!.user!.accessToken
        },
        body: JSON.stringify({}),
        cache: "no-store"
    });
    console.log("response", await response.json())
}
検証!

実際にテストしてみます。

cap15.PNG

NextAuthのログイン画面に設定したCognitoへの導線が表示されます。
1つ目のボタンをクリックすると、Cognitoのホストされたサインインページへ遷移します。

cap16.PNG

ログインが完了後、以下のコードが実行されます

src/app/api/auth/[...nextauth]/route.tsx
// 49行~
// デバッグ用に検証
const decoded = jwtDecode(token.accessToken);
console.log("decoded", decoded);

Cognitoから受け取ったTokenの中に、カスタムスコープが定義されていることが確認できました。

cap17.PNG

続いて、ヘッダーにAccessTokenを付与してAPI Gatewayへリクエストを行ってみます。

src/app/home/page.tsx
headers: {
    "Content-Type": "application/json",
    "Authorization": session!.user!.accessToken
},

cap18.PNG

ステータス200が返却されました!

認可されていないアプリケーションクライアントでの動作も確認します。
同様にCognitoのホストUIからログインすると、異なるカスタムスコープが定義されていることが確認できます。
画像では隠していますが、client_idも異なる値が返却されます。

cap19.PNG

ヘッダーにAccessTokenを付与してAPI Gatewayへリクエストを行ってみます。

cap20.PNG

ステータス401 Unauthorizedが返却されました!

無事、Cognitoオーソライザーを使用しつつ、アプリケーションクライアント別にAPI Gatewayへのアクセスを管理できました。

おわりに

軽い気持ちで手を出したのですが、凄く疲れました・・・。
疲れた気持ちで記事を書くと、内容が簡素になってよくないなという気づきを得られました。

Outputは今後も続けていきたいので、備忘として小刻みに残していく、などの工夫をしていければな、と思います。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1