Documentation

1. Architecture Overview

Startum is the native OpenID Connect (OIDC) and OAuth 2.0 Identity Provider. It provides unified single sign-on across web applications, microservices, and mobile clients.

Apps integrate with Startum via PKCE authorization code grants, retrieving cryptographic pairwise subject IDs (sub) and standard OIDC profile claims (display name, picture, primary email, phone number).

2. "Continue with Startum" Brand Buttons

Startum provides official, styled SSO pill-shaped brand buttons for Light Mode and Dark Mode matching the Startum / Napture design system.

Live Button Preview
Dark Theme (Startum / Napture Blue)
Continue with Startum
Light Theme (White with Blue Text & Logo)
Continue with Startum

Developer Documentation Guides

Select between the Official SDK Integration Guide (Recommended for Node.js / Express) and the Manual OAuth 2.0 + PKCE Protocol Guide (for Python, Go, Rust, Mobile, or custom runtimes):

Guide 1: Integration via Official Node.js SDK (@startum/auth-node)

The official SDK handles cryptographic PKCE key pairs, SHA-256 challenges, secure HTTP-only cookies, state verification, and token exchanges automatically in 2–3 lines of code.

1. Installation

npm install https://startum.cloud/startum-auth-node.tgz

2. Express App Setup

const express = require('express');
const cookieParser = require('cookie-parser');
const { startumAuth } = require('@startum/auth-node');

const app = express();
app.use(cookieParser());

const auth = startumAuth({
  clientId: process.env.STARTUM_CLIENT_ID || 'YOUR_CLIENT_ID',
  clientSecret: process.env.STARTUM_CLIENT_SECRET || 'YOUR_CLIENT_SECRET',
  redirectUri: 'http://localhost:3000/callback',
  portalUrl: 'https://startum.cloud',
  apiUrl: 'https://api.startum.cloud'
});

function escapeHtml(str) {
  if (!str) return '';
  return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
}

// Route 1: Initiates authentication and redirects user to Startum
app.get('/login', auth.redirect());

// Route 2: OAuth Callback handler - exchanges code and receives user profile
app.get('/callback', auth.handleCallback(async (user, tokens, req, res) => {
  // user contains { sub, username, email, picture }
  req.session.userId = user.sub;
  res.send(`
    <h2>Welcome, ${escapeHtml(user.username || user.name)}!</h2>
    <p><strong>Pairwise Sub Identifier:</strong> ${escapeHtml(user.sub)}</p>
    <p><strong>Email Address:</strong> ${escapeHtml(user.email)}</p>
  `);
}));

app.listen(3000, () => console.log('App running on http://localhost:3000'));

Guide 2: Manual Integration via OAuth 2.0 + PKCE Protocol (RFC 7636)

For custom non-Node runtimes (Python, Go, Rust, Ruby, Swift, Kotlin, PHP) or custom HTTP clients, follow the manual RFC 7636 Proof Key for Code Exchange (PKCE) protocol steps below.

Step 1: Generate PKCE Key Pair & State

Generate a cryptographically random Base64URL string (43–128 chars) as code_verifier, derive the SHA-256 challenge, and generate a random state for CSRF protection:

code_verifier = Base64URL(RandomBytes(32))
code_challenge = Base64URL(SHA256(code_verifier))
state = Base64URL(RandomBytes(16))

Save code_verifier and state in secure HttpOnly, SameSite=Lax session cookies.

Step 2: Redirect User to Startum Authorization Endpoint

GET https://startum.cloud/oauth/authorize
  ?client_id=YOUR_CLIENT_ID
  &response_type=code
  &redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback
  &scope=openid%20profile%20email
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  &state=YOUR_RANDOM_STATE

Step 3: Handle Callback & Validate State

Upon user authorization, Startum redirects to your registered redirect_uri with code and state:

HTTP 302 -> https://your-app.com/callback?code=st_code_a1b2c3d4...&state=YOUR_RANDOM_STATE

Verify that the returned state matches your stored cookie state before proceeding.

Step 4: Exchange Authorization Code for Tokens

POST the authorization code and PKCE code_verifier to the Startum token endpoint:

Client Types & Client Secrets:
  • Public Clients (SPAs, Mobile, Desktop): client_secret is optional and omitted when using PKCE. The token endpoint verifies the authorization code using code_verifier alone.
  • Confidential Clients (Backend Web Servers): client_secret is required alongside code_verifier to verify server identity while providing defense-in-depth against authorization code interception.
POST https://api.startum.cloud/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET 
&code=st_code_a1b2c3d4...
&code_verifier=YOUR_CODE_VERIFIER
&redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback

Step 5: Fetch Authenticated User Profile

GET https://api.startum.cloud/oauth/userinfo
Headers:
  Authorization: Bearer <access_token>

Manual Node.js / Express Protocol Code Example

const express = require('express');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const app = express();

app.use(cookieParser());

