- 12 Minutes to read
Authenticate with OAuth 2.0
- 12 Minutes to read
Abstract
Because Enhanced Attribution is a server-to-server implementation, it requires OAuth 2.0 authentication before any data can be posted to Yahoo Ad Tech servers. Follow the steps outlined in this guide to create your Client ID and Secret for secure authentication.
Overview
OAuth 2.0 is a mechanism that relies on continuously generating authentication tokens and then providing those tokens during the posting of data.
Note
Yahoo Ad Tech has no plans to support OAuth 1.0, which depends on static tokens.
Requesting Client Credentials
To complete the steps below, you will first need a Client ID and a Secret. Follow the below steps to get them.
Generate a private key.
>> openssl genpkey -aes256 -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private_key.pem
Generate a public key using the above private key.
>> openssl rsa -in private_key.pem -out public_key.pem -outform PEM -pubout
Send us the public key.
We send a file containing credentials encrypted with the above public key to you.
Decrypt the file with the private key.
>> openssl rsautl -decrypt -inkey private_key.pem -in credential.enc -out my_credentials.txt
Security Considerations
Be sure to keep your Secret secure. If you want to reset Secret or forget your Secret, follow the instructions above to get new credentials.
Important
It is critical to ensure that the Secret is protected and NEVER exposed. All interactions MUST be protected by TLS. Do not embed the Secret directly in code to avoid being accidentally exposed to the public. Instead of embedding your Secret in the applications, store them in environment variables or in files outside of your application’s source tree.
It’s recommended that you reset your Client ID/Secret periodically.
Important
If the credentials are compromised at any point, it is very important to reset your Client ID/Secret pair.
Generate a JSON Web Token (JWT)
To generate an access token, you’ll need to generate a JWT.
A JSON Web Token is composed of three main parts:
Header: normalized structure specifying how token is signed (generally using HMAC SHA-256 algorithm).
Free set of claims embedding whatever you want: client_id, aud, expiration date, etc.
Signature ensuring data integrity.
The signature mechanism is HMAC_SHA256 as defined by the JOSE specifications at https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31 .
JWT Header
{
"alg": "HS256",
"typ": "JWT"
}
JWT Claims
{
"aud": "{protocol}://{b2b.host}/identity/oauth2/access_token?realm=aaca",
"iss": "{client_id}",
"sub": "{client_id}",
"exp": {expiry time in seconds},
"iat": {issued time in seconds},
}
Note
The
exp
andiat
values should be numeric. Don’t set them as strings. Theexp
value should be less than 24 hrs. Preferable time is currentTime + 600 (i.e., 10 minutes). Don’t use currentTime + (24 60 60). You may get the JWT is has expired or is not valid error.urn:vm:claims:fedidp_tenant
is an optional value. You need to pass this only if you need to do token exchange using a federated token.
JWT Signature
jwt_signing_string = base64url_encode(jwt_header) + '.' + base64url_encode(jwt_body);
jwt_signature = base64url_encode(hmac_sha256(jwt_signing_string, client_secret))
JWS = jwt_signing_string + '.' + jwt_signature
Walking through the manual steps to build this JWT value:
jwt_header = '{"typ":"JWT","alg":"HS256"}';
jwt_body = '{
"iss":"client_id",
"sub":"client_id",
"aud":"https://id.b2b.yahooinc.com/identity/oauth2/access_token?realm=aaca",
"exp":<expiry-time-in-seconds>,
"iat":<issued-time-in-seconds>}';
jwt_signing_string = base64url_encode(jwt_header) + '.' +
base64url_encode(jwt_body);
jwt_signature = base64url_encode(hmac_sha256(jwt_signing_string,
client_secret))
JWS = jwt_signing_string + '.' + jwt_signature
A Final JWT token looks like this:
ew0KICAiYWxnIjogIkhTMjU2IiwNCiAgICJ0eXAiOiAiSldUIg0KfQ.ew0KICAiYXVkIjogIntwcm90b2NvbH06Ly97YjJiLmhvc3R9L2lkZW50aXR5L29hdXRoMi9hY2Nlc3NfdG9rZW4/cmVhbG09PHlvdXItcmVhbG0+IiwNCiAgImlzcyI6ICJ7Y2xpZW50X2lkfSIsDQogICJzdWIiOiAie2NsaWVudF9pZH0iLA0KICAiZXhwIjog4oCce2V4cGlyeSB0aW1lIGluIHNlY29uZHN94oCdLA0KICAiaWF0Ijog4oCce2lzc3VlZCB0aW1lIGluIHNlY29uZHN94oCdDQp9DQo.uKqU9dTB6gKwG6jQCuXYAiMNdfNRw98Hw_IWuA5MaMo
<base64url-encoded header>.<base64url-encoded claims>.<base64url-encoded signature> (They are separated with a “.”)
Sample codes to generate JWT and get an access token are provided below.
Request for an access token
Make this POST call:
POST https://id.b2b.yahooinc.com/identity/oauth2/access_token
Note
The Request POST format requires application/x-www-form-urlencoded.
OAuth2 Client Credentials
This API uses the OAuth2 client_credentials flow and identifies the client via a signed JSON object which will need to be created and included in the client_assertion
argument in the request.
Arguments
Field Name | Required | Description |
---|---|---|
| Yes | MUST be |
| Yes | MUST be |
| Yes | JWS value (varies for each client request). |
| Yes | MUST be |
| Yes | MUST be |
Example
Request
POST /identity/oauth2/access_token HTTP/1.1
Host: https://id.b2b.yahooinc.com
Content-Type: application/x-www-form-urlencoded
Accept: application/json grant_type=client_credentials&scope=upload&realm=aaca&client_assertion_type=urn:ietf:params:o auth:client-assertion-type:jwt-bearer&client_assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc 3MiOiJkNjI0YmI4My03MzViLTRmNTMtYjU1Ni03YTEzMGM5YzAxZjMiLCJzdWIiOiJkNjI0YmI4My03Mz ViLTRmNTMtYjU1Ni03YTEzMGM5YzAxZjMiLCJhdWQiOiJodHRwczovL2lkLXVhdDIuY29ycC5hb2wuY 29tL2lkZW50aXR5L29hdXRoMi9hY2Nlc3NfdG9rZW4_cmVhbG09YjJiIiwiaWF0IjoxNDc1MDk1Mjg1Ljk 1NCwiZXhwIjoxNDc1MDk1NTg1Ljk1NCwicmVhbG0iOiJiMmIifQ.JzeW4YvrN7HC1nAcrj21_9yn2i3Iq9b abpTmbNuPfcM
Response
success
Format: json Status: 200 Headers: Content-Type: application/json
{
"access_token": "3f94eb47-a295-4977-a375-e27bea5c828b",
"scope": "upload",
"token_type": "Bearer",
"expires_in": 599
}
Note
The token remains active for 10 minutes, so be sure to re-use the token instead of requesting a new token for every postback. Also, the token can be refreshed/regenerated at around 8-9 minutes instead of waiting for the 10 minutes.
Putting access token in the request header
You need to put your access token in the request header to invoke AACA APIs. The header name is Authorization and the value is access token.
Example format:
GET /?id=id123&vmcid=simple_click_id&dp=simple_dp&gv=10.0 HTTP/1.1
Host: https://aaca.yahooinc.com
Authorization: 3f94eb47-a295-4977-a375-e27bea5c828b
Sample Code for Token Generation
Java
package sample.aaca;
import com.google.gson.Gson;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
public class JavaSample {
private static String oAuthURL = "https://id.b2b.yahooinc.com/identity/oauth2/access_token";
private static String scope = "upload";
private static String realm = "aaca";
private String clientId = "//Insert ClientId here";
private String clientSecret = "//Insert ClientSecret here";
public static final long ACCESS_TOKEN_TTL = 600000;
private String generateJsonWebToken() throws UnsupportedEncodingException {
final HashMap<String, Object> claims = new HashMap<>();
long nowMillis = System.currentTimeMillis();
long expMillis = nowMillis + ACCESS_TOKEN_TTL;
claims.put("iss", clientId);
claims.put("sub", clientId);
claims.put("aud", oAuthURL + "?realm=" + realm);
claims.put("exp", expMillis / 1000);
claims.put("iat", nowMillis / 1000);
JwtBuilder jwtBuilder = Jwts.builder().setClaims(claims);
return jwtBuilder.signWith(SignatureAlgorithm.HS256, clientSecret.getBytes("UTF-8")).compact();
}
private Response getTokenFromAuthServer(String assertion) throws IOException {
Response response = null;
try (CloseableHttpClient httpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()).build()) {
StringBuilder payload = new StringBuilder().append("scope=").append(scope).append("&grant_type=")
.append("client_credentials").append("&client_assertion_type=")
.append("urn:ietf:params:oauth:client-assertion-type:jwt-bearer").append("&realm=").append(realm)
.append("&client_assertion=").append(assertion);
HttpPost request = new HttpPost(oAuthURL);
StringEntity body = new StringEntity(payload.toString(), ContentType.APPLICATION_FORM_URLENCODED);
request.setEntity(body);
request.addHeader("Accept", ContentType.APPLICATION_JSON.toString());
System.out.println("Starting token request..........");
HttpResponse result = httpClient.execute(request);
System.out.println("Token request completed.......... " +
result.getStatusLine().getStatusCode() + " " +
result.getStatusLine().getReasonPhrase());
String json = EntityUtils.toString(result.getEntity(), "UTF-8");
Gson gson = new Gson();
response = gson.fromJson(json, Response.class);
}
return response;
}
/**
* Get token from server.
* @return token generated.
* @throws Exception Throws exception if connection issues or encryption issues.
*/
public String getToken() throws Exception {
String assertion = generateJsonWebToken();
Response response = getTokenFromAuthServer(assertion);
String token = response.getAccessToken();
return token;
}
/**
* Helper class representing the json response from the IDB2B server
*/
public static class Response {
/**
* String with token value received from
* the IDB2B server
*/
private String access_token;
public Response() {}
public Response(String access_token) {
this.access_token = access_token;
}
public void setAccessToken(String access_token) {
this.access_token = access_token;
}
public String getAccessToken() {
return access_token;
}
}
public static void main(String[] args) {
JavaSample tokenGenerator = new JavaSample();
String assertion;
Response response;
try {
assertion = tokenGenerator.generateJsonWebToken();
response = tokenGenerator.getTokenFromAuthServer(assertion);
System.out.println(response.getAccessToken());
} catch (Exception e) {
System.out.println("Exception occured..." + e.getMessage());
}
}
}
Troubleshooting
Invalid client error - JWT is not valid.
You can view the invalid client if the JWT assertion is not correct. The reasons can be that JWT expired or is invalid, audience wrong, etc. Also, the client id is not found or client_id
, secret are invalid.
If you view the JWT expired error below, then ensure the JWT claim values exp
and iat
are correct. Both values should be in seconds (EPOCH time) and exp should be in the future, but it should be less than sthe erver side configured time (i.e., 24 hours).
{
"error_description": "JWT is has expired or is not valid",
"error": "invalid_client"
}
Invalid client error - Client authentication failed
If you view this error
{
"error_description": "Client authentication failed",
"error": "invalid_client"
}
then perform the following checks:
Ensure the
realm
value is correct.Ensure
client_id
,client_secret
used in JWT are correct.Ensure
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
is correct. Check for typos or any hidden special characters in the values.Log request and view whether you are viewing all endpoints, param names and values properly. Check the url encoded values to ensure they are correct.
Ensure you are hitting the correct endpoint.
If you still can’t find the reason, then delete static values for
grant_type
,client_assertion_type
, scope, realm, etc. and re-add manually just to avoid any copy paste resulting in invisible special characters.
Invalid request
If you view this error below, then check that grant_type
is set and the value is client_credentials
.
{
"error_description": "Grant type is not set",
"error": "invalid_request"
}
Invalid scope
If you view this error below, check that the scope is set correctly.
{
"error_description": "Unknown/invalid scope(s): [open]",
"error": "invalid_scope"
}
Server error
If the data is not in the expected format or the flow is not supported or some other reason, then you may view this error:
{
"error_description": "Client authentication failed",
"error": "invalid_client"
}
Perform these checks:
Ensure all requested parameters are passed.
No typos in parameters. All parameters are in lowercase.
Check the format of values like JWT(includes header, claims, signature). No truncation, etc.
client_assertion_type
andclient_assertion
are must.Ensure
exp
andiat
in JWT claims are numeric values. Don’t set them as strings.
Appendix
JWT generation (Java)
Add below dependency to pom.xml
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.5.2</version>
</dependency>
-------------------------------------------------------------------------------------------------------------------------------
clientId = OAuth2 Client ID
secret = OAuth2 Client Secret
audience = {protocol}://{b2b.host}/identity/oauth2/access_token?realm=
-------------------------------------------------------------------------------------------------------------------------------
public static String generateJsonWebToken(final String clientId, final String secret,
final String audience) throws OCAuthException {
JwtClaims claims = new JwtClaims();
claims.setIssuedAt(NumericDate.now());
claims.setExpirationTimeMinutesInTheFuture(10);
claims.setSubject(clientId);
claims.setIssuer(clientId);
claims.setAudience(audience);
claims.setGeneratedJwtId();
try {
Key key = new HmacKey(secret.getBytes("UTF-8"));
JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toJson());
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);
jws.setKey(key);
jws.setDoKeyValidation(false);
return jws.getCompactSerialization();
} catch (Exception e) {
throw new OCAuthException("JWT Generation failed", e);
}
}
JWT generation (JavaScript)
Include below CryptoJS dependencies to the html
-------------------------------------------------------------------------------------------------------------------------------
<script src="//cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/hmac-sha256.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/components/enc-base64-min.js"></script>
-------------------------------------------------------------------------------------------------------------------------------
client_id = OAuth2 Client ID
client_secret = OAuth2 Client Secret
audience = {protocol}://{b2b.host}/identity/oauth2/access_token?realm=
-------------------------------------------------------------------------------------------------------------------------------
// Defining our token parts
var header = {
"alg": "HS256",
"typ": "JWT"
};
var data = {
"aud": "{protocol}://{b2b.host}/identity/oauth2/access_token?realm=<your-realm>",
"iss": "{client_id}",
"sub": "{client_id}",
"exp": {expiry time in seconds},
"iat": {issued time in seconds},
"jti": “{UUID}”
};
var secret = "{client_secret}";
function base64url(source) {
// Encode in classical base64
encodedSource = CryptoJS.enc.Base64.stringify(source);
// Remove padding equal characters
encodedSource = encodedSource.replace(/=+$/, '');
// Replace characters according to base64url specifications
encodedSource = encodedSource.replace(/\+/g, '-');
encodedSource = encodedSource.replace(/\//g, '_');
return encodedSource;
}
var stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header));
var encodedHeader = base64url(stringifiedHeader);
document.getElementById("header").innerText = encodedHeader;
var stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data));
var encodedData = base64url(stringifiedData);
document.getElementById("payload").innerText = encodedData;
var signature = encodedHeader + "." + encodedData;
signature = CryptoJS.HmacSHA256(signature, secret);
signature = base64url(signature);
JWT generation (Python)
Getting access token using client credentials
-------------------------------------------------------------------------------------------------------------------------------
import base64
import hashlib
import hmac
import json
import time
import urlparse
import requests
def hmac_sha256(key, msg, encode_output=False):
message = bytes(msg).encode('utf-8')
secret = bytes(key).encode('utf-8')
signature = hmac.new(secret, message, digestmod=hashlib.sha256).digest()
return base64.b64encode(signature) if encode_output else signature
def get_access_token(client_config):
"""
Returns an access token for the given client credentials
:param client_config: A dict with the environment variables. Is different on QA/PROD
:return: the oauth access token for the client
"""
client_id = client_config['CLIENT_ID']
client_secret = client_config['CLIENT_SECRET']
realm = client_config['REALM']
base_url = client_config['BASE_URL']
scope = client_config['SCOPE']
access_token_url_path = 'identity/oauth2/access_token'
jwt_header = json.dumps({
"typ": "JWT",
"alg": "HS256",
})
issue_time = int(time.time()) # Seconds since epoch
expiry_time = issue_time + 600
aud = urlparse.urljoin(base_url, '{path}?realm={realm}'.format(path=access_token_url_path, realm=realm))
jwt_body = {
"iss": client_id,
"sub": client_id,
"aud": aud,
"exp": expiry_time,
"iat": issue_time,
}
jwt_body = json.dumps(jwt_body)
jwt_signing_string = base64.b64encode(jwt_header) + '.' + base64.b64encode(jwt_body)
signature = hmac_sha256(client_secret, jwt_signing_string)
jwt_signature = base64.b64encode(signature)
client_assertion = jwt_signing_string + '.' + jwt_signature
data = {
'grant_type': 'client_credentials',
'scope': scope,
'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
'client_assertion': client_assertion,
'realm': realm,
}
logger.info("getting access token")
resp = requests.post(urlparse.urljoin(base_url, access_token_url_path), data=data)
result = resp.json()
return result['access_token']
-------------------------------------------------------------------------------------------------------------------------------
Exchanging code for access token
-------------------------------------------------------------------------------------------------------------------------------
import base64
import hashlib
import hmac
import json
import time
import urlparse
import requests
def hmac_sha256(key, msg, encode_output=False):
message = bytes(msg).encode('utf-8')
secret = bytes(key).encode('utf-8')
signature = hmac.new(secret, message, digestmod=hashlib.sha256).digest()
return base64.b64encode(signature) if encode_output else signature
def get_access_token(client_config, oauth_code):
"""
Authenticates a user using Oauth and returns an access token
:param client_config: A dict with the environment variables. Is different on QA/PROD
:param oauth_code: The oauth_code included in the url when the user has logged-in in AOL and is redirected to the app
:return: the oauth access token for the user
"""
client_id = client_config['CLIENT_ID']
client_secret = client_config['CLIENT_SECRET']
realm = client_config['REALM']
base_url = client_config['BASE_URL']
redirect_uri = client_config['REDIRECT_URI']
access_token_url_path = 'identity/oauth2/access_token'
jwt_header = json.dumps({
"typ": "JWT",
"alg": "HS256",
})
issue_time = int(time.time()) # Seconds since epoch
expiry_time = issue_time + 600
aud = urlparse.urljoin(base_url, '{path}?realm={realm}'.format(path=access_token_url_path, realm=realm))
jwt_body = {
"iss": client_id,
"sub": client_id,
"aud": aud,
"exp": expiry_time,
"iat": issue_time,
}
jwt_body = json.dumps(jwt_body)
jwt_signing_string = base64.b64encode(jwt_header) + '.' + base64.b64encode(jwt_body)
signature = hmac_sha256(client_secret, jwt_signing_string)
jwt_signature = base64.b64encode(signature)
client_assertion = jwt_signing_string + '.' + jwt_signature
data = {
'grant_type': 'authorization_code',
'code': oauth_code,
'redirect_uri': redirect_uri,
'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
'client_assertion': client_assertion,
'realm': realm,
}
resp = requests.post(urlparse.urljoin(base_url, access_token_url_path), data=data)
result = resp.json()
return result['access_token']
-------------------------------------------------------------------------------------------------------------------------------