Cognito User Pools を利用した Sign in with Slack
Amazon Cognito User Pools は、 OpenID Connect 連携機能を提供しています。 アプリケーションのユーザーは、サードパーティのアカウントを利用してサインインできます。
Slack は Cognito の OpenID Connect Provider として利用でき、 OAuth 2.0 で構築されています。
この投稿では、 AWS CDK を利用して Cognito User Pools と Slack を設定する方法、および、 AWS Amplify を利用して React/Next.js アプリケーションを構築する方法を紹介します。 私の GitHub リポジトリから、サンプルをクローンできます。
概要
プロジェクト構造
この記事では、以下のプロジェクト構造を生成します。
/
|-- cdk/
| |-- bin/
| | `-- cdk.ts
| |-- lib/
| | `-- cdk-stack.ts
| |-- node_modules/
| |-- test/
| | `-- cdk.test.ts
| |-- .gitignore
| |-- .npmignore
| |-- biome.json
| |-- cdk.json
| |-- jest.config.js
| |-- package.json
| |-- package-lock.json
| |-- README.md
| `-- tsconfig.json
|-- my-app/
| |-- node_modules/
| |-- public/
| |-- src/
| | |-- app/
| | | |-- globals.css
| | | |-- layout.tsx
| | | `-- page.tsx
| | `-- auth.ts
| |-- .env.local
| |-- .gitignore
| |-- biome.json
| |-- next.config.mjs
| |-- next-env.d.ts
| |-- package.json
| |-- package-lock.json
| |-- postcss.config.mjs
| |-- README.md
| |-- tailwind.config.ts
| `-- tsconfig.json
|-- node_modules/
|-- .gitignore
|-- LICENSE
|-- package.json
`-- package-lock.json
はじめに
AWS CDK 環境の Bootstrapping
最初に、 AWS CDK 環境を Bootstrap してください。
この記事では、 Global の npm 環境を汚さないよう、ローカルで AWS CDK をインストールします。
npm i -D aws-cdk
npx cdk bootstrap aws://<AWS_ACCOUNT_ID>/<AWS_REGION>
CDK プロジェクト初期化
CDK プロジェクトを初期化するために、以下のコマンドを実行してください。
mkdir cdk && cd cdk
npx cdk init app --language typescript
React/Next.js インストール
React/Next.js をインストールするために、以下のコマンドを実行してください。
npx create-next-app@latest
✔ What is your project named? … my-app
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … No
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias (@/*)? … No
Creating a new Next.js app in sign-in-with-slack-using-cognito-user-pools/my-app.
Using npm.
Initializing project with template: app-tw
Installing dependencies:
- react
- react-dom
- next
Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- postcss
- tailwindcss
added 138 packages, and audited 139 packages in 17s
31 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Success! Created my-app at sign-in-with-slack-using-cognito-user-pools/my-app
AWS Amplify インストール
AWS Amplify を利用することで、 Cognito と簡単にやり取りできます。 以下のコマンドでインストールしてください。
cd my-app
npm i aws-amplify
バックエンド構築
Slack App 作成
下記の手順に従って、 Cognito User Pool と連携するための新規 Slack app を作成してください。
ワークスペースのメニューを開き、 Manage apps
に移動します。
Slack app directory ページで、 Build
ボタンを押します。
App ページで、 Create an App
ボタンを押します。
この投稿では、 From an app manifest
を選択します。
Use the app manifest system to quickly create, configure, and reuse Slack app configurations.
ワークスペースを選択します。
任意の App 名を入力します。ここでは、 Sign in with Slack
とします。
他のフィールドは、デフォルトのままにしてください。
Create
ボタンを押して完了してください。
Slack App 認証情報
後で作成する Cognito User Pool のために、 Slack App 認証情報が必要です。次の値を確認してください。
- Client ID
- Client Secret
Secrets Manager に認証情報を保管するため、以下のコマンドを実行します。
aws secretsmanager create-secret \
--name sign-in-with-slack \
--secret-string '{"clientId": "<YOUR_CLIENT_ID>", "clientSecret": "<YOUR_CLIENT_SECRET>"}'
Cognito User Pool 作成
次のコードを cdk/bin/cdk.ts
にペーストしてください。
環境変数の SLACK_SECRET_ARN
(行8) と COGNITO_DOMAIN_PREFIX
(行9) は、実行時にパスします。
#!/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 ?? '',
});
続いて、下記のコードを cdk/lib/cdk-stack.ts
にペーストしてください。
重要なポイントは、
- Amplify の
fetchUserAttributes
関数を使用する場合、UserPoolClient.oAuth.scopes
はOAuthScope.COGNITO_ADMIN
(行55) を含まなければなりません。- 詳細は、公式ドキュメントをご参照ください。
- 上で作成した Secrets Manager のシークレット (行65-69) から、Slack App 認証情報が取得されます。
- Slack App 認証情報は、公開されることなくセットされます。 (行75-82)
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'),
},
});
}
}
最後に、リソースを AWS へデプロイします。
以下のコマンドを実行する前に、 SLACK_SECRET_ARN
と COGNITO_DOMAIN_PREFIX
を実際の値で置換してください。
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
✨ Synthesis time: 3.98s
CdkStack: start: Building 3cbdfe791ed2ca2eec7e43ea613065781d765bb7b2597870810a2b4b38e03f6a:current_account-current_region
CdkStack: success: Built 3cbdfe791ed2ca2eec7e43ea613065781d765bb7b2597870810a2b4b38e03f6a:current_account-current_region
CdkStack: start: Publishing 3cbdfe791ed2ca2eec7e43ea613065781d765bb7b2597870810a2b4b38e03f6a:current_account-current_region
CdkStack: success: Published 3cbdfe791ed2ca2eec7e43ea613065781d765bb7b2597870810a2b4b38e03f6a:current_account-current_region
CdkStack: deploying... [1/1]
CdkStack: creating CloudFormation changeset...
✅ CdkStack
✨ Deployment time: 16.72s
Stack ARN:
arn:aws:cloudformation:<AWS_REGION>:<AWS_ACCOUNT_ID>:stack/CdkStack/<UUID>
✨ Total time: 20.7s
Slack App OAuth 設定
Redirect URL
OAuth & Permissions
ページに進み、 Slack App OAuth を設定します。
Add New Redirect URL
ボタンを押して、下記の値を入力し、 Save URLs
ボタンを押して完了してください。
https://<COGNITO_DOMAIN_PREFIX>.auth.ap-northeast-1.amazoncognito.com/oauth2/idpresponse
Register your user pool domain URL with the
/oauth2/idpresponse
endpoint with your OIDC IdP.
Cognito ドメインが不明な場合、 Cognito App Integration タブに記載されています。
スコープ
OAuth スコープ users:read
を User Token Scopes に追加します。
Slack App インストール
最後に、 Install to <WORKSPACE>
ボタンを押して Slack App をインストールしてください。
Sign in with Slack テスト
Cognito Hosted UI を利用して、 Sign in with Slack 機能をテストできます。
Cognito app client ページに進み、 View Hosted UI
ボタンを押してください。
Slack
ボタンを押します。
ワークスペース名を入力し、 Continue
ボタンを押します。
Slack ワークスペースへのサインインを完了してください。
Allow
ボタンを押して、次に進みます。
Cognito app client で設定可能な Callback URL にリダイレクトされます。
ここでは、デフォルトの https://example.com/
にリダイレクトされています。
連携されたユーザーを確認するために、次のコマンドを実行してください。 ユーザーが Cognito User Pool に連携されています。
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"
}
]
}
React/Next.js App 構築
Dot Env
下記の内容で ./my-app/.env.local
を作成し、実際の値で置換してください。
NEXT_PUBLIC_OAUTH_DOMAIN
(行3) は、 https://
で始めないよう、ご注意ください。 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
Auth Helper
Auth helper 関数を作成して、 ./my-app/src/auth.ts
に保存します。
重要なポイントは、
fetchUserAttributes
関数を利用予定の場合、oauth.scopes
は、aws.cognito.signin.user.admin
(行27) を含まなければなりません。- 詳細は、公式ドキュメントをご参照ください。
oauth.redirectSignIn
とoauth.redirectSignOut
(行31-32) は、末尾のスラッシュを含めて、 Cognito app client の設定に完全に一致しなければなりません。
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: {
// DO NOT START WITH `https://`
domain: process.env.NEXT_PUBLIC_OAUTH_DOMAIN ?? '',
scopes: [
'openid',
'email',
'profile',
// Needed for the `fetchUserAttributes` function
'aws.cognito.signin.user.admin',
],
providers: [{ custom: 'Slack' }],
// URLs must completely match the Cognito app client configuration including the trailing slash.
redirectSignIn: ['http://localhost:3000/'],
redirectSignOut: ['http://localhost:3000/'],
responseType: 'code',
},
},
},
};
Amplify.configure({ Auth: authConfig });
export async function authSession(): Promise<AuthSession> {
return await fetchAuthSession();
}
export async function authCurrentUser(): Promise<AuthUser> {
return await getCurrentUser();
}
export async function fetchAttributes(): Promise<AuthUserAttributes> {
// This requires the `aws.cognito.signin.user.admin` scope.
return await fetchUserAttributes();
}
export async function authSignIn(): Promise<void> {
await signInWithRedirect({
provider: { custom: 'Slack' },
});
}
export async function authSignOut(): Promise<void> {
await signOut();
}
Home コンポーネント
下記のコードを ./my-app/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 App テスト
ローカルサーバーを起動するために、下記のコマンドを実行してください。
npm run dev
> [email protected] dev
> next dev
▲ Next.js 14.2.5
- Local: http://localhost:3000
- Environments: .env.local
✓ Starting...
✓ Ready in 1507ms
アクセスすると、 Slack のサインインページが表示されるはずです。 サインインに成功すると、アプリのページにユーザーの情報が表示されます。
まとめ
多くの企業が、従業員と顧客のために Slack を導入しており、 Sign in with Slack の機能は、アプリケーションの UX を向上させます。
Amazon Cognito を利用すると、開発者は認証・認可の複雑なロジックを実装する必要はありません。 アプリケーションにもっと集中できるようになり、ユーザーにより多くの価値を提供できます。
Amazon Cognito と Azure Entra ID (Azure AD) で AWS Amplify を使用した認証を実装したい場合、次の投稿もご参考いただければと思います。
この投稿が、お役に立てば幸いです。