Skip to content

Complete login with code exchange

Process authentication callbacks and handle redirect flows after users authenticate with Scalekit

Once users have successfully verified their identity using their chosen login method, Scalekit will have gathered the necessary user information for your app to complete the login process. However, your app must provide a callback endpoint where Scalekit can exchange an authorization code to return your app the user details.

  1. Validate the state parameter recommended

    Section titled “Validate the state parameter ”

    Before exchanging the authorization code, your application must validate the state parameter returned by Scalekit. Compare it with the value you stored in the user’s session before redirecting them. This critical step prevents Cross-Site Request Forgery (CSRF) attacks, ensuring the authentication response corresponds to a request initiated by the same user.

    Validate state in Express.js
    const { state } = req.query;
    // Assumes you are using a session middleware like express-session
    const storedState = req.session.oauthState;
    delete req.session.oauthState; // State should be used only once
    if (!state || state !== storedState) {
    console.error('Invalid state parameter');
    return res.redirect('/login?error=invalid_state');
    }
  2. Once the state is validated, your app can safely exchange the authorization code for tokens. The Scalekit SDK simplifies this process with the authenticateWithCode method, which handles the secure server-to-server request.

    Express.js callback handler
    app.get('/auth/callback', async (req, res) => {
    const { code, error, error_description, state } = req.query;
    // Add state validation here (see previous step)
    11 collapsed lines
    // Handle errors first
    if (error) {
    console.error('Authentication error:', error);
    return res.redirect('/login?error=auth_failed');
    }
    if (!code) {
    return res.redirect('/login?error=missing_code');
    }
    try {
    // Exchange code for user data
    const authResult = await scalekit.authenticateWithCode(
    code,
    'https://yourapp.com/auth/callback'
    );
    const { user, accessToken, refreshToken } = authResult;
    11 collapsed lines
    // TODO: Store user session (next guide covers this)
    // req.session.user = user;
    res.redirect('/dashboard');
    } catch (error) {
    console.error('Token exchange failed:', error);
    res.redirect('/login?error=exchange_failed');
    }
    });

    The authorization code can be redeemed only once and expires in approx ~10 minutes. Reuse or replay attempts typically return errors like invalid_grant. If this occurs, start a new login flow to obtain a fresh code and state.

    The authResult object returned contains:

    {
    user: {
    email: "john.doe@example.com",
    emailVerified: true,
    givenName: "John",
    name: "John Doe",
    id: "usr_74599896446906854"
    },
    idToken: "eyJhbGciO..", // Decode for full user details
    accessToken: "eyJhbGciOi..",
    refreshToken: "rt_8f7d6e5c4b3a2d1e0f9g8h7i6j..",
    expiresIn: 299 // in seconds
    }
    KeyDescription
    userCommon user details with email, name, and verification status
    idTokenJWT containing verified full user identity claims
    accessTokenShort-lived token that determines current access
    refreshTokenLong-lived token to obtain new access tokens
  3. The idToken and accessToken are JSON Web Tokens (JWT) that contain user claims. These tokens can be decoded to retrieve comprehensive user and access information.

    Decode ID token
    // Use a library like 'jsonwebtoken'
    const jwt = require('jsonwebtoken');
    // The idToken from the authResult object
    const { idToken } = authResult;
    // Decode the token without verifying its signature
    const decoded = jwt.decode(idToken);
    console.log('Decoded claims:', decoded);

    The decoded token claims contain:

    ID token decoded
    {
    "iss": "https://scalekit-z44iroqaaada-dev.scalekit.cloud", // Issuer: Scalekit environment URL (must match your environment)
    "aud": ["skc_58327482062864390"], // Audience: Your client ID (must match for validation)
    "azp": "skc_58327482062864390", // Authorized party: Usually same as aud
    "sub": "usr_63261014140912135", // Subject: User's unique identifier
    "oid": "org_59615193906282635", // Organization ID: User's organization
    "exp": 1742975822, // Expiration: Unix timestamp (validate token hasn't expired)
    "iat": 1742974022, // Issued at: Unix timestamp when token was issued
    "at_hash": "ec_jU2ZKpFelCKLTRWiRsg", // Access token hash: For token binding validation
    "c_hash": "6wMreK9kWQQY6O5R0CiiYg", // Authorization code hash: For code binding validation
    "amr": ["conn_123"], // Authentication method reference: Connection ID used for auth
    "email": "john.doe@example.com", // User's email address
    "email_verified": true, // Email verification status
    "name": "John Doe", // User's full name (optional)
    "given_name": "John", // User's first name (optional)
    "family_name": "Doe", // User's last name (optional)
    "picture": "https://...", // Profile picture URL (optional)
    "locale": "en", // User's locale preference (optional)
    "sid": "ses_65274187031249433", // Session ID: Links token to user session
    "client_id": "skc_58327482062864390" // Client ID: Your application identifier
    }
    ID token claims reference

    ID tokens contain cryptographically signed claims about a user’s profile information. The Scalekit SDK automatically validates ID tokens when you use authenticateWithCode. If you need to manually verify or access custom claims, use the claim reference below.

    ClaimPresenceDescription
    issAlwaysIssuer identifier (Scalekit environment URL)
    audAlwaysIntended audience (your client ID)
    subAlwaysSubject identifier (user’s unique ID)
    oidAlwaysOrganization ID of the user
    expAlwaysExpiration time (Unix timestamp)
    iatAlwaysIssuance time (Unix timestamp)
    at_hashAlwaysAccess token hash for validation
    c_hashAlwaysAuthorization code hash for validation
    azpAlwaysAuthorized presenter (usually same as aud)
    amrAlwaysAuthentication method reference (connection ID)
    emailAlwaysUser’s email address
    email_verifiedOptionalEmail verification status
    nameOptionalUser’s full name
    family_nameOptionalUser’s surname or last name
    given_nameOptionalUser’s given name or first name
    localeOptionalUser’s locale (BCP 47 language tag)
    pictureOptionalURL of user’s profile picture
    sidAlwaysSession identifier
    client_idAlwaysYour application’s client ID
    Access token claims reference

    Access tokens contain authorization information including roles and permissions. Use these claims to make authorization decisions in your application.

    Roles group related permissions together and define what users can do in your system. Common examples include Admin, Manager, Editor, and Viewer. Roles can inherit permissions from other roles, creating hierarchical access levels.

    Permissions represent specific actions users can perform, formatted as resource:action patterns like projects:create or tasks:read. Use permissions for granular access control when you need precise control over individual capabilities.

    Scalekit automatically assigns the admin role to the first user in each organization and the member role to subsequent users. Your application uses the role and permission information from Scalekit to make final authorization decisions at runtime.

    ClaimPresenceDescription
    issAlwaysIssuer identifier (Scalekit environment URL)
    audAlwaysIntended audience (your client ID)
    subAlwaysSubject identifier (user’s unique ID)
    oidAlwaysOrganization ID of the user
    expAlwaysExpiration time (Unix timestamp)
    iatAlwaysIssuance time (Unix timestamp)
    nbfAlwaysNot before time (Unix timestamp)
    jtiAlwaysJWT ID (unique token identifier)
    sidAlwaysSession identifier
    client_idAlwaysClient identifier for the application
    rolesOptionalArray of role names assigned to the user
    permissionsOptionalArray of permissions in resource:action format
    scopeOptionalSpace-separated list of OAuth scopes granted
  4. Verifying access tokens optional

    Section titled “Verifying access tokens ”

    The Scalekit SDK provides methods to validate tokens automatically. When you use the SDK’s validateAccessToken method, it:

    1. Verifies the token signature using Scalekit’s public keys
    2. Checks the token hasn’t expired (exp claim)
    3. Validates the issuer (iss claim) matches your environment
    4. Ensures the audience (aud claim) matches your client ID

    If you need to manually verify tokens, fetch the public signing keys from the JSON Web Key Set (JWKS) endpoint:

    JWKS endpoint
    https://<YOUR_ENVIRONMENT_URL>/keys

    For example, if your Scalekit Environment URL is https://your-environment.scalekit.com, the keys can be found at https://your-environment.scalekit.com/keys.

