Below is the key information and diagrams to help you understand the authentication steps for the APIs.

The API are hosted on the API Gateway. As such, an API can only be accessed by users which have been added to the API Gateway Azure AD.

This document assumes that the registration and on-boarding process has been successfully completed for the environment which the consumer wants to connect to.

The usual scenario is where the user connects to their application calling the Placing API using a browser and logs in via the SSO. The application passes the unauthenticated user to a login page, via a series of redirects and ends up with an access token issued by the Azure AD tenant. This access token contains a number of claims about the user, but as a minimum has an email address. (Service accounts may have a dummy email address).

If there is a scope claim which includes “user_impersonation”, then it may be passed directly to the API Gateway. If not (and the API requires that scope) then the application needs to obtain an on-behalf-of token from Azure AD which will then include that scope.

Where APIs validate the audience claim of a token, then the gateway may also need to obtain an on-behalf-of token with the correct audience set.

The application will sit within the Azure AD tenant of the API Gateway and thus is treated as a trusted party.

Any token issued by the API Gateway must not have a TTL longer than that of the token which called it. In most cases, setting the not-before (‘nbf’) date to the current time and the expiry date (‘exp) to that of the original token will be sufficient.

The sequence diagram below shows the flow of control and messages between the browser, an client application, the Azure AD tenant as well as the API Gateway and the API which the browser is attempting to connect to. While this shows the entire flow in detail, the next two diagrams focus more on the actions which happen in the two scenarios:

  • Calling the API with an application user token
  • Calling the API with a service account token

Calling API with Application User Token

This is a slightly simplified version of the sequence of a web app calling an API through the API Gateway.

The most important point to understand is that the JWT that is returned when the user logs-in is not the JWT that is sent to the API-GW

The application must request a second JWT on behalf of the logged-in user. The request goes directly from the app backend to Azure Active Directory and the JWT comes back in the response.

Calling API with Service Account Token

It is important to note than in this case, there is usually no browser involved and the communication happens between two servers (headless).

Authentication Implementation

e steps involved in creating an app which allows a user to log in and make a call to a protected API using the on-behalf-of flow explained above.
ode of this sample application from which these samples have been taken can be found on GitHub.

Create a new project in Visual Studio

At the time of writing this document, the latest version of Dotnet Core is 3.1. The SDK was compatible with Visual Studio 2019 onwards.

Select the ‘ASP.NET Core Web Application’ template on the ‘Create a new project’ screen.

Give the project a suitable name and choose the location where it is created.

On the subsequent screen, make sure ‘.NET Core’ is selected from the frameworks dropdown list and ‘ASP.NET Core 3.1’ as the runtime version.

Select the ‘Web Application (Model-View-Controller)’ from the list of project types and click ‘Create’.

Add the following code in ‘appsettings.json’ or ‘appsettings<Environment>.json’

Note: Store this sensitive information in safe storage – For more info about safe storage refer to this Microsoft documentation page on App Secrets.

{
  "Authentication": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "< Insert Tenant Id of SAND, PreProd or Prod Common Service >",
    "ClientId": "< Client Id >",
    "CallbackPath": "/signin-oidc",
    "ResponseType": "code id_token",
    "ClientSecret": "< Client secret >",
    "Resource": "< API Gateway resource URL path >"
  }
}

The missing values from the above authentication configuration settings will have been provided to you as part of the on-boarding process.

