Authentication
📋 Overview
Pipedrive integration uses OAuth 2.0 for authentication with automatic token refresh on every API request. Each account receives a unique API domain that must be used for all subsequent requests.
Key Features:
- OAuth 2.0 Authorization Code Flow
- JWT state parameter (10-minute expiry)
- Pre-authentication check (skips OAuth if already connected)
- Automatic token refresh on every request (proactive approach)
- Dynamic API domain per account
- Basic Auth for token refresh requests
- Token invalidation detection
- Sub-account support
🔄 OAuth Flow Diagram
1. Frontend (App)
│
├─→ GET /auth/login?forward_url=...
│ (with JWT Bearer token)
│
2. API Server
│
├─→ Check existing token
│ └─→ If exists: redirect to forward_url?status=success
│
├─→ Generate JWT state parameter
│ (account_id, owner, forward_url, 10min expiry)
│
├─→ Redirect to Pipedrive OAuth
│ https://oauth.pipedrive.com/oauth/authorize
│
3. Pipedrive OAuth
│
├─→ User grants permissions
│
├─→ Redirect to callback
│ /auth/callback?code=...&state=...
│
4. API Server (Callback)
│
├─→ Verify JWT state
│
├─→ Exchange code for tokens
│ POST https://oauth.pipedrive.com/oauth/token
│ (with Basic Auth)
│
├─→ Store token + api_domain in MongoDB
│ integrations.pipedrive.key
│
├─→ Redirect to forward_url
│ ?status=success&integration=pipedrive
│
5. Subsequent API Requests
│
├─→ Middleware: getToken()
│ │
│ ├─→ Retrieve token from MongoDB
│ │
│ ├─→ Always refresh token
│ │ POST https://oauth.pipedrive.com/oauth/token
│ │ (grant_type=refresh_token, Basic Auth)
│ │
│ ├─→ Update token in MongoDB
│ │
│ └─→ Attach access_token + api_domain to request
│
└─→ Make API call to {api_domain}/v1/...
🔐 Authentication Endpoints
1. Initiate OAuth Flow
Endpoint: GET /v1/e/pipedrive/auth/login
Purpose: Start OAuth 2.0 authorization flow or skip if already authenticated
Headers:
Authorization: Bearer {jwt_token}
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
forward_url | string | Yes | URL to redirect after OAuth completion |
Pre-authentication Check:
Before redirecting to Pipedrive, the system checks if a valid token already exists:
// Check for existing token
const snapshot = await PipeDriveCollection.getData(account_id, uid);
if (snapshot && snapshot._doc && snapshot._doc.token) {
// Already authenticated - skip OAuth
return res.redirect(`${forward_url}?status=success&integration=pipedrive`);
}
OAuth Redirect (if no token exists):
// Generate JWT state parameter
const state = jwt.sign(
{
account_id: req.auth.account_id,
owner: req.auth.uid,
forward_url: forward_url,
},
process.env.APP_SECRET,
{ expiresIn: 600 }, // 10 minutes
);
// Redirect to Pipedrive
const authUrl =
`https://oauth.pipedrive.com/oauth/authorize?` +
`client_id=${process.env.PIPE_DRIVE_CLIENT_ID}&` +
`redirect_uri=${encodeURIComponent(process.env.PIPE_DRIVE_REDIRECT_URL)}&` +
`state=${state}&` +
`response_type=code`;
res.redirect(authUrl);
Response: HTTP 302 redirect to Pipedrive OAuth consent screen
Example Request:
curl -X GET \
'https://api.dashclicks.com/v1/e/pipedrive/auth/login?forward_url=https://app.dashclicks.com/integrations' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
Success Scenarios:
- Already authenticated: Redirects to
forward_url?status=success&integration=pipedrive - Not authenticated: Redirects to Pipedrive OAuth consent screen
Error Scenarios:
- Missing forward_url: Returns 400 error
- Invalid JWT token: Returns 401 error
- Account validation fails: Returns 403 error
2. OAuth Callback
Endpoint: GET /v1/e/pipedrive/auth/callback
Purpose: Handle OAuth callback, exchange code for tokens, store in database
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Authorization code from Pipedrive |
state | string | Yes | JWT state parameter from login |
Processing Steps:
Step 1: Verify State Parameter
// Decode and verify JWT state
const decoded = jwt.verify(req.query.state, process.env.APP_SECRET);
const { account_id, owner, forward_url } = decoded;
If verification fails: Redirect to forward_url?status=error&integration=pipedrive&reason=invalid_state
Step 2: Exchange Authorization Code
const tokenUrl = 'https://oauth.pipedrive.com/oauth/token';
// Create Basic Auth credentials
const base64Credentials = Buffer.from(
`${process.env.PIPE_DRIVE_CLIENT_ID}:${process.env.PIPE_DRIVE_CLIENT_SECRET}`,
).toString('base64');
const tokenData = qs.stringify({
grant_type: 'authorization_code',
code: req.query.code,
redirect_uri: process.env.PIPE_DRIVE_REDIRECT_URL,
});
const response = await axios.post(tokenUrl, tokenData, {
headers: {
Authorization: `Basic ${base64Credentials}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
});
Token Response Structure:
{
"access_token": "7507356:11465942:72cdfd552a1c4c2659fd8395aaf0da3e14934874",
"refresh_token": "7507356:11465942:cf3d769527455ee0beb3dd3fcf68276a45039570",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "base,deals:full,activities:full,contacts:full,products:full,users:read,recents:read,search:read",
"api_domain": "https://dashclicksllc.pipedrive.com"
}
Step 3: Store Token in MongoDB
await PipeDriveCollection.saveData({
account_id: account_id,
owner: owner,
token: {
generated_at: moment().unix(),
access_token: response.data.access_token,
refresh_token: response.data.refresh_token,
expires_in: response.data.expires_in,
token_type: response.data.token_type,
scope: response.data.scope,
api_domain: response.data.api_domain, // Critical: Store dynamic domain
},
});
Step 4: Redirect to Frontend
// Success redirect
res.redirect(`${forward_url}?status=success&integration=pipedrive`);
// Error redirect (if token exchange fails)
res.redirect(`${forward_url}?status=error&integration=pipedrive&reason=${error.message}`);
Example Callback URL:
https://api.dashclicks.com/v1/e/pipedrive/auth/callback?
code=abcd1234efgh5678&
state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Success Response: Redirect to forward_url?status=success&integration=pipedrive
Error Responses:
| Error | Redirect URL |
|---|---|
| Invalid state | forward_url?status=error&integration=pipedrive&reason=invalid_state |
| Token exchange failed | forward_url?status=error&integration=pipedrive&reason=token_exchange_failed |
| Database save failed | forward_url?status=error&integration=pipedrive&reason=database_error |
3. Disconnect Integration
Endpoint: DELETE /v1/e/pipedrive/auth
Purpose: Remove Pipedrive integration (delete stored tokens)
Headers:
Authorization: Bearer {jwt_token}
Implementation:
const account_id = req.auth.account_id;
const uid = req.auth.uid;
// Delete token document
await PipeDriveCollection.deleteData(account_id, uid);
res.status(200).json({
success: true,
message: 'Pipedrive integration disconnected successfully',
});
Example Request:
curl -X DELETE \
'https://api.dashclicks.com/v1/e/pipedrive/auth' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
Success Response:
{
"success": true,
"message": "Pipedrive integration disconnected successfully"
}
Error Response (token not found):
{
"success": false,
"message": "Token not found",
"error": "NOT_FOUND"
}
🔄 Automatic Token Refresh
Middleware: getToken
All export endpoints use the getToken middleware to automatically refresh the access token on every request.
Key Characteristics:
- ✅ Proactive approach: Always refreshes, doesn't check expiration
- ✅ Basic Auth: Uses base64-encoded client credentials
- ✅ Updates token: Saves new token to MongoDB
- ✅ Attaches to request: Adds
access_tokenandapi_domaintoreqobject
Middleware Implementation:
const getToken = async (req, res, next) => {
try {
const account_id = req.auth.account_id;
const uid = req.auth.uid;
// Step 1: Retrieve stored token
const snapshot = await PipeDriveCollection.getData(account_id, uid);
if (!snapshot || !snapshot._doc) {
return res.status(401).json({
success: false,
message: 'User oauth token not found. Please redirect to login',
error: 'TOKEN_NOT_FOUND',
});
}
const doc = snapshot._doc;
// Check if token is invalidated
if (doc.token_invalidated) {
return res.status(401).json({
success: false,
message: 'Token has been invalidated. Please re-authenticate.',
error: 'TOKEN_INVALIDATED',
});
}
// Step 2: Always refresh token (proactive approach)
const tokenUrl = 'https://oauth.pipedrive.com/oauth/token';
// Prepare Basic Auth header
const base64Credentials = Buffer.from(
`${process.env.PIPE_DRIVE_CLIENT_ID}:${process.env.PIPE_DRIVE_CLIENT_SECRET}`,
).toString('base64');
const requestData = qs.stringify({
grant_type: 'refresh_token',
refresh_token: doc.token.refresh_token,
});
// Step 3: Make refresh request
const response = await axios.post(tokenUrl, requestData, {
headers: {
Authorization: `Basic ${base64Credentials}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
});
// Step 4: Update token in MongoDB
await PipeDriveCollection.updateData(doc._id, {
token: {
generated_at: moment().unix(),
access_token: response.data.access_token,
refresh_token: response.data.refresh_token,
expires_in: response.data.expires_in,
token_type: response.data.token_type,
scope: response.data.scope,
api_domain: response.data.api_domain,
},
});
// Step 5: Attach to request for controllers
req.access_token = response.data.access_token;
req.api_domain = response.data.api_domain; // Critical for API calls
next();
} catch (error) {
// Handle token refresh errors
if (error.response?.status === 401 || error.response?.status === 400) {
return res.status(401).json({
success: false,
message: 'Token refresh failed. Please re-authenticate.',
error: 'TOKEN_REFRESH_FAILED',
});
}
next(error);
}
};
Why Refresh on Every Request?
- Always fresh tokens: Eliminates timing issues with expiration
- Correct API domain: Ensures
api_domainis always current - Simplicity: No expiration calculation logic needed
- Reliability: Pipedrive handles rate limiting for token refresh
Token Refresh Request:
POST https://oauth.pipedrive.com/oauth/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=7507356:11465942:cf3d769...
Token Refresh Response:
{
"access_token": "7507356:11465942:NEW_ACCESS_TOKEN",
"refresh_token": "7507356:11465942:NEW_REFRESH_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "base,deals:full,...",
"api_domain": "https://dashclicksllc.pipedrive.com"
}
🌐 API Domain Handling
Dynamic API Domain
Each Pipedrive account has a unique API domain returned in the OAuth response:
Sandbox Accounts:
https://{company}-sandbox.pipedrive.com
Production Accounts:
https://{company}.pipedrive.com
Storage:
The api_domain is stored in the token object:
{
account_id: "507f191e810c19729de860ea",
owner: "user_Lwh9EzeD8",
token: {
access_token: "...",
refresh_token: "...",
api_domain: "https://dashclicksllc.pipedrive.com" // Stored here
}
}
Usage in API Calls:
// Controller receives api_domain from middleware
const contactInformation = async (api_domain, access_token, type, start = 0) => {
const url = `${api_domain}/v1/${type}`;
const response = await axios.get(url, {
headers: { Authorization: `Bearer ${access_token}` },
params: { limit: 100, start },
});
return response.data;
};
Critical: Always use the api_domain from the middleware - never hard-code API endpoints.
🔒 OAuth Scopes
Pipedrive integration requests comprehensive access:
| Scope | Description |
|---|---|
base | Basic account information |
deals:full | Full read/write access to deals |
activities:full | Full read/write access to activities |
contacts:full | Full read/write access to persons and organizations |
products:full | Full read/write access to products |
users:read | Read user information |
recents:read | Read recent items |
search:read | Search functionality |
Scope String (URL-encoded in OAuth request):
base,deals:full,activities:full,contacts:full,products:full,users:read,recents:read,search:read
⚠️ Token Invalidation Detection
When Tokens Become Invalid
Tokens can be invalidated when:
- User revokes access in Pipedrive account settings
- User changes password (invalidates all OAuth tokens)
- Admin removes app from account
- Account suspended or deactivated
Detection Mechanism
The error handler in index.js detects token invalidation:
// Error handling middleware
if (error.response && error.response.data) {
const errorData = error.response.data;
// Detect Pipedrive 401 errors
if (errorData.errorCode === '401') {
const accountId = error.config?.accountId;
if (accountId) {
// Mark all tokens for this account as invalidated
await pipedriveKeys.updateMany(
{ account_id: accountId.toString() },
{ $set: { token_invalidated: true } },
);
}
error.message = 'TOKEN_INVALIDATED';
}
}
Pipedrive Error Response (401):
{
"success": false,
"error": "Unauthorized",
"errorCode": "401"
}
DashClicks API Response:
{
"success": false,
"message": "TOKEN_INVALIDATED",
"error": "TOKEN_INVALIDATED"
}
Frontend Handling:
When receiving TOKEN_INVALIDATED error, redirect user to re-authenticate:
if (error.response?.data?.message === 'TOKEN_INVALIDATED') {
// Redirect to OAuth login
window.location.href =
'/v1/e/pipedrive/auth/login?forward_url=' + encodeURIComponent(window.location.href);
}
🎯 Use Cases
1. First-time OAuth Connection
// User clicks "Connect Pipedrive" button in frontend
async function connectPipedrive(jwtToken) {
const forwardUrl = 'https://app.dashclicks.com/integrations';
// Initiate OAuth flow
window.location.href = `/v1/e/pipedrive/auth/login?forward_url=${encodeURIComponent(forwardUrl)}`;
// After OAuth completes, user is redirected back to:
// https://app.dashclicks.com/integrations?status=success&integration=pipedrive
}
2. Check OAuth Connection Status
// Check if Pipedrive is already connected
async function checkPipedriveConnection(jwtToken) {
try {
// Try to fetch data (will fail if not connected)
const response = await axios.get('/v1/e/pipedrive/export/deals?limit=1', {
headers: { Authorization: `Bearer ${jwtToken}` },
});
return { connected: true };
} catch (error) {
if (error.response?.data?.error === 'TOKEN_NOT_FOUND') {
return { connected: false };
}
throw error;
}
}
3. Handle Token Invalidation
// Wrapper function with automatic re-authentication
async function fetchPipedriveData(jwtToken, type) {
try {
const response = await axios.get(`/v1/e/pipedrive/export/${type}`, {
headers: { Authorization: `Bearer ${jwtToken}` },
});
return response.data;
} catch (error) {
if (error.response?.data?.message === 'TOKEN_INVALIDATED') {
// Redirect to re-authenticate
const forwardUrl = encodeURIComponent(window.location.href);
window.location.href = `/v1/e/pipedrive/auth/login?forward_url=${forwardUrl}`;
return;
}
throw error;
}
}
4. Disconnect Integration
// Remove Pipedrive connection
async function disconnectPipedrive(jwtToken) {
try {
await axios.delete('/v1/e/pipedrive/auth', {
headers: { Authorization: `Bearer ${jwtToken}` },
});
console.log('Pipedrive disconnected successfully');
} catch (error) {
console.error('Failed to disconnect:', error);
}
}
🛡️ Security Best Practices
1. State Parameter Security
- JWT-signed state: Prevents CSRF attacks
- 10-minute expiry: Limits window for replay attacks
- Includes account context: Validates ownership
// State includes critical context
const state = jwt.sign(
{
account_id: req.auth.account_id,
owner: req.auth.uid,
forward_url: forward_url,
},
process.env.APP_SECRET,
{ expiresIn: 600 },
);
2. Token Storage
- MongoDB encryption at rest: Protect tokens in database
- Never expose refresh_token: Only return access_token to frontend (if needed)
- Separate by account: Each user has isolated token document
3. Basic Auth for Token Refresh
- Client credentials: Never expose client_secret in frontend
- Base64 encoding: Standard OAuth 2.0 practice
- Server-side only: Token refresh happens in backend
4. API Domain Validation
- Use stored domain: Always use
api_domainfrom token - No hard-coding: Prevents attacks using incorrect domains
- Updated on refresh: Domain may change (company renamed, etc.)
5. Error Handling
- Generic error messages: Don't expose internal details
- Log security events: Track OAuth failures, token invalidations
- Rate limiting: Protect OAuth endpoints from abuse
📊 Error Scenarios
1. Missing JWT Token
Request:
GET /v1/e/pipedrive/auth/login?forward_url=...
# Missing Authorization header
Response: 401 Unauthorized
{
"success": false,
"message": "Authentication required",
"error": "UNAUTHORIZED"
}
2. Invalid State Parameter
Scenario: State JWT is expired or tampered
Response: Redirect to forward_url?status=error&integration=pipedrive&reason=invalid_state
3. Token Exchange Failure
Scenario: Pipedrive rejects authorization code
Response: Redirect to forward_url?status=error&integration=pipedrive&reason=token_exchange_failed
4. Token Not Found
Request: Export data without OAuth connection
Response: 401 Unauthorized
{
"success": false,
"message": "User oauth token not found. Please redirect to login",
"error": "TOKEN_NOT_FOUND"
}
5. Token Refresh Failed
Scenario: Refresh token is invalid or expired
Response: 401 Unauthorized
{
"success": false,
"message": "Token refresh failed. Please re-authenticate.",
"error": "TOKEN_REFRESH_FAILED"
}
6. Token Invalidated
Scenario: User revoked access in Pipedrive
Response: 401 Unauthorized
{
"success": false,
"message": "TOKEN_INVALIDATED",
"error": "TOKEN_INVALIDATED"
}
MongoDB Update: token_invalidated flag set to true for all account tokens