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:
- Create yourself a private/public key pair
- create an integration in the adobe.io console
- upload the key into the adobe.io console
- Convert your private key to DER
- access the adobe.io authentication API to get a JWT token
- 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).
Lastly, log into Launch itself, open the property you want to use here, copy one more thing…
mvn test
You should see a result like the following
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.
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
.
My colleague Feike Visser has published an article that describes how to access the APIs on adobe.io using Postman: https://blogs.adobe.com/experiencedelivers/uncategorized/calling-api-adobe-o/
LikeLike