Authenticate with OAuth 2.0

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.

  1. Generate a private key.

>> openssl genpkey -aes256 -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private_key.pem
  1. Generate a public key using the above private key.

>> openssl rsa -in private_key.pem -out public_key.pem -outform PEM -pubout
  1. Send us the public key.

  2. We send a file containing credentials encrypted with the above public key to you.

  3. 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:

  1. Header: normalized structure specifying how token is signed (generally using HMAC SHA-256 algorithm).

  2. Free set of claims embedding whatever you want: client_id, aud, expiration date, etc.

  3. 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 and iat values should be numeric. Don’t set them as strings. The exp 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

Table 5 Return Codes

Field Name

Required

Description

grant_type

Yes

MUST be client_credentials

client_assertion_type

Yes

MUST be urn:ietf:params:oauth:client-assertion-type:jwt-bearer

client_assertion

Yes

JWS value (varies for each client request).

scope

Yes

MUST be upload

realm

Yes

MUST be aaca

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 see 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 see 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 see this error

{
 "error_description": "Client authentication failed",
 "error": "invalid_client"
}

then perform the following checks:

  1. Ensure the realm value is correct.

  2. Ensure client_id, client_secret used in JWT are correct.

  3. 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.

  4. Log request and see whether you are seeing all endpoints, param names and values properly. Check the url encoded values to ensure they are correct.

  5. Ensure you are hitting the correct endpoint.

  6. 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 see 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 see 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 see this error:

{
 "error_description": "Client authentication failed",
 "error": "invalid_client"
}

Perform these checks:

  1. Ensure all requested parameters are passed.

  2. No typos in parameters. All parameters are in lowercase.

  3. Check the format of values like JWT(includes header, claims, signature). No truncation, etc.

  4. client_assertion_type and client_assertion are must.

  5. Ensure exp and iat 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']
-------------------------------------------------------------------------------------------------------------------------------