📘

AWS Cognito OIDC + NextAuth.jsを使用したSSO(シングルサインオン)認証機能の実装

2024/06/12に公開

はじめに

今回は下記のようにどちらもNext.jsのプロジェクトで
https://localhost:3000/
https://localhost:3001/ でシングルサインオンを実装しています。

ログアウトすると、https://localhost:3000/ にリダイレクトされるようにしています。
またの機会にNext.jsのプロジェクト以外の場合にSSOを認証機能を追加する方法も記述したいと思っています。

Cognitoとは

AWSが提供する認証、認可、およびユーザー管理サービスを提供するクラウドベースのサービスで、ユーザープールとIDプールという二つの機能で構成されています。
https://aws.amazon.com/jp/cognito/

料金

無料枠がありますが、無料枠を超えたものに関してはユーザープールの利用者数と認証イベント、IDプールを使用した認証リクエストの数に基づいて計算されます。
https://aws.amazon.com/jp/cognito/pricing/

OIDC(OpenID Connect)とは

2001年に導入されたJSONベースのプロトコルでOAuth 2.0をベースとしているインターネット上で安全にユーザー認証を行うための規格でユーザー認証に特化しています。
誰がこの人かを確認するための技術であり、ログインする際によく見る「Googleでログイン」などの機能に使われています。

OIDCは、認証時にIDトークンを使用します。このIDトークンは、ユーザーの身元情報を含むJWT(JSON Web Token)で、認証サービスによって発行され、安全に情報をエンコードし、デジタル署名されます。

JWT(JSON Web Token)とは

ウェブトークンの1つの形式です。
JWTはヘッダー、ペイロード、シグネチャの三つの部分から成り立っています。それぞれがピリオド(.)で区切られています。
https://jwt.io/

シングルサインオンとは

ユーザーが一度の認証で複数の異なるシステムやアプリケーションにアクセスできる技術です。これにより、ユーザーは多くの異なるサービスを使う際に、それぞれでログイン情報を入力する必要がなくなります。

シングルサインオンの仕組み

ユーザーが初めてログインする際、認証システムにユーザー名とパスワードを入力します。
→認証が成功すると、認証システムはユーザーのブラウザに一定期間有効な認証トークンを発行します。
→ユーザーが他のシステムにアクセスする際、そのシステムは認証システムにトークンの確認を求めます。
→トークンが有効であれば、追加のログインなしでそのシステムにアクセスできます。

OIDCはこの仕組みの中で、認証トークンの発行、確認を行っています。

インターネット上での認証や認可を扱うプロトコルには、OIDCの他に
SAML(Security Assertion Markup Language)、OAuthがありますが役割や用途において異なります。

SAML

2001年に導入されたXMLベースのプロトコルで、XMLを扱うための特別なパーサーやライブラリが必要です。

SAMLはセキュリティアサーション(主張)をXML形式で交換し、これによりユーザーが認証され、アプリケーション間でのユーザーのアイデンティティが確認されます。

OAuth

インターネット上でユーザーが自分のアカウント情報を安全に第三者アプリケーションに共有することを許可するためのプロトコルです。

例えば、ユーザーが新しいアプリに登録するときに「Googleでログイン」を選択する場合にOAuthが使用されてと、そのアプリはユーザーの基本情報にアクセスするための認可をGoogleから受け取っています。

ユーザープールとは

ユーザーの情報(名前やメールアドレス)などを保存し、ユーザー管理する機能で、認証プロバイダ(ユーザーのアイデンティティを検証するサービスやシステム)の役割も果たします。

ユーザープールを作成する

ユーザープールを作成を選択、

フェデレーテッドアイデンティティプロバイダーを選択、
Cognito ユーザープールのサインインオプションを選択(今回はEmailを選択しました)、
フェデレーティッドサインインのオプション > OpenID Connect (OIDC)を選択して、次へを押してください。

今回は多要素認証 > MFA なしを選択して、次へを押してください。

サインアップエクスペリエンスを設定では特に変更しないので、そのまま次へを押してください。
メッセージ配信を設定 > Eメールでは
Cognito で E メールを送信を選択して、次へを押してください。

Amazon SES で E メールを送信 - 推奨を選択した場合は、送信元の E メールアドレス(SESでEメールアドレスの検証が完了しているメールアドレス)を選択して、次へを押してください。
https://dev.classmethod.jp/articles/cognito-ses-domain-email-2023/

フェデレーテッドアイデンティティプロバイダーを接続では後でを選択し、次へを押してください。

アプリケーションを統合 > ユーザープール名を入力、
クライアントのシークレットを生成するを選択してください。

Cognito ドメインを入力、

最初のアプリケーションクライアント >  アプリケーションクライアント名を入力

許可されているコールバック URLにはhttps://<ドメイン名>/api/auth/callback/cognitoを入力したら、下へスクロースして
https://next-auth.js.org/providers/cognito

高度なアプリケーションクライアントの設定 > サインアウト URL を追加を選択して、

https://<ドメイン名>/signoutを入力したら次へを押してください。

内容を確認して、ユーザープールを作成を押してください。ユーザープールが作成されます。

IDプールとは

ユーザーが外部の認証プロバイダーや自身のCognitoユーザープールを通じて認証された後、AWSリソースに対するアクセス許可を付与するために使用される機能です。

IDプールを作成する

ID プールを作成を押してください。

ユーザーアクセス > 認証されたアクセスを選択、
認証された ID ソース > 認証プロバイダとしてAmazon Cognito ユーザープールを選択して、次へを押してください。

