SAML2 vs. JWT: Apigee & Azure AD Integration—A JWT Story

Blog

January 31, 2017

TABLE OF CONTENTS

Introduction

In this SAML2 vs JWT post, we are going to use a JWT with a very simple API that is proxied through Apigee Edge Public Cloud. The JWT token will be an OAuth2 access token generated by Azure Active Directory. In the last post in this series, we explored what JSON Web Tokens (JWTs) are and the information it contains. This post builds on the capabilities presented by Dino Chiesa at the Apigee “I Love APIs” conference in October, 2015. The example presented in this post is available in this github repository. This post briefly describes how to adapt the solution to working with other Azure Active Directory tenants, but primarily focuses on the details of making work with the example tenant setup here—keeping a functioning AAD tenant up and available for this example costs money and presents some security issues.

The figure below shows the system actors that will be involved in this story.

System actors involved in this story

An API Consumer will obtain an access token (a JWT) by authentication against Azure Active Directory using an OAuth2 Resource Owner Password Credentials Authorization Grant. The JWT is then placed into the Authorization header of an API request and sent to the Apigee Edge API Gateway that we’ve constructed. The API Proxy pulls down and caches the federation metadata that is published by Azure Active Directory. The API Proxy attempts to validate the JWT token included in the request; the token is determined to either be valid or not. If it is valid, then the JWT is removed from the request and passed back to the API Provider. If the token is not valid, then an error is returned to the API Consumer.

The API

First, we need an API. Let’s use https://timeapi.org. This will create a nice example that is harmless and mostly pointless (there are probably easier ways to find the time). Without the API Gateway layer, this API call will look something like:

Request:

GET https://www.timeapi.org/utc/now.json

Response:

