Securing an Azure APIm using OAuth 2.0 Client Credential Grant
There are a few methods to secure API’s on Azure’s API Management platform, and the one we are going to explore is using OAuth 2.0 Client Credential Grant. This will allow us to require an OAuth token (in the Authorization HTTP Header) on every request that is then pre-validated before the request is forwarded to the backend service.
Fortunately there is a step by step guide Protect an API by using OAuth 2.0 with Azure Active Directory and API Management by Microsoft on how to do this, and rather than repeat its steps I will explain some parts that were not overly obvious to me.
What is OAuth 2.0 Client Credential Grant?
OAuth provides for a few authentication flows, and Client Credential Grant is ideal for system to system calls that are not acting as a particular user. A good guide to the different flows is Alex Bilbie’s A Guide to OAuth 2.0 Grants.
How do I get a OAuth 2.0 Token?
You get a OAuth Token as the client application by requesting a token from the token endpoint (in our case hosted by Azure Active Directory). You pass in the Azure Active Directory Id, client id, client secret, and uniquely to Azure the resource we are trying to access (the app id of the API we are accessing). A request looks like the following (with id’s changed):
POST https://login.microsoftonline.com/2a6144c5-0aab-4aba-3ds2-3cavebd8afef/oauth2/token HTTP/1.1 Content-Type: application/x-www-form-urlencoded Host: login.microsoftonline.com Content-Length: 183 Expect: 100-continue Connection: Keep-Alive client_id=4ffassfg-asfg-4fff-b0f1-5f24a5456vca&client_secret=rdMjDRagvadfGasffttjWMg2fgqZgaaahfEg9zCVFEM%3D&grant_type=client_credentials&resource=ef12ba51-a14a-4a3f-af34-57b4945773b6
First I tried to request a token using Postman however it’s built in UI for getting token’s does not allow for the resource parameter, and the token I received was for audience 00000002-0000-0000-c000-000000000000 which is the Graph API.
To request a token from code I spent sometime looking to use an existing library that support OAuth 2.0, however I found them overly complex as they handle all flows. So I have created the following class instead:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CityWorksApiTest { public class AzureApiSecurityHandler : DelegatingHandler { private String clientId = ConfigurationManager.AppSettings["CleintId"]; private String clientSecret = ConfigurationManager.AppSettings["ClientSecret"]; private String accessingResource = ConfigurationManager.AppSettings["ApiResourceId"]; private String subscriptionKey = ConfigurationManager.AppSettings["SubscriptionKey"]; private String tokenEndpointUrl = ConfigurationManager.AppSettings["TokenEndpointUrl"]; private HttpClient tokenClient = new HttpClient(); private String currentToken = null; public AzureApiSecurityHandler(HttpMessageHandler innerHandler) : base(innerHandler) { } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { request.Headers.Add("Authorization", GetTokenAsync().Result); request.Headers.Add("Ocp-Apim-Subscription-Key", subscriptionKey); var response = await base.SendAsync(request, cancellationToken); // Token may have expired, fetch a new one on first 401 if (response.StatusCode == HttpStatusCode.Unauthorized) { currentToken = null; request.Headers.Remove("Authorization"); request.Headers.Add("Authorization", GetTokenAsync().Result); return await base.SendAsync(request, cancellationToken); } return response; } protected async Task<string> GetTokenAsync() { if (currentToken != null) { return currentToken; } var request = new HttpRequestMessage(HttpMethod.Post, tokenEndpointUrl); request.Content = new FormUrlEncodedContent(new Dictionary<string, string> { { "client_id", clientId }, { "client_secret", clientSecret }, { "grant_type", "client_credentials" }, { "resource", accessingResource } }); var response = await tokenClient.SendAsync(request); response.EnsureSuccessStatusCode(); var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); currentToken = payload.Value<string>("access_token"); return currentToken; } } }
What does an OAuth Token Look Like?
Once you have a token you can view it using jwt.io, simply paste it into the site.
{ "aud": "ef12ba51-a14a-4a3f-af34-57b4945773b6", "iss": "https://sts.windows.net/XXX/", "iat": 1536716908, "nbf": 1536716908, "exp": 1536720808, "aio": "XXX", "appid": "4ffassfg-asfg-4fff-b0f1-5f24a5456vca", "appidacr": "1", "idp": "https://sts.windows.net/XXX/", "oid": "XXX", "roles": [ "Api.Invoke.All" ], "sub": "XXX", "tid": "XXX", "uti": "XXX", "ver": "1.0" }
- “aud’ is the audience, and is the application id of the API we are accessing. This comes from the resource parameter on the request.
- “appid” is the application id of the client, and comes from the client_id parameter.
- “roles” are the permissions the client has been granted.
The token also includes a signature, and the part I found interesting during my research was that the server can cache the signing certificate meaning that when validating each request it will not always call back to the token issuer.
What are Azure Active Directory Applications?
- The API
- The Developer Portal client
- My client application
"appRoles":[ { "allowedMemberTypes":[ "Application", ], "displayName":"TestApi.Invoke.All", "id":"1b4f816e-5eaf-48b9-8613-7923830595ad", "isEnabled":true, "description":"Invoke all operations", "value":"TestApi.Invoke.All" } ],
A good guide on how to do this is Defining permission scopes and roles offered by an app in Azure AD.
I have then granted the new permissions to the client applications.
How is the OAuth Token Pre-Authorised?
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid."> <!-- URL contains the aad tennant name --> <openid-config url="https://login.microsoftonline.com/AAD_TENNANT_NAME/.well-known/openid-configuration" /> <audiences> <audience>{{ApiAppId}}</audience> </audiences> <required-claims> <claim name="roles" match="any" separator=""> <value>Api.Invoke.All</value> </claim> </required-claims> </validate-jwt>
- Specify the configuration url by adding your Active Directory tenant name
- Specify the Application Id of the API. I used a Named value as it will be different in each environment.
- Specify the required role(s).
How do the clients know how to get a token?
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid."> <!-- URL contains the aad tennant name --> <openid-config url="https://login.microsoftonline.com/AAD_TENNANT_NAME/.well-known/openid-configuration" /> <audiences> <audience>{{ApiAppId}}</audience> </audiences> <required-claims> <claim name="roles" match="any" separator=""> <value>Api.Invoke.All</value> </claim> </required-claims> </validate-jwt>
Categories
- Automation (2)
- Case Study (2)
- Cloud (7)
- Cloud Integration (6)
- Cloud Lift and Shift (1)
- Cloud Redevelopment (2)
- Cloud Replatform (2)
- Cloud Security (1)
- Cloud Stategy (3)
- Design (2)