Using ADFS with Azure API Management

Azure 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 way 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 organisation 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 straight forward. 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 colours 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 access_token_issuer

At the 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 in 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 behaviour 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 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 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 libaries 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 behaviour of the client will inevitably create compatibility issues in such a scenario.

3 comments

  1. Hello, what did you send on the Resource parameter?? I couldn’t find what to send on that field!

    I keep receiving Bearer error=”invalid_token”, error_description=”MSIS9921: Received invalid UserInfo request. Audience ‘microsoft:identityserver:d3bac6c0-b294-4369-9c38-e94cd02d1ee0’ in the access token is not same as the identifier of the UserInfo relying party trust ‘urn:microsoft:userinfo’.”

    Thanks

  2. After weeks of research as to why my AD FS setup does not work, I read your post finally got the solution I’ve been losing sleep over.

    THANK YOU!!

Leave a comment

Your email address will not be published. Required fields are marked *

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