Next, update the ‘ConfigureServices’ method in ‘Startup.cs’ to the following:

                        	
								public void ConfigureServices(IServiceCollection services) {
									services.AddMemoryCache();

									services.Configure  (options =>{
										// This lambda determines whether user consent for non-essential cookies is needed for a given request.
										options.CheckConsentNeeded = context =>true;
										options.MinimumSameSitePolicy = SameSiteMode.None;
									});

									services.AddAuthentication(auth =>{
										auth.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
										auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
										auth.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
									}).AddCookie().AddOpenIdConnect(opts =>{
										Configuration.GetSection("Authentication").Bind(opts);
										opts.SaveTokens = true;
										opts.Events = new OpenIdConnectEvents {
											OnAuthorizationCodeReceived = async ctx =>{
												var request = ctx.HttpContext.Request;
												var currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path);

												//Credentials for app itself
												var credential = new ClientCredential(ctx.Options.ClientId, ctx.Options.ClientSecret);

												//Construct token cache
												var cacheFactory = ctx.HttpContext.RequestServices.GetRequiredService < ITokenCacheFactory > ();
												var cache = cacheFactory.CreateForUser(ctx.Principal);
												var authContext = new AuthenticationContext(ctx.Options.Authority, cache);

												var resource = opts.Resource;
												var result = await authContext.AcquireTokenByAuthorizationCodeAsync(
												ctx.ProtocolMessage.Code, new Uri(currentUri), credential, resource);

												ctx.HandleCodeRedemption(result.AccessToken, result.IdToken);
											},
											OnTicketReceived = async ctx =>{
												var authenticationProperties = ctx.Properties;
												var accessToken = authenticationProperties.GetTokenValue("access_token");
												ctx.Principal.Identities.FirstOrDefault() ? .AddClaim(new Claim("access_token", accessToken));
												ctx.Response.Cookies.Append("access_token", accessToken);
											}
										};
									});
									services.AddScoped <ITokenCacheFactory, TokenCacheFactory> ();
									services.Configure <AuthOptions> (Configuration.GetSection("Authentication"));

									services.AddMvc(options => {
										var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
										options.Filters.Add(new AuthorizeFilter(policy));
									}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
								}
							
						
The required missing references to the authentication libraries will need to be added to the project.

Now that authentication is configured for the application, the next step is to make a request to a ‘protected’ resource. Two things need to happen to make this possible:

a. Add a valid certificate to the requests

b. Add authorization credentials to the request

Let’s look at how to load an x509 certificate from the local certificate store.

            
tificateProvider: ICertificateProvider {
y IMemoryCache _cache;

ateProvider(IMemoryCache cache) {
;
									}

ificate2 GetClientCertificateByThumbprint(string thumbprint) {
ullOrWhiteSpace(thumbprint)) {
											return null;
										}

 $ "{thumbprint}_cert";
yGetValue(cacheKey, out X509Certificate2 cert)) {
re = new X509Store(StoreName.My, StoreLocation.CurrentUser)) {
penFlags.ReadOnly);
ection = store.Certificates.Find(
.FindByThumbprint, thumbprint, true // Exclude invalid certificates
												);
ection.Any()) {
Collection[0];
tryOptions = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromHours(1));
cacheKey, cert, cacheEntryOptions);
												}
);
											}
										}

										return cert;
									}
								}
							
						

ks for a certificate from the ‘personal’ certificate store by the provided Thumbprint and returns the certificate if found. It also caches the certificate so it will not need to look into the certificate store for subsequent requests. The source code can be found on GitHub.

