Quantcast
Channel: ASP.NET Core – Software Engineering
Viewing all articles
Browse latest Browse all 169

ASP.NET Core delegated Microsoft OBO access token management (Entra only)

$
0
0

This blog shows how to implement a delegated Microsoft On-Behalf-Of flow in ASP.NET Core, and has a focus on access token management. The solution uses Microsoft.Identity.Web to implement the different flows and it really simple to implement, when you know how to use the Nuget package and use the correct Microsoft documentation. The application can request delegated access tokens On-Behalf-Of a user and another application, providing a seamless and secure access to protected resources using a zero trust strategy.

Code: https://github.com/damienbod/token-mgmt-ui-delegated-obo-entra

Blogs in this series

Setup

Three applications can used in this setup. A web UI application, an API and another API which implements the Microsoft On-Behalf-Of flow for the users delegated access token and the application. The Microsoft OBO works very like part of the OAuth token exchange standard, but it is not a standard, just a Microsoft flavor for a standard.

What must an application manage?

An access token management solution must ensure that tokens are securely stored per user session for delegated downstream API user tokens and updated after each UI authentication or refresh. The solution should be robust to handle token expiration, function seamlessly after restarts, and support multi-instance deployments. The tokens must be persisted safely in multiple instance setups. Additionally, it must effectively manage scenarios involving invalid or missing access tokens. Microsoft.Identity.Web implements this completely as long as as authentication and OAuth flows are implemented using Entra ID.

Properties of token management in the solution setup:

  • The access token is persisted per user session
  • The token expires
  • The token needs to be persisted somewhere safely (Safe and encrypted storage if not in-memory)
  • The token must be replaced after each UI authentication (per user)
  • The solution must work after restarts
  • The solution must work for multiple instances when deployed to multi-instance deployments.
  • The solution must handle invalid access tokens or missing access tokens
  • The application must handle a user logout

Web UI

The first step in the Microsoft On-Behalf-Of flow is to authenticate the user and a web application using Entra ID. This is implemented using the Microsoft.Identity.Web Nuget package. The Web application uses OpenID Connect code flow with PKCE and a confidential client. The application requests an access token for the first API. The access token is a delegated access token issued for the user and the specific API. The implementation uses a secret to assert the application. Microsoft recommends using a certificate and client assertions when deploying to production.

builder.Services.AddHttpClient();

builder.Services.AddOptions();

string[]? initialScopes = builder.Configuration
	.GetValue<string>("WebApiEntraId:ScopeForAccessToken")?
	.Split(' ');

builder.Services.AddDistributedMemoryCache();
builder.Services
	.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, 
		"EntraID",
        subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: true)
    .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
    .AddDistributedTokenCaches();

builder.Services
    .AddAuthorization(options =>
    {
        options.FallbackPolicy = options.DefaultPolicy;
    });

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        var policy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
        options.Filters.Add(new AuthorizeFilter(policy));
    }).AddMicrosoftIdentityUI();

builder.Services.AddServerSideBlazor()
    .AddMicrosoftIdentityConsentHandler();

The WebApiEntraIdService class is used to use the access token from the web application and call the downstream API. If the access token is missing, or invalid, an new access token is requested in the application.

using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Web;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Threading.Tasks;

namespace RazorPageEntraId.WebApiEntraId;

public class WebApiEntraIdService
{
    private readonly IHttpClientFactory _clientFactory;
    private readonly ITokenAcquisition _tokenAcquisition;
    private readonly IConfiguration _configuration;

    public WebApiEntraIdService(IHttpClientFactory clientFactory,
        ITokenAcquisition tokenAcquisition,
        IConfiguration configuration)
    {
        _clientFactory = clientFactory;
        _tokenAcquisition = tokenAcquisition;
        _configuration = configuration;
    }

    public async Task<string?> GetWebApiEntraIdDataAsync()
    {
        var client = _clientFactory.CreateClient();

        var scope = _configuration["WebApiEntraID:ScopeForAccessToken"];
        var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync([scope!]);

        client.BaseAddress = new Uri(_configuration["WebApiEntraID:ApiBaseAddress"]!);
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        var response = await client.GetAsync("/api/profiles/photo");
        if (response.IsSuccessStatusCode)
        {
            var responseContent = await response.Content.ReadFromJsonAsync<string>();

            return responseContent;
        }

        throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
    }
}

Web API using On-Behalf-Of Flow

The first Web API implements the Microsoft On-Behalf-Of flow to acquire a new access token for the existing access token and the user represented in the access token. The access token is a delegated access token. The API has no UI and does not use any UI flows. If the access token used in the request is invalid, a 401 is returned with an exception information on what permission or access token is required to use the API. If the API is requested using a valid access token, the API application uses the default scope and requests a new access token using a secret or a certificate. The new access token can be used to access the downstream API.

builder.Services.AddTransient<WebApiDownstreamService>();
builder.Services.AddHttpClient();
builder.Services.AddOptions();

builder.Services.AddDistributedMemoryCache();

builder.Services
	.AddMicrosoftIdentityWebApiAuthentication(
		builder.Configuration, "EntraID")
	.EnableTokenAcquisitionToCallDownstreamApi()
	.AddDistributedTokenCaches();

using Microsoft.Identity.Web;
using System.Net.Http.Headers;
using System.Text.Json;

namespace WebApiEntraIdObo.WebApiEntraId;

public class WebApiDownstreamService
{
    private readonly IHttpClientFactory _clientFactory;
    private readonly ITokenAcquisition _tokenAcquisition;
    private readonly IConfiguration _configuration;

    public WebApiDownstreamService(IHttpClientFactory clientFactory,
        ITokenAcquisition tokenAcquisition,
        IConfiguration configuration)
    {
        _clientFactory = clientFactory;
        _tokenAcquisition = tokenAcquisition;
        _configuration = configuration;
    }

    public async Task<string?> GetApiDataAsync()
    {
        var client = _clientFactory.CreateClient();

        // user_impersonation access_as_user access_as_application .default
        var scope = _configuration["WebApiEntraIdObo:ScopeForAccessToken"];
        if (scope == null) throw new ArgumentNullException(nameof(scope));

        var uri = _configuration["WebApiEntraIdObo:ApiBaseAddress"];
        if (uri == null) throw new ArgumentNullException(nameof(uri));

        var accessToken = await _tokenAcquisition
            .GetAccessTokenForUserAsync([scope]);

        client.DefaultRequestHeaders.Authorization
            = new AuthenticationHeaderValue("Bearer", accessToken);

        client.BaseAddress = new Uri(uri);
        client.DefaultRequestHeaders.Accept.Add(
			new MediaTypeWithQualityHeaderValue("application/json"));

        var response = await client.GetAsync("api/profiles/photo");
        if (response.IsSuccessStatusCode)
        {
            var data = await JsonSerializer.DeserializeAsync<string>(
                await response.Content.ReadAsStreamAsync());

            return data;
        }

        throw new ApplicationException($"Status code: {response.StatusCode}, 
			Error: {response.ReasonPhrase}");
    }
}

Web API

The downstream API validates the request API using standard JWT validation.

builder.Services.AddControllers(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        // .RequireClaim("email") // disabled this to test with users that have no email (no license added)
        .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
});

builder.Services.AddHttpClient();
builder.Services.AddOptions();

builder.Services.AddMicrosoftIdentityWebApiAuthentication(
    builder.Configuration, "EntraID");

Running the applications

When the applications are started, the data from the downstream APIs is returned to the web application.

Further examples of the Microsoft On-Behalf-Of flow

Microsoft authentication authorization libraries are complicated and many. They is no one way to implement this. Microsoft provides Microsoft Graph Nuget packages, Azure SDK packages, mixes application and delegation flows, managed identities solutions, direct token acquisition and some legacy Nuget packages to integrate the security. Here are further examples of using the Microsoft On-Behalf-Of flow using different client solutions.

Microsoft OBO with Azure Blob Storage (delegated)

ASP.NET Core Razor page using Azure Blob Storage to upload download files securely using OAuth and Open ID Connect

https://github.com/damienbod/AspNetCoreEntraIdBlobStorage

Microsoft OBO with OpenIddict (delegated)

This demo shows how to implement the On-Behalf-Of flow between an Microsoft Entra ID protected API and an API protected using OpenIddict.

https://github.com/damienbod/OnBehalfFlowOidcDownstreamApi

ASP.NET Core OBO using Microsoft Graph (delegated)

Backend for frontend security using Angular Standalone (nx) and ASP.NET Core backend using Microsoft Graph

https://github.com/damienbod/bff-aspnetcore-angular

Note

The Microsoft OBO flow is only used for integrating with Microsoft Entra. If using any other identity provider, the OAuth token exchange flow should be used for this type of solution.

Links

https://github.com/damienbod/OAuthGrantExchangeOidcDownstreamApi

https://docs.duendesoftware.com/identityserver/v7/tokens/extension_grants/token_exchange/

https://datatracker.ietf.org/doc/html/rfc8693

https://github.com/damienbod/OnBehalfFlowOidcDownstreamApi

https://www.rfc-editor.org/rfc/rfc6749#section-5.2

https://github.com/blowdart/idunno.Authentication/tree/dev/src/idunno.Authentication.Basic

https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-on-behalf-of-flow

Standards

JSON Web Token (JWT)

Best Current Practice for OAuth 2.0 Security

The OAuth 2.0 Authorization Framework

OAuth 2.0 Demonstrating Proof of Possession DPoP

OAuth 2.0 JWT-Secured Authorization Request (JAR) RFC 9101

OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens

OpenID Connect 1.0

Microsoft identity platform and OAuth 2.0 On-Behalf-Of flow

OAuth 2.0 Token Exchange

JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens

HTTP Semantics RFC 9110


Viewing all articles
Browse latest Browse all 169

Trending Articles