Quick Start Guide

API Version: 5.24.0

⚠️ OAuth 2.0 Authorization Code Flow Required
This SSO uses standard OAuth 2.0. No client library needed - implement OAuth flow in your backend.

1. Register Your Application

Login to SSO Admin Panel, navigate to OAuth Clients, and click + New Client.

Required Information:

  • Application name (e.g., "Launchmass", "Messmass")
  • Application description
  • Redirect URIs (development and production)
  • Required scopes (e.g., openid profile email offline_access)
  • Homepage URL

You will receive:

{
  "client_id": "your-client-uuid",
  "client_secret": "your-secret-uuid"
}

⚠️ Store client_secret securely! It will only be shown once.

Note: PKCE (Proof Key for Code Exchange) is recommended for enhanced security. See Authentication Guide for PKCE implementation.

2. Redirect Users to SSO Login

When users click "Login", redirect them to the SSO authorization endpoint with PKCE parameters:

// In your frontend (any framework)
async function handleLogin() {
  // Generate PKCE parameters
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = await generateCodeChallenge(codeVerifier);
  
  // Store verifier for token exchange
  sessionStorage.setItem('pkce_verifier', codeVerifier);
  
  const authUrl = new URL('https://sso.doneisbetter.com/api/oauth/authorize');
  authUrl.searchParams.set('client_id', 'YOUR_CLIENT_ID');
  authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/auth/callback');
  authUrl.searchParams.set('response_type', 'code');
  authUrl.searchParams.set('scope', 'openid profile email offline_access');
  authUrl.searchParams.set('code_challenge', codeChallenge);
  authUrl.searchParams.set('code_challenge_method', 'S256');
  authUrl.searchParams.set('state', generateRandomState()); // CSRF protection
  
  // Redirect user to SSO
  window.location.href = authUrl.toString();
}

function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64URLEncode(array);
}

async function generateCodeChallenge(verifier) {
  const data = new TextEncoder().encode(verifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return base64URLEncode(new Uint8Array(hash));
}

function base64URLEncode(buffer) {
  return btoa(String.fromCharCode(...buffer))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

Parameters:

  • client_id - Your OAuth client ID (from step 1)
  • redirect_uri - Where SSO redirects after login (must match registered URI)
  • response_type - Always code for authorization code flow
  • scope - Requested scopes (space-separated, include offline_access for refresh tokens)
  • code_challenge - PKCE challenge (SHA-256 hash of code_verifier)
  • code_challenge_method - Always S256
  • state - Random string for CSRF protection
  • prompt - (Optional) Controls auth behavior:
    • login - Force re-authentication (use on logout to require credentials)
    • consent - Force consent screen
    • none - No UI, fail if interaction required

Authentication Options: Users can login with email/password, magic link (passwordless), or social providers (Facebook, Google coming soon).

3. Handle OAuth Callback (Backend)

After user authenticates, SSO redirects to your redirect_uri with an authorization code:

https://yourapp.com/auth/callback?code=AUTHORIZATION_CODE

Exchange the code for tokens (server-side only!):

// Node.js / Express example
app.get('/auth/callback', async (req, res) => {
  const { code, state } = req.query;
  
  // Validate state parameter (CSRF protection)
  if (state !== req.session.oauth_state) {
    return res.status(400).send('Invalid state parameter');
  }
  
  // Retrieve PKCE verifier from session
  const codeVerifier = req.session.pkce_verifier;
  
  // WHAT: Exchange authorization code for tokens
  // WHY: Code is single-use and short-lived, need long-lived tokens
  const tokenResponse = await fetch('https://sso.doneisbetter.com/api/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      code: code,
      client_id: process.env.SSO_CLIENT_ID,
      client_secret: process.env.SSO_CLIENT_SECRET,
      redirect_uri: 'https://yourapp.com/auth/callback',
      code_verifier: codeVerifier  // PKCE verification
    })
  });
  
  const tokens = await tokenResponse.json();
  // tokens = { access_token, refresh_token, id_token, expires_in }
  
  // WHAT: Decode ID token to get user info
  // WHY: ID token contains user identity (works for all login methods)
  const jwt = require('jsonwebtoken');
  const userInfo = jwt.decode(tokens.id_token);
  // userInfo = { sub, email, name, email_verified, picture }
  
  // WHAT: Create your app's session
  // WHY: Store user identity and tokens for future API calls
  req.session.userId = userInfo.sub;
  req.session.email = userInfo.email;
  req.session.name = userInfo.name;
  req.session.accessToken = tokens.access_token;
  req.session.refreshToken = tokens.refresh_token;
  
  // Redirect to your app
  res.redirect('/dashboard');
});

