Just recently for a small hobby project I needed some way to inject claims to a user after they signed in with Azure AD. Specifically some roles and other things related to what the user can do in the app.

This turns out to be quite easy. This will be a short article.

OpenID Connect

Custom claims can be added in the OnTokenValidated event like so:

services
    .AddAuthentication(o =>
    {
        o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(o =>
    {
        //Additional config snipped
        o.Events = new OpenIdConnectEvents
        {
            OnTokenValidated = async ctx =>
            {
                //Get user's immutable object id from claims that came from Azure AD
                string oid = ctx.Principal.FindFirstValue("http://schemas.microsoft.com/identity/claims/objectidentifier");

                //Get EF context
                var db = ctx.HttpContext.RequestServices.GetRequiredService<AuthorizationDbContext>();

                //Check is user a super admin
                bool isSuperAdmin = await db.SuperAdmins.AnyAsync(a => a.ObjectId == oid);
                if (isSuperAdmin)
                {
                    //Add claim if they are
                    var claims = new List<Claim>
                    {
                        new Claim(ClaimTypes.Role, "superadmin")
                    };
                    var appIdentity = new ClaimsIdentity(claims);

                    ctx.Principal.AddIdentity(appIdentity);
                }
            }
        };
    });

Here we:

  1. Get an EF database context from the dependency injection container
  2. Use it to try to find the user in a table
  3. If found, apply the super admin role to them

All the existing claims will still be there after this. We are only adding new claims.

Since we use cookie authentication as the sign-in scheme, this new claim is also written to the cookie.

And so it will be available on all future requests.

JWT Bearer

Really similar to previous, just look at this:

services
    .AddAuthentication(o =>
    {
        o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(o =>
    {
        //Additional config snipped
        o.Events = new JwtBearerEvents
        {
            OnTokenValidated = async ctx =>
            {
                //Get the calling app client id that came from the token produced by Azure AD
                string clientId = ctx.Principal.FindFirstValue("appid");

                //Get EF context
                var db = ctx.HttpContext.RequestServices.GetRequiredService<AuthorizationDbContext>();

                //Check if this app can read confidential items
                bool canReadConfidentialItems = await db.Applications.AnyAsync(a => a.ClientId == clientId && a.ReadConfidentialItems);
                if (canReadConfidentialItems)
                {
                    //Add claim if yes
                    var claims = new List<Claim>
                    {
                        new Claim("ConfidentialAccess", "true")
                    };
                    var appIdentity = new ClaimsIdentity(claims);

                    ctx.Principal.AddIdentity(appIdentity);
                }
            }
        };
    });

If you need an explanation of what is happening, check the previous part on OIDC :D

You can do all sorts of logic there, and you have access to DI as well as the HTTP context.

Though if those claims are not needed in all parts of the app, it is probably better to just check the access in authorization policies :)

OnTokenValidated gets run on every request with JWT Bearer though, so it is definitely better if you can include the claims in the token!