Cognito User Pools と OIDC で Slack サインインを実装する

Cognito User Pools と OIDC で Slack サインインを実装する

Cognito user pool を OIDC 経由で Slack と連携させ、"Sign in with Slack" を Amplify で Next.js アプリケーションに組み込みます。

Takahiro Iwasa
14 min read

Cognito user pools には Slack 専用のプロバイダーがないため、OIDC 経由の汎用アイデンティティプロバイダーとして Slack と連携し、Next.js アプリケーションに “Sign in with Slack” を実装します。

はじめに

AWS CDK 環境のブートストラップ

AWS CDK 環境をまだブートストラップしていない場合は、次のコマンドで CDK をローカルにインストールし、環境をブートストラップします。すでに実行済みであれば、この手順は不要です。

Terminal window
npm i -D aws-cdk
npx cdk bootstrap aws://<AWS_ACCOUNT_ID>/<AWS_REGION>

CDK プロジェクトの初期化

CDK プロジェクト用のディレクトリを作成し、初期化します。

Terminal window
mkdir cdk && cd cdk
npx cdk init app --language typescript

React/Next.js のインストール

アプリケーションのフロントエンドとして、React/Next.js をインストールします。

Terminal window
npx create-next-app@latest

セットアップ中は、次のようにプロンプトに答えます。

  • プロジェクト名: my-app
  • TypeScript を使用する: Yes
  • ESLint を使用する: No
  • Tailwind CSS を使用する: Yes
  • src/ ディレクトリを使用する: Yes
  • App Router を使用する: Yes
  • デフォルトのインポートエイリアス(@/*)をカスタマイズする: No

AWS Amplify のインストール

AWS Amplify のフロントエンド API を使って、Cognito のリダイレクトフローを開始し、認証済みユーザーを取得します。

プロジェクトディレクトリに Amplify をインストールします。

Terminal window
cd my-app
npm i aws-amplify

バックエンドの構築

Slack アプリの作成

Cognito User Pool で Slack を OIDC アイデンティティプロバイダーとして使うため、Slack アプリを作成します。

Slack ワークスペースのメニューを開き、Manage apps に移動します。

Manage Apps

Slack アプリディレクトリのページで、Build ボタンを押します。

Build New App

アプリのページで、Create an App ボタンをクリックします。

Create App

プロンプトが表示されたら、From an app manifest を選択します。

https://api.slack.com/reference/manifests

Use the app manifest system to quickly create, configure, and reuse Slack app configurations.

App Manifest Option

アプリをインストールするワークスペースを選択します。

Select Workspace

アプリ名を入力します。この例では Sign in with Slack を使い、他のフィールドはデフォルト値のままにします。

Name Your App

Create ボタンを押して Slack アプリのセットアップを完了します。

Finalize Creation

Slack アプリの認証情報の確認

Slack アプリの認証情報を取得し、Cognito を設定する前に安全な場所へ保存します。

ステップ1. Slack アプリの認証情報を取得する

Slack アプリの Basic Information ページに移動し、Client IDClient Secret の値をコピーします。

これらの認証情報は、後で Cognito User Pool を設定する際に使用します。

Slack App Credentials

ステップ2. AWS Secrets Manager に認証情報を保存する

Client IDClient Secret を AWS CDK のソースコードへ直接埋め込まず、AWS Secrets Manager に保存します。

Important

AWS CDKファイルにSlackの認証情報をハードコーディングするのは避けてください。常にAWS Secrets Managerのような安全なストレージを使用してください。

Terminal window
aws secretsmanager create-secret \
--name sign-in-with-slack \
--secret-string '{"clientId": "<YOUR_CLIENT_ID>", "clientSecret": "<YOUR_CLIENT_SECRET>"}'

Cognito User Pool の作成

Slack と Cognito を統合するため、Cognito User Pool を設定し、AWS Secrets Manager に保存した Slack アプリの認証情報と紐付けます。

ステップ 1: cdk/bin/cdk.ts を更新する

実行時に必要な環境変数(SLACK_SECRET_ARNCOGNITO_DOMAIN_PREFIX)を渡すため、cdk/bin/cdk.ts ファイルに次のコードを追加します。

cdk/bin/cdk.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkStack } from '../lib/cdk-stack';
const app = new cdk.App();
new CdkStack(app, 'CdkStack', {
slackSecretArn: process.env.SLACK_SECRET_ARN ?? '',
cognitoDomainPrefix: process.env.COGNITO_DOMAIN_PREFIX ?? '',
});

ステップ 2: cdk/lib/cdk-stack.ts で Cognito User Pool を設定する

cdk/lib/cdk-stack.ts は次のようになります。

  • ユーザー属性を取得するため、UserPoolClient.oAuth.scopesOAuthScope.COGNITO_ADMIN を含めます(55 行目)。詳細は公式ドキュメントを参照してください。
  • AWS Secrets Manager から Slack の認証情報を安全に取得します(65〜69行目)。
  • Slack の認証情報はコードに露出させることなく注入されます(75〜82行目)。
cdk/lib/cdk-stack.ts
import * as cdk from 'aws-cdk-lib';
import type { Construct } from 'constructs';
import {
OAuthScope,
ProviderAttribute,
UserPool,
UserPoolClient,
UserPoolClientIdentityProvider,
UserPoolDomain,
UserPoolIdentityProviderOidc,
} from 'aws-cdk-lib/aws-cognito';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
export class CdkStack extends cdk.Stack {
constructor(
scope: Construct,
id: string,
props?: cdk.StackProps & {
slackSecretArn: string;
cognitoDomainPrefix: string;
},
) {
super(scope, id, props);
// Cognito User Pool
const userPool = new UserPool(this, 'user-pool', {
userPoolName: 'sign-in-with-slack-user-pool',
});
// Cognito User Pool Domain
new UserPoolDomain(this, 'user-pool-domain', {
userPool,
cognitoDomain: {
domainPrefix: props?.cognitoDomainPrefix ?? '',
},
});
// Cognito User Pool Client
new UserPoolClient(this, 'user-pool-client', {
userPool,
userPoolClientName: 'client',
oAuth: {
flows: {
authorizationCodeGrant: true,
},
callbackUrls: [
'https://example.com/', // Cognito app client default
'http://localhost:3000/',
],
logoutUrls: ['http://localhost:3000/'],
scopes: [
OAuthScope.OPENID,
OAuthScope.EMAIL,
OAuthScope.PROFILE,
OAuthScope.COGNITO_ADMIN,
],
},
supportedIdentityProviders: [
UserPoolClientIdentityProvider.COGNITO,
UserPoolClientIdentityProvider.custom('Slack'),
],
});
// Slack app credentials stored in your Secrets Manager
const slackSecret = Secret.fromSecretCompleteArn(
this,
'slack-secret',
props?.slackSecretArn ?? '',
);
// Cognito User Pool Identity Provider (OIDC)
new UserPoolIdentityProviderOidc(this, 'slack-oidc', {
userPool,
name: 'Slack',
clientId: slackSecret
.secretValueFromJson('clientId')
.unsafeUnwrap()
.toString(),
clientSecret: slackSecret
.secretValueFromJson('clientSecret')
.unsafeUnwrap()
.toString(),
// See https://api.slack.com/authentication/sign-in-with-slack#request
// > Which permissions you want the user to grant you.
// > Your app will request openid, the base scope you always need to request in any Sign in with Slack flow.
// > You may request email and profile as well.
scopes: ['openid', 'email', 'profile'],
// See https://api.slack.com/authentication/sign-in-with-slack#discover
issuerUrl: 'https://slack.com',
// The following endpoints do not need to be configured because the Cognito can find them by the issuer url.
// endpoints: {
// authorization: 'https://slack.com/openid/connect/authorize',
// token: 'https://slack.com/api/openid.connect.token',
// userInfo: 'https://slack.com/api/openid.connect.userInfo',
// jwksUri: 'https://slack.com/openid/connect/keys',
// },
attributeMapping: {
email: ProviderAttribute.other('email'),
profilePage: ProviderAttribute.other('profile'),
},
});
}
}

ステップ 3: スタックをデプロイする

デプロイする前に、プレースホルダーの環境変数を実際の値に置き換えます。

  • SLACK_SECRET_ARN: Slack の認証情報を含む Secrets Manager エントリの ARN。
  • COGNITO_DOMAIN_PREFIX: Cognito ドメイン用の一意なプレフィックス。

次のコマンドを実行してデプロイします。

Terminal window
export SLACK_SECRET_ARN=arn:aws:secretsmanager:<AWS_REGION>:<AWS_ACCOUNT_ID>:secret:sign-in-with-slack-<SUFFIX>
export COGNITO_DOMAIN_PREFIX=<ANY_PREFIX_YOU_LIKE>
npx cdk deploy

Slack アプリの OAuth 設定

Slack と Cognito の統合を仕上げる最後のステップとして、Slack アプリの OAuth 設定を行います。

ステップ 1: リダイレクト URL を設定する

Slack アプリの設定にある OAuth & Permissions ページに移動し、Cognito のリダイレクト URL を追加します。

  1. Add New Redirect URL ボタンを押します。
  2. 次の URL を入力し、<COGNITO_DOMAIN_PREFIX> を実際のドメインプレフィックスに置き換えます。
    https://<COGNITO_DOMAIN_PREFIX>.auth.ap-northeast-1.amazoncognito.com/oauth2/idpresponse
    Using OIDC identity providers with a user pool

    Register your user pool domain URL with the /oauth2/idpresponse endpoint with your OIDC IdP.

  3. Save URLs ボタンを押します。

Add Redirect URL

Cognito ドメインが分からない場合は、Cognito User Pool の App Integration タブで確認できます。

Cognito Domain

ステップ 2: Slack アプリをインストールする

Install to <WORKSPACE> ボタンをクリックし、プロンプトに従ってインストールを完了します。

Install Slack App Step 1

Install Slack App Step 2

Sign in with Slack のテスト

Cognito の App Client ページに移動し、View Hosted UI ボタンを押します。

View Hosted UI

Hosted UI で、Slack ボタンを押します。

Slack Button

ワークスペース名を入力し、Continue を押します。

Enter Workspace

Slack のログイン処理を完了し、Allow を押してアクセスを許可します。

Allow Access

Allow Access

ログインが成功すると、Cognito アプリクライアントで設定されたコールバック URL にリダイレクトされます。この例では、デフォルトは https://example.com/ です。

Callback URL

次のコマンドを実行して、連携されたユーザーが Cognito User Pool に追加されていることを確認します。

Terminal window
aws cognito-idp list-users \
--user-pool-id <COGNITO_USER_POOL_ID>

出力例:

{
"Users": [
{
"Username": "Slack_U07G7NBRPN2",
"Attributes": [
{
"Name": "email",
"Value": "<SLACK_USER_EMAIL>"
},
{
"Name": "email_verified",
"Value": "false"
},
{
"Name": "sub",
"Value": "<UUID>"
},
{
"Name": "identities",
"Value": "<SLACK_IDENTITIES>"
}
],
"UserCreateDate": "2024-08-12T15:12:47.047000+09:00",
"UserLastModifiedDate": "2024-08-12T15:12:47.047000+09:00",
"Enabled": true,
"UserStatus": "EXTERNAL_PROVIDER"
}
]
}

フロントエンドの構築

Cognito および Slack での認証に対応するよう、React/Next.js アプリケーションを設定します。

Dot Env 設定

Next.js アプリのルートディレクトリに .env.local ファイル(./my-app/.env.local)を作成し、次の値を追加します。

.env.local
NEXT_PUBLIC_USER_POOL_ID=<COGNITO_USER_POOL_ID>
NEXT_PUBLIC_USER_POOL_CLIENT_ID=<COGNITO_USER_POOL_CLIENT_ID>
NEXT_PUBLIC_OAUTH_DOMAIN=<COGNITO_DOMAIN_PREFIX>.auth.ap-northeast-1.amazoncognito.com
🔥 Caution

NEXT_PUBLIC_OAUTH_DOMAIN(3行目)がhttps://始まっていないことを確認してください。

Auth ヘルパーの作成

認証を管理するため、アプリの src ディレクトリに auth.ts./my-app/src/auth.ts)を作成し、次の関数を実装します。

  • スコープ:
    • fetchUserAttributes 関数を有効にするため、oauth.scopes には aws.cognito.signin.user.admin を含める必要があります(27 行目)。詳細は公式ドキュメントを参照してください。
  • リダイレクト URL:
    • oauth.redirectSignInoauth.redirectSignOut の値(31〜32 行目)が、末尾のスラッシュも含めて Cognito アプリクライアントの設定と完全に一致することを確認してください。
src/auth.ts
import { Amplify } from 'aws-amplify';
import {
type AuthSession,
fetchAuthSession,
fetchUserAttributes,
getCurrentUser,
signInWithRedirect,
signOut,
} from 'aws-amplify/auth';
import type { AuthConfig } from '@aws-amplify/core';
import type { AuthUser } from '@aws-amplify/auth';
import type { AuthUserAttributes } from '@aws-amplify/auth/dist/esm/types';
const authConfig: AuthConfig = {
Cognito: {
userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID ?? '',
userPoolClientId: process.env.NEXT_PUBLIC_USER_POOL_CLIENT_ID ?? '',
loginWith: {
oauth: {
// Ensure this does not start with "https://"
domain: process.env.NEXT_PUBLIC_OAUTH_DOMAIN ?? '',
scopes: [
'openid',
'email',
'profile',
// Required for the `fetchUserAttributes` function
'aws.cognito.signin.user.admin',
],
providers: [{ custom: 'Slack' }],
// Redirect URLs must match Cognito app client configuration exactly, including the trailing slash.
redirectSignIn: ['http://localhost:3000/'],
redirectSignOut: ['http://localhost:3000/'],
responseType: 'code',
},
},
},
};
Amplify.configure({ Auth: authConfig });
// Fetch the current session
export async function authSession(): Promise<AuthSession> {
return await fetchAuthSession();
}
// Fetch the currently authenticated user
export async function authCurrentUser(): Promise<AuthUser> {
return await getCurrentUser();
}
// Fetch user attributes
export async function fetchAttributes(): Promise<AuthUserAttributes> {
// Requires the 'aws.cognito.signin.user.admin' scope
return await fetchUserAttributes();
}
// Redirect to the Slack sign-in page
export async function authSignIn(): Promise<void> {
await signInWithRedirect({
provider: { custom: 'Slack' },
});
}
// Sign out the user
export async function authSignOut(): Promise<void> {
await signOut();
}

Home コンポーネント

ユーザー情報を表示し、サインインとサインアウトを処理する Home コンポーネントを実装します。次のコードで ./my-app/src/app/page.tsx ファイルを作成します。

  • 認証処理(20〜25行目):
    • 有効なセッショントークンを確認します。
    • セッションが見つからない場合はサインインを開始します。
    • メールアドレスやユーザー名などのユーザー属性を取得します。
  • サインアウト機能(49〜55行目):
    • サインアウト操作を処理するボタンを含みます。
  • ユーザー情報(33〜46行目):
    • ユーザー情報を表示します。
src/app/page.tsx
'use client';
import { useEffect, useState } from 'react';
import {
authSession,
authSignOut,
authSignIn,
authCurrentUser,
fetchAttributes,
} from '@/auth';
import type { AuthUser } from '@aws-amplify/auth';
import type { AuthUserAttributes } from '@aws-amplify/auth/dist/esm/types';
export default function Home() {
const [user, setUser] = useState<AuthUser>();
const [attributes, setAttributes] = useState<AuthUserAttributes>();
useEffect(() => {
(async () => {
const session = await authSession();
if (session.tokens) {
setUser(await authCurrentUser());
setAttributes(await fetchAttributes());
} else {
await authSignIn();
}
})();
}, []);
return (
<div className="flex flex-col items-center w-full h-screen max-w-screen-md mx-auto mt-8">
<div className="flex flex-col gap-4">
<div className="flex gap-2">
<div className="w-20">Username:</div>
<div>{user?.username}</div>
</div>
<div className="flex gap-2">
<div className="w-20">User ID:</div>
<div>{user?.userId}</div>
</div>
<div className="flex gap-2">
<div className="w-20">Email:</div>
<div>{attributes?.email}</div>
</div>
<div className="self-end">
<button
type="button"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={authSignOut}
>
Sign out
</button>
</div>
</div>
</div>
);
}

Next.js アプリのテスト

ローカル開発サーバーを起動します。

Terminal window
npm run dev
> next dev
Next.js 14.2.5
- Local: http://localhost:3000
- Environments: .env.local
Starting...
Ready in 1507ms

ブラウザを開き、http://localhost:3000 にアクセスします。Slack のサインインページにリダイレクトされるはずです。サインインに成功すると、アプリがユーザー情報を表示します。

App Page

まとめ

OIDC 経由で Cognito user pool を Slack と連携させ、Amplify で Next.js アプリに組み込んだことで、「Sign in with Slack」がエンドツーエンドで動作し、連携されたユーザーが Cognito user pool に表示されるようになりました。

この連携では、2 種類のスコープを別々に扱います。Cognito は上流の OIDC プロバイダーである Slack に openidemailprofile を要求します。一方、Next.js アプリケーションは Cognito の認可サーバーに対して、ユーザープールのセルフサービス API に必要な aws.cognito.signin.user.admin などを要求します。Slack の OIDC 用ではない users:read スコープは不要であり、同じ Sign in with Slack の認可リクエストに混在させることもできません。

Slack のクライアントシークレットを Secrets Manager に保存し、slackSecretArn を通じて参照すれば、CDK のソースコードへ認証情報を直接書かずに済みます。通常の認証情報管理と同様に、合成したテンプレート、デプロイログ、シークレットへのアクセスも制限してください。

About the author

Takahiro Iwasa

Takahiro Iwasa

Software Developer

This blog shares technical notes from hands-on projects—architecture, implementation, and AWS service integrations.