Skip to content

Quick Start

Every client app integrates with the auth service via the standard OAuth2 Authorization Code flow with PKCE. The flow is:

  1. User visits your app → no token → redirect to auth.beshoy.ai
  2. User authenticates (password, PIN, or magic link)
  3. Auth service redirects back with an authorization code
  4. Your app exchanges the code for tokens (server-side)
  5. Tokens stored in httpOnly cookies
  • A project registered in the auth service (via admin API or authdash)
  • Your app’s redirect URI added to the project’s allowlist
  • The project’s signing_key as your JWT_SECRET environment variable
  • The project’s client_secret for token exchange
Terminal window
JWT_SECRET=<project signing_key, 64-char hex>
AUTH_CLIENT_SECRET=<project client_secret, 64-char hex>
APP_URL=https://yourapp.beshoy.ai
  1. Generate PKCE parameters and redirect

    When a user needs to authenticate, generate a code verifier, compute the challenge, encrypt state, and redirect:

    import crypto from 'node:crypto';
    // Generate PKCE
    const codeVerifier = crypto.randomBytes(32).toString('base64url');
    const codeChallenge = crypto
    .createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');
    // Encrypt state (code verifier + return URL)
    const state = await encryptState({ v: codeVerifier, r: returnTo });
    // Redirect to auth service
    const authUrl = new URL('https://auth.beshoy.ai/oauth/authorize');
    authUrl.searchParams.set('client_id', 'proj_yourapp');
    authUrl.searchParams.set('redirect_uri', `${APP_URL}/api/auth/callback`);
    authUrl.searchParams.set('response_type', 'code');
    authUrl.searchParams.set('code_challenge', codeChallenge);
    authUrl.searchParams.set('code_challenge_method', 'S256');
    authUrl.searchParams.set('state', state);
    redirect(authUrl.toString());
  2. Handle the callback

    Create a callback route that exchanges the code for tokens:

    // GET /api/auth/callback
    export async function GET(req: Request) {
    const url = new URL(req.url);
    const code = url.searchParams.get('code');
    const state = url.searchParams.get('state');
    // Decrypt state to get code verifier
    const { v: codeVerifier, r: returnTo } = await decryptState(state);
    // Exchange code for tokens
    const res = await fetch('https://auth.beshoy.ai/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
    grant_type: 'authorization_code',
    code,
    code_verifier: codeVerifier,
    client_id: 'proj_yourapp',
    client_secret: process.env.AUTH_CLIENT_SECRET,
    redirect_uri: `${process.env.APP_URL}/api/auth/callback`,
    }),
    });
    const { access_token, refresh_token } = await res.json();
    // Set cookies and redirect
    setAuthCookies(access_token, refresh_token);
    redirect(returnTo || '/');
    }
  3. Verify tokens on requests

    Use the project’s signing key to verify JWTs locally:

    import { jwtVerify } from 'jose';
    const secret = new TextEncoder().encode(process.env.JWT_SECRET);
    async function getUser(request: Request) {
    const token = getCookie(request, 'access_token');
    if (!token) return null;
    try {
    const { payload } = await jwtVerify(token, secret);
    return {
    userId: payload.sub,
    email: payload.email,
    role: payload.role,
    status: payload.status,
    };
    } catch {
    return null;
    }
    }
  4. Handle token refresh

    When the access token expires, use the refresh token to get new ones:

    const res = await fetch('https://auth.beshoy.ai/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
    grant_type: 'refresh_token',
    refresh_token: currentRefreshToken,
    client_id: 'proj_yourapp',
    client_secret: process.env.AUTH_CLIENT_SECRET,
    }),
    });
    const { access_token, refresh_token } = await res.json();
    // Update cookies with new tokens
TokenLifetimeStorage
Access token5 minuteshttpOnly cookie
Refresh token (user)7 dayshttpOnly cookie
Refresh token (PIN)30 dayshttpOnly cookie