Skip to main content

๐Ÿ” OAuth Module

๐Ÿ“– Overviewโ€‹

The OAuth module implements an OAuth 2.0 Authorization Server, enabling third-party applications to securely access DashClicks platform resources on behalf of users. It provides the complete OAuth 2.0 authorization code flow with refresh tokens, scope-based permissions, and multi-account selection.

File Path: internal/api/v1/oauth/

Key Featuresโ€‹

  • OAuth 2.0 Compliance: Full authorization code grant flow
  • Multi-Account Support: Users can choose which account to authorize
  • Scope-Based Permissions: Granular access control per application
  • Refresh Tokens: Long-lived tokens for background access
  • Web UI: Built-in authorization consent screens (EJS templates)
  • Basic Auth: Client authentication via HTTP Basic Auth or body parameters
  • State Parameter: CSRF protection via state validation

๏ฟฝ Directory Structureโ€‹

oauth/
โ”œโ”€โ”€ ๐Ÿ“„ index.js - OAuth endpoints (611 lines)
โ”œโ”€โ”€ ๐Ÿ“„ README.md - Module documentation
โ”œโ”€โ”€ ๐Ÿ“‚ views/ - EJS templates for consent UI
โ”‚ โ”œโ”€โ”€ signin.ejs - Login page
โ”‚ โ”œโ”€โ”€ account-list.ejs - Account selection
โ”‚ โ”œโ”€โ”€ scopes.ejs - Permission consent
โ”‚ โ”œโ”€โ”€ loading.ejs - Processing screen
โ”‚ โ”œโ”€โ”€ error.ejs - Error display
โ”‚ โ””โ”€โ”€ css.ejs - Shared styles
โ””โ”€โ”€ ๐Ÿ“‚ controllers/
โ””โ”€โ”€ (handlers if separated)

Note: Main OAuth logic is in index.js - not separated into controllers/services.

๏ฟฝ๐Ÿ—„๏ธ Collections Usedโ€‹

api.appsโ€‹

  • Purpose: Registered third-party applications
  • Model: shared/models/api-app.js
  • Key Fields:
    • _id (String) - Client ID (public identifier)
    • name (String) - Application name
    • client_secret (String, hashed) - Secret key for authentication
    • redirect_uris (Array[String]) - Whitelisted redirect URIs
    • scopes (Array[ObjectId]) - Allowed permission scopes
    • active (Boolean) - Production vs development mode
    • created_by (ObjectId) - Developer account
    • webhook_url (String) - Event notification endpoint

api.scopesโ€‹

  • Purpose: Define available permission scopes
  • Model: shared/models/api-scope.js
  • Key Fields:
    • scope (String, unique) - Scope identifier (e.g., 'crm.contacts.read')
    • description (String) - Human-readable explanation
    • category (String) - Grouping (e.g., 'CRM', 'Billing', 'Reports')

Common Scopes:

const SCOPES = [
'crm.contacts.read',
'crm.contacts.write',
'crm.deals.read',
'crm.deals.write',
'billing.read',
'projects.tasks.read',
'analytics.read',
'sites.read',
'sites.write',
];

api.grantsโ€‹

  • Purpose: Store authorization codes (short-lived)
  • Model: shared/models/api-grant.js
  • Key Fields:
    • code (String, unique) - Authorization code (JWT)
    • client_id (String) - Application identifier
    • user_id (ObjectId) - Authorizing user
    • account_id (ObjectId) - Selected account
    • scopes (Array[String]) - Granted permissions
    • redirect_uri (String) - Callback URL
    • expires_at (Date) - Code expiration (10 minutes)
    • used (Boolean) - Prevent code reuse

api.refresh_tokensโ€‹

  • Purpose: Store refresh tokens (long-lived)
  • Model: shared/models/api-refresh-token.js
  • Key Fields:
    • token (String, unique) - Refresh token (JWT)
    • client_id (String)
    • user_id (ObjectId)
    • account_id (ObjectId)
    • scopes (Array[String])
    • expires_at (Date) - Token expiration (90 days)
    • revoked (Boolean) - Manual revocation

๐Ÿ”„ OAuth 2.0 Authorization Flowโ€‹

Authorization Code Flowโ€‹

sequenceDiagram
participant ThirdParty as Third-Party App
participant Browser as User Browser
participant OAuth as OAuth Server
participant User as User Login
participant API as DashClicks API

ThirdParty->>Browser: Redirect to /authorize?client_id=...&redirect_uri=...
Browser->>OAuth: GET /authorize
OAuth->>Browser: Show login screen (signin.ejs)

Browser->>User: Enter credentials
User->>OAuth: POST /authorize (email, password)
OAuth->>OAuth: Authenticate user
OAuth->>Browser: Show account selection (account-list.ejs)