{“dateString”:”2017–01–06T21:12:20+00:00"}

The full documentation of this API is available at https://www.timeapi.org.

There are numerous API interface documentation paradigms out there. I like to use Swagger 2.0 or OpenAPI v2.0. Apigee is built around this interface language, I work for an Apigee partner, and it is simple to setup for most APIs. I created a simple Swagger definition that describes the functionality of this API that we are using. It can be found here. Apigee has the ability to define an API Proxy based upon the structure of an existing Swagger interface definition. This can definitely eliminate some of the busy work associated with creating a new API Proxy on Apigee Edge, but it will lead to a much more complex example for what we are trying to show.

Now, our goal is to offload the authentication and authorization functions of this API onto the API Gateway layer. To do this securely, we would normally establish a trust relationship between the API Gateway and the API Provider. Unfortunately, timeapi.org is a public API that doesn’t take any steps to provide such a mechanism. So, we won’t be able to do that for this example. If we did control the API Provider, that trust relationship could be accomplished through a variety of mechanisms including, but not limited to:

  • Mutual Auth SSL
  • Basic Authentication and some type of service account that identifies the API Gateway
  • A properly scoped JWT token that describes the API Provider and is restricted through a delegation relationship in terms of who can request it

The authentication step will be the validation of a JWT token (per the spec) including:

  • Not yet expired
  • After “Not Before” timestamp
  • Digital Signature check
  • Expected audience
  • Expected issuer

The Authorization step could be quite elaborate, but for our purposes, we will rely upon the OAuth2/JWT authorization mechanisms that are provided to us in the form of the audience (“aud” parameter). As long as the this field contains the expected string, it is accepted — a very coarse grained authorization policy. The audience parameter in the JWT token, multiple roles described by its values, and a non-trivial mapping of API endpoints to roles provides the basis of a rich Role-Based Access Control (RBAC) policy. But, again, for our purposes, as long as the JWT token included with the request contains the expected audience value, the request will be considered authorized.

First, let’s construct a simple API Proxy that can proxy requests back to https://www.timeapi.org. This can be done with this tutorial.

  • In step #3, the Proxy Name is time0001, Proxy Base Path is /time0001, and Existing API is https://www.timeapi.org.
  • In step #7, also select “Add CORS header” options.
  • In step #8, deselect “default” (only using https).

Apigee

The resulting API Proxy will look something close to the API Proxy in the image above. To access this API endpoint, one must call:

https://organization-environment.apigee.net/base-path/path

If your organization name is rcbjBlueMars (see upper right-hand corner), environment is called test (see upper right-hand corner), and the base path is “/time”, then this would be:

https://rcbjbluemars-test.apigee.net/time

Since Apigee automatically does a translation of the front-end path and backend path,

https://rcbjbluemars-test.apigee.net/time/utc/now.json

will be translated to

https://www.timeapi.org/utc/now.json

To have this solution look a little more professional and have the DNS names and OAuth2 scope information match it, we will setup a DNS CNAME entry called rcbj0001-api.rcbj.net that points at rcbjbluemars-test.apigee.net. This particular domain is registered at godaddy.com. So, that is done through standard configuration mechanisms in its domain management application. Apigee Support would need to add this name to the Virtual Host configuration in Apigee Edge Public Cloud. I’m doing this in the community edition; so, this isn’t available, but in any of the paid-for versions of the platform, this can be done quite easily. Then, the API request could be sent to:

https://rcbj0001-api.rcbj.net/time001/utc/now.json

The response looks just like the original API call response above. The Apigee Trace (think of it as an API processing policy debugger) session for this request looks like the following:

Apigee Trace

Obviously, there isn’t much going on here at this point. We are going to change that.

Now, consider this presentation from the Apigee I Love APIs conference in October, 2015, describing the use of JWT with Apigee API Proxies. This presentation and accompanying code were created by Dino Chiesa who I did two webcasts with in 2016. The code that accompanies this presentation can be found here. Apigee doesn’t have out of the box support for JWT token generation or validation. But, this project contains custom Java Code that can be used to validate JWT tokens from a variety of sources (Google, Azure Active Directory, SalesForce, etc). Instructions for how to deploy the sample API Proxy are included on the GitHub repository; I’m not going to cover the build and deploy process for the sample proxy.

I extracted the conditional rule that can validate JWTs generated by Azure Active Directory. We are going to use AAD as the Identity Provider that generates JWT tokens. Azure Active Directory uses JWT as the OAuth2 access token, which works out well for our goals. The details of how an Azure AD tenant was configured to work with this tutorial can be found here. If we add the “parse + validate alg=RS256-ms” conditional rule to our sample proxy from above, we have something that looks similar to:

AAD JWT API

I added additional actions that extract the JWT token from Authorization header and, if valid, remove the Authorization header before forwarding the API request to the API Provider endpoint. CORS has also been added to this project following the instuctions outlined here.

This API Proxy can be downloaded here.

The original example from the Apigee conference requires the APigee Edge enterprise pricing plan. To get this to run in a non-Enterprise Apigee organization, a couple of tweaks had to be made to the Java code. The source code and instructions for building the modified libraries are available in the github repository.

To run this in your own environment, a couple of configuration changes must be made to the API Proxy including:

  • Update the Federation Metadata URL that is referenced in the Service Callout policy in the conditional rule called “parse + validate alg=RS256-ms”.
  • In the Java Callout Policy, the claim_iss and claim_aud properties must be updated to reflect your configuration.

For an API Consumer, we will use the following script that runs a series of curl commands. The first curl command is the call to AAD to obtain an access token (JWT); the second curl command is the call to the API.

#!/bin/bash
set -x
CLIENT_ID=80363411-f180-4a51-80ba-9b63770b9ac4
RESOURCE_URL=https://rcbj0001-api.rcbj.net
USERNAME_=test1@rcbj.net
PASSWORD_="********************"
TENANT_ID=75de389c-8f67-4084-9065-3a9c31e1db13
ASSERTION=`curl -X POST "https://login.microsoftonline.com/${TENANT_ID}/oauth2/token" -d "grant_type=password&client_id=${CLIENT_ID}&resource=${RESOURCE_URL}&username=${USERNAME_}&password=${PASSWORD_}" --insecure| awk -F"," '{print $(NF-1)}' | awk -F":" '{print $2}' | sed 's/\}//g' | sed 's/\"//g'`
if [ -z "${ASSERTION}" ];
then
  echo "ASSERTION is blank."
  exit 0
fi
echo "ASSERTION=${ASSERTION}"
echo "Making API Call."
curl -X GET "https://rcbjbluemars-test.apigee.net/time/utc/now.json" -H "Authorization:Bearer ${ASSERTION}" --insecure -D headers.out
echo
exit 0

This script does not require any arguments. If it is called test-client.sh (as it is in the github repository), then one would simply run:

./test-client.sh

If you are trying to get this to work in your own environment and your own AAD tenant, then the CLIENT_ID, USERNAME_, PASSWORD_, RESOURCE_URL, and TENANT_ID variables must be updated to match the details for your tenant.

The first curl command (OAuth2 call) response looks something like:

{
“token_type”: “Bearer”,
“scope”: “user_impersonation”,
“expires_in”: “3599”,
“ext_expires_in”: “0”,
“expires_on”: “1484020326”,
“not_before”: “1484016426”,
“resource”: “https://rcbj0001-api.rcbj.net",
“access_token”: “eyJ0eXAiOiJKV1QiLCJhb…07g”,
“refresh_token”: “AQABAAAAARNYRQ3d…IAA”
}

The access_token property is a JWT token. This can be copied directly into the Authorization HTTP Request Header (per RFC 6750) as “Bearer JWT…”.

The payload of this JWT token looks like:

{
“aud”: “https://rcbj0001-api.rcbj.net",
“iss”: “https://sts.windows.net/75de389c-8f67-4084-9065-3a9c31e1db13/",
“iat”: 1484016426,
“nbf”: 1484016426,
“exp”: 1484020326,
“acr”: “1”,
“amr”: [
        “pwd”
],
“appid”: “80363411-f180–4a51–80ba-9b63770b9ac4”,
“appidacr”: “0”,
“family_name”: “User1”,
“given_name”: “Test”,
“ipaddr”: “1.1.1.1”,
“name”: “Test User1”,
“oid”: “42cd4e91–1c39–45ec-a0cb-13361157487b”,
“platf”: “14”,
“scp”: “user_impersonation”,
“sub”: “Hd6Ymh1ICumL3_MxJcdM1LaVFlbkSCNmmo6wlG7OFDg”,
“tid”: “75de389c-8f67–4084–9065–3a9c31e1db13”,
“unique_name”: “test1@rcbj.net”,
“upn”: “test1@rcbj.net”,
“ver”: “1.0”
}

The details of what is in this token are explored in our last post.

An application such as a web application, SPA app, or Mobile App could cache the access token and refresh token. The access token can be used across API calls until it expires. The refresh token can be used to obtain a new access token—more on this later. Eventually, the user would have to log into the app again; this would be a configurable set of parameters. Applications such as these would likely use an interactive login with the OAuth2 Authorization Code Authorization Grant or Implicit Authorization Grant.

The trace session associated with running the test script will look similar to the following:

Trace Session

The first policy (colored box) in the request is a Cache Lookup Policy. This looks in an Apigee Cache called signer-cert for the cached copy of the Azure Active Directory signer certificate included in the Federation Metadata for the AAD tenant that this API Proxy trusts. Since this isn’t the first time this API Proxy has been run in the past hour, the certificate is found and assigned to a flow variable called “cached.ms.cert”. Since there was an entry in the cache, the next four policies are skipped (as indicated by the arced arrow in the upper, left-hand corner of each. Had the cached metadata not been found, the second policy (a Service Callout Policy) would have made a call out to:

‘https://login.windows.net/75de389c-8f67-4084-9065-3a9c31e1db13/FederationMetadata/2007-06/FederationMetadata.xml’

The metadata document would be written to a flow variable called “msCert”.

The third policy (which also didn’t run because the metadata was found in the cache) is a Javascript callout that removes the XML Declaration from the XML document stored in the “msCert” flow variable. The fourth policy (also didn’t run this time) is an Extract Variable policy that reads the signer certificate value from the retrieved XML document via an XPath expression and would assign the certificate value to the “ms.certificate” flow variable. The fifth policy (ran this time) is a Populate Cache Policy that writes the “ms.certificate” flow variable to the signer-cert Apigee Cache. The sixth policy (also didn’t run this time) writes the value stored in the “ms.certificate” flow variable to the signer-cert cache so that it can be efficiently retrieved later. The seventh policy, which always runs, extracts the JWT token from the Authorization header and places it in “authn.jwt” flow variable. The eighth policy is a Java Callout that runs the code from the original GitHub project and validates the JWT token. The next policy is a Raise Fault Policy that only runs if an error condition flow variable was set in the Java Callout Policy that just ran — any failure of JWT validation returns a 401. The final policy is a Javascript Callout that removes the Authorization header and the JWT token it contains.

The response processing only runs one Policy, which sets several CORS-related HTTP Response Headers.

Another way to test this API is to use Swagger Editor. Load the Time API Swagger document into the Swagger Editor by choosing File->Import File from the menu. Then, click the “Choose File”. Load the $REPOSITORY_HOME/swagger/swagger.json. Finally, click the Import button. You will see a screen similar to the following.

Import

Click the “Authentication” button.

Authentication

Run the test-client.sh script again. Use the ASSERTION variable value as the access token value that is put in this pop up’s field. Then, click the Authenticate button.

Now, scroll down to the “Try this operation” button for the GET on /{timezone}/{when}.json. The screen expands to show fields to put values in for all varibles.

Make sure the check box next to “oauth2ResourcePasswordGrant” is checked. In the “timezone” field, add a mainland US timezone (as defined in the swagger interface definition) such as “pst”. In the “when” field, add a natural language time description as defined by the chronic documentation (such as “now”). The request should look similar to the following at this point:

Request status

Click the “Send Request” button. The response will look something like the following:

Send Request Succeess

Conclusion

With that, we have covered this JWT-based authentication and authorization example end-to-end.

Authored By

Robert C. Broeckelmann Jr.

Robert C. Broeckelmann Jr.

Meet our Experts

Robert C. Broeckelmann Jr.

Robert C. Broeckelmann Jr.

Let's chat.

You're doing big things, and big things come with big challenges. We're here to help.

Read the Blog

By clicking the button below you agree to our Terms of Service and Privacy Policy.

levvel mark white

Let's improve the world together.

© Levvel & Endava 2024