Slack

Slack OpenID Connect

Authorization Code Exchange for web and mobile — Sign in with Slack (OpenID Connect) setup, API reference, and frontend examples.

Overview

Slack login is optional and controlled by SLACK_AUTH_ENABLED. When disabled, Slack endpoints return 503 Service Unavailable. JWTs are never placed in the redirect URL; the API issues a one-time exchangeCode (Redis, 60 seconds). The same POST /auth/exchange endpoint is shared with other social providers.

  • • Feature-flagged via environment
  • • Custom passport-oauth2 strategy + SlackAuthGuard (not legacy passport-slack / identity.*)
  • • Profile fetched via GET https://slack.com/api/openid.connect.userInfo after token exchange
  • • Redirect allowlist prevents open redirects
  • • Same token response shape as POST /auth/login

Slack API → Your Apps

Create an app at https://api.slack.com/apps → Create App. Under OAuth & Permissions, add a Redirect URL matching SLACK_CALLBACK_URL. Under User Token Scopes, add openid, email, and profile.

  1. Open https://api.slack.com/apps and click Create App
  2. Under OAuth & Permissions, add a Redirect URL (e.g. http://localhost:3000/api/v1/auth/slack/callback)
  3. Under User Token Scopes, add openid, email, and profile
  4. Install the app to your workspace if prompted
  5. Copy Client ID and Client Secret into your .env
  6. Ensure SLACK_CALLBACK_URL matches the Redirect URL registered in Slack

Environment variables

Loaded from .env via src/config/slack-oauth.config.ts (namespace slackOAuth on ConfigService). See .env.example.

Environment.env
SLACK_AUTH_ENABLED=true
SLACK_CLIENT_ID=your-slack-client-id
SLACK_CLIENT_SECRET=your-slack-client-secret
SLACK_CALLBACK_URL=http://localhost:3000/api/v1/auth/slack/callback
SLACK_REDIRECT_ALLOWLIST=myapp://success,http://localhost:5173/auth/callback
SLACK_OAUTH_DEFAULT_ROLES=

Default roles for new Slack users

New Slack users receive roles: [] by default (same as password register). Set SLACK_OAUTH_DEFAULT_ROLES in .env to a comma-separated list of Role values (e.g. user or user,manager). Existing users linked by email keep their current roles.

Sequence

  1. Client opens GET /api/v1/auth/slack?redirect=<allowlisted URL>
  2. User consents on Slack; browser hits GET /api/v1/auth/slack/callback
  3. API calls GET https://slack.com/api/openid.connect.userInfo, upserts user, stores exchangeCode in Redis, redirects to redirect?code=...
  4. Client POSTs { code } to /api/v1/auth/exchange and receives JWTs

API reference

1. Start OAuth

Opens Slack consent at https://slack.com/openid/connect/authorize. Query redirect must match a prefix in SLACK_REDIRECT_ALLOWLIST (or omit to use the first allowlist entry). Scopes: openid email profile.

HTTPGET /api/v1/auth/slack
GET /api/v1/auth/slack?redirect=http%3A%2F%2Flocalhost%3A5173%2Fauth%2Fcallback

400 invalid redirect · 503 feature disabled

2. Callback

Handled by the API. Token exchange uses https://slack.com/api/openid.connect.token. Redirects to your app with ?code= (or &code= if the URL already has a query).

HTTPGET /api/v1/auth/slack/callback
GET /api/v1/auth/slack/callback?code=SLACK_AUTH_CODE&state=...
→ 302 Location: http://localhost:5173/auth/callback?code=EXCHANGE_CODE

3. Exchange code

Public endpoint shared with other social providers. Consumes the code (single use) and returns the same shape as login.

HTTP RequestPOST /api/v1/auth/exchange
POST /api/v1/auth/exchange
Content-Type: application/json

{
  "code": "EXCHANGE_CODE"
}
HTTP Response200 OK
{
  "user": {
    "id": "...",
    "email": "[email protected]",
    "firstName": "Jane",
    "lastName": "Doe",
    "roles": [],
    "slackId": "U0123456789",
    "isActive": true
  },
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}

401 invalid/expired/used code · 503 social auth disabled

Frontend — Web (SPA)

Typical flow: button starts OAuth in the same window (or popup); a callback route on your frontend origin reads code from the query string and calls exchange.

JavaScriptStart Slack login
const API = 'http://localhost:3000/api/v1';
const redirect = encodeURIComponent(
  'http://localhost:5173/auth/callback'
);

// Full-page redirect to Nest → Slack → back to SPA
window.location.href = `${API}/auth/slack?redirect=${redirect}`;
JavaScriptCallback page /auth/callback
// /auth/callback on the SPA
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
if (!code) throw new Error('Missing exchange code');

const res = await fetch('http://localhost:3000/api/v1/auth/exchange', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ code }),
});
if (!res.ok) throw new Error(await res.text());

const { user, accessToken, refreshToken } = await res.json();
// persist tokens as your app requires
sessionStorage.setItem('accessToken', accessToken);
console.log('signed in', user.email);
  • • Add your frontend callback URL to SLACK_REDIRECT_ALLOWLIST (e.g. http://localhost:5173/auth/callback)
  • • SLACK_CALLBACK_URL must point at the Nest API, not the SPA
  • • Store accessToken / refreshToken securely (memory + httpOnly cookie patterns as you prefer)

Frontend — Mobile (deep link)

Use a custom scheme (or universal link) in the allowlist. After Slack, the API redirects to myapp://success?code=.... The app opens, parses the code, and calls exchange.

React NativeOpen OAuth in browser
import * as Linking from 'expo-linking';
import * as WebBrowser from 'expo-web-browser';

const API = 'https://api.example.com/api/v1';
const redirect = encodeURIComponent('myapp://success');

async function signInWithSlack() {
  const url = `${API}/auth/slack?redirect=${redirect}`;
  await WebBrowser.openAuthSessionAsync(url, 'myapp://success');
}
React NativeHandle deep link + exchange
import * as Linking from 'expo-linking';

async function exchangeFromUrl(url) {
  const { queryParams } = Linking.parse(url);
  const code = queryParams?.code;
  if (!code) return;

  const res = await fetch(`${API}/auth/exchange`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ code }),
  });
  const data = await res.json();
  // data.accessToken, data.refreshToken, data.user
  return data;
}

Linking.addEventListener('url', ({ url }) => {
  exchangeFromUrl(url);
});
  • • Register the scheme in the OS / Expo app.json
  • • Prefer Linking.addEventListener('url', ...) and getInitialURL()
  • • Exchange over HTTPS against your API base URL

Security notes

  • • Redirect URLs must match SLACK_REDIRECT_ALLOWLIST prefixes
  • • Exchange codes are single-use and expire in 60 seconds (Redis)
  • • Existing local users are linked by verified Slack email (slackId set; roles unchanged)
  • • Slack-only users have password null — password login / change-password will fail until a password is set
  • • Email is required from openid.connect.userInfo; login fails if Slack does not return an email (scopes openid email profile)

User model

Postgres entity / interface fields used by Slack login:

  • • slackId — unique nullable Slack OIDC subject (sub from userInfo)
  • • password — nullable for OAuth-only accounts
  • • isActive — set true for new Slack users
  • • roles — from SLACK_OAUTH_DEFAULT_ROLES env (default [])

Related