Authenticate with OAuth 2.0
  • 12 Minutes to read

    Authenticate with OAuth 2.0


      Article summary

      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
      2. Generate a public key using the above private key.

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

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

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

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

      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 view whether you are viewing 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 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:

      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']
      -------------------------------------------------------------------------------------------------------------------------------


      Was this article helpful?