SSO

This guide explains how to authenticate users from your platform directly into the poker client using single-use login codes. The SSO flow has two modes: cookie mode (a browser session on the poker platform) and token mode (JWT tokens for your own code to use).

Required scope: auth:code

How SSO Works

The SSO flow involves two separate HTTP requests made by two different parties:

  1. Your backend requests a one-time login code from the poker engine (HMAC-signed).
  2. The user’s browser exchanges that code for a full session on the poker platform (public endpoint).
┌──────────────────┐                        ┌──────────────────┐
│  Partner Backend  │                        │   Poker Engine   │
└────────┬─────────┘                        └────────┬─────────┘
         │                                           │
         │  1. POST /developer/auth/code             │
         │     (HMAC-signed, auth:code scope)        │
         │ ─────────────────────────────────────────► │
         │                                           │
         │  ◄──── { code: "a1b2c3...", expiresIn: 60 }
         │                                           │
         │  2. Pass code to frontend                 │
         │     (URL param, postMessage, API, etc.)   │
         ▼                                           │
┌──────────────────┐                                 │
│ Partner Frontend  │                                 │
│ (User's Browser)  │                                 │
└────────┬─────────┘                                 │
         │                                           │
         │  3. POST /auth/exchange                   │
         │     (public — code IS the credential)     │
         │ ─────────────────────────────────────────► │
         │                                           │
         │  ◄──── Session (cookies or tokens)        │
         │                                           │
         │  4. User is now authenticated              │
         ▼                                           │
┌──────────────────┐                                 │
│  Poker Platform   │                                 │
│  (Lobby / Game)   │                                 │
└──────────────────┘                                 │

Key points:

  • The login code is a 64-character hex string, valid for 60 seconds, single-use.
  • The code is hashed (SHA-256) before storage — the plaintext is never persisted.
  • The exchange endpoint (POST /auth/exchange) is public and rate-limited (10 per minute per IP).
  • The exchange endpoint requires the developerApi feature toggle to be enabled.
  • The mode is fixed when the code is generated. The exchange request is the same for both, and the response follows the code’s mode.

Choose a mode when generating the login code. The mode determines how the session is established after code exchange.

FeatureCookie ModeToken Mode
Use caseSign the user in to the poker web clientCall poker APIs from your own code
Session deliveryaccess_token and refresh_token HttpOnly cookiesJWT tokens returned in response body
Cross-siteCookies are SameSite=Strict, so your site and the poker platform must be on the same site. Your origin must also be an allowed CORS originNo cookies. Browser callers need an allowed CORS origin
Frontend codeRedirect to the poker URL after exchangeStore tokens, send them as Authorization: Bearer <accessToken>
SecurityCookies are HttpOnly, SameSite=Strict, and Secure in productionTokens must be stored securely by your code
DefaultYes (if mode is omitted)Must be explicitly requested
  • Your integration sends the user to the poker platform in a new tab or the same window.
  • The poker platform and your site share the same registrable domain (for example www.example.com and poker.example.com).
  • You want the simplest integration — session cookies are handled automatically by the browser.

When to Use Token Mode

  • You need to manage authentication tokens programmatically, for example from your own client calling poker APIs.
  • The poker platform is on a different site than yours, so SameSite=Strict cookies can’t be set by a cross-site request.

Token mode does not sign the user in to the poker web client. The web client authenticates with its own cookies and has no way to accept tokens from a parent page, and the platform does not set up cross-origin framing, so embedding it in an iframe on another site is not a supported flow.

Step 1: Generate a Login Code (Backend)

Your backend requests a login code from the poker engine using HMAC-signed authentication.

// backend/generate-code.js
const { apiRequest } = require('./hmac-client'); // See quickstart.md

async function generateLoginCode(externalUserId) {
  const result = await apiRequest('POST', '/developer/auth/code', {
    externalUserId,
    mode: 'cookie',
  });

  return result; // { code: "a1b2c3...", expiresIn: 60, mode: "cookie" }
}

If the user does not yet have a poker account, the code generation endpoint auto-provisions one. You can include optional user data:

const result = await apiRequest('POST', '/developer/auth/code', {
  externalUserId: 'user_12345',
  mode: 'cookie',
  username: 'john_doe',       // Optional: auto-provisions with this username
  email: 'john@example.com',  // Optional: auto-provisions with this email
  firstName: 'John',          // Optional
  lastName: 'Doe',            // Optional
});

Step 2: Pass the Code to Your Frontend

Deliver the code to the user’s browser. Common patterns:

  • API endpoint: Your frontend fetches the code from your backend via a button click.
  • Server-rendered page: Include the code in the page HTML as a data attribute or hidden input.
  • WebSocket/SSE: Push the code to the frontend in real-time.
// Example: Frontend fetches a login code from your backend
async function getLoginCode() {
  const response = await fetch('/api/poker/login-code', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ userId: currentUser.id }),
  });

  const { code } = await response.json();
  return code; // 64-char hex string
}

