Cognito User Pools を利用した Sign in with Slack

Cognito User Pools を利用した Sign in with Slack

岩佐 孝浩
岩佐 孝浩
13 min read
Cognito OIDC

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.scopesOAuthScope.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_ARNCOGNITO_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_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.redirectSignInoauth.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 を使用した認証を実装したい場合、次の投稿もご参考いただければと思います。

この投稿が、お役に立てば幸いです。

岩佐 孝浩

岩佐 孝浩

Software Developer at KAKEHASHI Inc.
AWS を活用したクラウドネイティブ・アプリケーションの要件定義・設計・開発に従事。 株式会社カケハシで、処方箋データ収集の新たな基盤の構築に携わっています。 Japan AWS Top Engineers 2020-2023