Sign in with Slack Using Cognito User Pools

Sign in with Slack Using Cognito User Pools

Takahiro Iwasa
Takahiro Iwasa
13 min read
Cognito OIDC

Introduction

Amazon Cognito User Pools enable federated authentication through OpenID Connect (OIDC). This means your application users can log in with their preferred third-party accounts.

In this post, we’ll walk you through the process of setting up Slack as an OpenID Connect provider for Amazon Cognito. Using AWS CDK, you’ll build the backend, and with React/Next.js and AWS Amplify, you’ll create the frontend for the “Sign in with Slack” feature. You can clone the full example from this GitHub repository.

Project Structure

Here’s the file structure you’ll build in this post:


|-- 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

Getting Started

Bootstrapping AWS CDK Environment

First, bootstrap your AWS CDK environment. If you’ve already done this, you can skip this step.

Run the following commands to install AWS CDK locally and bootstrap your environment:

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

Initializing a CDK Project

Create a directory for your CDK project and initialize it:

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

Installing React/Next.js

To create the frontend for your application, install React/Next.js by running the following command:

npx create-next-app@latest

During the setup, answer the prompts as follows:

  • 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

This will initialize a Next.js app with the necessary dependencies.

Installing AWS Amplify

AWS Amplify simplifies the interaction with Amazon Cognito, allowing you to manage authentication with ease.

Install Amplify in your project directory:

cd my-app
npm i aws-amplify

Once installed, you are ready to integrate AWS Amplify into your React/Next.js application for authentication and user management.

Building Backend

Creating a Slack App

To enable Slack authentication with Cognito User Pools, you’ll need to create a new Slack app. Follow these steps:

1. Navigate to Manage Apps

Open your Slack workspace menu and go to Manage Apps.

Manage Apps

2. Build a New App

On the Slack app directory page, press the Build button.

Build New App

3. Create an App

On your apps page, click the Create an App button.

Create App

4. Choose App Manifest Option

When prompted, select the From an App Manifest option. This simplifies the process of creating and configuring your Slack app.

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

App Manifest Option

5. Select Your Workspace

Choose the workspace where your app will be installed.

Select Workspace

6. Name Your App

Provide a name for your app. In this post, we use Sign in with Slack. Leave the other fields as default.

Name Your App

7. Complete the Creation Process

Press the Create button to finalize the setup of your Slack app.

Finalize Creation

Your Slack app is now ready to be configured for integration with Amazon Cognito.

Checking Slack App Credentials

To integrate your Slack app with Cognito User Pools, you will need to retrieve and store the app credentials securely. Follow these steps:

1. Retrieve Slack App Credentials

Navigate to your Slack app’s Basic Information page and locate the following values:

  • Client ID
  • Client Secret

These credentials will be used to configure the Cognito User Pool later.

Slack App Credentials

2. Store Credentials in AWS Secrets Manager

To enhance security, store the Client ID and Client Secret in AWS Secrets Manager instead of embedding them directly in your AWS CDK code.

Run the following command to create a new secret in AWS Secrets Manager:

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

Replace <YOUR_CLIENT_ID> and <YOUR_CLIENT_SECRET> with the actual values retrieved from your Slack app.

Your Slack app credentials are now securely stored and ready to be referenced in the Cognito User Pool configuration.

Creating Cognito User Pool

To integrate Slack with Cognito, you’ll configure a Cognito User Pool and link it with the Slack app credentials stored in AWS Secrets Manager.

Step 1: Update cdk/bin/cdk.ts

Add the following code to the cdk/bin/cdk.ts file to pass the required environment variables (SLACK_SECRET_ARN and COGNITO_DOMAIN_PREFIX) at runtime:

#!/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 ?? '',
});

Step 2: Configure the Cognito User Pool in cdk/lib/cdk-stack.ts

Paste the following code into cdk/lib/cdk-stack.ts.

Key points:

  1. Include OAuthScope.COGNITO_ADMIN in the UserPoolClient.oAuth.scopes (line 55) for fetching user attributes. For more information, see the official documentation.
  2. Securely retrieve Slack credentials from AWS Secrets Manager. (line 65-69)
  3. Ensure Slack credentials are injected without exposing them in the code. (line 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'),
      },
    });
  }
}

Step 3: Deploy the Resources

Before deploying, replace the placeholder environment variables with actual values:

  • SLACK_SECRET_ARN: ARN of the Secrets Manager entry containing Slack credentials.
  • COGNITO_DOMAIN_PREFIX: A unique prefix for the Cognito domain.

Run the following commands to deploy:

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