In ‘Startup.cs’ class, there exists the code to cache the user’s access token at the time of logging in to the application. However, if that token has expired or is no longer in the cache, the application needs to request for a new on-behalf-of (OBO) access token and cache it. The complete source code of the 'AccessTokenProvider' class can be found on GitHub.

                    
							public class AccessTokenProvider: IAccessTokenProvider {
								private readonly ITokenCacheFactory _tokenCacheFactory;
								private readonly HttpContext _httpContext;
								private readonly AuthOptions _authOptions;

								public AccessTokenProvider(ITokenCacheFactory tokenCacheFactory, IOptions <AuthOptions> authOptions, IHttpContextAccessor httpContextAccessor) {
									_tokenCacheFactory = tokenCacheFactory;
									_httpContext = httpContextAccessor.HttpContext;
									_authOptions = authOptions.Value;
								}

								public async Task <AuthenticationResult> RetrieveOnBehalfToken() {
									var authority = _authOptions.Authority;
									var cache = _tokenCacheFactory.CreateForUser(_httpContext.User);
									var authContext = new AuthenticationContext(authority, cache);

									//App's credentials may be needed if access tokens need to be refreshed with a refresh token
									var clientId = _authOptions.ClientId;
									var clientSecret = _authOptions.ClientSecret;
									var credential = new ClientCredential(clientId, clientSecret);
									var userId = _httpContext.User.GetObjectId();

									AuthenticationResult tokenResult = null;

									try {
										tokenResult = await authContext.AcquireTokenSilentAsync(
										_authOptions.Resource, credential, new UserIdentifier(userId, UserIdentifierType.UniqueId));
									}
									catch(AdalException adalException) {
										Console.WriteLine($ "RetrieveOnBehalfToken error: {adalException.Message}");
										if (adalException.ErrorCode == AdalError.FailedToAcquireTokenSilently || adalException.ErrorCode == AdalError.InteractionRequired) {
											if (_httpContext.User.Identity.IsAuthenticated) {
												var accessToken = await _httpContext.GetTokenAsync("access_token");
												var idToken = await _httpContext.GetTokenAsync("id_token");
												string userName = _httpContext.User.FindFirst(ClaimTypes.Upn) != null ? _httpContext.User.FindFirst(ClaimTypes.Upn).Value: _httpContext.User.FindFirst(ClaimTypes.Email).Value;

												var userAssertion = new UserAssertion(accessToken, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
												tokenResult = await authContext.AcquireTokenAsync(_authOptions.Resource, credential, userAssertion);

											}
										}
									}
									catch(Exception ex) {
										Console.WriteLine($ "RetrieveOnBehalfToken generic error: {ex.Message}");
									}

									return tokenResult;

								}

							}
						
					

The above two steps have enabled the application to load a certificate and get an access token for the logged in user. Now, let’s see how to use these in the HttpClient. The full source code can be found on GitHub.

                    
							public class PlacingHttpClientFactory: IPlacingHttpClientFactory {
								private readonly ICertificateProvider _certificateProvider;
								private readonly IAccessTokenProvider _accessTokenProvider;
								private readonly HttpContext _httpContext;
								private readonly PlacingApiConfiguration _placingApiConfiguration;

								public PlacingHttpClientFactory(IOptions <PlacingApiConfiguration> placingApiConfiguration, ICertificateProvider certificateProvider, IAccessTokenProvider accessTokenProvider, IHttpContextAccessor httpContextAccessor) {
									_certificateProvider = certificateProvider;
									_accessTokenProvider = accessTokenProvider;
									_httpContext = httpContextAccessor.HttpContext;
									_placingApiConfiguration = placingApiConfiguration.Value;
								}
								public async Task <HttpClient> Build() {
									var clientHandler = new HttpClientHandler();

									if (_placingApiConfiguration.UseCertificate) {
										var clientCert = _certificateProvider.GetClientCertificateByThumbprint(_placingApiConfiguration.CertificateThumbprint);
										if (clientCert != null) {
											clientHandler.ClientCertificates.Add(clientCert);
										}
									}

									var httpClient = new HttpClient(clientHandler) {
										DefaultRequestHeaders = {
											{
												HttpRequestHeader.Accept.ToString(),
												"application/json"
											}
										}
									}

									httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
									httpClient.DefaultRequestHeaders.Add(Constants.Headers.CorrelationId, GetCorrelationId());

									if (_placingApiConfiguration.RequireOnBehalfOfToken) {
										var tokenResult = await _accessTokenProvider.RetrieveOnBehalfToken();
										if (tokenResult != null) {
											httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(tokenResult.AccessTokenType, tokenResult.AccessToken);
										}
									}
									else {
										var emailAddress = GetUserEmailAddress();
										if (!string.IsNullOrWhiteSpace(emailAddress)) {
											httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("X-Assert", emailAddress);
										}
									}

									httpClient.BaseAddress = new Uri(_placingApiConfiguration.BaseUrl);

									return httpClient;
								}

								private string GetCorrelationId() {
									if (_httpContext.Request.Headers.ContainsKey(Constants.Headers.CorrelationId) && !string.IsNullOrWhiteSpace(_httpContext.Request.Headers[Constants.Headers.CorrelationId])) {
										return _httpContext.Request.Headers[Constants.Headers.CorrelationId];
									}
									return CorrelationIdMiddleware.GenerateCorrelationId();
								}

								private string GetUserEmailAddress() {
									return _httpContext.User ? .Identity ? .Name;
								}
							}
						
					

Dependency injection takes care of instantiating the ‘PlacingHttpClientFactory’ class with the right ‘CertificateProvider’ and ‘AccessTokenProvider’ described previously.

In the application's ‘Service’ layer, the ‘PlacingHttpClientFactory’ class is injected using dependency injection and then uses it as follows:

                    
							public async Task <QuoteGet> GetQuoteById(string quoteId) {
								var requestUrl = $ "{_placingApiConfiguration.BaseUrl}/v1/Quotes/{quoteId}";
								using(var httpClient = await _placingHttpClientFactory.Build()) {
									var response = await httpClient.GetAsync(requestUrl);
									response.EnsureSuccessStatusCode();

									var responseBody = await response.Content.ReadAsStringAsync();
									var quote = Newtonsoft.Json.JsonConvert.DeserializeObject < QuoteGet > (responseBody);
									return quote;
								}
							}