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経由の汎用アイデンティティプロバイダーとして連携させることで、“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を使うと、Cognitoとのやり取りが簡素化され、認証を簡単に管理できます。

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

Terminal window
cd my-app
npm i aws-amplify

バックエンドの構築

Slackアプリの作成

Cognito User PoolsでSlack認証を有効にするには、新しいSlackアプリを作成する必要があります。

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

Manage Apps

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

Build New App

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

Create App

プロンプトが表示されたら、From an app manifestオプションを選択します。これにより、Slackアプリの作成と設定を簡単に行えます。

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 User Poolsを統合するには、アプリの認証情報を取得し、安全に保管する必要があります。

ステップ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: OAuthスコープを追加する

OAuth & Permissionsページで、User Token Scopesの下に必要なスコープusers:readを追加します。変更を保存して完了します。

Add OAuth Scopes

ステップ3: 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に表示されるようになりました。CognitoはSlackを名前付きのソーシャルIDプロバイダーではなく汎用的なOIDCプロバイダーとして扱うため、この統合の要点は2つの一致に集約されます。1つはaws.cognito.signin.user.adminusers:readスコープがそれぞれの側で一致していること、もう1つはSlackのOAuth設定とCognitoアプリクライアントの間でリダイレクトURLが末尾のスラッシュも含めて一致していることです。Slackのクライアントシークレットを、CDKスタックにハードコーディングするのではなくSecrets Managerに保存しslackSecretArn経由で注入するという方法は、この特定の統合にとどまらず持ち帰る価値のある実践です。これにより、どのIDプロバイダーと連携していても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.