Browser->>OAuth: Select account
OAuth->>Browser: Show scope consent (scopes.ejs)

Browser->>OAuth: Approve scopes
OAuth->>OAuth: Generate authorization code (JWT)
OAuth->>Browser: Redirect to redirect_uri?code=xxx&state=xxx

Browser->>ThirdParty: Deliver authorization code
ThirdParty->>OAuth: POST /token (code, client_secret)
OAuth->>OAuth: Validate code & client credentials
OAuth-->>ThirdParty: { access_token, refresh_token, expires_in }

ThirdParty->>API: Request with Bearer token
API->>API: Validate JWT token
API-->>ThirdParty: Protected resource data

Refresh Token Flowโ€‹

sequenceDiagram
participant ThirdParty as Third-Party App
participant OAuth as OAuth Server
participant DB as Database

Note over ThirdParty: Access token expired

ThirdParty->>OAuth: POST /token (grant_type=refresh_token, refresh_token=xxx)
OAuth->>DB: Find refresh token
DB-->>OAuth: { user_id, account_id, scopes }

OAuth->>OAuth: Validate token not revoked/expired
OAuth->>OAuth: Generate new access token
OAuth->>DB: Update last_used timestamp
OAuth-->>ThirdParty: { access_token, expires_in }

ThirdParty->>ThirdParty: Store new access token

๐Ÿ”ง Core Endpointsโ€‹

GET /authorizeโ€‹

Initiates OAuth authorization flow with user consent.

Query Parameters:

{
response_type: 'code', // Required: OAuth flow type
client_id: String, // Required: App identifier
redirect_uri: String, // Required: Callback URL
scope: String, // Optional: Space-separated scopes
state: String, // Recommended: CSRF token
user_locale: String // Optional: UI language
}

Process:

  1. Validate Client: Check client_id exists and is active
  2. Validate Redirect URI: Must be in app's whitelist
  3. Render Login Screen: Show signin.ejs template
  4. User Authentication: Verify email/password
  5. Account Selection: Show account-list.ejs if user has multiple accounts
  6. Scope Consent: Display scopes.ejs with requested permissions
  7. Generate Code: Create JWT authorization code
  8. Redirect: redirect_uri?code=xxx&state=yyy

Error Responses:

// Invalid client_id
redirect_uri?error=unauthorized_client&error_description=Invalid Client ID

// App not active
redirect_uri?error=unauthorized_client&error_description=App is in dev mode

// Missing response_type
redirect_uri?error=invalid_request&state=xxx

// Unsupported response_type
redirect_uri?error=unsupported_response_type&state=xxx

// User denies access
redirect_uri?error=access_denied&state=xxx

POST /tokenโ€‹

Exchanges authorization code for access token or refreshes expired token.

Request Body (Authorization Code Grant):

{
grant_type: 'authorization_code',
code: String, // From /authorize callback
redirect_uri: String, // Must match original
client_id: String, // Optional if using Basic Auth
client_secret: String // Optional if using Basic Auth
}

Request Body (Refresh Token Grant):

{
grant_type: 'refresh_token',
refresh_token: String,
client_id: String, // Optional if using Basic Auth
client_secret: String // Optional if using Basic Auth
}

Authentication:

  • HTTP Basic Auth: Authorization: Basic base64(client_id:client_secret)
  • Body Parameters: Include client_id and client_secret in body

Success Response:

{
access_token: String, // JWT token (expires in 1 hour)
token_type: 'Bearer',
expires_in: 3600, // Seconds until expiration
refresh_token: String, // Long-lived token (90 days)
scope: String // Granted scopes
}

Error Responses:

// Invalid authorization code
{ error: 'invalid_grant', error_description: 'Authorization code not found or expired' }

// Invalid client credentials
{ error: 'invalid_client', error_description: 'Client authentication failed' }

// Redirect URI mismatch
{ error: 'invalid_grant', error_description: 'Redirect URI does not match' }

// Revoked refresh token
{ error: 'invalid_grant', error_description: 'Refresh token has been revoked' }

GET /findUsersโ€‹

Search for users by email (used in authorization flow).

Query Parameters:

{
email: String, // User email to search
limit: Number, // Results per page (1-50)
page: Number // Page number
}

Response:

{
success: true,
data: [
{
_id: ObjectId,
email: 'user@example.com',
name: 'John Doe',
accounts: [
{ _id: ObjectId, name: 'Main Account', main: true },
{ _id: ObjectId, name: 'Sub Account', main: false }
]
}
]
}

๐ŸŽจ Authorization UI Templatesโ€‹

EJS Viewsโ€‹

