How to Integrate “Sign in with Slack” Using AWS Cognito User Pools

This note describes how to federate Cognito user pools with Slack using OIDC to implement “Sign in with Slack”.
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-cdknpx cdk bootstrap aws://<AWS_ACCOUNT_ID>/<AWS_REGION>
Initializing CDK Project
Create a directory for your CDK project and initialize it:
mkdir cdk && cd cdknpx 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
Installing AWS Amplify
AWS Amplify simplifies the interaction with Cognito, allowing you to manage authentication with ease.
Install Amplify in your project directory:
cd my-appnpm i aws-amplify
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:
Open your Slack workspace menu and go to Manage apps
.
On the Slack app directory page, press the Build
button.
On your apps page, click the Create an App
button.
When prompted, select the From an app manifest
option. This simplifies the process of creating and configuring your Slack app.
https://api.slack.com/reference/manifests
Use the app manifest system to quickly create, configure, and reuse Slack app configurations.
Choose the workspace where your app will be installed.
Provide a name for your app. In this note, we use Sign in with Slack
. Leave the other fields as default.
Press the Create
button to finalize the setup of your Slack app.
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:
Step 1. Retrieve Slack App Credentials
Navigate to your Slack app’s Basic Information
page and copy the Client ID
and Client Secret
values.
These credentials will be used to configure the Cognito User Pool later.
Step 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:
Avoid hardcoding your Slack credentials in your AWS CDK files. Always use secure storage like AWS Secrets Manager.
aws secretsmanager create-secret \ --name sign-in-with-slack \ --secret-string '{"clientId": "<YOUR_CLIENT_ID>", "clientSecret": "<YOUR_CLIENT_SECRET>"}'
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 nodeimport '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
.
- Include
OAuthScope.COGNITO_ADMIN
in theUserPoolClient.oAuth.scopes
(line 55) for fetching user attributes. Refer to the official documentation. - Securely retrieve Slack credentials from AWS Secrets Manager. (line 65-69)
- 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 Stack
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
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:
- Press the
Add New Redirect URL
button. - Enter the following URL, replacing
<COGNITO_DOMAIN_PREFIX>
with your actual domain prefix.
Using OIDC identity providers with a user poolhttps://<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. - Press the
Save URLs
button.
If you’re unsure of your Cognito domain, locate it in the App Integration
tab of the Cognito User Pool.
Step 2: Add OAuth Scopes
In the OAuth & Permissions
page, add the required scope users:read
under User Token Scopes
. Save your changes to complete.
Step 3: Install the Slack App
Click the Install to <WORKSPACE>
button and follow the prompts to complete the installation.
Testing Sign in with Slack
Verify that the federation works by testing the “Sign in with Slack” flow.
Go to the Cognito App Client
page and press the View Hosted UI
button.
On the Hosted UI, press the Slack
button.
Enter your workspace name and press Continue
.
Complete the Slack login process and press Allow
to grant access.
After successful login, you will be redirected to the callback URL configured in your Cognito app client. For this example, the default is https://example.com/
.
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" } ]}
Building Frontend
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
Ensure the NEXT_PUBLIC_OAUTH_DOMAIN
(line 3) does not start with https://
.
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:
- Scopes:
- The
oauth.scopes
must includeaws.cognito.signin.user.admin
(line 27) to enable thefetchUserAttributes
function. For more details, refer to the official documentation.
- The
- Redirect URLs:
- Ensure
oauth.redirectSignIn
andoauth.redirectSignOut
values (lines 31-32) exactly match the URLs configured in your Cognito app client, including trailing slashes.
- Ensure
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 sessionexport async function authSession(): Promise<AuthSession> { return await fetchAuthSession();}
// Fetch the currently authenticated userexport async function authCurrentUser(): Promise<AuthUser> { return await getCurrentUser();}
// Fetch user attributesexport async function fetchAttributes(): Promise<AuthUserAttributes> { // Requires the 'aws.cognito.signin.user.admin' scope return await fetchUserAttributes();}
// Redirect to the Slack sign-in pageexport async function authSignIn(): Promise<void> { await signInWithRedirect({ provider: { custom: 'Slack' }, });}
// Sign out the userexport async function authSignOut(): Promise<void> { await signOut();}
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:
- 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.
'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 test the app, start the local development server:
npm run dev
> [email protected] dev> next dev
▲ Next.js 14.2.5 - Local: http://localhost:3000 - Environments: .env.local
✓ Starting... ✓ Ready in 1507ms
Open your browser and navigate to http://localhost:3000
. You should be redirected to the Slack sign-in page. Upon successful sign-in, you will see the app displaying user details.