简体   繁体   中英

How to add own claims after logging in to Azure AD with OIDC in ASP.NET Core 2 with dependency injection?

In ASP.NET Core 2 logging in to Azure AD is fairly easy, in ConfigureServices(IServiceCollection services) just add the following

// Azure AD login
services.AddAuthentication(a =>
{
    a.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    a.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    a.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(o => o.LoginPath = new PathString("/Account/SignIn"))
.AddOpenIdConnect(o =>
{
    o.ClientId = Configuration["Authentication:AzureAd:ClientId"];
    o.ClientSecret = Configuration["Authentication:AzureAd:ClientSecret"];
    o.Authority = Configuration["Authentication:AzureAd:AADInstance"] + 
                  Configuration["Authentication:AzureAd:TenantId"];
    o.CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"];
    o.ResponseType = OpenIdConnectResponseType.CodeIdToken;
    o.Events = new OpenIdConnectEvents
    {
        OnRemoteFailure = RemoteFailure,
        OnTokenValidated = TokenValidated
    };
});

and everything works fine. Then I can add Claims in TokenValidated and that works fine aswell:

private Task TokenValidated(TokenValidatedContext context)
{
    var claims = new List<Claim>();
    var claim = new Claim(ClaimTypes.Role, "Test", ClaimValueTypes.String, "Issuername")
    context.Principal.AddIdentity(new ClaimsIdentity(claims));
    return Task.FromResult(0);
}

However, it's never quite that easy. The Claims I want are dependent on a external calls to a service, and the address is stored in the configuration.

In ConfigureServices I also have various classes added for dependency injection with works fine for the controllers.

services.AddTransient<IRoleClaims, RoleClaims>();

This RoleClaims is a class I want to call from the TokenValidated method, but as far as I can see I cannot use DI here. Nor can I access the ServiceCollection to get it via ActivatorUtilities.CreateInstance.

The constructor to RoleClaims looks like this:

public RoleClaims(IOptions<EmployeeConfiguration> configuration)

So, the big question: How is this supposed to work? Can I somehow use dependency injection in the TokenValidated method? Am I trying to add my own claims in the wrong place?

In ASP.NET Core 2.0, you can get a service from the contain using:

private async Task TokenValidated(TokenValidatedContext context)
{
  var widget = ctx.HttpContext.RequestServices.GetRequiredService<Widget>();
  ...
}

I succeeded in authenticating against IdentityServer4 in a multi-tenancy scenario where I needed to inject client credentials and other stuff on a per-request basis. That's why I also " messed up " my code with custom OpenIdConnectEvents .

The OnTokenValidated func is the right spot. The goal is to assign a value to the TokenValidatedContext.Result (whose setter is unfortunately protected ). You can however call the .Success() method, which sets the property accordingly to what available:

Task TokenValidated(TokenValidatedContext context)
{
    //[...] gathering claims   
    var ci = new ClaimsIdentity(context.Scheme.Name, "name", "role");
    ci.AddClaims(my_previously_gathered_Claims);
    context.Principal = new ClaimsPrincipal(ci);  
    // .Success() uses 
    // 1. the principal just set above  
    // 2. the context properties
    // 3. the context scheme
    // to create the underlying ticket                       
    context.Success();
}

That should do the trick.

I personally would have preferred a public setter for .Result .

Found a way to do it. It might not be pretty, but it seems to work.

If anyone have any better way to do it, of if some of this is bad practice I would like to hear it.

public class Startup
{
    private IServiceCollection _serviceCollection;
    public void ConfigureServices(IServiceCollection services)
    {
        _serviceCollection = services; // Hacky way to access services in other methods :s
        // services.AddStuff() down here, including the AzureAD OIDC
    }
    private async Task TokenValidated(TokenValidatedContext context)
    {
        IRoleClaims roleClaims; // My class for reading from services/database
                                // and create claims

        // This is the magic DI workaround I was looking for
        var scopeFactory = _serviceCollection.BuildServiceProvider()
                           .GetRequiredService<IServiceScopeFactory>();
        using (var scope = scopeFactory.CreateScope())
        {
            var provider = scope.ServiceProvider;
            roleClaims = provider.GetRequiredService<IRoleClaims>();
        }

        // Getting the ObjectID for the user from AzureAD
        var objectId = context.SecurityToken.Claims
            .Where(o => o.Type == "oid")
            .Select(o => o.Value)
            .SingleOrDefault();

        var claims = await roleClaims.CreateRoleClaimsForUser(objectId);
        context.Principal.AddIdentity(new ClaimsIdentity(claims));
    }
    // Rest of the methods not shown
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM