Transparent Auth Gateway providing an OIDC authentication from a linked Azure AD tenant and implementing OAuth 2 authorisation for client apps
A custom build Identity Server that implements OAuth 2 Authorization Code Flow with PKCE to serve other client apps as a trusted authority and perform authentication from a linked Identity Provider (a specified tenant of Azure AD).
As a bonus for the API integration with third-parties, it also implements the Client Credentials Flow.
Prerequisite: an Azure AD tenant supporting authentication.
- To support Azure Entra ID authentication, configure the
appsettings.json
(by setting parameters directly in the file or via the user secrets):AzureAd:Tenant
– the name of your tenant (e.g. contoso.onmicrosoft.com) or its tenant ID (a GUID). Sometime it's referred as the issuer
The parameter is used in forming a set of HTTP endpoints for the Identity Provider (Azure AD in our case). E.g.https://login.microsoftonline.com/{TENANT}/oauth2/v2.0/authorize
.AzureAd:ClientId
– the Application (client) ID (a GUID) of the App Registration.AzureAd:Scope
– The requested scope (also called delegated permission), for the client apps to obtain an access token. Note that all APIs must publish a minimum of one scope and this app is using just one for simplicity.
- Launch the Server.
- Try the two available end-points.
/anonymous
must return a HTTP 200 code, while/protected
gives a 401. - Pass the Swagger authentication. Either of the 2 provided options:
- The Authorization Code Flow (for users) to authenticate via the linked Azure Entra ID.
It will pop up a new tab with a bunch of redirects that brings the user to the Azure Entra ID login page and will close it on successful authentication. - The Client Credentials Flow (for API integration) to authenticate via the
client_id
andclient_secret
only.
- The Authorization Code Flow (for users) to authenticate via the linked Azure Entra ID.
- Try
/protected
end-point and it must return a HTTP 200 code.
The API has just two end-points:
/anonymous
that always returns HTTP code 200 on any request./protected
that requires user to authenticate and provide a self-issued Bearer token. Otherwise, it returns HTTP code 401 Unauthorized.
Handling the OAuth 2 is implemented with using OpenIdDict NuGet package with the key implementation is in HandleAuthorizationRequestContext
handler of its 'degraded mode' (see this article from the author of the library on detailed implementation).
The authentication client is implemented by NSwag.
From the consumer's perspective, the Authorization Code Flow looks like:
- The user gets redirected to
/authorize
route.
E.g./connect/authorize?response_type=code&client_id=TestApp&redirect_uri=https%3A%2F%2Flocalhost%3A5003%2Fswagger%2Foauth2-redirect.html&scope=openid&state={STATE}&realm=realm&code_challenge={CODE_CHALLENGE}&code_challenge_method=S256
- If a relevant user identity cookie not found,
- the user gets redirected further to the login page of the linked Identity Provider (for Azure AD it's
https://login.microsoftonline.com/{TENANT}/oauth2/v2.0/authorize
). - on successful authentication withing the tenant, the user gets redirected back to the Auth Gateway to continue the authentication/authorisation process.
- the user gets redirected further to the login page of the linked Identity Provider (for Azure AD it's
- On successful authentication/authorisation, the user gets redirected back to Swagger
/swagger/oauth2-redirect.html?code={CODE}&session_state={RANDOM_STATE}
,
whereCODE
is a reference token to the auth code stored in memory cache on the server. - Then NSwag's JavaScript exchanges the received code to an access token by running a
POST
request to/token
.
The Client Credentials Flow is simpler and involves only a single call to the /token
route. E.g.:
curl --location '/connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=client_credentials' \
--data 'scope=profile' \
--data 'client_id=TestApp' \
--data 'client_secret=test'