๐ 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 nameclient_secret(String, hashed) - Secret key for authenticationredirect_uris(Array[String]) - Whitelisted redirect URIsscopes(Array[ObjectId]) - Allowed permission scopesactive(Boolean) - Production vs development modecreated_by(ObjectId) - Developer accountwebhook_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 explanationcategory(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 identifieruser_id(ObjectId) - Authorizing useraccount_id(ObjectId) - Selected accountscopes(Array[String]) - Granted permissionsredirect_uri(String) - Callback URLexpires_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:
- Validate Client: Check
client_idexists and is active - Validate Redirect URI: Must be in app's whitelist
- Render Login Screen: Show
signin.ejstemplate - User Authentication: Verify email/password
- Account Selection: Show
account-list.ejsif user has multiple accounts - Scope Consent: Display
scopes.ejswith requested permissions - Generate Code: Create JWT authorization code
- 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_idandclient_secretin 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:
-
signin.ejs - Login form
- Email/password fields
- Client app name display
- Branding customization
-
account-list.ejs - Account selection
- Lists user's accounts
- Radio button selection
- Main/sub-account indicators
-
scopes.ejs - Permission consent
- Requested scope descriptions
- Approve/deny buttons
- Privacy policy link
-
loading.ejs - Processing screen
- Loading animation
- Status messages
-
error.ejs - Error display
- Error messages
- Return to app link
-
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)