Step 3: Exchange the Code in the Browser

The user’s browser calls the poker platform’s exchange endpoint directly. On success, session cookies are set automatically.

// frontend/poker-login.js
const POKER_URL = 'https://poker.example.com';

async function loginToPoker(code) {
  const response = await fetch(`${POKER_URL}/api/v1/auth/exchange`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include', // Required — tells the browser to accept cookies
    body: JSON.stringify({ code }),
  });

  if (!response.ok) {
    const error = await response.json();
    // error.error.code will be: INVALID_CODE, CODE_EXPIRED, or CODE_ALREADY_USED
    throw new Error(`Login failed: ${error.error.code} — ${error.error.message}`);
  }

  const data = await response.json();
  // data.data.user contains the authenticated user's profile
  console.log('Logged in as:', data.data.user.username);

  return data.data;
}

Cookie mode response:

{
  "success": true,
  "data": {
    "user": {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "username": "john_doe",
      "email": "john@example.com",
      "firstName": "John",
      "lastName": "Doe",
      "group": { "id": "...", "name": "player", "permissions": "f" }
    },
    "expiresIn": 900
  }
}

user is abridged here. It is the user’s profile record without credential fields, plus their group.

The browser now has HttpOnly session cookies. The user can navigate to the poker lobby.

Step 4: Redirect to the Poker Platform

After a successful exchange, redirect the user to the poker lobby:

async function playPoker() {
  const code = await getLoginCode();
  await loginToPoker(code);

  // User is authenticated — redirect to the poker lobby
  window.location.href = `${POKER_URL}/lobby`;
}
<!DOCTYPE html>
<html>
<head><title>Play Poker</title></head>
<body>
  <button id="play-btn">Play Poker</button>

  <script>
    const POKER_URL = 'https://poker.example.com';

    document.getElementById('play-btn').addEventListener('click', async () => {
      try {
        // Step 1: Get a login code from YOUR backend
        const codeRes = await fetch('/api/poker/login-code', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ userId: 'current_user_id' }),
        });
        const { code } = await codeRes.json();

        // Step 2: Exchange the code on the poker platform
        const exchangeRes = await fetch(`${POKER_URL}/api/v1/auth/exchange`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          credentials: 'include',
          body: JSON.stringify({ code }),
        });

        if (!exchangeRes.ok) {
          const err = await exchangeRes.json();
          alert(`Login failed: ${err.error.message}`);
          return;
        }

        // Step 3: Redirect to the poker lobby
        window.location.href = `${POKER_URL}/lobby`;
      } catch (err) {
        console.error('Poker login error:', err);
        alert('Could not connect to poker platform. Please try again.');
      }
    });
  </script>
</body>
</html>

Step-by-Step: Token Mode

Token mode is designed for integrations that call poker APIs themselves, in environments where cookies cannot be shared.

Step 1: Generate a Login Code (Backend)

Same as cookie mode, but with mode: 'token':

const result = await apiRequest('POST', '/developer/auth/code', {
  externalUserId: 'user_12345',
  mode: 'token',
});

// { code: "a1b2c3...", expiresIn: 60, mode: "token" }

Step 2: Exchange the Code for Tokens

The exchange endpoint returns JWT tokens in the response body (no cookies are set).

const POKER_URL = 'https://poker.example.com';

async function exchangeForTokens(code) {
  const response = await fetch(`${POKER_URL}/api/v1/auth/exchange`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    // No `credentials: 'include'` needed — tokens come in the body
    body: JSON.stringify({ code }),
  });

  if (!response.ok) {
    const { error } = await response.json();
    throw new Error(`Exchange failed: ${error.code}`);
  }

  const data = await response.json();
  return data.data;
  // {
  //   accessToken: "eyJhbGciOi...",
  //   refreshToken: "eyJhbGciOi...",
  //   expiresIn: 900,
  //   user: { id, username, email, ... }
  // }
}

Token mode response:

{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expiresIn": 900,
    "user": {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "username": "john_doe",
      "email": "john@example.com",
      "firstName": "John",
      "lastName": "Doe",
      "group": { "id": "...", "name": "player", "permissions": "f" }
    }
  }
}

Step 3: Use the Tokens

Send the access token as a bearer token on authenticated poker API calls. expiresIn is the access token’s lifetime in seconds.

const res = await fetch(`${POKER_URL}/api/v1/auth/me`, {
  headers: { Authorization: `Bearer ${tokens.accessToken}` },
});

When the access token expires, send the refresh token in the body of POST /api/v1/auth/refresh as { "refreshToken": "..." }.

Auto-Provisioning

The POST /developer/auth/code endpoint automatically provisions a poker account if the user does not already have one mapped to the given externalUserId. This means you can skip the explicit user provisioning step (POST /developer/users) and go straight to generating login codes.

// This single call provisions the user AND generates a login code
const result = await apiRequest('POST', '/developer/auth/code', {
  externalUserId: 'new_user_999',
  mode: 'cookie',
  username: 'new_player',
  email: 'newplayer@example.com',
});
// User is created, code is returned — ready for exchange

If the user already exists (same externalUserId for your partner), a code is generated for the existing account.

Error Handling

Code Generation Errors

Error CodeHTTP StatusCauseResolution
MODE_NOT_ALLOWED400Token or cookie mode is disabled in developer settingsContact the platform admin to enable the desired mode
MISSING_USERNAME400The user is new and the policy needs a value that wasn’t sent: username under require, or username or email under generate_from_emailInclude the field in the request body
EMAIL_CONFLICT409Email already exists for a different userUse a different email or provision without email
USERNAME_CONFLICT409Username is already takenChoose a different username

These use the flat format { "error": "CODE", "message": "..." }.

Code Exchange Errors

Exchange errors use the nested format: { "success": false, "error": { "code": "CODE", "message": "..." } }.

Error CodeHTTP StatusCauseResolution
INVALID_CODE401The code does not exist or was never generatedVerify you are sending the correct 64-character hex code
CODE_EXPIRED401The code is older than 60 secondsGenerate a new code — they expire quickly for security
CODE_ALREADY_USED401The code has already been exchangedGenerate a new code — codes are single-use
VALIDATION_ERROR400The code is not exactly 64 lowercase hex charactersSend the code exactly as returned
ACCOUNT_BLOCKED403The user’s account is blocked. The code is used upContact the platform admin
ACCOUNT_FROZEN403The user’s account is frozen. The code is used upContact the platform admin
AUTHORIZATION_ERROR403The developerApi feature toggle is disabledContact the platform admin
FEATURE_TIER_RESTRICTED403The platform’s license tier does not include the Developer APIContact the platform admin
RATE_LIMIT_EXCEEDED429Too many exchange attempts from this IPWait 1 minute before retrying

Handling Errors in Code

async function safeExchange(code) {
  const response = await fetch(`${POKER_URL}/api/v1/auth/exchange`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include',
    body: JSON.stringify({ code }),
  });

  if (!response.ok) {
    const { error } = await response.json();

    switch (error.code) {
      case 'CODE_EXPIRED':
      case 'CODE_ALREADY_USED':
        // Code is no longer valid — request a new one from your backend
        console.log('Code invalid, requesting new code...');
        return { retry: true };

      case 'INVALID_CODE':
        // Something went wrong with code transmission
        console.error('Invalid code — check your backend integration');
        return { error: 'Invalid login code' };

      case 'RATE_LIMIT_EXCEEDED':
        console.warn('Rate limited — wait before retrying');
        return { error: 'Too many attempts, please wait' };

      default:
        console.error('Unexpected error:', error);
        return { error: 'Login failed' };
    }
  }

  return await response.json();
}

Security Considerations

Codes Are Single-Use

Each login code can only be exchanged once. After a successful exchange, the code is marked as used and cannot be reused. This prevents session hijacking even if the code is intercepted.

Codes Expire in 60 Seconds

Login codes are intentionally short-lived. Generate a code only when the user is ready to be authenticated, not in advance. If the code expires, generate a new one.

Never Put Codes in URLs

Do not pass login codes as URL query parameters (e.g., ?code=abc123). URL parameters are logged by browsers, web servers, proxies, and analytics tools. Instead:

  • Pass the code via a POST request body from your frontend to the exchange endpoint.
  • If you need to transfer the code between pages, use sessionStorage, a hidden form field, or postMessage.

The Exchange Endpoint Is Public

POST /auth/exchange does not require HMAC authentication — the code itself is the credential. This is by design: the request comes from the user’s browser, which does not have access to your API secret.

The endpoint is protected by:

  • Rate limiting: 10 attempts per minute per IP.
  • Code expiry: 60-second validity window.
  • Single use: Codes cannot be reused after exchange.
  • Feature toggle: The endpoint returns 403 when the developerApi feature toggle is disabled.

CORS Configuration

For cookie mode, the poker platform must have CORS configured to allow your origin, and your site must be on the same site as the platform because the cookies are SameSite=Strict. Contact the platform admin to add your domain to the allowed origins list.

For token mode, CORS must allow your origin but credentials are not required (tokens are in the response body).

Token Storage (Token Mode)

When using token mode, you are responsible for securely storing the JWT tokens:

  • Do not store tokens in localStorage if XSS is a concern — use sessionStorage or in-memory storage.
  • Refresh tokens can be used to obtain new access tokens when they expire.
  • Access tokens are short-lived. Use expiresIn rather than hard-coding a lifetime, since the platform configures it.

Next Steps

Try the live demo Pricing

Have questions about Poker Engine? We reply fast.