Your Cognito User Pool is now ready and configured to work with Slack as an OIDC provider.

Configuring Slack App OAuth

To finalize the integration between Slack and Cognito, configure your Slack app’s OAuth settings.

Step 1: Configure Redirect URL

Navigate to the OAuth & Permissions page in your Slack app settings. Add the Cognito redirect URL:

  1. Press the Add New Redirect URL button.
  2. Enter the following URL, replacing <COGNITO_DOMAIN_PREFIX> with your actual domain prefix:
    https://<COGNITO_DOMAIN_PREFIX>.auth.ap-northeast-1.amazoncognito.com/oauth2/idpresponse
    
  3. Press the Save URLs button.

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

Add Redirect URL

If you’re unsure of your Cognito domain, locate it in the App Integration tab of the Cognito User Pool:

Cognito Domain

Step 2: Add OAuth Scopes

In the OAuth & Permissions page, add the required scope:

  1. Under User Token Scopes, add users:read.
  2. Save your changes.

Add OAuth Scopes

Step 3: Install the Slack App

  1. Click the Install to <WORKSPACE> button.
  2. Follow the prompts to complete the installation.

Install Slack App Step 1

Install Slack App Step 2

Testing Sign in with Slack

Verify that the integration works by testing the “Sign in with Slack” flow:

Step 1: Access the Cognito Hosted UI

  1. Go to the Cognito App Client page.
  2. Press the View Hosted UI button.

View Hosted UI

Step 2: Initiate Slack Login

  1. On the Hosted UI, press the Slack button.

Slack Button

  1. Enter your workspace name and press Continue.

Enter Workspace

  1. Complete the Slack login process and press Allow to grant access.

Allow Access

Allow Access

Step 3: Verify the Callback URL

After successful login, you will be redirected to the callback URL configured in your Cognito app client. For this guide, the default is https://example.com/.

Callback URL

Step 4: Confirm Federated User in Cognito

Run the following command to confirm that the federated user is added to your Cognito User Pool:

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

Sample output:

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

Your “Sign in with Slack” integration is now fully configured and operational.

Building React/Next.js App

This section guides you through configuring your React/Next.js application for authentication with Cognito and Slack.

Dot Env Configuration

Create an .env.local file in the root directory of your Next.js app (./my-app/.env.local) and add the following values:

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

Replace the placeholders (<COGNITO_USER_POOL_ID>, <COGNITO_USER_POOL_CLIENT_ID>, <COGNITO_DOMAIN_PREFIX>) with the actual values from your Cognito setup.

Creating Auth Helper

To manage authentication, create a file named auth.ts in the src directory of your app (./my-app/src/auth.ts) and implement the following functions:

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();
}

Key Points:

  • Scopes:
  • Redirect URLs:
    • Ensure oauth.redirectSignIn and oauth.redirectSignOut values (lines 31-32) exactly match the URLs configured in your Cognito app client, including trailing slashes.

Home Component

To display user information and handle sign-in and sign-out actions, implement the Home component. Create the ./my-app/src/app/page.tsx file with the following code:

'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>
  );
}

Key Features:

  • Authentication Handling (lines 20-25):
    • Checks for valid session tokens.
    • Initiates sign-in if no session is found.
    • Fetches user attributes like email and username.
  • Sign-Out Functionality (lines 49-55):
    • Includes a button to handle sign-out actions.
  • User Information (lines 33-46):
    • Displays user information in a structured format using Tailwind CSS.

Testing Next.js App

To test the app, start the local development server:

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. Open your browser and navigate to http://localhost:3000.
  2. You should be redirected to the Slack sign-in page.
  3. Complete the sign-in process.
  4. Upon successful sign-in, you will see the app displaying user details.

App Page

Conclusion

The “Sign in with Slack” feature is a powerful tool for improving user experience (UX) in enterprise applications. By integrating Slack with Amazon Cognito, you offload complex authentication logic and focus on building your application’s core functionality.

Key Benefits:

  • Seamless User Experience: Users can authenticate using their Slack accounts.
  • Scalability: Cognito handles authentication workflows at scale.
  • Enhanced Security: Utilize AWS’s secure infrastructure for identity management.

For authentication with Azure Entra ID, explore the following post.

Happy Coding! 🚀

Takahiro Iwasa

Takahiro Iwasa

Software Developer at KAKEHASHI Inc.
Involved in the requirements definition, design, and development of cloud-native applications using AWS. Now, building a new prescription data collection platform at KAKEHASHI Inc. Japan AWS Top Engineers 2020-2023.