- Step 1: Send an Authorization Request to the Authorization Server
- Step 2: Provide Authentication and Authorization Information to the User
- Step 3: User Authenticates and Authorizes your Application
- Step 4: Request an Access Token
- Step 5: Validate the Access Token
- Step 6: Add the Access Token to the Authorization Request
- Step 7 (Optional): Check the Access Token Expiration Timestamp
- Step 8: Refresh the Access Token
In order to use this flow, you must first create and configure a V3 API application configured to use the Device Flow. For more information, see Quick Start Guide.
Use the Device Flow if your application runs on a device that is input constrained. For example, a command line application that cannot provide a web browser to users. Unlike most OAuth2 flows, Device Flow does not require using redirect URLs, callbacks, or the client secret. Instead, it requires getting a device_code
, and then the application’s client_id
and the device_code
are used to get an access token.
On behalf of a Constant Contact user that wants to use your application, your application sends an authorization request to the Constant Contact Authorization Server. Next, you must provide the user with the information they then use on a device with a web browser, such as their phone or computer. This allows them to sign in to authenticate their Constant Contact account and to grant your application access to their data.
The steps in complete the OAuth2 Device Flow are as follows:
Step 1: Send an Authorization Request to the Authorization Server
Your application sends an authorization request to the Authorization Server.
To create an authorization request, make a POST call to the authorization endpoint https://authz.constantcontact.com/oauth2/default/v1/device/authorize
and include all required request query parameters in the request body or as query parameters.
For example, the following shows an encoded URL authorization request:
https://authz.constantcontact.com/oauth2/default/v1/device/authorize?client_id={your_client_id}&response_type=code&scope=contact_data%20campaign_data%20offline_access&state=235o250eddsdff
-
client_id
: Uniquely identifies your application. -
scope
: Includes the permissions the user needs to grant to the application access to their Constant Contact user data. For more information on scopes and the specific scopes required by each V3 API endpoint, see the Scopes Overview page.
The Authorization Server returns authentication information in the response body, for example:
{
"device_code": "03567b9d-967e-402d-af64-1b53453e9321",
"user_code": "PLSSKCWS",
"verification_uri": "https://identity.constantcontact.com/activate",
"verification_uri_complete": "https://identity.constantcontact.com/activate?user_code=PLSSKCWS",
"expires_in": 600,
"interval": 5
}
-
device_code
: The code that the device exchanges for an access token after the user verifies your application. -
user_code
: The code that the user enters to verify that your application can have access to their Constant Contact data. -
verification_uri
: The URI from which the user can verify ypur application. -
verification_uri_complete
: The verification URL with the user code included. -
expires_in
: The number of seconds before thedevice_code
expires. Your application begins polling the Authorization Server until either the user authenticates or the code expires. -
interval
: The recommended number of seconds to wait before polling attempts to see if the user has verified your application.
Example Authorization Requests
<?php
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://authz.constantcontact.com/oauth2/default/v1/device/authorize',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => 'client_id=63f2671a-571e-4c25-876b-f6c501f534e2&scope=openid',
CURLOPT_HTTPHEADER => array(
'Content-Type: application/x-www-form-urlencoded',
'Accept: application/json'
),
));
$response = curl_exec($curl);
curl_close($curl);
echo $response;
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");
RequestBody body = RequestBody.create(mediaType, "client_id=63f2671a-571e-4c25-876b-f6c501f534e2&scope=openid");
Request request = new Request.Builder()
.url("https://authz.constantcontact.com/oauth2/default/v1/device/authorize")
.method("POST", body)
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.addHeader("Accept", "application/json")
.build();
Response response = client.newCall(request).execute();
}
curl --location --request POST 'https://authz.constantcontact.com/oauth2/default/v1/device/authorize' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Accept: application/json' \
--data-urlencode 'client_id=63f2671a-571e-4c25-876b-f6c501f534e2' \
--data-urlencode 'scope=openid contact_data'
Step 2: Provide Authentication and Authorization Information to the User
Your application provides the the information the user needs to authenticate their user account and to authorize your application from a device using a web browser, such as a cell phone. How you choose to provide the information is up to you.
You can choose to provide the user with:
-
The URI to use and the code to enter.
-
The QR code to scan to open the URL and the code to enter, or a shortened URL to use that includes the embedded user code.
-
If running natively on a browser-based device, returns the direct verification URI with embedded code.
Step 3: User Authenticates and Authorizes your Application
If the user goes to the verification_uri
, Constant Contact prompts them to enter their user_code
. If the user goes to the verification_uri_complete
, the user_code
will be filled in automatically.
After the user clicks Next, they are prompted to sign in to their Constant Contact account and to grant your application access to their data. After, Constant Contact notifies the user that their device is activated and informs them to go to their device for instructions.
Step 4: Request an Access Token
To make API calls to access a user’s Constant Contact data, your application needs to request an access token and optionally, a refresh token from the Authorization Server.
To request an access_token
and refresh_token
, make a POST request to the Authorization Server using the https://authz.constantcontact.com/oauth2/default/v1/token
endpoint.
Include the following required query parameters in the form-data request body:
client_id
: The ID that is associated with your application.device_code
: The authorization code returned in the authorization request.grant_type
: The value should always beurn:ietf:params:oauth:grant-type:device_code
.
In the header, specify the content type as application/x-www-form-urlencoded
Example Access Token Requests
<?php
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://authz.constantcontact.com/oauth2/default/v1/token',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => 'client_id=63f2671a-571e-4c25-876b-f6c501f534e2&device_code=c215c338-ad46-448e-ba91-1ead64fd907a&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code',
CURLOPT_HTTPHEADER => array(
'Content-Type: application/x-www-form-urlencoded'
),
));
$response = curl_exec($curl);
curl_close($curl);
echo $response;
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");
RequestBody body = RequestBody.create(mediaType, "client_id=63f2671a-571e-4c25-876b-f6c501f534e2&device_code=c215c338-ad46-448e-ba91-1ead64fd907a&grant_type=urn:ietf:params:oauth:grant-type:device_code");
Request request = new Request.Builder()
.url("https://authz.constantcontact.com/oauth2/default/v1/token")
.method("POST", body)
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.build();
Response response = client.newCall(request).execute();
curl --location --request POST 'https://authz.constantcontact.com/oauth2/default/v1/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=63f2671a-571e-4c25-876b-f6c501f534e2' \
--data-urlencode 'device_code=c215c338-ad46-448e-ba91-1ead64fd907a' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:device_code'
Access Token Responses
Device Flow does not use redirects or callbacks. Your application must poll for a successful access token response.
Expired Authorization
If the user does not authorize your application before the device_code
expires (expires_in
number of seconds), you get an authorization expired response. For example:
{
"error": "expired_token",
"error_description": "The device code has expired."
}
Pending Authorization
If the user has not yet authorized the application device but the device_code
has not yet expired (expires_in
number of seconds), you get an authorization pending response. For example:
{
"error": "authorization_pending",
"error_description": "The device authorization is pending. Please try again later."
}
Step 5: Validate the Access Token
For each new access token that you receive, follow best security practices by validating the signature and claims for the access token by completing the procedures that follow.
Load the JSON web-key Set
Use the method that follows to load the Constant Contact public JSON web-key set used to get the access token.
/**
* This method uses the java11 http client to load Constant Contact's public json web key set.
* In real use cases, this value should be cached.
* @return Constant Contact's public json web key set for the v3 API.
*/
public String getPublicJsonWebKeySet() throws IOException {
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create("https://identity.constantcontact.com/oauth2/aus1lm3ry9mF7x2Ja0h8/v1/keys"))
.GET()
.header("Accept","application/json")
.header("Content-Type", "application/x-www-form-urlencoded")
.build();
HttpResponse<String> httpResponse = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return httpResponse.body();
}
Verify the Access Token Claims
Use the method that follows to parse the access token, and then validate the signature and claims of the access token. The code examples in this step use the org.jose4j library.
- Use the
jwt
parameter to pass in the access token. - Use the
jsonWebKeySetJson
parameter to pass the JSON Web Key Set (JWKS) value (this is the value that was returned in the previous Load the JSON web-key set step).
/**
* This method uses the jose4j library to verify the claims on the JWT.
* It throws an exception if it fails to parse the JWT, or the JWT does not include the expected claim.
* @param jwt String containing a JSON web token.
* @param jsonWebKeySetJson the set of keys available at https://identity.constantcontact.com/oauth2/aus1lm3ry9mF7x2Ja0h8/v1/keys
* @throws JoseException
* @throws InvalidJwtException
*/
public void verifyJwtToken(String jwt, String jsonWebKeySetJson) throws JoseException, InvalidJwtException {
JsonWebKeySet jsonWebKeySet = new JsonWebKeySet(jsonWebKeySetJson);
VerificationJwkSelector jwkSelector = new VerificationJwkSelector();
JsonWebSignature jws = new JsonWebSignature();
jws.setCompactSerialization(jwt);
JsonWebKey jwk = jwkSelector.select(jws, jsonWebKeySet.getJsonWebKeys());
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
.setRequireExpirationTime()
.setRequireSubject()
.setExpectedIssuer("https://identity.constantcontact.com/oauth2/aus1lm3ry9mF7x2Ja0h8")
.setExpectedAudience("https://api.cc.email/v3")
.setVerificationKey(jwk.getKey())
.setJwsAlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT, AlgorithmIdentifiers.RSA_USING_SHA256)
.build();
jwtConsumer.processToClaims(jwt);
}
If the access token is not valid or if a required claim is missing or incorrect, an exception occurs.
Check the following claims:
- Compare the
cid
(client identifier) value to ensure that it matches your application API key - The
platform_user_id
claim in the access token uniquely identifies a Constant Contact user.
To check the claims, use the previous code example, except replace jwtConsumer.processToClaims(jwt);
with the following:
JwtClaims claims = consumer.processToClaims(token);
String cid = claims.get("cid")
if (! apiKey.equals(cid)) {
throw new InvalidJwtException("Api key in token incorrect");
}
String userId = claims.get("platform_user_id")
if (userId == null) {
throw new InvalidJwtException("Token is missing platform_user_id");
}
Step 6: Add the Access Token to the Authorization Request
After verifying the access token claims and signature, use the access token to send requests in the V3 API by adding it to the Authorization
request header in the format Authorization: Bearer {your_access_token}
.
If the access token expires, users must reauthenticate and reauthorize your application through Constant Contact. Making an API call with an expired access token returns a 401 unauthorized status code.
Step 7 (Optional): Check the Access Token Expiration Timestamp
You can quickly check to see if the access token is expired by using the method that follows.
Use the jwt
parameter to pass in the access token. This method returns True
for an expired token if the expiration time is within 5 minutes (300 seconds) of the current time, or False
if the token has not expired. If the access token expires, you can exchange it for a new access and refresh token using the Refresh the Access Token procedure.
/**
* This method checks if the current time is greater than or equal to the JWT expiration claim.
* @param jwt A String JSON web token.
* @return True if the jwt token is expired. False if the token is not expired.
*/
public boolean checkIfTokenIsExpired(String jwt) throws InvalidJwtException, MalformedClaimException {
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
.setRequireExpirationTime()
.setSkipDefaultAudienceValidation()
.setDisableRequireSignature()
.setSkipSignatureVerification()
.build();
JwtClaims jwtClaims = jwtConsumer.processToClaims(jwt);
return NumericDate.now().getValue() >= jwtClaims.getExpirationTime().getValue();
}
Step 8: Refresh the Access Token
To avoid having to prompt a user to re-authenticate with Constant Contact due to an expired access token, use the refresh token to get a new access token and a new refresh token. To refresh the access token, send a POST request to the https://authz.constantcontact.com/oauth2/default/v1/token
authorization endpoint.
Constant Contact rate limits the Token endpoint. A 429 response will likely be returned if you attempt to refresh an access token before every V3 API request. Constant Contact recommends that you only send a refresh token request to get a new access token if your existing access token is expired or about to expire.
Request parameters
-
client_id
— Required. The API key for your integration. -
refresh_token
— Required. The refresh token corresponds to the access token you are trying to refresh. -
grant_type
— Required. The value isrefresh_token
. This specifies that the purpose of this request is to refresh an access token.
Header Formats
In the header, specify the preferred response formats to return as follows:
-
"Accept", "application/json"
-
"Content-Type", "application/x-www-form-urlencoded"
Example Request
curl --location --request POST 'https://authz.constantcontact.com/oauth2/default/v1/token?refresh_token={refresh_token}&grant_type=refresh_token&client_id={client_id}' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded'
/*
* This function can be used to exchange a refresh token for a new access token and refresh token.
* Make this call by passing in the refresh token returned with the access token.
* The response will contain a new 'access_token' and 'refresh_token'
*/
/**
* @param $refreshToken - The refresh token provided with the previous access token
* @param $clientId - API Key
* @param $clientSecret - API Secret
* @return string - JSON String of results
*/
function refreshToken($refreshToken, $clientId, $clientSecret) {
// Use cURL to get a new access token and refresh token
$ch = curl_init();
// Define base URL
$base = 'https://authz.constantcontact.com/oauth2/default/v1/token';
// Create full request URL
$url = $base . '?refresh_token=' . $refreshToken . '&grant_type=refresh_token&client_id=' . $clientId;
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: application/json', 'Content-Type: application/x-www-form-urlencoded'));
// Set method and to expect response
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Make the call
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
/*
* This method is used to exchange a refresh token for a new access token and refresh token.
* Make this call by passing in the refresh token returned with the access token.
* The response will contain a new 'access_token' and 'refresh_token'.
*/
/**
* @param clientId - API Key
* @param clientSecret - API Secret
* @param refresh_token - Refresh Token
* @return - JSON string containing a new access_token and refresh_token
* @throws InterruptedException
* @throws IOException
* @throws URISyntaxException
*/
public static String refreshToken(String clientId, String clientSecret, String refresh_token) throws IOException, InterruptedException, URISyntaxException
{
// Create request URL
StringBuilder requestBuilder = new StringBuilder()
.append("https://authz.constantcontact.com/oauth2/default/v1/token")
.append("?refresh_token=")
.append(refresh_token)
.append("&grant_type=refresh_token")
.append("&client_id=")
.append(client_id);
URI requestUri = new URI(requestBuilder.toString());
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(requestUri)
.POST(HttpRequest.BodyPublishers.ofString(""))
.header("Accept","application/json")
.header("Content-Type", "application/x-www-form-urlencoded")
.build();
HttpResponse<String> httpResponse = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return httpResponse.body();
}
Example Response
{
"token_type": "Bearer",
"expires_in": "28800",
"access_token": "eyJraWQiOiIzSDdLVVpkSDlyNTh6RVVDSzNYMlR1b2o0SjV0eldWUF9FRUxQTDd6X3hjIiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULkZ6YUUtTVFFb2dKQ1VzZlhSUGtBZldobDllckJndVlYRWphOGFXd1RyRzgiLCJpc3MiOiJodHRwczovL2lkZW50aXR5LmNvbnN0YW50Y29udGFjdC5jb20vb2F1dGgyL2F1czFsbTNyeTltRjd4MkphMGg4IiwiYXVkIjoiaHR0cHM6Ly9hcGkuY2MuZW1haWwvdjMiLCJpYXQiOjE2NDQ0Mjc3NzMsImV4cCI6MTY0NDQ1NjU3MywiY2lkIjoiY2M5NGMyZDYtNmU3MC00MjZjLWFmZDEtNzFjOGZhNWY0ODkwIiwidWlkIjoiMDB1MWlieDhsaGIzYW12SFIwaDgiLCJzY3AiOlsiYWNjb3VudF9yZWFkIiwiZW1haWwiLCJjYW1wYWlnbl9kYXRhIiwicHJvZmlsZSIsImFkZHJlc3MiLCJjb250YWN0X2RhdGEiLCJvcGVuaWQiXSwic3ViIjoiZ2Vla3NxdWFkIiwicGxhdGZvcm1fdXNlcl9pZCI6ImQzYTA2ZTYyLTJmMTQtNGQ5Ni05NDUzLTI2NTkxYWI1M2YwZCJ9.saJxgUYx1_OKUteH01KV71vzjaxqTKnZ3XfD4QKtsIkjrmcf4tUWEFSzjuF1kdxLvlMoCyr_MwO81ApwYX2UU0YRhkVsfw2VLR4KpO89ILul7ZJZyzzsVKak8BrgLhqR1AB7X116VRjsH74vD4D-1e4uKKUlK4yefsZE14kFgDZNi74_WPn5FBGeicwMsStdG3qms8X-6LqIxuZ5fXgSxK9wDEBe-xJfJSqAqo1vJKMqw2JT_NvBBLDXqoSg0_q_0IqbxXEMHdX2J_DHGKrY7w1QQGmeBU_BIlewQPUxUGX-dbRavxncBpItt1tsRLBIiPbPkQ_zDN8_J9lPIxJksA",
"scope": "account_read offline_access",
"refresh_token": "s8Mu4hfiwmug7ru4Rcoo4hjkawiw4OUTH2ixvsy3b8"
}
-
token_type
— The value is always set toBearer
. -
expires_in
- Theaccess_token
expiration timestamp, in seconds. -
access_token
— The response body returns a newaccess_token
value. -
refresh_token
— The response body returns a newrefresh_token
value.