Use Open.IdentityServer for OpenID Connect in an Aspire setup

This article demonstrates how to build an OpenID Connect server using Open.IdentityServer (Rock Solid Knowledge) integrated with ASP.NET Core Identity to provide identity and access management (IAM). The solution is built with Aspire and includes an ASP.NET Core web application and a protected Web API for validating and testing OpenID Connect and OAuth 2.0 standards, flows.

Code: https://github.com/damienbod/OidcOpenIdentityServer

Setup

The solution is built with Aspire and consists of an Open.IdentityServer identity provider, an ASP.NET Core web application, and a protected Web API. Security is implemented using OpenID Connect and OAuth 2.0. The web application uses an OpenID Connect confidential client with the Authorization Code Flow and PKCE, enhanced with OAuth PAR and client assertions for client authentication. The API is secured using delegated access tokens issued by Open.IdentityServer.

Open ID Connect client setup in UI

The following code implements OpenID Connect authentication for an ASP.NET Core application using the Authorization Code Flow. Users authenticate through the identity provider and successful sign-ins are persisted using the __Host-Http-ui-web authentication cookie with SameSite=Lax. Tokens are stored locally for subsequent API access, additional claims are retrieved from the UserInfo endpoint. Custom claims such as loa, loi, and email are explicitly mapped. The setup requests the scope-api scope, uses OAuth Pushed Authorization Requests (PAR) and authenticates the client using a client assertion rather than a client secret.

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    options.DefaultSignOutScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
    options.Cookie.Name = "__Host-Http-ui-web";
    options.Cookie.SameSite = SameSiteMode.Lax;
    // can be strict if same-site
    //options.Cookie.SameSite = SameSiteMode.Strict;
})
.AddOpenIdConnect(options =>
{
    options.Events = OidcEventHandlers.OidcEvents(builder.Configuration);

    options.ClientId = builder.Configuration["WebOidcClientId"];
    options.Authority = builder.Configuration["WebOidcAuthority"];
    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.ResponseType = OpenIdConnectResponseType.Code;

    // client_assertion used, set in oidc events
    //options.ClientSecret = "test";

    options.SaveTokens = true;

    options.GetClaimsFromUserInfoEndpoint = true;
    options.MapInboundClaims = false;

    options.ClaimActions.MapUniqueJsonKey("loa", "loa");
    options.ClaimActions.MapUniqueJsonKey("loi", "loi");
    options.ClaimActions.MapUniqueJsonKey(JwtClaimTypes.Email, JwtClaimTypes.Email);

    // OAuth PAR
    options.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Require;

    options.Scope.Add("scope-api");
    options.TokenValidationParameters = new TokenValidationParameters
    {
        NameClaimType = "name"
    };
});

The OpenID Connect events are implemented in a separate class. The required handlers depend on the flow configuration of the OpenID Connect client. Using OAuth PAR results in different handlers.

using Duende.IdentityModel;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

namespace Ui;

public static class OidcEventHandlers
{
    public static OpenIdConnectEvents OidcEvents(IConfiguration configuration)
    {
        return new OpenIdConnectEvents
        {
            OnAuthorizationCodeReceived = async context => await OnAuthorizationCodeReceivedHandler(context, configuration),

            // use OAuth PAR
            OnPushAuthorization = async context => await OnPushAuthorizationHandler(context, configuration),

            // standard OIDC flow handlers using JAR and client assertions - not using OAuth PAR
            //OnRedirectToIdentityProvider = async context => await OnRedirectToIdentityProviderHandler(context, configuration),
        };
    }

    private static async Task OnAuthorizationCodeReceivedHandler(AuthorizationCodeReceivedContext context, IConfiguration configuration)
    {
        // https://openid.net/specs/openid-connect-eap-acr-values-1_0-final.html
        if (context.Properties != null && context.Properties.Items.ContainsKey("acr_values"))
        {
            context.ProtocolMessage.AcrValues = context.Properties.Items["acr_values"];
        }

        if (context.TokenEndpointRequest != null)
        {
            context.TokenEndpointRequest.ClientAssertionType = OidcConstants.ClientAssertionTypes.JwtBearer;
            context.TokenEndpointRequest.ClientAssertion = AssertionService.CreateClientToken(configuration);
        }
    }

    /// <summary>
    /// Not using OAuth PAR
    /// </summary>
    //private static async Task OnRedirectToIdentityProviderHandler(RedirectContext context, IConfiguration configuration)
    //{
    //    var request = AssertionService.SignAuthorizationRequest(context.ProtocolMessage, configuration);
    //    var clientId = context.ProtocolMessage.ClientId;
    //    var redirectUri = context.ProtocolMessage.RedirectUri;

    //    context.ProtocolMessage.Parameters.Clear();
    //    context.ProtocolMessage.ClientId = clientId;
    //    context.ProtocolMessage.RedirectUri = redirectUri;
    //    context.ProtocolMessage.SetParameter("request", request);
    //}

    private static async Task OnPushAuthorizationHandler(PushedAuthorizationContext context, IConfiguration configuration)
    {
        context.ProtocolMessage.Parameters.Add("client_assertion", AssertionService.CreateClientToken(configuration));
        context.ProtocolMessage.Parameters.Add("client_assertion_type", OidcConstants.ClientAssertionTypes.JwtBearer);

        context.HandleClientAuthentication();

        // https://openid.net/specs/openid-connect-eap-acr-values-1_0-final.html
        if (context.Properties.Items.ContainsKey("acr_values"))
        {
            context.ProtocolMessage.AcrValues = context.Properties.Items["acr_values"];
        }
    }
}

OpenID Connect features

At present Open.IdentityServer supports a lot of OpenID and OAuth standards. OAuth DPoP and CIBA are two standards not implement at present.

Conclusion

Open.IdentityServer looks like a good solution to implement an identity provider supporting OpenID and OAuth. Rock Solid Knowledge is a mature company. There are in this field a long time and have supported and provided multiple security solutions. I have used for example Admin UI which is well maintained and stable.

Links

https://docs.identityserver.com/openidentityserver/migrating/from_duende

https://github.com/RockSolidKnowledge/Open.IdentityServer

https://docs.identityserver.com/openidentityserver

https://www.identityserver.com

https://aspire.dev

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.