Skip to content
Scalekit Docs

Manage user sessions

Store tokens safely with proper cookie security, validate on every request, and refresh with rotation to keep sessions secure

User sessions determine how long users stay signed in to your application. After users successfully authenticate, you receive session tokens that manage their access. These tokens control session duration, multi-device access, and cross-product authentication within your company’s ecosystem.

Store those tokens securely, validate them on every request, and refresh them before they expire. Session middleware writes one encrypted sk_session cookie and does that work for you. You can still store and refresh tokens yourself if you prefer.

Review the session management sequence

User session management flow diagram showing how access tokens and refresh tokens work together

  1. After hosted login, session middleware writes one encrypted sk_session cookie. You do not set accessToken or refreshToken cookies.

    Add COOKIE_ENCRYPTION_SECRET to .env. Generate it with openssl rand -base64 32. Use the same value on every server.

    Install @scalekit-sdk/node 2.12.0 or later. Mount auth.router. It registers /login, /callback, and /logout. The callback writes sk_session.

    server.ts
    import express from 'express';
    import { ScalekitAuth } from '@scalekit-sdk/node/express';
    const auth = new ScalekitAuth({
    envUrl: process.env.SCALEKIT_ENVIRONMENT_URL,
    clientId: process.env.SCALEKIT_CLIENT_ID,
    clientSecret: process.env.SCALEKIT_CLIENT_SECRET,
    redirectUri: process.env.REDIRECT_URI,
    cookieEncryptionSecret: process.env.COOKIE_ENCRYPTION_SECRET,
    });
    const app = express();
    app.use(auth.router);
  2. Check the session before handling requests

    Section titled “Check the session before handling requests”

    Session middleware refreshes sk_session and redirects to /login when the session is missing. A missing session returns 302, not JSON 401.

    Guard a route with auth.requiresAuth. On success, req.scalekitUser is access-token claims. sub is always present.

    server.ts
    app.get('/account', auth.requiresAuth, (req, res) => {
    res.json({ sub: req.scalekitUser?.sub });
    });
    Build it yourself — store cookies and call validateAccessToken

    Use this path when you do not use session middleware. Request offline_access so you receive a refresh token. Scalekit rotates refresh tokens. Store the new refresh token and discard the old one.

    Store the access token in an HttpOnly cookie. Store the refresh token in a separate HttpOnly cookie. Encrypt both values.

    Express.js — set cookies
    const { accessToken, expiresIn, refreshToken } = authResult;
    res.cookie('accessToken', encrypt(accessToken), {
    maxAge: (expiresIn - 60) * 1000,
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    });
    res.cookie('refreshToken', encrypt(refreshToken), {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    });
    Express.js — verify and refresh
    async function verifyToken(req, res, next) {
    const { accessToken, refreshToken } = req.cookies;
    if (!accessToken) {
    return res.status(401).json({ error: 'Authentication required' });
    }
    const decryptedAccessToken = decrypt(accessToken);
    const isValid = await scalekit.validateAccessToken(decryptedAccessToken);
    if (!isValid && refreshToken) {
    const authResult = await scalekit.refreshAccessToken(decrypt(refreshToken));
    res.cookie('accessToken', encrypt(authResult.accessToken), {
    maxAge: (authResult.expiresIn - 60) * 1000,
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    });
    res.cookie('refreshToken', encrypt(authResult.refreshToken), {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    });
    return next();
    }
    if (!isValid) {
    return res.status(401).json({ error: 'Session expired. Please sign in again.' });
    }
    next();
    }
    Flask — verify and refresh
    import os
    from functools import wraps
    from flask import request, jsonify, make_response
    def verify_token(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
    access_token = request.cookies.get("accessToken")
    refresh_token = request.cookies.get("refreshToken")
    if not access_token:
    return jsonify({"error": "Authentication required"}), 401
    is_valid = scalekit_client.validate_access_token(decrypt(access_token))
    if not is_valid and refresh_token:
    auth_result = scalekit_client.refresh_access_token(decrypt(refresh_token))
    response = make_response(f(*args, **kwargs))
    response.set_cookie(
    "accessToken",
    encrypt(auth_result.access_token),
    max_age=auth_result.expires_in - 60,
    httponly=True,
    secure=os.environ.get("FLASK_ENV") == "production",
    samesite="Lax",
    )
    response.set_cookie(
    "refreshToken",
    encrypt(auth_result.refresh_token),
    httponly=True,
    secure=os.environ.get("FLASK_ENV") == "production",
    samesite="Lax",
    )
    return response
    if not is_valid:
    return jsonify({"error": "Session expired. Please sign in again."}), 401
    return f(*args, **kwargs)
    return decorated_function
    TypeScript: get typed claims from validateToken

    Use a generic type parameter to get properly typed claims instead of unknown. Pass JWTPayload from jose for access tokens, or IdTokenClaim from @scalekit-sdk/node for ID tokens:

    import type { JWTPayload } from 'jose';
    import type { IdTokenClaim } from '@scalekit-sdk/node';
    // Access token — typed as JWTPayload
    const claims = await scalekit.validateToken<JWTPayload>(accessToken);
    console.log(claims.sub); // user ID
    // ID token — typed with full user profile claims
    const idClaims = await scalekit.validateToken<IdTokenClaim>(idToken);
    console.log(idClaims.email);
  3. Manage user session behavior directly from your Scalekit dashboard without modifying application code. Configure session durations and authentication frequency to balance security and user experience for your application.

    Scalekit Session Policy page with fields for absolute session timeout, idle session timeout, and access token lifetime

    In your Scalekit dashboard, the Session settings page lets you set these options:

    • Absolute session timeout: This is the maximum time a user can stay signed in, no matter what. After this time, they must log in again. For example, if you set it to 30 minutes, users will be logged out after 30 minutes, even if they are still using your app.

    • Idle session timeout: This is the time your app waits before logging out a user who is not active. If you turn this on, the session will end if the user does nothing for the set time. For example, if you set it to 10 minutes, and the user does not click or type for 10 minutes, they will be logged out.

    • Access token lifetime: This is how long an access token is valid. When it expires, your app needs to get a new token (using the refresh token) so the user can keep using the app without logging in again. For example, if you set it to 5 minutes, your app will need to refresh the token every 5 minutes.

    Shorter timeouts provide better security, while longer timeouts reduce authentication interruptions.

  4. Beyond client-side session management, Scalekit provides powerful APIs to manage user sessions remotely from your backend application. This enables you to build features like active session management in user account settings, security incident response, or administrative session control.

    These APIs are particularly useful for:

    • Displaying all active sessions in user account settings
    • Allowing users to revoke specific sessions from unfamiliar devices
    • Security incident response and suspicious session termination
    Session Management SDK
    // Get details for a specific session
    const sessionDetails = await scalekit.session.getSession('ses_1234567890123456');
    // List all sessions for a user with optional filtering
    const userSessions = await scalekit.session.getUserSessions('usr_1234567890123456', {
    pageSize: 10,
    filter: {
    status: ['ACTIVE'], // Filter for active sessions only
    startTime: new Date('2025-01-01T00:00:00Z'),
    endTime: new Date('2025-12-31T23:59:59Z')
    }
    });
    // Revoke a specific session (useful for "Sign out this device" functionality)
    const revokedSession = await scalekit.session.revokeSession('ses_1234567890123456');
    // Revoke all sessions for a user (useful for "Sign out all devices" functionality)
    const revokedSessions = await scalekit.session.revokeAllUserSessions('usr_1234567890123456');
    console.log(`Revoked sessions for user`);

After you add the helper, open /account in the browser. A missing session goes to /login. After login, the browser returns to /account.

Session middleware validates sk_session on each request and refreshes it before expiry. If the refresh token is invalid, the helper sends the browser to /login.