Authentication Guide
API Version: 5.24.0
Overview
DoneIsBetter SSO implements OAuth 2.0 Authorization Code Flow with PKCE support. This is a standard, secure authentication protocol used by major providers (Google, GitHub, etc.).
Key Features:
- Standard OAuth 2.0 - No proprietary client library needed
- JWT-based tokens (access_token, refresh_token, id_token)
- App-level permission control (pending → approved → revoked)
- Role-based access within apps (user vs admin)
- PKCE support for public clients (mobile apps, SPAs)
- OpenID Connect compliance (/.well-known endpoints)
OAuth 2.0 Flow Diagram
┌─────────────┐ ┌──────────────┐
│ Your App │ │ SSO Server │
│ (Client) │ │ │
└──────┬──────┘ └──────┬───────┘
│ │
│ 1. Redirect to /api/oauth/authorize │
│ with client_id, redirect_uri, scope │
├─────────────────────────────────────────────────>│
│ │
│ 2. User logs in
│ 3. Check app permissions
│ - approved? → continue
│ - pending? → show message
│ - revoked? → deny
│ │
│ 4. Redirect to your redirect_uri with code │
│<─────────────────────────────────────────────────┤
│ │
│ 5. POST /api/oauth/token │
│ with code, client_id, client_secret │
├─────────────────────────────────────────────────>│
│ │
│ 6. Return tokens │
│ { access_token, refresh_token, id_token } │
│<─────────────────────────────────────────────────┤
│ │
│ 7. Use access_token for API calls │
│ Authorization: Bearer <access_token> │
├─────────────────────────────────────────────────>│
│ │
│ 8. Token expires after 1 hour │
│ │
│ 9. POST /api/oauth/token with refresh_token │
├─────────────────────────────────────────────────>│
│ │
│ 10. Return new access_token │
│<─────────────────────────────────────────────────┤
OAuth 2.0 Flow Steps
Step 1: Authorization Request
Redirect the user to the SSO authorization endpoint:
GET https://sso.doneisbetter.com/api/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.com/auth/callback&response_type=code&scope=openid%20profile%20email&state=RANDOM_STATE_STRING
Required Parameters:
client_id- Your OAuth client ID (UUID)redirect_uri- Where SSO redirects after authentication (must be registered)response_type- Alwayscodefor authorization code flowscope- Space-separated scopes (e.g.,openid profile email)
Optional Parameters:
state- Random string to prevent CSRF attacks (recommended)code_challenge- For PKCE (required for public clients)code_challenge_method- Should beS256
Step 2: User Authentication
SSO presents a login page. Users can authenticate via:
- Email + Password - Traditional login
- Magic Link - Passwordless email link (15 min expiry)
- PIN Code - 6-digit time-based code
Step 3: Permission Check
After authentication, SSO checks if user has permission to access your app:
// SSO checks appPermissions collection:
{
userId: "user-uuid",
clientId: "your-client-id",
status: "approved", // Can be: approved, pending, revoked, none
role: "admin" // App-level role: user or admin
}Status Outcomes:
approved- User can access (proceed to step 4)pending- SSO shows "Access Pending Approval" messagerevokedornone- SSO shows "Access Denied"
🔒 Security Note: Only SSO admins can approve/revoke app access. Apps cannot grant themselves permission. See Admin Approval Guide.
Step 4: Authorization Code Redirect
If approved, SSO redirects back to your app with an authorization code:
https://yourapp.com/auth/callback?code=AUTH_CODE&state=RANDOM_STATE_STRING
Important: Authorization codes are:
- Single-use only
- Expire after 10 minutes
- Must be exchanged server-side (never in browser)
Step 5: Token Exchange (Server-Side Only!)
Exchange the authorization code for tokens:
POST https://sso.doneisbetter.com/api/oauth/token
Content-Type: application/json
{
"grant_type": "authorization_code",
"code": "AUTH_CODE",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"redirect_uri": "https://yourapp.com/auth/callback"
}Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Step 6: Decode ID Token
The id_token contains user identity and app-level role:
// Decode JWT (no verification needed if received from /token directly)
const jwt = require('jsonwebtoken');
const userInfo = jwt.decode(id_token);
// userInfo payload:
{
"sub": "550e8400-e29b-41d4-a716-446655440000", // User UUID
"email": "user@example.com",
"name": "John Doe",
"role": "admin", // App-level role: 'user' or 'admin'
"iat": 1234567890,
"exp": 1234571490,
"iss": "https://sso.doneisbetter.com",
"aud": "YOUR_CLIENT_ID"
}Step 7: Use Access Token
Include access_token in API requests:
GET https://sso.doneisbetter.com/api/public/session
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
// Response:
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name": "John Doe",
"role": "admin"
}Step 8: Token Refresh
Access tokens expire after 1 hour. Refresh without re-login:
POST https://sso.doneisbetter.com/api/oauth/token
Content-Type: application/json
{
"grant_type": "refresh_token",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}
// Response:
{
"access_token": "new_access_token",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "new_refresh_token" // May rotate
}Step 9: Token Revocation (Logout)
Revoke tokens when user logs out:
POST https://sso.doneisbetter.com/api/oauth/revoke
Content-Type: application/json
{
"token": "access_token_or_refresh_token",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}Security Considerations
- HTTPS Only
All OAuth endpoints require HTTPS in production. HTTP only allowed in development.
- Client Secret Protection
NEVER expose
client_secretin browser code. Token exchange MUST happen server-side. - State Parameter
Always use
stateparameter to prevent CSRF attacks. Validate it matches after redirect. - PKCE for Public Clients
If your app is a SPA or mobile app (can't securely store client_secret), use PKCE.
- Token Storage
Store tokens in HttpOnly cookies (backend) or secure memory (frontend - never localStorage for refresh tokens).
- Token Expiration
Access tokens expire after 1 hour. Refresh tokens expire after 30 days of inactivity.
- Redirect URI Validation
SSO validates redirect_uri exactly matches registered URIs. No wildcards or partial matches.
- Permission Revocation
If SSO admin revokes app access, user's existing tokens remain valid until expiry. Implement periodic session validation.
Error Handling
Common OAuth errors and how to handle them:
Authorization Errors (Step 1-3)
access_pending- User account exists but access not yet approvedShow: "Your access request is pending admin approval."
access_denied- User denied authorization or doesn't have permissionShow: "You don't have permission to access this application."
invalid_client- client_id not found or client suspendedAction: Check client_id, contact SSO admins
unauthorized_client- Client not allowed to use this grant typeAction: Verify OAuth client configuration
invalid_scope- Requested scope not allowed for this clientAction: Request only registered scopes
Token Errors (Step 5, 8)
invalid_grant- Authorization code invalid, expired, or already usedAction: Start OAuth flow again from step 1
invalid_client_secret- Client credentials wrongAction: Verify environment variables
redirect_uri_mismatch- redirect_uri doesn't match registered URIAction: Use exact registered URI (including protocol, port)
API Errors (Step 7)
401 Unauthorized- Access token invalid, expired, or revokedAction: Try refresh token, then re-authenticate if that fails
403 Forbidden- User lost app permissionAction: Show "Access revoked" message, clear session
Implementation Best Practices
- ✅ Always use
stateparameter to prevent CSRF - ✅ Exchange authorization code server-side (never in browser)
- ✅ Store
client_secretin environment variables, never in code - ✅ Implement token refresh before access token expires
- ✅ Handle permission errors gracefully (pending/revoked states)
- ✅ Validate
stateparameter on callback - ✅ Use HTTPS in production (required by SSO)
- ✅ Clear tokens on logout (call /api/oauth/revoke)
- ✅ Periodically validate session (detect revoked access)
- ✅ Log OAuth errors for debugging (but never log secrets!)
OpenID Connect Support
DoneIsBetter SSO is OpenID Connect compliant. Discovery endpoints:
// Discovery document GET https://sso.doneisbetter.com/.well-known/openid-configuration // JWKS (public keys for token verification) GET https://sso.doneisbetter.com/.well-known/jwks.json
Use these endpoints to automatically configure OAuth libraries (e.g., Passport.js, NextAuth.js).
Related Documentation
- Quick Start Guide - Get started quickly
- App Permissions - Understanding approval workflow
- Admin Approval Process - How users get access
- API Reference - Complete endpoint docs
- Security Best Practices