An IdToken contains comprehensive profile information about the user. You can save this in your database for app use cases, using your own identifier. Now, let’s utilize access and refresh tokens to manage user access and maintain active sessions.

Customize the login flow by passing different parameters when creating the authorization URL. These scenarios help you route users to specific organizations, force re-authentication, or direct users to signup.

How do I route users to a specific organization?

For multi-tenant applications, you can route users directly to their organization’s authentication method using organizationId. This is useful when you already know the user’s organization.

Express.js
const orgId = getOrganizationFromRequest(req)
const redirectUri = 'https://your-app.com/auth/callback'
const options = {
scopes: ['openid', 'profile', 'email', 'offline_access'],
organizationId: orgId,
}
const url = scalekit.getAuthorizationUrl(redirectUri, options)
return res.redirect(url)
How do I route users based on email domain?

If you don’t know the organization ID beforehand, you can use loginHint to let Scalekit determine the correct authentication method from the user’s email domain. This is common for enterprise logins where the email domain is associated with a specific SSO connection. The domain must be registered to the organization either manually from the Scalekit Dashboard or through the admin portal when onboarding an enterprise customer.

Express.js
const redirectUri = 'https://your-app.com/auth/callback'
const options = {
scopes: ['openid', 'profile', 'email', 'offline_access'],
loginHint: userEmail
}
const url = scalekit.getAuthorizationUrl(redirectUri, options)
return res.redirect(url)
How do I route users to a specific SSO connection?

