Deploying a real-world application with components secured by Azure Active Directory (AAD), a point of confusion is how many AAD applications to register. Many blog posts focus on simple scenarios and unless one has a deep understanding of the OAuth2 and OpenID Connect standards, the Azure Portal's App registration page can be confusing.
From a real-world use case, this post provides guidance on the number of app registrations required to support a secure architecture. It then details how the use case affects the choice of client authentication library and the parts of OAuth2 and OpenID Connect at work to support it.
Consider the following application implemented as a distributed set of components: (1) an Angular app making secured requests to a custom REST service, and (2) a native iOS app also making secured requests to the same REST service, and (3) an ASP.NET Web API with which the clients interact. All components are secured by AAD, meaning that sign-in to (1) and (2) happens against AAD and with AAD users and that (3) receives signed tokens issues by AAD to (1) and (2), and passed along with calls to (3).
Reading along, keep in mind that OAuth2 is an authorization standard. It's OpenID Connect, a thin layer on top of OAuth2, which defines authentication. In other words, an OAuth2 access token is passed along from (1) and (2) to (3) and authorizes that (1) and (2) are allowed to access (3) on the user's behalf and only with the permissions consented to. The OpenID Connect token, a special kind of access token, is what authenticates a user to (1) and (2). It holds username, full name, Active Directory group memberships, and so on, and is never passed along to (3).
Inside AAD, user-agent-based application maps to "Web app/API" with oauth2AllowImplicitFlow turned on in the registration manifest. The meaning of turning on oauth2AllowImplicitFlow is that implicit flow is added to the already supported flows and that because AAD knows the app requesting access to an endpoint, AAD allows implicit flow for whatever endpoint the app requests access to. For iOS and Web API, oauth2AllowImplicitFlow is left turned off.
From these definitions, it's clear that the iOS app maps to "Native" and that both Angular and Web API maps to "Web app/API". But does that mean that Angular and Web API should run under the same registration?
Perhaps in the name of brevity, many blog posts share the app registration between Web API and Angular app. But for anything but the simplest use cases, reusing an app registration is at best a shortcut to avoid an additional app registration and at worst a potential security risk:
Almost always should the Angular app have its own app registration. Only in a small, contained Angular app/Web API use case where both are designed to only work with each other as a single unit, without integrating with other APIs, is sharing a registration a viable option.
Authorization on a per-application basis doesn't apply with only one app registration for the Angular app and Web API. With both assigned the same permission scope, to a downstream API such as Graph, the Angular app and Web API share the same access rights and are indistinguishable.
An Angular app may be consuming multiple endpoints: our Web API, Graph, Delve, SharePoint, and Exchange. It isn't tied to one API, except perhaps for the convenience of not having to create an additional app registration. While the extra app registration may come with a small mental cost, its monetary is cost. There's no need to be frugal with app registrations.
App registration metadata differ between an Angular app and a Web API. Maybe not in a significant way at first, but in time they're likely to drift further apart. At that point, we must either split the registration, maybe forcing users to re-consent, or make the single registration contain the settings of both, if possible.
By design, an Angular app requires the implicit grant type (of authorization flow or method to acquire an access token) whereas the Web API doesn't. An Angular app also requires reply URLs to be setup as part of the registration (to avoid a rogue app requesting a token by passing a non-whitelisted reply URL to the authorization server) whereas a Web API doesn't, and over time an Angular app will likely access a different set of APIs than any one Web API. Thus, an Angular app have registration settings specific to it, meaning it should have its own app registration.
To support our use case, we're forced into using ADAL.js over MSAL.js as client authorization library (through a wrapper as with this example). To quote Microsoft's comparison of v1.0 and v2.0 endpoint capabilities:
You can use the v2.0 endpoint [with MSAL.js, not ADAL.js] to build a Web API that is secured with OAuth 2.0. However, that Web API can receive tokens only from an application that has the same Application ID. You cannot access a Web API from a client that has a different Application ID. The client won't be able to request or obtain permissions to your Web API.
It's an inherent limitation in Microsoft's OAuth2 implementation rather than the OAuth2 standard itself. In our case, we have separate app registrations for iOS and Angular, causing their application IDs to differ from that of the Web API.
Despite known security concerns, ADAL.js implements authorization using the implicit grant type, causing the access token to be transmitted on the less secure front channel and stored in the browser. The implicit grant type also doesn't use a refresh token for access token renewal. ADAL.js periodically requests a new access from the authorization endpoint by including in the request a session cookie previously returned by the authorization server. For as long as the session cookie is valid, ADAL.js can request new access tokens.
Compare the implicit grant type to the authorization code grant type used by the iOS app (implemented by ADAL for Objective C). Only the short-lived authorization code grant is transmitted on the front channel via a browser control. ADAL registers a custom protocol handler (like http or ftp, but app specific and matching the name setup with AAD) making the app the default for URLs with that protocol. When the authorization server redirects the browser to myapp://..., the app receives the code as part of the URL. The app then switches to using the more secure back channel to connect to the authorization server's token endpoint to exchange the short lived, single use authorization code, or refresh token, for the access and refresh tokens.
Like with the implicit grant, because the authorization code grant originates from a public client (see OAuth2 client types), not even on the back channel does the client authenticate with the authorization server. Any client in possession of the code can exchange it for access and refresh tokens. Client authentication would involve submitting with the code and client ID a previously agreed upon client secret. But since anyone would be able to discover the secret of a public client, and it cannot easily be changed after deployment, public client authentication is meaningless. Only confidential clients, code running on a server, authenticate using a secret shared between themselves and the authorization server.
To convince ourselves that iOS is using the authorization code grant type, we can turn on diagnostics logging to see the custom protocol URL and authorization code and its conversion to tokens. Otherwise, we'd only know that implicit grant type couldn't have been used because oauth2AllowImplicitFlow is turned off with the iOS app registration.
With our use case, the Web API is solely a receiver of access tokens from the Angular and iOS apps. It doesn't make call to any AAD secured endpoint, but if it had, it would do so using the client credentials grant type.
As the receiver of an access token, the Web APIs is responsible for validating both the token's claims and its signature. Each token consists of a header, payload, and signature. From the payload, the issuer (iss) claim must be AAD as defined in the OpenID Connect federation metadata document and the audience (aud) claim, the intended recipient, must be Web API. As for the token's lifetime, the not before (nbf) and expiration time (exp) claims must be valid.
Signature validation must adhere to AAD's key rotation policy stating that
At any given point in time, Azure AD may sign an id_token [the OpenID Connect token or an access token] using any one of a certain set of public-private key pairs. Azure AD rotates the possible set of keys on a periodic basis, so your app should be written to handle those key changes automatically. A reasonable frequency to check for updates to the public keys used by Azure AD is every 24 hours.
Within the metadata document mentioned above, jwks_uri point to the location of the JSON Web Key Set, the public keys to verify the signature. Web API must use the header's algorithm (alg) and Key ID (kid) claims to locate the public key and perform validation. For all the X509 certificate details, we can paste the value of each x5c into an online decoder.
Have a look at this post on how to validate tokens with ASP.NET Core. While the post shows how to validate tokens, it doesn't show how to reload metadata on a period basis. For that we either require a periodically executing method or perhaps the Web API is already using Hangfire. Otherwise, we risk the Web API failing token validation following a key rotation and before the app pool is recycled. Even worse, validation failure will appear sporadic and will eventually disappear without intervention.
With on-premise Web API hosting, we can rely on idle and periodic app pool restarts to keep metadata up to date. Although by default the app pool recycles at least every 29 hours, exceeding AAD's 24 hour policy, both periods are configurable. With Azure App Service, however, running a site in "Always on" mode prevents idle restarts and periodic restarts isn't an IIS but a WAS feature, unavailable in Azure.
An IT pros guide to Open ID Connect, OAuth 2.0 with the V1 and V2 Azure Active Directory endpoints. An overview talk focused on AAD and how it related to OAuth2 and Open ID Connect. At around 28m50s, OAuth2 and AAD terminology is compared: the authorization server is AAD, client (Angular app) is an app in AAD, resource server (Web API) is an app in AAD, and resource owner is a user in AAD.
OAuth 2.0 and OpenID Connect (in plain English). Covers the history and use cases leading up to the OAuth2.0 and OpenID Connect standards and explains in-depth the URL components that go into the common grant types. With OAuth 2.0 allowing for implementation dependent choices, a non-Microsoft view helps better understand how the standards and AAD concepts work together.
To support the diverse needs of our three components, now and in the future, we ended up with three app registrations. Besides details of registration, to fully understand what's going on as part of app authentication and authorization, relevant parts of OAuth2 and OpenID Connect standards were outlined. Without such knowledge, debugging amounts to time consuming guess work.