Using ADFS With Azure API Management
A DZone MVB explores some issues he ran into while trying to use these two technologies to create an API and push it online.
Join the DZone community and get the full member experience.
Join For FreeAzure API Management is an API gateway that can be used to publish APIs to the Internet. It provides features such as per-developer API keys, request throttling, and request authentication. One of the ways requests can be authenticated is through standard OAuth2 bearer tokens. I assume that the most common scenario is to use Azure AD to issue those tokens. But if an organization is not that cloud-enabled yet and the users are in an on-prem AD, the natural token issuer is to use ADFS. And ADFS on Windows Server 2016 supports OpenID Connect, so it should work, right?
Well, it turns out it didn't just work. The OpenID Connect implementation in ADFS has some quirks that need to be handled. In the end, it worked, but with some limitations.
Issuer and Access Token Issuer
One of the neat things with OpenID Connect is that it provides a metadata-based convention for configuration. There's no need to download and handle certificates to register signing keys, it generally just works. Until it doesn't. Which was the case here.
First, the configuration in the Azure API Management Policy was fairly straightforward. The policy checks that a matched query string parameter colour
from the public facing URL is also present as a claim. This carries all the way to the active directory user object, where the "other pager" field was used to list the colors that a certain user is allowed to use in the URL to the API.
<policies>
<inbound>
<validate-jwt header-name="Authorization">
<openid-config url="https://adfs.example.org/adfs/.well-known/openid-configuration" />
<required-claims>
<claim name="colour" match="all">
<value>@((string)context.Request.MatchedParameters["colour"])</value>
</claim>
</required-claims>
</validate-jwt>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
This code turned out to work in the end, after some workarounds had been applied.
ADFS, Audiences, and the Resource Parameter
The first problem was obvious when I used jwt.io to inspect the access token I received from the ADFS. It didn't contain the requested colours
scope and didn't contain the colours
claims.
{
"aud": "urn:microsoft:userinfo",
"iss": "https://adfs.example.org/adfs/services/trust",
"iat": 1511714437,
"exp": 1511718037,
"apptype": "Public",
"appid": "8059f5ed-fa9b-4165-815c-663dec49b965",
"authmethod": "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified",
"auth_time": "2017-11-26T16:40:36.000Z",
"ver": "1.0",
"scp": "openid",
"sub": "r5PvRoaOXFmaJ+q6LyeVslYVXZl38F/UkrBvQNlyoY8="
}
Apparently, ADFS has added a non-standard parameter resource
that must be supplied in the token request to get an access token aimed for an API. The default access token as returned above is only meant for the user info endpoint on the ADFS server. With a resource parameter added, I got a better access token. It now includes the colours
scope and the ADFS issuance transform rules for the Web API now kicks in and includes the colour
claim in the access token. Note that it now also has a different audience - the identifier of the API.
{
"aud": "https://my-example-api.azure-api.net/colours",
"iss": "https://adfs.example.org/adfs/services/trust",
"iat": 1511718387,
"exp": 1511721987,
"colour": "Blue",
"apptype": "Public",
"appid": "8059f5ed-fa9b-4165-815c-663dec49b965",
"authmethod": "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified",
"auth_time": "2017-11-26T17:46:24.000Z",
"ver": "1.0",
"scp": "colours"
}
So it turns out that ADFS is issuing different access tokens for different APIs and the way to request an access token for a specific API is to use the non-standard resource
parameter. I'll write some more on this in another post.
For now, just let's get on with the work and try to use the access token to access the API.
ADFS and acces_token_issuer
During first try with my new access token, things just didn't work. Finding out why wasn't obvious. I copied the access token I had got in my client application into the API test tools in the Azure portal to get a trace. The trace just reveals that the JWT validation failed. To get the actual JWT validation error, one has to follow the link that's listed in the trace. In that log, the error message is clear (kudos to the Microsoft dev who decided to include the actual values in the exception message).
message: "JWT Validation Failed: IDX10205: Issuer validation failed. Issuer: 'http://adfs.example.org/adfs/services/trust'. Did not match: validationParameters.ValidIssuer: '' or validationParameters.ValidIssuers: 'https://adfs.example.org/adfs'
Now, what happened here? Looks like the iss
field of the JWT doesn't match the one listed in the OpenID Connect configuration information. Looking at the ADFS OpenID Connect configuration information available at https://adfs.example.org/adfs/.well-known/openid-configuration
showed another non-standard OpenID Connect behavior of ADFS. At the top, an issuer
value of https://adfs.example.org/adfs
is shown. This also corresponds to the OpenID Connect standard that the configuration document path is formed by concatenating the issuer
URL with /.well-known/openid-configuration
. The id_token correctly contains https://adfs.example.org/adfs
as the issuer. However, that's not the issuer found in the access token. The access token uses the ID I've set up in ADFS as the Federation Service Identifier. That's the value used as the Entity ID in SAML-based tokens. And that identifier is actually present in the metadata in the non-standard field access_token_issuer
.
{
"issuer": "https://adfs.example.org/adfs",
"authorization_endpoint": "https://adfs.example.org/adfs/oauth2/authorize/",
"token_endpoint": "https://adfs.example.org/adfs/oauth2/token/",
"jwks_uri": "https://adfs.example.org/adfs/discovery/keys",
"token_endpoint_auth_methods_supported": [
"client_secret_post",
"client_secret_basic",
"private_key_jwt",
"windows_client_authentication"
],
"response_types_supported": [
"code",
"id_token",
"code id_token",
"id_token token",
"code token",
"code id_token token"
],
"response_modes_supported": [
"query",
"fragment",
"form_post"
],
"grant_types_supported": [
"authorization_code",
"refresh_token",
"client_credentials",
"urn:ietf:params:oauth:grant-type:jwt-bearer",
"implicit",
"password",
"srv_challenge"
],
"subject_types_supported": [
"pairwise"
],
"scopes_supported": [
"aza",
"logon_cert",
"user_impersonation",
"winhello_cert",
"profile",
"email",
"allatclaims",
"vpn_cert",
"openid",
"colours"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"token_endpoint_auth_signing_alg_values_supported": [
"RS256"
],
"access_token_issuer": "http://adfs.example.org/adfs/services/trust",
"claims_supported": [
"aud",
"iss",
"iat",
"exp",
"auth_time",
"nonce",
"at_hash",
"c_hash",
"sub",
"upn",
"unique_name",
"pwd_url",
"pwd_exp",
"sid"
],
"microsoft_multi_refresh_token": true,
"userinfo_endpoint": "https://adfs.example.org/adfs/userinfo",
"capabilities": [],
"end_session_endpoint": "https://adfs.example.org/adfs/oauth2/logout",
"as_access_token_token_binding_supported": true,
"as_refresh_token_token_binding_supported": true,
"resource_access_token_token_binding_supported": true,
"op_id_token_token_binding_supported": true,
"rp_id_token_token_binding_supported": true,
"frontchannel_logout_supported": true,
"frontchannel_logout_session_supported": true
}
Apparently, not even Microsoft's own API Management platform knows about that field. So the incoming access token is rejected by Azure API Management due to issuer names not matching. First, I tried to solve that by manually adding the access token issuer value in the API Management policy, but I never got it working (I think it was something with an incorrect trailing space added). Instead, I went back and renamed my ADFS server, so that the Federation Service Identifier now is https://adfs.example.org/adfs
(the ADFS service needs a restart for the rename to take effect). That gives a new OpenID Connect configuration document where the issuer
and access_token_issuer
fields are the same.
Finally, my JWT validation works.
Solving this by renaming the ADFS server identifier is nothing that can easily be done in an existing federation setup. All other applications (relying parties) and upstream identity providers (claims providers) must, of course, be updated with the new federation service ID.
Conclusion
Using ADFS as an OAuth2 token issuer for Azure API Management kind of works. A workaround is required to handle the issuer
vs. access_token_issuer
issue. In a fresh ADFS setup that's possible through a rename. In an existing environment probably not.
What's more severe is that to get the access token the extra resource
parameter must be added. Microsoft's OpenID Connect handler for ASP.NET Core 2 supports that and their ADAL.js library for JavaScript does. But other standard-conforming libraries such as Brock Allen's oidc-client.js or libraries for non-.NET server-side applications won't work. And the entire purpose of the API Management platform is to publish APIs on the Internet for other developers to use. Using an OpenID Connect Provider that requires non-standard behavior of the client will inevitably create compatibility issues in such a scenario.
Published at DZone with permission of Anders Abel, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments