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
| Variable | Description | Example |
|---|---|---|
CONSTANTCONTACT_CLIENT_ID | OAuth client ID | bd5e80bb-7317-4b22-8839-031b8a9c8417 |
CONSTANTCONTACT_SECRET_ID | OAuth client secret | 0MtgSHFWiMix3g4pEaKXvg |
CONSTANTCONTACT_REDIRECT_URL | OAuth callback URL | https://api.dashclicks.com/v1/integrations/constantcontact/auth/callback |
CONSTANTCONTACT_AUTH_ENDPOINT | Authorization endpoint | https://api.cc.email/v3/idfed |
CONSTANTCONTACT_ACCESS_TOKEN_ENDPOINT | Token endpoint | https://idfed.constantcontact.com/as/token.oauth2 |
CONSTANTCONTACT_GRANT_TYPE | Grant type | authorization_code |
CONSTANTCONTACT_AUTH_SCOPE | OAuth scope | contact_data |
📋 API Endpoints
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| GET | /v1/integrations/constantcontact/auth/login | Initiate OAuth flow | ✅ JWT |
| GET | /v1/integrations/constantcontact/auth/callback | OAuth callback handler | ❌ |
| DELETE | /v1/integrations/constantcontact/auth | Delete 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:
- Check for existing token in database
- If token exists and valid → Redirect to
forward_urlwith success - If token invalidated → Delete and re-authenticate
- 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 Contactstate(required) - JWT state token from Step 1
Process:
- Decode JWT state token
- Exchange authorization code for access token
- Store tokens with generation timestamp in MongoDB
- Redirect to
forward_urlwith 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:
- Retrieve token from database
- Check
generated_attimestamp - If expired (> 24 hours), refresh automatically
- Update database with new access token
- 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
| Operation | Required Scopes |
|---|---|
| Login | constantcontact, constantcontact.create |
| Delete | constantcontact, constantcontact.delete |
| Export | constantcontact, constantcontact.read |
⚠️ Error Handling
| Error | Status | Response |
|---|---|---|
Missing forward_url | 405 | { success: false, errno: 400, message: "Forward url required." } |
| Unauthorized user | 302 | Redirects with error |
| Token not found (delete) | 404 | { success: false, errno: 400, message: "Access Token Not Found" } |
| OAuth exchange failed | 302 | Redirects to forward_url with error |
| Refresh token failed | 400 | Error 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_datascope - 👤 Single Connection: One token set per user/account pair
- ⚡ Refresh Latency: ~200ms added when token needs refresh