Skip to main content

Facebook Authentication

The Facebook integration uses OAuth 2.0 with a multi-source architecture that allows separate token management for different use cases (analytics, lead generation, reputation).

OAuth 2.0 Flow

Step 1: Initiate Authentication

Endpoint: GET /v1/e/facebook/auth/login

Query Parameters:

ParameterTypeRequiredDescription
forward_urlStringYesURL to redirect after authentication completes
sourceStringYesIntegration source: meta-ads, facebook-leads, or reputation
account_idStringNoSub-account ID (for agency/sub-account scenarios)

Example Request:

GET /v1/e/facebook/auth/login?forward_url=https://app.dashclicks.com/analytics&source=meta-ads
Authorization: Bearer <jwt_token>

Response:

HTTP/1.1 301 Moved Permanently
Location: https://www.facebook.com/v18.0/dialog/oauth?client_id=...&state=...

Step 2: User Authorization

User is redirected to Facebook where they:

  1. Log in to Facebook (if not already logged in)
  2. Review requested permissions
  3. Authorize the application
  4. Get redirected back to callback URL

Step 3: Handle Callback

Endpoint: GET /v1/e/facebook/auth/callback

Query Parameters:

ParameterTypeDescription
codeStringAuthorization code from Facebook
stateStringJWT token with encrypted authentication data

Callback Process:

sequenceDiagram
participant FB as Facebook
participant API as DashClicks API
participant DB as MongoDB
participant User as User Browser

FB->>API: GET /callback?code=xxx&state=jwt
API->>API: Verify JWT state token
API->>FB: POST /oauth/access_token (exchange code)
FB->>API: Return short-lived token
API->>FB: POST /oauth/access_token (exchange for long-lived)
FB->>API: Return 60-day token
API->>DB: Store token in integrations.facebookads.key
API->>DB: Send FCM notification to user
API->>User: 301 Redirect to forward_url?status=success&token=<id>

Success Response:

HTTP/1.1 301 Moved Permanently
Location: https://app.dashclicks.com/analytics?status=success&integration=facebookads&token=507f1f77bcf86cd799439011

Error Response:

HTTP/1.1 301 Moved Permanently
Location: https://app.dashclicks.com/analytics?status=error&reason=Invalid+State&integration=facebookads

Function Documentation

loginAuth(req, res, next)

Initiates the OAuth 2.0 flow by redirecting to Facebook's authorization page.

Parameters:

  • req.query.forward_url (String, required) - Redirect destination after auth
  • req.query.source (String, required) - Integration source type
  • req.query.account_id (String, optional) - Sub-account ID
  • req.auth.account_id (ObjectId) - Authenticated account from JWT

Process:

  1. Validate Account Access: Verify user has access to target account
  2. Check Existing Tokens: Handle existing integrations based on source:
    • meta-ads: Always delete existing token (force re-auth)
    • facebook-leads: Return existing token without re-auth
    • reputation: Delete existing token (force re-auth)
  3. Create JWT State Token: Encrypt authentication context
  4. Redirect to Facebook: Build OAuth URL with scopes and state

JWT State Token Payload:

{
data: '&account_id=<id>&forward_url=<url>&created_by=<id>&source=<source>&user_id=<id>';
}

OAuth URL Construction:

const oauthURL =
`https://www.facebook.com/${FACEBOOK_API_VERSION}/dialog/oauth?` +
`client_id=${FACEBOOK_CLIENT_ID}&` +
`display=popup&` +
`state=${jwtToken}&` +
`response_type=code&` +
`redirect_uri=${FACEBOOK_ADS_REDIRECT_URL}&` +
`scope=public_profile,ads_management,ads_read,pages_show_list,` +
`pages_manage_ads,pages_manage_metadata,business_management,` +
`leads_retrieval,pages_read_engagement,read_insights,` +
`pages_read_user_content,pages_manage_engagement`;

Source-Specific Behavior:

