Sign in with Slack using Cognito User Pools

Sign in with Slack using Cognito User Pools

Takahiro Iwasa
Takahiro Iwasa
11 min read
Cognito OIDC

Amazon Cognito User Pools provides a feature to federate with OpenID Connect. This allows your application users to sign in using their third-party accounts.

Slack can be used as an OpenID Connect provider for Cognito, which is built on top of OAuth 2.0.

This post shows you how to configure Cognito User Pools and Slack using AWS CDK and how to build a React/Next.js application using AWS Amplify. You can clone the example from my GitHub repository.

Overview

Project Structure

This post creates the following project structure.

/
|-- 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
|-- package.json
`-- package-lock.json

Getting Started

Bootstrapping AWS CDK Environment

First of all, bootstrap your AWS CDK environment.

In this post, we install AWS CDK locally to avoid polluting the global npm environment.

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

Initializing CDK Project

To initialize your CDK project, run the following command.

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

Installing React/Next.js

To install React/Next.js, run the following command.

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

Installing AWS Amplify

AWS Amplify allows us to interact easily with Cognito. Run the following command to install.

cd my-app
npm i aws-amplify

Building Backend

Creating Slack App

Create a new Slack app to interact with Cognito User Pool according to the following steps.

Open your workspace menu and navigate to the Manage apps.

Press the Build button on the Slack app directory page.

Press the Create an App button on your apps page.

In this post, choose the From an app manifest.

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

Choose your workspace.

Enter the app name which you like. In this post, Sign in with Slack is used. Leave the other fields default.

Finish the step to press the Create button.

Checking Slack App Credentials

The Slack app credentials are needed for a Cognito user pool which will be created later. Check the following values.

  • Client ID
  • Client Secret

To store those credentials in your Secrets Manager, run the following command.

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

Creating Cognito User Pool

Paste the following code to cdk/bin/cdk.ts. The environment variables SLACK_SECRET_ARN (line 8) and COGNITO_DOMAIN_PREFIX (line 9) are passed 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 ?? '',
});

Then, paste the following code to cdk/lib/cdk-stack.ts.

The key points here are:

  • That the UserPoolClient.oAuth.scopes must include OAuthScope.COGNITO_ADMIN (line 55) to use the Amplify fetchUserAttributes function.
  • That the Slack app credentials are looked up for from the Secrets Manager secret (line 65-69) which has been created above.
  • That the Slack app credentials are set without them exposed (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'),
      },
    });
  }
}

Finally, deploy the resources to your AWS. Before running the following command, replace SLACK_SECRET_ARN and COGNITO_DOMAIN_PREFIX with your actual values.

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

Configuring Slack App OAuth

Redirect URL

Navigate to the OAuth & Permissions page, and configure your Slack app OAuth.

Press the Add New Redirect URL button, enter the following value, and press the Save URLs button to complete.

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.

If you do not know your cognito domain, you can find it in the Cognito App Integration tab.

Scopes

Add the OAuth scope users:read to User Token Scopes.

Install Slack App

Finally, to install your Slack app, press the Install to <WORKSPACE> button.

Testing Sign in with Slack

You can test the sign in with Slack feature using the Cognito Hosted UI.

Go to the Cognito app client page, and press the View Hosted UI button.

Press the Slack button.

Enter your workspace name and press the Continue button.

Complete the sign-in process to the Slack workspace.

Press the Allow button to proceed.

You will be redirected to the callback url which can be configured in the Cognito app client.

In this time, the URL https://example.com/ is used by default.

To confirm the federated user, run the following command. You would see the user federated to your 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"
        }
    ]
}

Building React/Next.js App

Dot Env

Create the ./my-app/.env.local with the following values, and replace those with your actual ones.

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

Create auth helper functions and save them to the ./my-app/src/auth.ts.

The key points here are:

  • That the oauth.scopes must include aws.cognito.signin.user.admin (line 27) if you are going to use the fetchUserAttributes function.
  • That the oauth.redirectSignIn and oauth.redirectSignOut (line 31-32) must completely match the Cognito app client configuration including the trailing slash.
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 Component

Paste the following code to the ./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>
  );
}

Testing Next.js App

To launch the local server, run the following command.

npm run dev

> [email protected] dev
> next dev

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

 ✓ Starting...
 ✓ Ready in 1507ms

Accessing to it, you would see the Slack sign-in page. After signing in successfully, you would see the app page displaying the user information.

Conclusion

More and more enterprises have introduced Slack for their employees and customers. The “Sign in with Slack” feature can significantly improve your application’s user experience (UX).

By using Amazon Cognito, developers can avoid implementing complex authentication and authorization logic. This allows them to focus more on their applications, delivering more value to users.

If you want to implement authentication using AWS Amplify with Amazon Cognito and Azure Entra ID (Azure AD), you might also like the following post.

I hope you will find this post useful.

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.