Skip to main content

OAuth 2.0 Authentication

🔐 OAuth 2.0 Flow

Constant Contact uses OAuth 2.0 authorization code flow with automatic token refresh for access token renewal.

🌐 Environment Variables

VariableDescriptionExample
CONSTANTCONTACT_CLIENT_IDOAuth client IDbd5e80bb-7317-4b22-8839-031b8a9c8417
CONSTANTCONTACT_SECRET_IDOAuth client secret0MtgSHFWiMix3g4pEaKXvg
CONSTANTCONTACT_REDIRECT_URLOAuth callback URLhttps://api.dashclicks.com/v1/integrations/constantcontact/auth/callback
CONSTANTCONTACT_AUTH_ENDPOINTAuthorization endpointhttps://api.cc.email/v3/idfed
CONSTANTCONTACT_ACCESS_TOKEN_ENDPOINTToken endpointhttps://idfed.constantcontact.com/as/token.oauth2
CONSTANTCONTACT_GRANT_TYPEGrant typeauthorization_code
CONSTANTCONTACT_AUTH_SCOPEOAuth scopecontact_data

📋 API Endpoints

MethodEndpointDescriptionAuth Required
GET/v1/integrations/constantcontact/auth/loginInitiate OAuth flow✅ JWT
GET/v1/integrations/constantcontact/auth/callbackOAuth callback handler
DELETE/v1/integrations/constantcontact/authDelete stored token✅ JWT

🔄 Authentication Flow

Step 1: Initiate OAuth

Endpoint: GET /auth/login

Query Parameters:

  • forward_url (required) - URL to redirect after OAuth completes

Request:

GET /v1/integrations/constantcontact/auth/login?forward_url=https://app.dashclicks.com/integrations
Authorization: Bearer {jwt_token}

Process:

  1. Check for existing token in database
  2. If token exists and valid → Redirect to forward_url with success
  3. If token invalidated → Delete and re-authenticate
  4. If no token → Generate JWT state token and redirect to Constant Contact

JWT State Token (valid for 2 hours):

{
aid: "account_id", // DashClicks account ID
uid: "user_id", // DashClicks user ID
forward_url: "https://..." // Return URL
}

Authorization URL Format:

https://api.cc.email/v3/idfed
?client_id={CLIENT_ID}
&redirect_uri={REDIRECT_URI}
&response_type=code
&scope=contact_data
&state={JWT_STATE_TOKEN}

Success Response (existing connection):

HTTP/1.1 301 Moved Permanently
Location: https://app.dashclicks.com/integrations?status=success&integration=constant_contact&token={user_id}

Redirect Response (new connection):

HTTP/1.1 302 Found
Location: https://api.cc.email/v3/idfed?...

Error Responses:

// Missing forward_url
{
"success": false,
"errno": 400,
"message": "Forward url required."
}

// Unauthorized user
HTTP/1.1 302 Found
Location: https://app.dashclicks.com/integrations?status=error&integration=constant_contact&reason=Unauthorized User

Step 2: OAuth Callback

Endpoint: GET /auth/callback

Query Parameters:

  • code (required) - Authorization code from Constant Contact
  • state (required) - JWT state token from Step 1

Process:

  1. Decode JWT state token
  2. Exchange authorization code for access token
  3. Store tokens with generation timestamp in MongoDB
  4. Redirect to forward_url with success status

Token Exchange Request:

POST https://idfed.constantcontact.com/as/token.oauth2
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id={CLIENT_ID}
&client_secret={CLIENT_SECRET}
&redirect_uri={REDIRECT_URI}
&code={AUTHORIZATION_CODE}

Token Exchange Response:

{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE0NTk5...",
"refresh_token": "xRVSMemH03UD3wNf07dXBspwrestling8h7cAOibykI",
"token_type": "Bearer",
"expires_in": 86400,
"scope": "contact_data"
}

MongoDB Document Created:

{
token: {
access_token: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE0NTk5...",
refresh_token: "xRVSMemH03UD3wNf07dXBspwrestling8h7cAOibykI",
token_type: "Bearer",
expires_in: 86400, // 24 hours
scope: "contact_data"
},
account_id: "12345",
owner: "user_Lwh9EzeD8",
generated_at: 1696934400, // Unix timestamp
token_invalidated: false
}