if (source === 'meta-ads') {
// Always delete for analytics - force fresh connection
await facebookModel.update(
{ account_id: accountId, source: source },
{ $set: { deleted: new Date() } },
);
} else if (source === 'facebook-leads') {
// Return existing token for inbound leads
if (searchRecord?._id) {
return res
.status(301)
.redirect(forward_url + '?status=success&integration=facebookads&token=' + searchRecord._id);
}
} else if (source === 'reputation') {
// Delete existing token for reputation management
await facebookModel.update(
{ account_id: accountId, source: source },
{ $set: { deleted: new Date() } },
);
}

Side Effects:

  • May soft-delete existing tokens (sets deleted timestamp)
  • Creates JWT state token for security

Error Handling:

  • Returns 400 if forward_url is missing
  • Returns 400 if account access check fails
  • Redirects to forward_url with error reason on exceptions

Example Implementation:

exports.loginAuth = async (req, res, next) => {
let forward_url;
try {
let accountId = await checkAccountAccess(req);
const user = await User.findOne({ account: accountId, is_owner: true });
let user_id = user?._id;
let creatorId = req.auth.account_id;

forward_url = req.query.forward_url;
const source = req.query.source;

if (!forward_url) {
return res.status(400).json({
success: false,
errno: 400,
message: 'forward url is required!',
});
}

// Check for existing token
const searchRecord = await faceBookAdsKey
.findOne({
account_id: accountId.toString(),
source: source,
deleted: { $exists: false },
})
.sort({ _id: -1 })
.lean()
.exec();

// Source-specific logic...

token = jwt.sign(
{
data: `&${[
`account_id=${accountId}`,
`forward_url=${forward_url}`,
`created_by=${creatorId}`,
`source=${source}`,
...(user_id ? [`user_id=${user_id}`] : []),
].join('&')}`,
},
process.env.APP_SECRET,
{ algorithm: 'HS256' },
);

res
.status(301)
.redirect(
`https://www.facebook.com/${process.env.FACEBOOK_API_VERSION}/dialog/oauth?` +
`client_id=${process.env.FACEBOOK_CLIENT_ID}&` +
`display=popup&state=${token}&response_type=code&` +
`redirect_uri=${process.env.FACEBOOK_ADS_REDIRECT_URL}&` +
`scope=public_profile,ads_management,ads_read,...`,
);
} catch (error) {
// Error handling...
}
};

postApiKey(req, res, next)

Handles the OAuth callback, exchanges authorization code for long-lived token, and stores credentials.

Parameters:

  • req.query.code (String, required) - Authorization code from Facebook
  • req.query.state (String, required) - JWT state token from initial request

Process:

  1. Verify JWT State: Decode and validate state parameter
  2. Extract Auth Context: Parse account_id, forward_url, source from state
  3. Check for Duplicate: Prevent duplicate tokens for same account/source
  4. Exchange Authorization Code: Get short-lived token from Facebook
  5. Exchange for Long-Lived Token: Convert to 60-day token
  6. Store Token: Save to integrations.facebookads.key collection
  7. Send Notification: FCM notification to account owner
  8. Redirect: Return to forward_url with success status

Token Exchange:

// Step 1: Exchange authorization code for short-lived token
const authcode = await facebookProvider.getAccessToken(
code,
process.env.FACEBOOK_ADS_REDIRECT_URL
);

// Step 2: Inside getAccessToken - exchange for long-lived token
const longURL = `https://graph.facebook.com/${FACEBOOK_API_VERSION}/oauth/access_token?` +
`grant_type=fb_exchange_token&` +
`client_id=${FACEBOOK_CLIENT_ID}&` +
`client_secret=${FACEBOOK_CLIENT_SECRET}&` +
`fb_exchange_token=${shortToken}`;

const longToken = await axios(longURL);

// Result: 60-day token
{
access_token: 'EAAxxxxxxxx...',
token_type: 'bearer',
expires_in: 5183944 // Seconds until expiration
}

Token Storage:

authcode.expires_in = authcode.expires_in - 600 + Math.floor(Date.now() / 1000);

const dataToSave = {
token: authcode,
source: source,
created_by: new mongoose.Types.ObjectId(creatorId),
account_id: new mongoose.Types.ObjectId(accountId.toString()),
};

const save = await facebookModel.save(dataToSave);

