Annotated Code for ASP.NET Core Web MVC OpenID Connect Client

01/14/2021

Updated 1/15/2021

Project Settings

Code in Startup.ConfigureServices()

// JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

The claim type names used in .NET are different from the ones in JWT standards. For example, "sub" in JWT would be "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" in .NET. JwtSecurityTokenHandler can map JWT claim types names to .NET ones. This behavior can be disabled by setting DefaultMapInboundClaims to false, though it's probably better to do it since things in .NET (e.g. User.Identity.Name) expect .NET claim types.

For reference:

JwtSecurityTokenHandler will use the claims in JWT to create a ClaimsIdentity.

services.AddAuthentication(options =>
{
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";
})

This code specifies that if an anonymous user tries to access a protected resource, the user will be "challenged" with OpenID Connect authentication. If authentication is successful, the user claims will be stored in a cookie.

.AddCookie(opt =>
{
    opt.AccessDeniedPath = "/Home/AccessDenied";
    opt.Cookie.MaxAge = TimeSpan.FromDays(90);
})

This is where we can customize the cookie. The full set of options are in CookieAuthenticationOptions.

.AddOpenIdConnect("oidc", options =>
{
    options.Authority = Configuration["OIDC:Authority"];
    options.ClientId = Configuration["OIDC:ClientId"];
    options.ClientSecret = Configuration["OIDC:ClientSecret"];
    options.ResponseType = "code";
    options.Scope.Add("email");
    options.Scope.Add("ascent_claims");
    options.SaveTokens = true;
    // options.GetClaimsFromUserInfoEndpoint = true;
    // options.ClaimActions.MapUniqueJsonKey("ascent_admin", "ascent_admin");
    // options.ClaimActions.MapUniqueJsonKey("ascent_department_read", "ascent_department_read");
    // options.ClaimActions.MapUniqueJsonKey("ascent_department_write", "ascent_department_write");

    options.Events = new OpenIdConnectEvents
    {
        OnRemoteFailure = context =>
        {
            context.Response.Redirect("/");
            context.HandleResponse();
            return Task.FromResult(0);
        }
    };

    if (Environment.IsDevelopment())
    {
        options.RequireHttpsMetadata = false;
    }
});

This is where we set various OIDC options. The full set of options are in OpenIdConnectOptions.

By default only openid (sub and other claims) and profile (family_name, given_name etc.) scopes will be requested. Here we added email and ascent_claims scope. ascent_claims scope contains some claims specific to the application. Note that the OIDC provider must be configured to allow these scopes for this client.

There is an option in IdentityServer4 called "Always Include User Claims in Id Token". It's generally a good idea to set this option for a client unless the resulting token would be too large (e.g. more than 8KB, which is the limit of HTTP header size for some web servers). If this option is not set for a client, most user claims (basically anything other than sub) will not be in the Id token; instead, the client code must make another request to the /userinfo endpoint to get the rest of the claims. Furthermore, anything received from /userinfo that is not a standard claim will not be included in the ClaimsIdentity by default. In other words, any custom claim needs to be explicitly copied to ClaimsIdentity using MapUniqueJsonKey().