A few months ago, I gave an overview of the libraries I use to implement OpenID Connect implicit flow in an angular app, and On-Behalf-Of (OBO) flow in ASP.NET core backend APIs. You can checkout this post for more info. In that post, I talk about the security flow from the angular app to the downstream APIs. The angular app communicates only with a single backend API which acts as a gateway that forwards the requests from to other downstream APIs.
In this post, I go over the details of obtaining an access token via the OBO flow to call protected endpoints from a web API (which I refer to as the gateway in this post) to another web API .
To call the gateway, the angular app first obtains an access token via the implicit flow. The token contains claims which state what kind of resources the client (angular) can access on behalf of the user. However, the API gateway is not a resource owner. The gateway simply calls out to the downstream API to retrieve the data for the user. Therefore, we need a way to pass over the user’s claims to the downstream API which decide whether to authorize the access. One approach is to merely pass the same access token we get from the angular app to the downstream API. However, for security, we may not intend for the downstream API to be accessible by other services besides the gateway. In addition, the intended audience of the access token coming from the angular app is the gateway, not the downstream API. Another approach is using client credentials flow and specify the resources the gateway API can access. However, with client credentials flow, we lose trace of the original user. We also can’t allow the user to grant permissions only for specific resources because the client credentials flow does not involve the user’s interactions.
How can we authorize access from the gateway to the downstream API without losing trace of the user? That’s what the OBO flow is for.
The OAuth 2.0 On-Behalf-Of flow (OBO) serves the use case where an application invokes a service/web API, which in turn needs to call another service/web API. The idea is to propagate the delegated user identity and permissions through the request chain.
Microsoft identity platform and OAuth 2.0 On-Behalf-Of flow
One caveat is if your app authenticates the user against Azure ADB2C, you may not be able to use the OBO flow as Azure ADB2C currently does not support the OBO flow. In that case, you may consider the OAuth2 Client Credentials flow to secure access between the APIs.
Web API chains (On-Behalf-Of) is not supported by Azure AD B2C. – Many architectures include a web API that needs to call another downstream web API, both secured by Azure AD B2C. This scenario is common in clients that have a web API back end, which in turn calls a another service. This chained web API scenario can be supported by using the OAuth 2.0 JWT Bearer Credential grant, otherwise known as the On-Behalf-Of flow. However, the On-Behalf-Of flow is not currently implemented in Azure AD B2C
Request an access token in Azure Active Directory B2C
As an example, consider the two access tokens below. The first token authorizes the angular client to call the gateway API. In the first token, the aud value refers to the client id of the gateway, and the app id refers to the client id of the angular client. In the second token, the aud value refers to the client id of the downstream API, and the app id is the client id of the gateway.
Access token #1 obtained via implicit flow.
// some keys have been omitted for brevity.
{
"aud": "api://5346846f-e97f-4b84-9c18-5a86eba8c1b5", "appid": "98765432-2b08-43a7-9b0f-1234567891011", "family_name": "John", "given_name": "Doe", "name": "John Doe", "scp": "AccessAsUser", "sub": "123456789VMFNkJBjrXvYfWdWydYgSWBzQb512345", "unique_name": "johndoe@email.com", "upn": "johndoe@email.com" }
Access token #2 obtained via OBO flow.
{ "aud": "api://cca42534-1b08-47ae-9995-e381a16a1762", "appid": "5346846f-e97f-4b84-9c18-5a86eba8c1b5", "family_name": "John", "given_name": "Doe", "name": "John Doe", "scp": "AccessAsUser", "sub": "FMQrNj9BDoFdvl89UjQbmMjMmecBsnkk80a51zpbeAE", "unique_name": "johndoe@email.com", "upn": "johndoe@email.com" }
Because the user’s identity and permissions are the same but the aud values are different, we can grant access to the resources at the downstream API based on the user’s permissions, while at the time ensure that angular client cannot call the downstream API directly.
The document provides good details on how to construct a HTTP POST request to obtain an access token via OBO flow. Below I show the request and indicate the parameters as relating to this post.
POST /oauth2/v2.0/token HTTP/1.1 Host: login.microsoftonline.com Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer &client_id={Replace with client id of API gateway} &client_secret={Replace with the secret of the API gateway app. } &assertion={The original access token coming from the angular app} &scope={app id of downstream API}/{scope name} &requested_token_use=on_behalf_of
First, you need to register all the applications that involve in the security model. For instance, in the sample project i have for this post, I register three applications in Azure AD: the angular client, the API gateway and the downstream API.
Part of the application’s registration/setup is to define the permissions. For instance, when OBO-Angular-Client requests an access token from Microsoft Identity Platform (v2.0 endpoint) to talk to OBO-Gateway, the angular app needs to specify the scopes under OBO-Gateway that it wants to access .
In Angular-Client, I list the permissions the angular app needs. One of the permissions is the scope defined in OBO-Gateway.
When you register the applications in Azure AD, pay attention to the permissions between the applications. In your program, you can selectively ask the user to grant only the permissions your app needs at a moment to complete the user’s request. However, under API permissions, you must list all the permissions your app may ever need. If you specify a permission/scope that is not in the list of API permissions, you may get an error.
The sample snippets below show the high level steps of obtaining an access token via OBO flow, given a prior access token obtained by a client app via the implicit or authorization flow.
public static IApplicationBuilder UseProxy(this IApplicationBuilder app, IConfiguration configuration) { // base url of the downstream API to where the request is forwarded. var baseUrl = configuration.GetValue<string>("DownstreamAPI:BaseUrl"); // function that extracts out the access token coming from // angular application, obtains another access token for the // gateway via OBO flow, for accessing the downstream API, and // forward the request to the API. Func<HttpContext, string, string[], Task<HttpResponseMessage>> callDownstreamAPI = async (context, apiBaseUrl, scopes) => { // if we don't have the token from the client, then throw 401. if (!context.User.Identity.IsAuthenticated) { return new HttpResponseMessage(HttpStatusCode.Unauthorized); } StringValues userAccessToken = new StringValues(); context.Request.Headers.TryGetValue("Authorization", out userAccessToken); UserAssertion userAssertion; // see Microsoft documentation on constructing a OBO request: // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example userAssertion = new UserAssertion(userAccessToken.ToString().Replace("Bearer ", ""), "urn:ietf:params:oauth:grant-type:jwt-bearer"); // AzureADAppCredentials contains the service credentials // (client id/secret) of the gateway as registered in Azure AD, // and other OpenIDConnect configurations. var appOptions = configuration.GetSection("AzureADAppCredentials").Get<ConfidentialClientApplicationOptions>(); string authority = $"{appOptions.Instance}{appOptions.TenantId}/"; var application = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(appOptions).WithAuthority(authority).Build(); // requires Microsoft.Identity.Client and
// Microsoft.Identity.Client.Extensions.Web.Resource
// packages. var result = await application.AcquireTokenOnBehalfOf(scopes, userAssertion).ExecuteAsync(); string accessToken = result.AccessToken; context.Request.Headers.Set("Authorization", $"Bearer {accessToken}"); return await context .ForwardTo(apiBaseUrl) .AddXForwardedHeaders() .Send(); }; string apiAppId = "Replace with the app id of the downstream " + "API you registered in Azure AD, under Expose an API"; string scopeName = "Replace with the scope name as specified under " + "Expose an API section."; HandleProxyRequest downstreamAPIRequest = context => { return callDownstreamAPI(context, baseUrl, new string[] { $"api://{apiAppId}/{scopeName}" }); }; app.Map("/api", api => { api.RunProxy(downstreamAPIRequest); }); return app; }
In the above snippets, I use the library called ProxyKit to simply forward the request coming from the angular application to the downstream API, after obtaining the new access token via the OBO flow. As a side now, if you need to forward a request without changing the payload, you should use a library such as ProxyKit so that you don’t have to write codes to construct a HTTP request, serialize or deserialize between objects and json strings.
When I implement the OBO flow, I consulted the Azure-Samples repo on GitHub. You can check it out at https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2/tree/master/Microsoft.Identity.Web.
Microsoft identity platform and OAuth 2.0 On-Behalf-Of flow
Microsoft Identity Web library
Gaining consent for the middle-tier application
Delegation Patterns for OAuth2.0
Understanding azure ads on behalf of flow aka obo flowBuilding a fully multitenant system using Microsoft Identity Framework and SQL Row Level Security
Cache angular components using RouteReuseStrategy
Using MSAL angular to authenticate a user against azure ADB2C via authorization code flow with Proof Key for Code Exchange.
Displaying text in a tool tip when text overflows its container
Azure AD authentication in angular using MSAL angular v2 library
Common frameworks, libraries and design patterns I use
Rendering a PDF in angular that works with mobile browsers using ng2-pdf-viewer
Differences between a Promise and an Observable