Skip to main content

๐ŸŽฏ 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:

  1. Validate Request

    • Check if forward_url query parameter exists
    • Verify req.auth contains valid JWT authentication
  2. Check Existing Token

    • Query integrations.hubspot.key collection by account_id and owner
    • If token exists and not invalidated, return success immediately
  3. 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
  4. Build Authorization URL

    • Call providers.authorizeURL(stateToken)
    • Redirect user to Hubspot OAuth consent screen
  5. Handle Errors

    • Catch errors and redirect to forward_url with error parameters

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 OAuth
  • state (String) - JWT state token created during login

Hubspot API Endpoint: POST https://api.hubapi.com/oauth/v1/token

Business Logic Flow:

  1. Validate Authorization Code

    • Check if req.query.code exists
    • If missing, redirect with error
  2. Decode State Token

    • Verify and decode JWT state parameter using APP_SECRET
    • Extract account_id, owner, and forward_url
  3. 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 ID
      • client_secret: Hubspot client secret
      • redirect_uri: Callback URL
      • code: Authorization code
  4. Prepare Data for Storage

    {
    token: response.data,
    account_id: "507f191e810c19729de860ea",
    owner: "507f191e810c19729de860eb",
    generated_at: 1704844800 // moment().unix()
    }
  5. Check Existing Token

    • Query database for existing token by account_id and owner
    • If exists, update existing document
    • If not, create new document
  6. Save to Database

    • Call DatabaseQuery.saveRefreshToken(docId, dataToSave)
    • Store complete token object in integrations.hubspot.key
  7. Redirect to Success URL

    • Redirect to forward_url with success parameters and token ID

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:

  1. Verify Authentication

    • Check req.auth for valid JWT
    • Extract account_id and uid
  2. Find Token Document

    • Query integrations.hubspot.key by account_id and owner
    • Return 404 if token not found
  3. Delete Token

    • Call DatabaseQuery.deleteToken(token.id)
    • Permanently remove document from database
  4. 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 database
    • id (String) - MongoDB document ID
    • token.refresh_token (String) - Stored refresh token

Returns: Promise<Object> - New token data

Business Logic Flow:

  1. Extract Refresh Token

    • Get refresh_token from stored token data
  2. 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
    }
  3. Call Hubspot API

    • POST to token endpoint with refresh grant
  4. Update Database

    • Save new tokens with updated generated_at timestamp
    • Call DatabaseQuery.saveRefreshToken(queryData.id, dataToSave)
  5. 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
Automatic Token Update

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 ID
  • owner_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 (or false for 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 ID
  • verifyScope(['contacts', 'contacts.external']) - Checks user has required scopes
Scope Requirements

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_url with 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

๐Ÿ’ฌ

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:30 AM