Success Response:

HTTP/1.1 301 Moved Permanently
Location: https://app.dashclicks.com/integrations?status=success&integration=constant_contact&token={user_id}

Error Response:

HTTP/1.1 301 Moved Permanently
Location: https://app.dashclicks.com/integrations?status=error&integration=constant_contact&reason={error_message}

Step 3: Delete Token

Endpoint: DELETE /auth

Request:

DELETE /v1/integrations/constantcontact/auth
Authorization: Bearer {jwt_token}

Success Response:

{
"success": 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"
}

🔄 Automatic Token Refresh

Token Expiration

Access tokens expire after 24 hours (86400 seconds). The integration automatically refreshes tokens before API calls.

Refresh Process

Triggered on: Every contact export request

Steps:

  1. Retrieve token from database
  2. Check generated_at timestamp
  3. If expired (> 24 hours), refresh automatically
  4. Update database with new access token
  5. Continue with API request

Refresh Token Request:

POST https://idfed.constantcontact.com/as/token.oauth2
Authorization: Basic {BASE64_ENCODED_CREDENTIALS}
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token={REFRESH_TOKEN}

Base64 Credentials:

const string = CLIENT_ID + ':' + CLIENT_SECRET;
const base64 = Buffer.from(string).toString('base64');

Refresh Response:

{
"access_token": "new_access_token_here",
"refresh_token": "new_refresh_token_here",
"token_type": "Bearer",
"expires_in": 86400,
"scope": "contact_data"
}

Database Update

After successful refresh:

{
token: {
access_token: "new_token",
refresh_token: "new_refresh_token",
// ... other fields
},
generated_at: 1696945600 // Updated timestamp
}

🔑 Token Management

Token Storage

Tokens stored in constant_contact.keys collection:

{
_id: ObjectId("..."),
token: {
access_token: "...",
refresh_token: "...",
token_type: "Bearer",
expires_in: 86400,
scope: "contact_data"
},
account_id: "12345",
owner: "user_Lwh9EzeD8",
generated_at: 1696934400,
token_invalidated: false,
createdAt: ISODate("2023-10-10T12:00:00Z"),
updatedAt: ISODate("2023-10-10T12:00:00Z")
}

Token Lookup

Model Method: findQuery(account_id, uid)

const result = await DatabaseQuery.findQuery(account_id, uid);

Returns:

  • Success: { id, token, account_id, owner, generated_at, ... }
  • Not found: { error: 'No token found' }

Token Invalidation

Tokens can be marked as invalidated:

{
token_invalidated: true; // Flag for soft deletion
}

When invalidated token found during login, it's deleted and user re-authenticates.

🎯 Authorization Middleware

All protected endpoints require JWT authentication:

req.auth = {
account_id: '12345',
uid: 'user_Lwh9EzeD8',
// ... other JWT claims
};

Required Scopes

OperationRequired Scopes
Loginconstantcontact, constantcontact.create
Deleteconstantcontact, constantcontact.delete
Exportconstantcontact, constantcontact.read

⚠️ Error Handling

ErrorStatusResponse
Missing forward_url405{ success: false, errno: 400, message: "Forward url required." }
Unauthorized user302Redirects with error
Token not found (delete)404{ success: false, errno: 400, message: "Access Token Not Found" }
OAuth exchange failed302Redirects to forward_url with error
Refresh token failed400Error propagated to caller

🔒 Security Features

JWT State Token

  • Expiry: 2 hours
  • Secret: process.env.APP_SECRET
  • Claims: Account ID, User ID, Forward URL
  • Purpose: Prevent CSRF attacks and maintain session state

Token Encryption

  • Tokens stored in MongoDB
  • Should be encrypted at rest (deployment-specific)
  • Access controlled by DashClicks authentication

Single Connection Per User

Only one active connection per user/account to prevent token proliferation.

📝 Important Notes

  • 🔄 Auto Refresh: Access tokens refreshed automatically on expiry
  • ⏱️ Token Lifetime: 24 hours for access tokens
  • 🔁 Refresh Tokens: Long-lived, no expiration
  • 🎯 Scope: Currently only uses contact_data scope
  • 👤 Single Connection: One token set per user/account pair
  • Refresh Latency: ~200ms added when token needs refresh
💬

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