const CLIENT_ID = 'YOUR_CLIENT_ID';
const CLIENT_SECRET = 'YOUR_CLIENT_SECRET';
const REDIRECT_URI = 'http://localhost:3000/callback';
const STARTUM_API = 'https://api.startum.cloud';
const STARTUM_PORTAL = 'https://startum.cloud';

// 1. Redirect to Startum Authorization with manual PKCE & State
app.get('/login', (req, res) => {
  const codeVerifier = crypto.randomBytes(32).toString('base64url');
  const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
  const state = crypto.randomBytes(16).toString('base64url');

  res.cookie('pkce_verifier', codeVerifier, { httpOnly: true, maxAge: 600000, sameSite: 'lax' });
  res.cookie('oauth_state', state, { httpOnly: true, maxAge: 600000, sameSite: 'lax' });

  const authUrl = new URL(`${STARTUM_PORTAL}/oauth/authorize`);
  authUrl.searchParams.set('client_id', CLIENT_ID);
  authUrl.searchParams.set('response_type', 'code');
  authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
  authUrl.searchParams.set('scope', 'openid profile email');
  authUrl.searchParams.set('code_challenge', codeChallenge);
  authUrl.searchParams.set('code_challenge_method', 'S256');
  authUrl.searchParams.set('state', state);

  res.redirect(authUrl.toString());
});

// 2. OAuth Callback Handler with manual state validation & PKCE exchange
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;
  const savedState = req.cookies.oauth_state;
  const codeVerifier = req.cookies.pkce_verifier;

  res.clearCookie('pkce_verifier');
  res.clearCookie('oauth_state');

  if (!code) return res.status(400).send('Missing authorization code');
  if (!state || state !== savedState) return res.status(403).send('Invalid state (CSRF risk)');
  if (!codeVerifier) return res.status(400).send('Missing PKCE verifier');

  const params = new URLSearchParams({
    grant_type: 'authorization_code',
    client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET,
    code: code,
    code_verifier: codeVerifier,
    redirect_uri: REDIRECT_URI
  });

  const tokenRes = await fetch(`${STARTUM_API}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: params.toString()
  });
  const tokens = await tokenRes.json();
  if (!tokenRes.ok) return res.status(tokenRes.status).json(tokens);

  const userRes = await fetch(`${STARTUM_API}/oauth/userinfo`, {
    headers: { Authorization: `Bearer ${tokens.access_token}` }
  });
  const user = await userRes.json();

  function escapeHtml(str) {
    if (!str) return '';
    return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
  }

  res.send(`<h2>Welcome ${escapeHtml(user.name || user.username)}!</h2><p>Sub: ${escapeHtml(user.sub)}</p>`);
});

app.listen(3000, () => console.log('App running on http://localhost:3000'));

B. Manual Node.js / Express Example (Without SDK)

const express = require('express');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const app = express();

app.use(cookieParser());

const CLIENT_ID = 'YOUR_CLIENT_ID';
const CLIENT_SECRET = 'YOUR_CLIENT_SECRET';
const REDIRECT_URI = 'http://localhost:3000/callback';
const STARTUM_API = 'https://api.startum.cloud';
const STARTUM_PORTAL = 'https://startum.cloud';

// 1. Redirect to Startum Authorization with PKCE & State
app.get('/login', (req, res) => {
  const codeVerifier = crypto.randomBytes(32).toString('base64url');
  const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
  const state = crypto.randomBytes(16).toString('base64url');

  res.cookie('pkce_verifier', codeVerifier, { httpOnly: true, maxAge: 600000, sameSite: 'lax' });
  res.cookie('oauth_state', state, { httpOnly: true, maxAge: 600000, sameSite: 'lax' });

  const authUrl = new URL(`${STARTUM_PORTAL}/oauth/authorize`);
  authUrl.searchParams.set('client_id', CLIENT_ID);
  authUrl.searchParams.set('response_type', 'code');
  authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
  authUrl.searchParams.set('scope', 'openid profile email');
  authUrl.searchParams.set('code_challenge', codeChallenge);
  authUrl.searchParams.set('code_challenge_method', 'S256');
  authUrl.searchParams.set('state', state);

  res.redirect(authUrl.toString());
});

// 2. OAuth Callback Handler
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;
  const savedState = req.cookies.oauth_state;
  const codeVerifier = req.cookies.pkce_verifier;

  res.clearCookie('pkce_verifier');
  res.clearCookie('oauth_state');

  if (!code) return res.status(400).send('Missing authorization code');
  if (!state || state !== savedState) return res.status(403).send('Invalid state');
  if (!codeVerifier) return res.status(400).send('Missing PKCE verifier');

  const params = new URLSearchParams({
    grant_type: 'authorization_code',
    client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET,
    code: code,
    code_verifier: codeVerifier,
    redirect_uri: REDIRECT_URI
  });

  const tokenRes = await fetch(`${STARTUM_API}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: params.toString()
  });
  const tokens = await tokenRes.json();

  const userRes = await fetch(`${STARTUM_API}/oauth/userinfo`, {
    headers: { Authorization: `Bearer ${tokens.access_token}` }
  });
  const user = await userRes.json();

  function escapeHtml(str) {
    if (!str) return '';
    return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
  }

  res.send(`<h2>Welcome ${escapeHtml(user.name || user.username)}!</h2>`);
});

app.listen(3000, () => console.log('App running on http://localhost:3000'));

Client-Side JavaScript Popup SSO Example

// Helper: Base64URL Encoding
function base64url(buffer) {
  return btoa(String.fromCharCode(...new Uint8Array(buffer)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

// Helper: Web Crypto PKCE Generator
async function generatePKCE() {
  const bytes = new Uint8Array(32);
  window.crypto.getRandomValues(bytes);
  const verifier = base64url(bytes);

  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await window.crypto.subtle.digest('SHA-256', data);
  const challenge = base64url(digest);

  const stateBytes = new Uint8Array(16);
  window.crypto.getRandomValues(stateBytes);
  const state = base64url(stateBytes);

  sessionStorage.setItem('pkce_verifier', verifier);
  sessionStorage.setItem('oauth_state', state);
  return { challenge, state };
}

// 1. Inside your app: Trigger Startum SSO in a popup window
async function loginWithStartum() {
  const { challenge, state } = await generatePKCE();
  const width = 520, height = 680;
  const left = (window.screen.width / 2) - (width / 2);
  const top = (window.screen.height / 2) - (height / 2);

  const authUrl = new URL("https://startum.cloud/oauth/authorize");
  authUrl.searchParams.set('client_id', 'YOUR_CLIENT_ID');
  authUrl.searchParams.set('response_type', 'code');
  authUrl.searchParams.set('redirect_uri', 'https://your-app.com/popup-callback');
  authUrl.searchParams.set('scope', 'openid profile email');
  authUrl.searchParams.set('code_challenge', challenge);
  authUrl.searchParams.set('code_challenge_method', 'S256');
  authUrl.searchParams.set('state', state);
  authUrl.searchParams.set('display', 'popup');

  window.open(authUrl.toString(), "StartumSSO", `width=${width},height=${height},top=${top},left=${left}`);
}

// 2. Inside https://your-app.com/popup-callback (Callback page in popup window):
// Retrieve code and state, verify, then notify opener and close popup:
/*
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
const savedState = sessionStorage.getItem('oauth_state');
const verifier = sessionStorage.getItem('pkce_verifier');

if (window.opener && state === savedState) {
  window.opener.postMessage({
    type: "STARTUM_SSO_SUCCESS",
    code: code,
    verifier: verifier
  }, window.location.origin);
  window.close();
}
*/

// 3. In parent window: Listen for popup message on callback completion
window.addEventListener("message", async (event) => {
  if (event.origin !== window.location.origin) return;
  if (event.data && event.data.type === "STARTUM_SSO_SUCCESS") {
    const { code, verifier } = event.data;

    // ------------------------------------------------------------------------
    // SECURITY & ARCHITECTURE NOTE:
    // - Confidential Clients (Backend-Only):
    //   Never perform token exchange directly in browser JavaScript, as that would
    //   require exposing your client_secret. Instead, pass the authorization code
    //   (and verifier) to your backend server endpoint, which performs the token
    //   exchange with /oauth/token and establishes your session cookie:
    //
    //   const res = await fetch('/api/auth/startum-exchange', {
    //     method: 'POST',
    //     headers: { 'Content-Type': 'application/json' },
    //     body: JSON.stringify({ code, verifier })
    //   });
    //   if (res.ok) window.location.reload();
    //
    // - Public Clients (SPAs / Desktop / Mobile):
    //   Public clients may exchange the authorization code directly via
    //   POST https://api.startum.cloud/oauth/token using code_verifier
    //   while omitting client_secret.
    // ------------------------------------------------------------------------

    console.log("Logged in authorization code:", code);
    window.location.reload();
  }
});

5. Subject Identifier (sub) & Token Claims

Startum provides standard OpenID Connect subject identifiers (sub) unique to each client app for user privacy protection:

sub = HMAC-SHA256(Startum Master Key, startum_id + client_id)

Returned User Claims (/oauth/userinfo)

Claim Type Description
sub string Unique immutable pairwise subject ID for your application
name string User's display name or username
picture string (URI) HTTPS URL to the user's profile avatar
email string User's primary verified email address
phone_number string User's primary contact phone number (if provided)

6. Standard Endpoints Reference

Endpoint Method Description
https://startum.cloud/oauth/authorize GET Interactive User Consent & PKCE authorization flow
https://api.startum.cloud/oauth/token POST Exchange authorization code or refresh tokens
https://api.startum.cloud/oauth/userinfo GET Fetch user profile and standard identity claims
https://api.startum.cloud/.well-known/openid-configuration GET OpenID Connect Discovery metadata document
https://api.startum.cloud/.well-known/jwks.json GET Public RSA signing keys for JWT ID token verification