IAM ロール > 新しい IAM ロールを作成を選択し、IAM ロール名を入力して、次へを押してください。

ユーザープール IDアプリクライアント IDを選択して、次へを押してください。(アプリクライアント IDはユーザープール IDを選択すると自動で表示されます。)

ID プール名を入力して、次へを押してください。

内容を確認して、ID プールを作成を押してください。

認証プロバイダーの統合

サインインエクスペリエンスを選択、フェデレーテッドアイデンティティプロバイダーのサインイン > アイデンティティプロバイダーを追加を選択してください。

フェデレーテッドアイデンティティプロバイダー

異なるシステムやアプリケーション間でユーザーの認証情報を共有するために使用されるサービスのことです。

OpenID Connect (OIDC)を選択して、

プロバイダー名、クライアントID、クライアントシークレットを入力してください。

クライアントIDはユーザープール > アプリケーションの統合を選択、下にスクロールするとあります。

この部分にあります。

発行者URLには、https://cognito-idp.<リージョン名>.amazonaws.com/<ユーザープールID>
OpenID Connect属性は、emailと入力して、アイデンティティプロバイダーを追加を押してください。

ちなみに東京リージョンは、ap-northeast-1です。

以降の記述を各プロジェクトに記述することでSSOを実現しています。

NextAuth.jsライブラリのインストール

npm install next-auth

NextAuth.jsとは

NextAuth.jsは、Next.jsアプリケーション向けの認証ライブラリです。

このライブラリは、開発者がNext.jsプロジェクトに簡単に認証機能を組み込むことができるように設計されています。OAuth、Email、さらには自分で定義した認証プロバイダを使用してユーザーを認証する機能を提供しています。
https://next-auth.js.org/

認証APIの設定

Next.jsのAPIルートを使用した認証APIを設定するためのファイルを作成します。
シングルクォート (') を使ってファイル名全体を囲むことで、角括弧を特別な意味を持たないただの文字として扱うようにしています。

mkdir -p 'src/app/api/auth/[...nextauth]' && touch 'src/app/api/auth/[...nextauth]/route.ts'

Next.jsプロジェクトでNextAuth.jsを使ってCognitoを認証プロバイダとして設定してください。具体的には、ユーザーがAWS Cognitoを通じて認証できるようにするための設定をしています。

src/app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import CognitoProvider from 'next-auth/providers/cognito';

const handler = NextAuth({
  providers: [
    CognitoProvider({
      clientId: process.env.COGNITO_CLIENT_ID || '',
      clientSecret: process.env.COGNITO_CLIENT_SECRET || '',
      issuer: process.env.COGNITO_DOMAIN,
    }),
  ],
});

export { handler as GET, handler as POST };

export { handler as GET, handler as POST }

サーバーが受け取るGETリクエストとPOSTリクエストに対して、handler関数を使って応答を処理するように設定しています。
https://next-auth.js.org/configuration/initialization#route-handlers-app

認証セッション管理の設定

アプリケーション全体に認証セッション情報を提供するためのコンポーネントを定義します。
https://qiita.com/k-sukesakuma/items/4b56b9e81c1788d38440

mkdir -p src/app/providers && touch src/app/providers/NextAuth.tsx
src/app/providers/NextAuth.tsx
'use client';

import { SessionProvider } from 'next-auth/react';
import { ReactNode } from 'react';

const NextAuthProvider = ({ children }: { children: ReactNode }) => {
  return <SessionProvider>{children}</SessionProvider>;
};

export default NextAuthProvider;
src/app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import NextAuthProvider from './providers/NextAuth';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang='ja'>
      <NextAuthProvider>
        <body className={inter.className}>{children}</body>
      </NextAuthProvider>
    </html>
  );
}

ログイン状況の表示

src/app/page.tsx
'use client';

import { signIn, signOut, useSession } from 'next-auth/react';

const { data: session } = useSession();
console.log('session', session);

if (session && session.user) {
  return (
    <main>
      <p>ログイン中</p>
      <br />
      {session.user.email}でログインしています。
      <br />
      <button onClick={() => signOut()}>Sign out</button>
    </main>
  );
}

return (
  <main>
    ログインしていません。
    <br />
    <button onClick={() => signIn()}>Sign in</button>
    <br />
    <button onClick={() => signIn('cognito')}>Cognito直通Sign in</button>
  </main>
);

Sign inの場合

Cognito直通Sign inの場合

環境変数の設定

.env
COGNITO_CLIENT_ID=
COGNITO_CLIENT_SECRET=
COGNITO_DOMAIN=https://cognito-idp.<リージョン名>.amazonaws.com/<ユーザープール ID>

NEXTAUTH_URL=https://<ドメイン名>
NEXTAUTH_SECRET=何か適当な文字列

https://docs.aws.amazon.com/ja_jp/general/latest/gr/cognito_identity.html

Try signing in with a different account.

私の場合、間違った認証情報(COGNITOのドメイン)が誤っておりこちらのエラーが表示されていました。

ユーザー情報を表示したい場合

認証コードを元にアクセストークンを取得
→アクセストークンを元にユーザー情報を取得してください。

UIのカスタマイズ

https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-app-ui-customization.html

https://zenn.dev/yuji/articles/4d910df6d2d08c

終わりに

何かありましたらお気軽にコメント等いただけると助かります。
ここまでお読みいただきありがとうございます🎉

Discussion