Slack でのサインインを Cognito ユーザープールで実現する方法

Slack でのサインインを Cognito ユーザープールで実現する方法

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

はじめに

Amazon Cognitoユーザープールは、OpenID Connect (OIDC) を使用したフェデレーション認証を可能にします。これにより、アプリケーションのユーザーは、好きなサードパーティアカウントでログインできます。

この投稿では、Slack を Amazon Cognito の OpenID Connect プロバイダーとして設定するプロセスを解説します。AWS CDK を使用してバックエンドを構築し、React/Next.jsAWS Amplify を使用して、Sign in with Slack 機能のフロントエンドを作成します。完全な例は この 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 環境のブートストラップ

まず、AWS CDK 環境をブートストラップします。このステップを既に終えている場合はスキップできます。

以下のコマンドを実行して 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

セットアップ中、以下のプロンプトに答えてください:

  • Project name: my-app
  • Use TypeScript: Yes
  • Use ESLint: No
  • Use Tailwind CSS: Yes
  • Use src/ directory: Yes
  • Use App Router: Yes
  • Customize default import alias (@/*): No

これで必要な依存関係を含む Next.js アプリが初期化されます。

AWS Amplify のインストール

AWS Amplify は、Amazon Cognito とのやり取りを簡単にし、認証を効率的に管理することができます。

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

cd my-app
npm i aws-amplify

インストールが完了したら、React/Next.js アプリに AWS Amplify を統合して、認証とユーザー管理を行えるようになります。

バックエンドの構築

Slack アプリの作成

Slack の認証を Cognito ユーザープールで有効にするために、新しい Slack アプリを作成する必要があります。以下の手順に従ってください:

1. アプリ管理画面を開く

Slack ワークスペースのメニューを開き、Manage Apps に進みます。

Manage Apps

2. 新しいアプリを作成する

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

Build New App

3. アプリを作成する

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

Create App

4. アプリマニフェストを選択

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

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

App Manifest Option

5. ワークスペースを選択

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

Select Workspace

6. アプリに名前を付ける

アプリに名前を付けます。この投稿では Sign in with Slack を使用します。他のフィールドはデフォルトのままにします。

Name Your App

7. 作成プロセスを完了する

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

Finalize Creation

これで、Amazon Cognito との統合のために Slack アプリが準備されました。

Slack アプリの資格情報を確認

Slack アプリを Cognito ユーザープールと統合するには、アプリの資格情報を取得して安全に保存する必要があります。以下の手順に従ってください:

1. Slack アプリ資格情報を取得する

Slack アプリの 基本情報 ページに移動し、以下の値を見つけてください:

  • クライアント ID
  • クライアントシークレット

これらの資格情報は後で Cognito ユーザープールを設定する際に使用します。

Slack App Credentials

2. AWS Secrets Manager に資格情報を保存する

セキュリティを強化するため、クライアント IDクライアントシークレット を AWS Secrets Manager に保存し、AWS CDK コードに直接埋め込まないようにします。

以下のコマンドを実行して AWS Secrets Manager に新しいシークレットを作成します:

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

<YOUR_CLIENT_ID><YOUR_CLIENT_SECRET> を Slack アプリから取得した実際の値に置き換えてください。

これで、Slack アプリ資格情報が安全に保存され、Cognito ユーザープールの設定で参照可能になりました。

Cognito ユーザープールの作成

Slack を Cognito と統合するには、Cognito ユーザープールを設定し、AWS Secrets Manager に保存した Slack アプリ資格情報をリンクします。

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

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

#!/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 ユーザープールを設定

cdk/lib/cdk-stack.ts に以下のコードを貼り付けます。

ポイント:

  1. ユーザー属性を取得するために UserPoolClient.oAuth.scopesOAuthScope.COGNITO_ADMIN を含めます(55行目)。詳細は 公式ドキュメント をご参照ください。
  2. AWS Secrets Manager から Slack の資格情報を安全に取得します(65-69行目)。
  3. 資格情報がコード内に露出しないようにします(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'),
      },
    });
  }
}

ステップ 3: リソースのデプロイ

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

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

以下のコマンドを実行してデプロイします:

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

これで、Cognito ユーザープールが準備され、Slack を OIDC プロバイダーとして利用できるようになりました。

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
    
  3. Save URLs ボタンを押します。

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

Add Redirect URL

Cognito ドメインが不明な場合は、Cognito ユーザープールの App Integration タブで確認してください:

Cognito Domain

ステップ 2: OAuth スコープを追加

Slack アプリの OAuth & Permissions ページで必要なスコープを追加します:

  1. User Token Scopesusers:read を追加します。
  2. 変更を保存します。

Add OAuth Scopes

ステップ 3: Slack アプリのインストール

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

Install Slack App Step 1

Install Slack App Step 2

Slackでのサインインのテスト

Slack でのサインインのフローをテストして、統合が正常に動作することを確認します。

ステップ 1: Cognito ホステッド UI にアクセス

  1. Cognito の アプリクライアント ページに移動します。
  2. View Hosted UI ボタンを押します。

View Hosted UI

ステップ 2: Slack ログインを開始

  1. ホステッド UI 上で Slack ボタンを押します。

Slack Button

  1. ワークスペース名を入力して Continue を押します。

Enter Workspace

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

Allow Access

Allow Access

ステップ 3: コールバック URL を確認

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

Callback URL

ステップ 4: Cognito 内でのフェデレートユーザーの確認

以下のコマンドを実行して、フェデレートユーザーが Cognito ユーザープールに追加されていることを確認します:

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"
    }
  ]
}

これで、Sign in with Slack が完全に設定され、正常に動作するようになりました。

React/Next.js アプリの構築

このセクションでは、Cognito と Slack を使用した認証のために React/Next.js アプリケーションを設定する方法を説明します。

環境変数の設定

Next.js アプリのルートディレクトリに .env.local ファイルを作成し、以下の値を追加します (./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

プレースホルダー(<COGNITO_USER_POOL_ID><COGNITO_USER_POOL_CLIENT_ID><COGNITO_DOMAIN_PREFIX>)を、Cognito セットアップから取得した実際の値に置き換えてください。

認証ヘルパーの作成

認証を管理するために、アプリの src ディレクトリに auth.ts ファイルを作成し、以下の関数を実装します (./my-app/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();
}

ポイント:

  • スコープ:
    • oauth.scopesaws.cognito.signin.user.admin(27行目)を含める必要があります。これにより、fetchUserAttributes 関数が利用できます。
    • 詳細は Cognito リソースサーバードキュメント をご参照ください。
  • リダイレクト URL:
    • oauth.redirectSignInoauth.redirectSignOut の値(31-32行目)は、Cognito アプリクライアントに設定された URL と 完全に一致 している必要があります(末尾のスラッシュを含む)。

ホームコンポーネント

ユーザー情報を表示し、サインインとサインアウトアクションを処理するために 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>
  );
}

主要な機能:

  • 認証処理(20-25行目):
    • 有効なセッショントークンをチェックします。
    • セッションがない場合はサインインを開始します。
    • Email やユーザー名などのユーザー属性を取得します。
  • サインアウト機能(49-55行目):
    • サインアウトアクションを処理するボタンを含みます。
  • ユーザー情報(33-46行目):
    • Tailwind CSS を使用して、構造化された形式でユーザー情報を表示します。

Next.js アプリのテスト

アプリをテストするには、ローカル開発サーバーを起動します:

npm run dev

> my-app@0.1.0 dev
> next dev

  ▲ Next.js 14.2.5
  - Local:        http://localhost:3000
  - Environments: .env.local

 ✓ Starting...
 ✓ Ready in 1507ms
  1. ブラウザで http://localhost:3000 にアクセスします。
  2. Slack サインインページにリダイレクトされるはずです。
  3. サインインプロセスを完了します。
  4. サインインが成功すると、ユーザー名、ユーザーID、Email がアプリに表示されます。

App Page

まとめ

“Sign in with Slack” は、エンタープライズアプリケーションにおけるユーザー体験 (UX) を向上させる強力なツールです。Slack を Amazon Cognito と統合することで、複雑な認証ロジックをオフロードし、アプリケーションのコア機能に集中できます。

主なメリット:

  • シームレスなユーザー体験: ユーザーが Slack アカウントを使用して認証可能。
  • スケーラビリティ: Cognito が認証ワークフローを大規模に処理。
  • セキュリティの向上: AWS の安全なインフラストラクチャを活用。

Azure Entra ID を使用した認証については、以下の記事をご覧ください:

Happy Coding! 🚀

岩佐 孝浩

岩佐 孝浩

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