Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Using ADFS With Azure API Management

DZone's Guide to

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.

· Integration Zone ·
Free Resource

SnapLogic is the leading self-service enterprise-grade integration platform. Download the 2018 GartnerMagic Quadrant for Enterprise iPaaS or play around on the platform, risk free, for 30 days.

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 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.

With SnapLogic’s integration platform you can save millions of dollars, increase integrator productivity by 5X, and reduce integration time to value by 90%. Sign up for our risk-free 30-day trial!

Topics:
integration ,api development ,api management

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}