4. Validate User Sessions

Before serving protected content, validate the user's session:

// Middleware to protect routes
async function requireAuth(req, res, next) {
  const accessToken = req.session.accessToken;
  
  if (!accessToken) {
    return res.redirect('/login');
  }
  
  // WHAT: Validate access token with SSO
  // WHY: Ensure user still has approved access (not revoked)
  const validation = await fetch('https://sso.doneisbetter.com/api/public/session', {
    headers: {
      'Authorization': `Bearer ${accessToken}`
    }
  });
  
  if (!validation.ok) {
    // Token invalid or expired - try refresh
    const refreshed = await refreshAccessToken(req.session.refreshToken);
    if (!refreshed) {
      return res.redirect('/login');
    }
    req.session.accessToken = refreshed.access_token;
  }
  
  next();
}

// Use middleware
app.get('/dashboard', requireAuth, (req, res) => {
  res.render('dashboard', { user: req.session });
});

5. Refresh Access Tokens

Access tokens expire after 1 hour. Use refresh tokens to get new ones:

async function refreshAccessToken(refreshToken) {
  const response = await fetch('https://sso.doneisbetter.com/api/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: process.env.SSO_CLIENT_ID,
      client_secret: process.env.SSO_CLIENT_SECRET
    })
  });
  
  if (!response.ok) {
    return null; // Refresh token invalid or expired
  }
  
  return await response.json();
  // Returns { access_token, refresh_token, expires_in }
}

6. Implement Logout

Revoke tokens and clear session (both your app and SSO):

app.post('/logout', async (req, res) => {
  const refreshToken = req.session.refreshToken;
  
  // WHAT: Revoke refresh token at SSO
  // WHY: Invalidate all tokens (access tokens become invalid too)
  await fetch('https://sso.doneisbetter.com/api/oauth/revoke', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      token: refreshToken,
      token_type_hint: 'refresh_token',
      client_id: process.env.SSO_CLIENT_ID,
      client_secret: process.env.SSO_CLIENT_SECRET
    })
  });
  
  // Clear your app's session
  req.session.destroy();
  
  // WHAT: Redirect to SSO logout endpoint
  // WHY: Clear SSO cookie session across all apps
  const logoutUrl = 'https://sso.doneisbetter.com/api/oauth/logout' +
    '?post_logout_redirect_uri=' + encodeURIComponent('https://yourapp.com');
  res.redirect(logoutUrl);
});

Note: The post_logout_redirect_uri must match a registered redirect URI for your OAuth client.

Security Tip: After logout, when redirecting user back to login, add prompt=login parameter to force re-authentication:

// After logout redirect
const loginUrl = 'https://sso.doneisbetter.com/api/oauth/authorize' +
  '?client_id=' + encodeURIComponent(CLIENT_ID) +
  '&redirect_uri=' + encodeURIComponent(REDIRECT_URI) +
  '&response_type=code' +
  '&scope=openid profile email offline_access' +
  '&state=' + generateRandomState() +
  '&code_challenge=' + codeChallenge +
  '&code_challenge_method=S256' +
  '&prompt=login';  // Force re-authentication

window.location.href = loginUrl;

Next Steps

You now have basic OAuth 2.0 integration! Continue with:

Common Issues

  • "Access Pending Approval"

    User registered but SSO admin hasn't approved access yet. Check Admin Approval Guide.

  • "Redirect URI Mismatch"

    The redirect_uri parameter must exactly match a registered URI. Check with SSO admins.

  • "Invalid Client Credentials"

    client_id or client_secret is wrong. Verify environment variables.

  • Token expired errors

    Implement token refresh (step 5) to handle expiration gracefully.