In the previous blog post, I talked about the OAuth client credentials flow and how to implement it with Azure Active Directory. At the end of the post, I briefly talked about the need to validate the token in either your application or an intermediary layer. In this post, we will take a look at Azure API Management as that intermediary layer.
Remember that we obtained a token for a specific resource. In this case, the resource is an Azure AD application (App Registration) that represents our API. I will call it the API app from now on. The API app has the following app id: 06b2a484-141c-42d3-9d73-32bec5910b06. In our token, the app id is in the aud (audience) claim.
To verify that our client has access rights to the API, we created an application role on the API app called invokeRole. That role should be in the roles claim of the token. If it is not, the client has no access.
We also want to pass the client application id as a header to our backend API. We can use the azp claim for this purpose. That claim will be extracted by API management and passed as a header.
To validate the API connection, we will check for both the aud and the roles claims. If the aud or the invokeRole claim is not present, we reject the call. Let’s take a look how that works.
Configuring the API in API Management
I deployed an Azure API Management instance in the Development tier (any tier will do). I created a simple API with just one GET operation that can add numbers:
At the All Operations level, the API has the following inbound policy defined:
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" require-expiration-time="false" require-signed-tokens="false"> <openid-config url="https://login.microsoftonline.com/625422dd-8ffb-45a9-9232-4132babb1324/v2.0/.well-known/openid-configuration" /> <audiences> <audience>06b2a484-141c-42d3-9d73-32bec5910b06</audience> </audiences> <required-claims> <claim name="roles" match="any"> <value>invokeRole</value> </claim> </required-claims> </validate-jwt>
The validate-jwt does what it says. It validates a JWT (JSON Web Token) passed via the HTTP Authorization header. If the validation fails, a 401 code is returned. The openid-config element sets the URL to the openid configuration of our tenant. You can browse to that URL to see its content. It is open to anyone. Information in that document is used to validate the JWT.
Note: in the openid config URL you can use the domain name of your tenant instead of the tenant ID
In the audiences section we specify we want that specific value in the aud claim. It is the app id of our API app. In the required-claims section we check that the roles claim contains the invokeRole.
Testing the API
With the validate-jwt policy present, we need a valid token to test the API. We can simply use curl to get the token:
curl -d 'grant_type=client_credentials&client_id=f1f695cb-2d00-4c0f-84a5-437282f3f3fd&client_secret=SECRET&audience=api%3A%2F%2F06b2a484-141c-42d3-9d73-32bec5910b06&scope=api%3A%2F%2F06b2a484-141c-42d3-9d73-32bec5910b06%2F.default' -X POST 'https://login.microsoftonline.com/019486dd-8ffb-45a9-9232-4132babb1324/oauth2/v2.0/token'
The result of this call is the access token. In API Management, we can use the access token to test the API:
If the token is invalid, the following response is received:
Retrieving a claim and set the value as a header
To retrieve the azp claim and set it as a header, just add the set-header policy AFTER the validate-jwt policy (in API design; all operations):
<set-header name="client" exists-action="override"> <value>@(context.Request.Headers["Authorization"].First().Split(' ').AsJwt()?.Claims["azp"].FirstOrDefault())</value> </set-header>
Oh, this is so readable! Well not really but it does extract the azp claim from the token and sets the client header to that value. When you test the API and trace the backend call, the header will be shown in the trace. It is up to the backend API to process it.
If you want, you can remove the Authorization header and not send it to the backend.
When you protect APIs with OAuth, you can perform the validation at the API Management layer. Azure API Management can do this very easily with the validate-jwt policy. You can extract claims from the policy and set them as headers so that the backend can handle them without having to know anything about OAuth. Happy coding!
2 thoughts on “Token checking at the API Management layer”
What is the use of the role? If I have a second consumer, presumably I would create a new client-appregistration for them. If they use the same backend-appregistration they would get the same Audience and Role. Or do I misunderstand and is it somehow possible to have multiple Roles within one backend-appregistration? And how is it then possible to link the roles to the separate client-appregistrations?
This post is about the client credentials grant flow to allow a daemon or a server-side process to obtain a token from Azure AD. If you have multiple processes requiring access, you can use the same app registration or create multiple ones. At the API level, you create an application role because this flow does not support user scopes (users do not connect, processes do). The role is created in order to grant your app registration (or multiple app registrations) access to the API via that role. Multiple roles could be created that provide read or write access etc… So yes, you can have multiple roles that lead to different access rights in the backend API. In this case I just called the role invokeRole as an example. Inside the backend app, this is not actually checked. You explicitly give the client app registstrations access to the roles via the UI. Also see https://blog.baeke.info/2019/08/22/using-the-oauth-client-credentials-flow/