When you know the exact enterprise connection a user should use, you can pass its connectionId for the highest routing precision. This bypasses any other routing logic.

Express.js
const redirectUri = 'https://your-app.com/auth/callback'
const options = {
scopes: ['openid', 'profile', 'email', 'offline_access'],
connectionId: 'conn_123...'
}
const url = scalekit.getAuthorizationUrl(redirectUri, options)
return res.redirect(url)
How do I force users to re-authenticate?

You can require users to authenticate again, even if they have an active session, by setting prompt: 'login'. This is useful for high-security actions that require recent authentication.

Express.js
const redirectUri = 'https://your-app.com/auth/callback'
const options = {
scopes: ['openid', 'profile', 'email', 'offline_access'],
prompt: 'login'
}
return res.redirect(scalekit.getAuthorizationUrl(redirectUri, options))
How do I let users choose an account or organization?

To show the organization or account chooser, set prompt: 'select_account'. This is helpful when a user is part of multiple organizations and needs to select which one to sign into.

Express.js
const redirectUri = 'https://your-app.com/auth/callback'
const options = {
scopes: ['openid', 'profile', 'email', 'offline_access'],
prompt: 'select_account'
}
return res.redirect(scalekit.getAuthorizationUrl(redirectUri, options))
How do I send users directly to signup?

To send users directly to the signup form instead of the login page, use prompt: 'create'.

Express.js
const redirectUri = 'https://your-app.com/auth/callback'
const options = {
scopes: ['openid', 'profile', 'email', 'offline_access'],
prompt: 'create'
}
return res.redirect(scalekit.getAuthorizationUrl(redirectUri, options))
How do I redirect users back to the page they requested after authentication?

When users bookmark specific pages or their session expires, redirect them to their original destination after authentication. Store the intended path in a secure cookie before redirecting to Scalekit, then read it after the callback.

Step 1: Capture the intended destination

Before redirecting to Scalekit, store the user’s requested path in a secure cookie:

Express.js
app.get('/login', (req, res) => {
const nextPath = typeof req.query.next === 'string' ? req.query.next : '/'
// Only allow internal paths to prevent open redirects
const safe = nextPath.startsWith('/') && !nextPath.startsWith('//') ? nextPath : '/'
res.cookie('sk_return_to', safe, { httpOnly: true, secure: true, sameSite: 'lax', path: '/' })
// Build authorization URL and redirect to Scalekit
})

Step 2: Redirect after callback

After exchanging the authorization code, read the cookie and redirect to the stored path:

Express.js
app.get('/auth/callback', async (req, res) => {
// ... exchange code ...
const raw = req.cookies.sk_return_to || '/'
const safe = raw.startsWith('/') && !raw.startsWith('//') ? raw : '/'
res.clearCookie('sk_return_to', { path: '/' })
res.redirect(safe || '/dashboard')
})