Accessing Adobe IO using Java

You might remember how, a couple of years ago, I was whining about how the authentication on the Reporting API was difficult. At the time, I wrote some Java code that was able to log into it, and I have been using that code, more or less unchanged, for a long time.

Now, all APIs are in the process of being centralised behind the adobe.io infrastructure, so here we go again.

Don’t get me wrong — aligning all the APIs is a good thing! It’ll make our lives as developers easier, especially the moment we start building cross-solution tools. And even if adobe.io is but a wrapper for some of the APIs, it is still easier to use the same wrapper everywhere than to learn a bunch of different APIs.

So I’m absolutely in favour.

Which is why I shall today document how to authenticate against adobe.io.

The second reason for me to write this down is that in order to publish a Launch Extension, or to program against the (awesome by sheer existence) Launch API, you have to use adobe.io, and that means I need to tell you how to do that, if only in order to complete the mini-series on Extensions.

Process & Initial Setup

Signing into adobe.io is based on keys and tokens. The process works like this:

1. Create yourself a private/public key pair
2. create an integration in the adobe.io console
3. upload the key into the adobe.io console
4. Convert your private key to DER
5. access the adobe.io authentication API to get a JWT token
6. use the JWT token to get an access token each time you work with the API

Steps 1 to 4 have to be done once, steps 5 and 6 must be done every time you start using the API, and step 6 has to be repeated whenever the access token has expired.

I will not explain steps 1 to 3, because the documentation doing so does exist, for example the JWT Authentication Quickstart.

Walk through that, then let’s start by checking out the Java code from github.

git clone https://github.com/janexner/adobeio-authentication-java-sample.git

Now we create a “secret.key” file, which you will have to copy into the project folder.

Assuming you went through steps 1 to 3, you should have a file called “private.key” somewhere, and we will convert that file.

That is as easy as running the following command:

openssl pkcs8 -topk8 -inform PEM -outform DER -in private.key  -nocrypt > secret.key

Copy or move the “secret.key” file into the adobeio-authentication-java-sample folder that was created by the git clone command.

Now put proper values into all those fields in lines 51 to 64 in AppTest (in the “AppTest.java” file).

[screenshot]
This part has to be configured
I have tried to name the variables as obviously as possible, and I’ve put them in the right order so you can log into the Adobe IO Console, start copying at the top of the Overview page of your integration…

[screenshot]
Copy these from the Console Overview page
Then you move on to the JWT page, copy some more…

[screenshot]
Copy these from the JWT page
Load https://developer.adobelaunch.com/api/properties/fetch/, and copy some more…

[screenshot]
Copy from the Launch documentation
Notice the yellow bit? The documentation points to the Launch Integration environment. You want to remove that if you want to access your “normal” Launch properties.

Lastly, log into Launch itself, open the property you want to use here, copy one more thing…

[screenshot]
Copy from Launch itself
Once you are sure your values are ok, just run

mvn test

You should see a result like the following

[screenshot]
Test result – OK
Now you know that you can use the getAccessToken method in your own projects, and that with the values you copied into the `AppTest` class, the code will actually work.

Debugging

If you choose to spy on the network traffic using Charles or another proxy, look for two requests.

One request will go to the authentication server, and the response should contain a valid JSON that includes an “access_token” element.

[screenshot]
Charles – successful token request
The second request will go to the Launch API (or whatever API you use to test you have access). We’re looking for a non-empty reponse here, that ideally makes some sense, too.

[screenshot]
Charles – successful request to Launch API
In my case, I’m requesting information for a Property, and the “id” element of the response contains the same ID I requested. Looks good, I’d say.

Explain