Location: views/*.ejs

Templates:

  1. signin.ejs - Login form

    • Email/password fields
    • Client app name display
    • Branding customization
  2. account-list.ejs - Account selection

    • Lists user's accounts
    • Radio button selection
    • Main/sub-account indicators
  3. scopes.ejs - Permission consent

    • Requested scope descriptions
    • Approve/deny buttons
    • Privacy policy link
  4. loading.ejs - Processing screen

    • Loading animation
    • Status messages
  5. error.ejs - Error display

    • Error messages
    • Return to app link
  6. css.ejs - Shared styles

Template Variablesโ€‹

res.locals = {
client_name: 'Third-Party App Name',
userscope: 'crm.contacts.read crm.deals.read',
redirect_uri: 'https://app.example.com/callback',
token: 'jwt_token_for_state',
base_url: process.env.API_PUBLIC_BASE_URL,
error: 'Error message if applicable',
};

๐Ÿ”€ Integration Pointsโ€‹

API Authenticationโ€‹

Third-party apps use OAuth access tokens to authenticate API requests:

// HTTP Header
Authorization: Bearer <
access_token >
// Decoded JWT payload
{
type: 'access_token',
uid: 'user_id',
account_id: 'account_id',
parent_account: 'parent_account_id',
client_id: 'app_client_id',
scope: 'crm.contacts.read crm.deals.read',
exp: timestamp,
iat: timestamp,
};

Webhook Notificationsโ€‹

Apps can register webhook URLs to receive real-time event notifications:

// api.apps.webhook_url configuration
POST https://app.example.com/webhooks
{
event: 'contact.created',
data: { ... },
account_id: 'xxx',
timestamp: Date
}

Rate Limitingโ€‹

OAuth apps subject to rate limits:

  • Authorization Requests: 10 per minute per IP
  • Token Requests: 100 per hour per client_id
  • API Requests: Based on account tier (1000-10000 per hour)

๐Ÿ” Security Featuresโ€‹

Client Authenticationโ€‹

Basic Auth:

curl -X POST https://api.dashclicks.com/v1/auth/oauth/token \
-u "client_id:client_secret" \
-d "grant_type=authorization_code&code=xxx&redirect_uri=..."

Body Parameters:

curl -X POST https://api.dashclicks.com/v1/auth/oauth/token \
-d "client_id=xxx&client_secret=yyy&grant_type=authorization_code&code=zzz&redirect_uri=..."

Secret Hashingโ€‹

Client secrets stored as bcrypt hashes:

const { newHash, verifyHash } = require('../utilities/auth');

// On app creation
const hashed_secret = await newHash(client_secret);

// On authentication
const valid = await verifyHash(provided_secret, stored_hash);

State Parameterโ€‹

CSRF protection via state validation:

// App generates random state
const state = crypto.randomBytes(16).toString('hex');

// Pass to /authorize
const authUrl = `${baseUrl}/authorize?client_id=...&state=${state}`;

// Validate in callback
if (req.query.state !== storedState) {
throw new Error('State mismatch - possible CSRF attack');
}

Token Expirationโ€‹

  • Authorization Codes: 10 minutes
  • Access Tokens: 1 hour
  • Refresh Tokens: 90 days

Redirect URI Validationโ€‹

// Whitelist enforcement
if (!app.redirect_uris.includes(req.query.redirect_uri)) {
throw new Error('Redirect URI not authorized');
}

// Exact match required (no wildcards)

โš ๏ธ Important Notesโ€‹

  • ๐Ÿ”’ HTTPS Required: All OAuth endpoints require HTTPS in production
  • ๐Ÿ”„ Code Single-Use: Authorization codes can only be exchanged once
  • ๐Ÿ“Š Audit Logging: All token issuance and revocation events logged
  • ๐ŸŒ CORS Enabled: Cross-origin requests allowed for /token endpoint
  • โฐ Clock Skew: 5-minute tolerance for JWT expiration validation
  • ๐Ÿšซ No Implicit Flow: Only authorization code grant supported
  • ๐Ÿ”‘ Secret Rotation: Client secrets can be rotated without downtime

๐Ÿงช Edge Cases & Special Handlingโ€‹

Case: User Has Multiple Accountsโ€‹

Handling: Display account selection screen before scope consent

Case: App Requests Unknown Scopeโ€‹

Handling: Filter to intersection of requested and allowed scopes

Case: User Changes Accounts Mid-Flowโ€‹

Handling: State token includes account_id to prevent switching

Case: Refresh Token Reuseโ€‹

Handling: Track last_used timestamp, allow reuse within grace period


Last Updated: 2025-10-08
Module Path: internal/api/v1/oauth/
Primary File: index.js (611 lines)
OAuth Version: 2.0 (RFC 6749)

๐Ÿ’ฌ

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM