๐ฏ Hubspot - Authentication
๐ Overviewโ
The Hubspot integration uses OAuth 2.0 Authorization Code Grant flow to securely authenticate users and obtain API access tokens. The implementation includes automatic token refresh, state-based CSRF protection, and persistent token storage in MongoDB.
Source Files:
- Controller:
external/Integrations/Hubspot/Controllers/authController.js - Provider:
external/Integrations/Hubspot/Providers/api.js - Model:
external/Integrations/Hubspot/Models/keys.js - Routes:
external/Integrations/Hubspot/Routes/authRoutes.js
Hubspot OAuth API: https://app.hubspot.com/oauth/authorize, https://api.hubapi.com/oauth/v1/token
๐๏ธ Collections Usedโ
integrations.hubspot.keyโ
- Operations: Create, Read, Update, Delete
- Model:
shared/models/hubspot-key.js - Usage Context: Store OAuth access tokens, refresh tokens, and authentication metadata
Document Structure:
{
_id: ObjectId("507f1f77bcf86cd799439011"),
account_id: "507f191e810c19729de860ea",
owner: "507f191e810c19729de860eb",
token: {
access_token: "CJzg...",
refresh_token: "6f8a...",
expires_in: 1800,
token_type: "bearer"
},
generated_at: 1704844800,
token_invalidated: false
}
๐ OAuth 2.0 Flowโ
Complete Authentication Sequenceโ
sequenceDiagram
participant User as User Browser
participant DC as DashClicks API
participant JWT as JWT Handler
participant DB as MongoDB
participant Hubspot as Hubspot OAuth
User->>DC: GET /auth/login?forward_url=...
DC->>DC: Verify authorization (JWT)
DC->>DB: Check existing token
alt Token exists and valid
DC-->>User: Redirect to forward_url?status=success
else Token missing or invalidated
DC->>JWT: Create state token (account_id, uid, forward_url)
JWT-->>DC: Signed JWT (6h expiration)
DC->>DC: Build authorize URL with state
DC-->>User: Redirect to Hubspot OAuth
User->>Hubspot: User authorizes app
Hubspot-->>User: Redirect to callback with code + state
User->>DC: GET /auth/callback?code=...&state=...
DC->>JWT: Verify and decode state
DC->>Hubspot: POST /oauth/v1/token (exchange code)
Hubspot-->>DC: Access token + refresh token
DC->>DB: Save tokens with generated_at timestamp
DC-->>User: Redirect to forward_url?status=success&token=...
end
๐ง Authentication Functionsโ
getLogin(req, res)โ
Purpose: Initiate OAuth 2.0 authorization flow or return existing valid token
Source: Controllers/authController.js
Endpoint: GET /v1/integrations/hubspot/auth/login
Query Parameters:
forward_url(String, required) - Callback URL after successful authentication
Authorization:
- Requires valid JWT in
req.auth - Account ID and user ID extracted from JWT
Business Logic Flow:
-
Validate Request
- Check if
forward_urlquery parameter exists - Verify
req.authcontains valid JWT authentication
- Check if
-
Check Existing Token
- Query
integrations.hubspot.keycollection byaccount_idandowner - If token exists and not invalidated, return success immediately
- Query
-
Handle Invalid/Missing Token
- If
token_invalidated: true, delete the token - Generate JWT state token containing:
aid(account_id)uid(owner)forward_url
- Sign with
APP_SECRET, 6-hour expiration
- If
-
Build Authorization URL
- Call
providers.authorizeURL(stateToken) - Redirect user to Hubspot OAuth consent screen
- Call
-
Handle Errors
- Catch errors and redirect to
forward_urlwith error parameters
- Catch errors and redirect to
Success Response (token exists):
HTTP/1.1 302 Found
Location: {forward_url}?status=success&integration=hubspot&token={docId}
Success Response (new auth):
HTTP/1.1 302 Found
Location: https://app.hubspot.com/oauth/authorize?client_id=...&state={jwt}
Error Response:
HTTP/1.1 302 Found
Location: {forward_url}?status=error&integration=hubspot&reason={error_message}
Error Response (missing forward_url):
{
"success": false,
"message": "Please provide forward_url"
}
Example Usage:
// Frontend initiates OAuth
window.location.href =
'/v1/integrations/hubspot/auth/login?forward_url=https://app.dashclicks.com/integrations/success';
State Token Structure:
// JWT payload
{
"aid": "507f191e810c19729de860ea",
"uid": "507f191e810c19729de860eb",
"forward_url": "https://app.dashclicks.com/integrations/success",
"exp": 1704866400, // 6 hours from creation
"iat": 1704844800
}
callback(req, res)โ
Purpose: Handle OAuth callback, exchange authorization code for access tokens, and persist credentials
Source: Controllers/authController.js
Endpoint: GET /v1/integrations/hubspot/auth/callback
Query Parameters:
code(String) - Authorization code from Hubspot OAuthstate(String) - JWT state token created during login
Hubspot API Endpoint: POST https://api.hubapi.com/oauth/v1/token
Business Logic Flow:
-
Validate Authorization Code
- Check if
req.query.codeexists - If missing, redirect with error
- Check if
-
Decode State Token
- Verify and decode JWT state parameter using
APP_SECRET - Extract
account_id,owner, andforward_url
- Verify and decode JWT state parameter using
-
Exchange Code for Tokens
- Call
providers.accessToken(req.query.code) - POST request to Hubspot token endpoint with:
grant_type: "authorization_code"client_id: Hubspot client IDclient_secret: Hubspot client secretredirect_uri: Callback URLcode: Authorization code
- Call
-
Prepare Data for Storage
{
token: response.data,
account_id: "507f191e810c19729de860ea",
owner: "507f191e810c19729de860eb",
generated_at: 1704844800 // moment().unix()
} -
Check Existing Token
- Query database for existing token by
account_idandowner - If exists, update existing document
- If not, create new document
- Query database for existing token by
-
Save to Database
- Call
DatabaseQuery.saveRefreshToken(docId, dataToSave) - Store complete token object in
integrations.hubspot.key
- Call
-
Redirect to Success URL
- Redirect to
forward_urlwith success parameters and token ID
- Redirect to
Hubspot API Request:
POST https://api.hubapi.com/oauth/v1/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
client_id=2acbec95-d71a-4f7a-a039-0f5c2c3e9282&
client_secret=45e4abbc-41d3-40bc-9dc0-c175f8b01336&
redirect_uri=http://localhost:5000/v1/integrations/hubspot/auth/callback&
code=abc123def456
Hubspot API Response:
{
"access_token": "CJzg_asd9fa8s7df98a7sdf...",
"refresh_token": "6f8a_s8df7asdf87asdf8a...",
"expires_in": 1800,
"token_type": "bearer"
}
Success Response:
HTTP/1.1 302 Found
Location: {forward_url}?status=success&integration=hubspot&token={docId}
Error Responses:
# Missing code
HTTP/1.1 302 Found
Location: ?status=error&integration=hubspot&reason=Authentication not initiated
# Token exchange failed
HTTP/1.1 302 Found
Location: {forward_url}?status=error&integration=hubspot&reason={hubspot_error_message}
# Data not in response
HTTP/1.1 302 Found
Location: ?status=error&integration=hubspot&reason=Data is not found in the response
deleteAccessToken(req, res)โ
Purpose: Delete stored OAuth tokens and revoke integration access
Source: Controllers/authController.js
Endpoint: DELETE /v1/integrations/hubspot/auth/
Authorization: Requires valid JWT in req.auth
Business Logic Flow:
-
Verify Authentication
- Check
req.authfor valid JWT - Extract
account_idanduid
- Check
-
Find Token Document
- Query
integrations.hubspot.keybyaccount_idandowner - Return 404 if token not found
- Query
-
Delete Token
- Call
DatabaseQuery.deleteToken(token.id) - Permanently remove document from database
- Call
-
Return Success Response
- Confirm deletion to client
Success Response:
{
"succes": true,
"message": "Access Token has been successfully deleted"
}
Error Responses:
// Token not found
{
"success": false,
"errno": 400,
"message": "Access Token Not Found"
}
// Unauthorized
{
"success": false,
"errno": 400,
"message": "Unauthorized User"
}
// Generic error
{
"success": false,
"errno": 400,
"message": "Something went wrong"
}
Example Usage:
// Delete integration
await fetch('/v1/integrations/hubspot/auth/', {
method: 'DELETE',
headers: {
Authorization: 'Bearer {jwt_token}',
},
});
๐ง Provider Functionsโ
authorizeURL(stateParameter)โ
Purpose: Build Hubspot OAuth authorization URL with state parameter
Source: Providers/api.js
Parameters:
stateParameter(String) - JWT state token
Returns: Promise<String> - Complete OAuth authorization URL
URL Structure:
https://app.hubspot.com/oauth/authorize?
client_id={HUBSPOT_CLIENT_ID}&
client_Secret={HUBSPOT_CLIENT_SECRET}&
scope={HUBSPOT_SCOPES}&
redirect_uri={HUBSPOT_REDIRECT_URL}&
state={stateParameter}
Example:
const url = await providers.authorizeURL('eyJhbGciOiJIUzI1...');
// Returns: https://app.hubspot.com/oauth/authorize?client_id=...
accessToken(queryCode)โ
Purpose: Exchange authorization code for access and refresh tokens
Source: Providers/api.js
Hubspot API Endpoint: POST https://api.hubapi.com/oauth/v1/token
Parameters:
queryCode(String) - Authorization code from OAuth callback
Returns: Promise<Object> - Axios response object with token data
Request Configuration:
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: {
grant_type: 'authorization_code',
client_id: HUBSPOT_CLIENT_ID,
client_secret: HUBSPOT_CLIENT_SECRET,
redirect_uri: HUBSPOT_REDIRECT_URL,
code: queryCode
}
}
Response:
{
data: {
access_token: "CJzg...",
refresh_token: "6f8a...",
expires_in: 1800,
token_type: "bearer"
},
status: 200,
statusText: "OK"
}
getRefreshAccessToken(queryData)โ
Purpose: Obtain new access token using refresh token and update database
Source: Providers/api.js
Hubspot API Endpoint: POST https://api.hubapi.com/oauth/v1/token
Parameters:
queryData(Object) - Token document from databaseid(String) - MongoDB document IDtoken.refresh_token(String) - Stored refresh token
Returns: Promise<Object> - New token data
Business Logic Flow:
-
Extract Refresh Token
- Get
refresh_tokenfrom stored token data
- Get
-
Build Token Request
{
grant_type: 'refresh_token',
client_id: HUBSPOT_CLIENT_ID,
client_secret: HUBSPOT_CLIENT_SECRET,
redirect_uri: HUBSPOT_REDIRECT_URL,
refresh_token: refreshToken
} -
Call Hubspot API
- POST to token endpoint with refresh grant
-
Update Database
- Save new tokens with updated
generated_attimestamp - Call
DatabaseQuery.saveRefreshToken(queryData.id, dataToSave)
- Save new tokens with updated
-
Return New Tokens
- Return fresh access token to caller
Hubspot API Request:
POST https://api.hubapi.com/oauth/v1/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
client_id=2acbec95-d71a-4f7a-a039-0f5c2c3e9282&
client_secret=45e4abbc-41d3-40bc-9dc0-c175f8b01336&
redirect_uri=http://localhost:5000/v1/integrations/hubspot/auth/callback&
refresh_token=6f8a_s8df7asdf87asdf8a...
Hubspot API Response:
{
"access_token": "NEW_ACCESS_TOKEN",
"refresh_token": "NEW_REFRESH_TOKEN",
"expires_in": 1800,
"token_type": "bearer"
}
Example Usage:
const tokenData = await DatabaseQuery.findQuery(account_id, owner);
const freshToken = await providers.getRefreshAccessToken(tokenData);
// Use freshToken.access_token for API requests
This function automatically updates the database with new tokens. The refresh token may also be rotated by Hubspot.
๐ง Database Model Functionsโ
findQuery(account_id, owner_id)โ
Purpose: Retrieve stored Hubspot token for a specific account and user
Source: Models/keys.js
Parameters:
account_id(String) - DashClicks account IDowner_id(String) - User ID who owns the integration
Returns: Promise<Object> - Token document or error object
Query:
HubspotKey.findOne({
account_id: account_id,
owner: owner_id,
}).lean();
Success Response:
{
id: "507f1f77bcf86cd799439011", // _id converted to string
account_id: "507f191e810c19729de860ea",
owner: "507f191e810c19729de860eb",
token: {
access_token: "CJzg...",
refresh_token: "6f8a...",
expires_in: 1800,
token_type: "bearer"
},
generated_at: 1704844800,
token_invalidated: false
}
Error Response (no token found):
{
error: 'No token found';
}
saveRefreshToken(id, data)โ
Purpose: Create new token document or update existing token
Source: Models/keys.js
Parameters:
id(String|Boolean) - MongoDB document ID (orfalsefor new document)data(Object) - Token data to save
Returns: Promise<Object> - Saved/updated document
Logic:
if (id) {
// Update existing document
result = await HubspotKey.findByIdAndUpdate({ _id: id }, data, {
new: true,
});
} else {
// Create new document
result = await new HubspotKey(data).save();
}
Example Usage:
// Create new token
const newToken = await DatabaseQuery.saveRefreshToken(false, {
token: tokenData,
account_id: '507f191e810c19729de860ea',
owner: '507f191e810c19729de860eb',
generated_at: moment().unix(),
});
// Update existing token
const updatedToken = await DatabaseQuery.saveRefreshToken(docId, {
token: newTokenData,
generated_at: moment().unix(),
});
deleteToken(id)โ
Purpose: Permanently delete token document from database
Source: Models/keys.js
Parameters:
id(String) - MongoDB document ID
Returns: Promise<Boolean> - Always returns true
Operation:
await HubspotKey.deleteOne({ _id: id });
๐ Route Configurationโ
Auth Routesโ
Source: Routes/authRoutes.js
// Initialize OAuth flow
router.get(
'/login',
verifyAccessAndStatus({ accountIDRequired: true }),
verifyScope(['contacts', 'contacts.external']),
authController.getLogin,
);
// OAuth callback
router.get('/callback', authController.callback);
// Delete token
router.delete(
'/',
verifyAccessAndStatus({ accountIDRequired: true }),
verifyScope(['contacts', 'contacts.external']),
authController.deleteAccessToken,
);
Middleware:
verifyAccessAndStatus({ accountIDRequired: true })- Validates JWT and extracts account IDverifyScope(['contacts', 'contacts.external'])- Checks user has required scopes
All authentication endpoints (except callback) require contacts or contacts.external scope for authorization.
๐งช Edge Cases & Special Handlingโ
Token Invalidationโ
Issue: Tokens may need to be manually invalidated (user revokes access, security issue)
Handling:
if (query?.token_invalidated && query.token_invalidated == true) {
await DatabaseQuery.deleteToken(query.id);
// Proceed with new OAuth flow
}
State Token Expirationโ
Issue: JWT state tokens expire after 6 hours
Handling: If user takes too long to authorize, JWT verification fails and error is caught:
const decodedStateToken = jwt.verify(req.query.state, process.env.APP_SECRET);
// Throws error if expired
Missing Forward URLโ
Issue: OAuth flow cannot complete without return destination
Handling: Return 400 error immediately if forward_url not provided
Duplicate Token Creationโ
Issue: Multiple OAuth flows could create duplicate tokens
Handling: Check for existing token before saving:
let query = await DatabaseQuery.findQuery(account_id, owner);
if (query) {
// Update existing
storeResult = await DatabaseQuery.saveRefreshToken(query.id, dataToSave);
} else {
// Create new
storeResult = await DatabaseQuery.saveRefreshToken(false, dataToSave);
}
โ ๏ธ Important Notesโ
- ๐ State Parameter Security: JWT state tokens prevent CSRF attacks by binding OAuth flow to authenticated session
- โฑ๏ธ Token Expiration: Access tokens expire in 1800 seconds (30 minutes), refresh tokens are long-lived
- ๐ Automatic Refresh: Token refresh happens automatically on each API call via
getRefreshAccessToken() - ๐ Token Rotation: Hubspot may rotate refresh tokens during refresh, always save the new refresh token
- ๐จ Error Redirection: All errors redirect to
forward_urlwith error parameters for frontend handling - ๐พ Flexible Schema:
{strict: false}allows storing additional fields from Hubspot OAuth responses - ๐ Callback Security: Callback endpoint has no authentication to allow OAuth redirect, validation happens via JWT state token
๐ Related Documentationโ
- Integration Overview: Hubspot Index
- Hubspot OAuth Documentation: OAuth 2.0 Guide
- Data Export: CRM Data - Contacts, Companies, Deals synchronization
- JWT Specification: RFC 7519