FCM Notification:

const notification = {
title: 'Facebook Ads integration added',
body: 'A new Facebook Ads integration has been added.',
data: {
subType: 'bell', // If bell notifications enabled
},
module: 'projects',
type: 'project_added',
};

await fcm.schedule([accountId], [user_id], notification);

Response Flow:

// Success - redirect with token ID
return res
.status(301)
.redirect(forward_url + '?status=success&integration=facebookads&token=' + save.id);

// Error - redirect with reason
return res
.status(301)
.redirect(forward_url + '?status=error&reason=' + reason + '&integration=facebookads');

Side Effects:

  • Creates new document in integrations.facebookads.key
  • Sends FCM notification to account owner (if preferences allow)
  • May return existing token if duplicate detected

Security:

  • JWT state verification prevents CSRF attacks
  • Returns 403 if state token is invalid
  • Uses HMAC-SHA256 for JWT signing

deleteDocument(req, res, next)

Disconnects the Facebook integration by soft-deleting tokens and cleaning up related data.

Parameters:

  • req.query.source (String, optional) - Specific source to delete
  • req.query.global (String, optional) - Set to 'true' for global deletion (all sources)
  • req.query.account_id (String, optional) - Sub-account ID

Process:

  1. Validate Account Access: Verify user permissions
  2. Delete Token: Soft-delete by setting deleted timestamp
  3. Cleanup Related Data:
    • meta-ads: Delete analytics config
    • facebook-leads: Disable all lead campaigns
    • global=true: Delete all sources + campaigns + config
  4. Send Notifications: Email and FCM notifications

Deletion Modes:

// Mode 1: Source-specific deletion
DELETE /v1/e/facebook/auth/delete?source=meta-ads
// Deletes only meta-ads token + analytics config

// Mode 2: Global deletion (all sources)
DELETE /v1/e/facebook/auth/delete?global=true
// Deletes all tokens + campaigns + config

Cleanup Logic:

if (source === 'facebook-leads') {
// Disable all Facebook Ads campaigns
await campaignModel.updateMany(
{
account_id: accountID,
is_deleted: false,
integration: 'facebook_ads',
},
{ $set: { is_deleted: true } },
);
}

if (source === 'meta-ads') {
// Delete analytics configuration
await analyticsuserconfig.deleteOne({ account_id: accountID });
}

// Soft-delete token
await facebookModel.update(
{ account_id: accountID, source: source },
{ $set: { deleted: new Date() } },
);

Global Deletion:

if (global && global === 'true') {
// Disable all campaigns
await campaignModel.updateMany(
{ account_id: accountID, is_deleted: false, integration: 'facebook_ads' },
{ $set: { is_deleted: true } },
);

// Delete config
await analyticsuserconfig.deleteOne({ account_id: accountID });

// Delete ALL tokens (all sources)
await facebookModel.update({ account_id: accountID }, { $set: { deleted: new Date() } });
}

Notification System:

const sendNotifications = async (isProject = false) => {
const accountTarget = isProject ? req.auth.parent_account : accountID;

// Browser/Bell notifications
if (config_notifications.browser || config_notifications.bell) {
await fcm.schedule([accountTarget], [req.auth.uid], {
title: 'Facebook integration is Disconnected',
body: 'Facebook integration has been disconnected.',
module: 'analytics',
type: 'integration_disconnected',
click_action: `${domainName}/analytics`,
});
}

// Email notifications
if (config_notifications.email) {
await new Mail().schedule({
mailBody: {
subject: 'Facebook integration disconnected',
content: 'Facebook integration has been disconnected.',
recipients: [{ name, email, first_name, last_name }],
},
accountID: accountTarget,
userID: req.auth.uid,
});
}
};

// Send to primary account
await sendNotifications();

// Send to parent account if sub-account scenario
if (req?.auth?.parent_account !== accountID) {
await sendNotifications(true);
}

Side Effects:

  • Soft-deletes token(s) in integrations.facebookads.key
  • May delete analytics-facebook.ads.userconfig document
  • May disable campaigns in campaign-data collection
  • Sends email and FCM notifications

Response:

