By (alphabetically): Akinlolu Akindele, Dan Balma, Maarten Van De Bospoort, Erin Corson, Nick Drouin, Heba Elayoty, Andrei Ermilov, David Giard, Michael Green, Alfredo Chavez Hernandez, Hao Luo, Maggie Marxen, Siva Mullapudi, Nsikan Udoyen, William Zhang
Using an API gateway in front of REST APIs is a common design pattern which allows us to offload the cross-cutting capabilities such as OAuth2 authorization to the gateway instead of letting security code get scattered in application code. Azure API Management (APIM) is such an API gateway service. For the common case that the OAuth2 server in APIM is Azure AD (AAD), it has been well documented. In this document we will cover the case that the OAuth2 server is MITREid Connect instead of AAD.
MITREid Connect is an open source Identity Provider, popular in Java community. MITREid Connect is compliant to OpenID Connect and OAuth 2.0 protocol. This document is based on MITREid Connect v 1.3.3.
NOTE: This article does not cover the security between APIM and the REST API which is a separate topic. If a REST API is deployed to AKS, there are a few options which have been documented in Use Azure API Management with microservices deployed in Azure Kubernetes Service.
If API Management Premium is used, we can leverage VNET so that the security between API Management and AKS is simpler. Otherwise, we need to secure the REST calls from API Management to AKS thru technique such as mutual TLS (mTLS) (note that it is mutual TLS, not just TLS). Please see this sister document.
In order to prepare for OAuth2 setup in API Management, we need to perform the following steps in MITREid Connect:
The concepts and the steps for the above OAuth2 steps between MITREid Connect and Azure AD are not fundamentally different. You may get detailed steps from Protect an API by using OAuth 2.0 with Azure Active Directory and API Management
The following parameters from MITREid Connect must be prepared and available for our setup in API Management.
|Client ID||A string. Unlike Azure AD, a client ID in MITREid Connect does not have to be a GUID. But it needs to be unique within its tenant.|
|Client secret||Hexadecimal representation of a cryptographically secure pseudorandom number|
|OpenID config endpoint||https://[host]/.well-known/openid-configuration|
|Default Scopes||Optional, and it could have multiple values|
For Authorization grant types, you may choose to checkmark the following:
For Default scope, you can either leave it blank or enter any valid scope as defined in MITREid Connect client registration.
You can keep Resource owner username and Resource owner password as blank.
For additional details on creating OAuth2 server in API Management, please see this document.
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid."> <openid-config url="[openid-config endpoint]" /> <required-claims> <claim name="aud"> <value>[audience]</value> </claim> <claim name="iss"> <value>[issuer]<value> </claim> <claim name="scope"> <value>[scope 1]</value> <value>[scope 2]</value> </claim> </required-claims> </validate-jwt>
The openid-config endpoint is the OpenID Config URL from MITREid Connect. It is critical to make sure you have the correct URL since this is the URL thru which API Management acquires JWKS (JSON Web Key Set) for JWT validation.
The values for audience and *issuer" are from MITREid Connect and can be found from its token sample.
For *scope" claim, you can have 0, 1 or multiple scope values. Notice that if you have mutliple scope values, it means AND (all required) instead of OR (one of scope values is enough).
In OAuth2 authorization, it is typical to have multiple parts involved in the authorization flow:
|Client application||The client application can be either an app for end users or a service/server process depending on the secured REST API. In case of a client app for end users, it can be either a private client which can hide client_secret or a public client which cannot. Depending on use case, different OAuth2 authorization flows can be used. If the client is a service, a Client Credentials Flow is used. For a public end user client, Authorization Code Flow with PKCE is used such as in the OAuth2 Test Tool.|
|Identity Provider||Users get authenticated and are issued authorization code which can be used to acquire access_token.|
|Token issuer||The MITREid Connect component which issues access_tokens and refresh_tokens|
|Token introspector||The API Management component which inspects and validates JWT tokens based on policy settings, such as claims, issuer and audience|
|OpenID config endpoint||An endpoint provided by MITREid Connect from which API Management acquires public keys for token introspection. This is based on OpenID Discovery spec. API Management is never configured to hold a static public key from MITREid Connect.|
|Registered client app||A client app pointer in MITREid Connect which defines the scopes granted by either admin or users, as well as its client ID and client_secret|
|Registered server app||A server app pointer in MITREid Connect which defines an abstraction of REST API and defines the scopes|
The most common error in OAuth2 authorization is Unauthorized (401). This could be caused by any of the following:
By default, when an API is installed into API Management service, its subscription-key authorization is enabled. Make sure it is disabled since we use OAuth2 instead of subscription-key. You can find its settings under Settings tab in the API.
<cors allow-credentials="true"> <allowed-origins> <origin>http://localhost:3000</origin> </allowed-origins> <allowed-methods preflight-result-max-age="300"> <method>GET</method> <method>POST</method> <method>PATCH</method> <method>DELETE</method> <method>PUT</method> </allowed-methods> </cors>
<outbound> <base /> <set-header name="Access-Control-Allow-Origin" exists-action="override"> <value>@(context.Request.Headers.GetValueOrDefault("Origin",""))</value> </set-header> <set-header name="Access-Control-Allow-Credentials" exists-action="override"> <value>true</value> </set-header> </outbound>
Another potential issue is: for a production REST API, it has custom headers such as transaction or correlation ID for logging and troubleshooting. Such correlation ID may be scoped to the enterprise or the service itself.
More info on this restriction can be found here.
In order to allow such client to get access to custom headers (such as correlation ID), we need to add the following section in our inbound/cors section:
<allowed-headers> <header>content-type</header> <header>accept</header> <header>authorization</header> <header>x-correlation-id</header> <header>x-my-request-id</header> </allowed-headers> <expose-headers> <header>x-correlation-id</header> <header>x-my-request-id</header> </expose-headers>
Needless to say, it is desirable to have the creation and configuration automated. We could use Terraform for this purpose. Detailed document on Terraform Azuure API Management Resources can be found here.
The Terraform (.tf file) should cover the following tasks:
In addition to the variables defined in variables.tf file, the Terraform also requires a XML policy file as input. In the policy file, the following parameters (XML node/attribute values) are required.
|XML node||Node attribute||Definition|
|openid-config||url||The OpenID Config endpoint URL for public keys used in token introspection|
|claim||name (aud)||Audience defined in the registered server app in MITREid Connect|
|claim||name (iss)||The issuer of JWT tokens|
|claim||name (scope)||The list of required scopes. All of the listed scopes listed must be present in a token before it can be validated.|
|allowed-origins||List of allowed origins for CORS preflights|
The APIM inbound policy with these parameters is critical in ensuring the following to work:
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.