Above and beyond the rough flow outlined above, here is what the getAccessToken method does, and where I struggled, line by line.

    public String getAccessToken(String secretKeyFileName, String apiKey, String techAccountID, String organizationID, String clientSecret, String[] metaContexts, HttpClient httpClient) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, KeyManagementException, KeyStoreException, ClientProtocolException, JsonParseException, JsonMappingException {
        System.out.println("Step 1 - generating a JWT...");
        // Set expirationDate in milliseconds since epoch to 24 hours ahead of now
        Long expirationTime = System.currentTimeMillis() / 1000 + 86400L;

        // Secret key as byte array. Secret key file should be in DER encoded format.
        byte[] privateKeyFileContent = Files.readAllBytes(Paths.get(secretKeyFileName));
        // Create the private key
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        KeySpec ks = new PKCS8EncodedKeySpec(privateKeyFileContent);
        RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(ks);

        // Create JWT payload
        HashMap<String, Object> jwtClaims = new HashMap<String, Object>();
        jwtClaims.put("iss", organizationID);
        jwtClaims.put("sub", techAccountID);
        jwtClaims.put("exp", expirationTime);
        jwtClaims.put("aud", "https://" + AUTH_SERVER_FQDN + "/c/" + apiKey);
        for (int i = 0; i < metaContexts.length; i++) {
            jwtClaims.put("https://" + AUTH_SERVER_FQDN + "/s/" + metaContexts[i], TRUE);
        }

        // Create the final JWT token
        String jwtToken = Jwts.builder().setClaims(jwtClaims).signWith(RS256, privateKey).compact();

        System.out.println("Step 2 - getting an access token...");
        HttpHost authServer = new HttpHost(AUTH_SERVER_FQDN, 443, "https");
        HttpPost authPostRequest = new HttpPost(AUTH_ENDPOINT);
        authPostRequest.addHeader("Cache-Control", "no-cache");
        List<NameValuePair> params = new ArrayList<NameValuePair>();
        params.add(new BasicNameValuePair("client_id", apiKey));
        params.add(new BasicNameValuePair("client_secret", clientSecret));
        params.add(new BasicNameValuePair("jwt_token", jwtToken));
        authPostRequest.setEntity(new UrlEncodedFormEntity(params, Consts.UTF_8));
        HttpResponse response = httpClient.execute(authServer, authPostRequest);
        if (200 != response.getStatusLine().getStatusCode()) {
            throw new IOException("Server returned error: " + response.getStatusLine().getReasonPhrase());
        }
        HttpEntity entity = response.getEntity();
        ObjectMapper mapper = new ObjectMapper();
        JsonNode jnode = mapper.readValue(entity.getContent(), JsonNode.class);
        String accessToken = jnode.get("access_token").textValue();
        return accessToken;
    }

In line 7, the code tries to read the private key from a file. Note that because I use the Path class here, you are not limited to putting the file into the project folder.

Rather than specifying “secret.key” as the filename, you could put “~/jexner/.secret/secret.key”, or maybe “c:\Users\jexner\keys\secret.key” and the JVM would figure out where to get the file. (_Note_: those backslashes might have to be escaped, though).

Up until line 11, the code creates an RSAPrivateKey, which will later be used in the generation of the JWT.

Lines 14 to 21 are about the attributes of the JWT.

Line 24 is where those attributes are put together, and the private key is used to sign. The result is a JWT.

Lines 27 to 34 make an HTTPS request, which will be used to request an “access token” from the Adobe login infrastructure.

The sample code I found on … claims there should be a “Content-type” header, too, but for me putting that header broke the code.

Line 35 is where the request is sent.

I added rudimentary error handling on lines 36 to 38.

Lines 39 to 41 read the HTTP response and extract it into a JSON structure, and line 42 finally pulls the access token out of that structure.

Notes

Two things really got me when I started working on this, both have to do with the documentation.

Firstly, the Launch API documentation points toward the integration environment rather than the real thing. I don’t happen to have a property on the integration environment, so all API requests I sent always returned empty results.

Took a bunch of hours, and a second pair of eyes (Thanks again, Ben Robison!) to figure that one out.

Secondly, the “Content-type” header lead to my code sending a somewhat deformed request, which the API didn’t understand.

Watch out for those two!

You might have noticed that in my code, there are two “host” variables: AUTH_SERVER_FQDN and apiHostFQDN… apart from the crappy naming (“server” vs “host”), this illustrates one thing: using Adobe IO means coding against multiple endpoints.

You will always at least need to use two: the login endpoint on “ims-na1.adobelogin.com/ims/exchange/jwt/”, and whatever endpoint you want to use (in my kind, Launch on “mc-api-activation-reactor.adobe.io/properties/”).

And what’s with those “entities”?

The JWT and access token give you access to all the APIs (“entities”) that you specify when you create the JWT, provided you have the right to actually access them.

So, if you want to program against Launch, you need an entity (“ent_reactor_admin_sdk”), and you need to know what the endpoint(s) is/are that you are going to use (“mc-api-activation-reactor.adobe.io/properties/” and others).

A JWT with a list of entities is not uncommon, and that’s why “entities” is an Array.

One thought on “Accessing Adobe IO using Java

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.