{
"success": true,
"message": "SUCCESS"
}

Error Handling:

  • Returns 400 if account access check fails
  • Preserves existing status code on errors
  • Continues execution even if notification sending fails

Token Management

Long-Lived Tokens

Facebook tokens have a 60-day lifespan but can be automatically refreshed:

// Token stored with absolute expiration timestamp
{
access_token: 'EAAxxxxxxxx...',
token_type: 'bearer',
expires_in: 1704844800 // Unix timestamp (not seconds from now)
}

Token Validation & Auto-Refresh

The integration automatically checks and refreshes expired tokens:

static checkAccessTokenValidity = async ({ req, expirationTime, accessToken, docId, source }) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);

if (currentTimeStamp > expirationTime) {
// Token is expired - get new access token
const tokenData = await fbProvider.getNewAccessToken(accessToken);

tokenData.expires_in = tokenData.expires_in + Math.floor(Date.now() / 1000);

const dataToSave = {
token: tokenData,
owner: req.auth.uid.toString(),
account_id: req.auth.account_id.toString()
};

if (source) {
dataToSave.source = source;
// Delete old token
await faceBookAdsKey.deleteMany({ account_id, source });
}

await new faceBookAdsKey(dataToSave).save();
return tokenData.access_token;
}

return accessToken; // Token still valid
};

Token Invalidation Detection

OAuth errors automatically flag tokens as invalid:

// Middleware in index.js
if (error?.response?.data?.error?.code && error?.response?.data?.error?.type) {
let errorCode = error?.response?.data?.error?.code;
let type = error?.response?.data?.error?.type;

// OAuth error codes that indicate token issues
if (
(errorCode == '100' || // Invalid parameter
errorCode == '102' || // Session expired
errorCode == '190' || // Access token invalid
(errorCode >= 200 && errorCode <= 299)) && // Permission errors
type == 'OAuthException'
) {
await faceBookAdsKey.updateMany(
{ account_id: new mongoose.Types.ObjectId(accId.toString()) },
{ $set: { token_invalidated: true } },
);

error.message = 'TOKEN_INVALIDATED';
}
}

Security Considerations

JWT State Parameter

Protects against CSRF attacks by encrypting authentication context:

const stateToken = jwt.sign(
{
data: `&account_id=${accountId}&forward_url=${forward_url}&created_by=${creatorId}&source=${source}`,
},
process.env.APP_SECRET,
{ algorithm: 'HS256' },
);

// On callback:
const payload = jwt.verify(req.query.state, process.env.APP_SECRET);

Sub-Account Support

Validates account access before any operation:

const accountId = await checkAccountAccess(req);
// Returns parent account if sub-account, or current account

Token Storage

  • Tokens stored in MongoDB with account-level isolation
  • Soft-delete pattern preserves audit trail
  • token_invalidated flag prevents using bad tokens

Integration Checklist

Development

  • Set up Facebook App in Meta for Developers
  • Configure OAuth redirect URI in app settings
  • Add required scopes to app review
  • Set environment variables
  • Test OAuth flow with personal account
  • Test token refresh mechanism
  • Test source-specific isolation (meta-ads vs facebook-leads)

Production

  • Submit app for Facebook review (all required permissions)
  • Enable Business Manager integration
  • Configure production redirect URI
  • Set up token expiration monitoring
  • Implement alerting for token invalidation
  • Test sub-account scenarios
  • Verify notification delivery

Common Issues

Issue: "Invalid OAuth Redirect URI"

Cause: Redirect URI doesn't match app configuration

Solution:

# Ensure exact match in Facebook App settings
FACEBOOK_ADS_REDIRECT_URL=https://api.dashclicks.com/v1/e/facebook/auth/callback

Issue: "Missing Permissions"

Cause: App not approved for required scopes

Solution: Submit app for review with all required permissions explained

Issue: TOKEN_INVALIDATED

Cause: User revoked access or password changed

Solution: User must re-authenticate through OAuth flow

Issue: Duplicate Token Creation

Cause: Multiple concurrent OAuth flows

Solution: Integration checks for existing tokens before creating new ones

Additional Resources

💬

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