Transforming Open Id Connect claims in ASP.Net Core
Asked Answered
U

5

14

I'm writing an ASP.Net Core Web Application and using UseOpenIdConnectAuthentication to connect it to IdentityServer3. Emulating their ASP.Net MVC 5 sample I'm trying to transform the claims received back from Identity Server to remove the "low level protocol claims that are certainly not needed." In MVC 5 they add a handler for the SecurityTokenValidated Notification that swaps out the AuthenticationTicket for one with just the required claims.

In ASP.Net Core, to do the equivalent, I thought that I would need to handle the OnTokenValidated in the OpenIdConnectEvents. However, at that stage it doesn't appear that the additional scope information has been retrieved. If I handle the OnUserInformationReceived, the extra information is present, but stored on the User rather than the principal.

None of the other events seem like the obvious place to permanently remove the claims I'm not interested in retaining after authentication has completed. Any suggestions gratefully received!

Urinate answered 19/8, 2016 at 10:34 Comment(0)
U
12

I like LeastPrivilege's suggestion to transform earlier in the process. The code provided doesn't quite work. This version does:

var oidcOptions = new OpenIdConnectOptions
{
   ...

   Events = new OpenIdConnectEvents
   {
       OnTicketReceived = e =>
       {
          e.Principal = TransformClaims(e.Ticket.Principal);
          return Task.CompletedTask;
       }
   }
};

This replaces the Principal rather than the Ticket. You can use the code from my other answer to create the new Principal. You can also replace the Ticket at the same time but I'm not sure it is necessary.

So thank you to LeastPrivilege and Adem for suggesting ways that pretty much answered my question... just the code needed slight adjustments. Overall, I prefer LeastPrivilege's suggestion of transforming claims early.

Urinate answered 23/8, 2016 at 9:52 Comment(0)
I
6

You can implement OnSigningIn event of SignInScheme. Here is an example:

        app.UseCookieAuthentication(new CookieAuthenticationOptions()
        {
            AuthenticationScheme = "OpenIdCookies",
            AutomaticAuthenticate = true,
            Events = new CookieAuthenticationEvents()
            {
                OnSigningIn = async (context) =>
                {
                    ClaimsIdentity identity = (ClaimsIdentity)context.Principal.Identity;
                    identity.Claims = identity.Claims.Where(...);
                }
            }
        });

        var oidcOptions = new OpenIdConnectOptions
        {
            AuthenticationScheme = "oidc",
            SignInScheme = "OpenIdCookies"
        };

        //.. set other options

        app.UseOpenIdConnectAuthentication(oidcOptions); 
Intermediate answered 19/8, 2016 at 11:33 Comment(1)
Thank you for setting me down the right track.. the only issue is that identity.Claim is a readonly property. I've added an answer that works, I'm just not sure if it is the "right" approach.Urinate
U
6

Thank you Adem for your reply... it solved the vast majority of the problem... the only issue being that identity.Claim is a read only property. I found creating a new Principal did work though:

Events = new CookieAuthenticationEvents()
{
    OnSigningIn = (context) =>
    {
        ClaimsIdentity identity = (ClaimsIdentity)context.Principal.Identity;

        var givenName = identity.FindFirst(Constants.ClaimTypes.GivenName);
        var familyName = identity.FindFirst(Constants.ClaimTypes.FamilyName);
        var sub = identity.FindFirst(Constants.ClaimTypes.Subject);

        var claimsToKeep = new List<Claim> {givenName, familyName, sub};

        var newIdentity = new ClaimsIdentity(claimsToKeep, identity.AuthenticationType);

        context.Principal = new ClaimsPrincipal(newIdentity);

        return Task.FromResult(0);
    }
}

Whether this is the correct approach I'm not sure, but it appears to work.

Urinate answered 20/8, 2016 at 8:3 Comment(1)
Have you made any conclusions if replacing Principal instead of Ticket is OK to do?Participation
P
6

I personally prefer to do the claims transformation in the middleware where the actual authentication happens.

You can use the OnTicketReceived event on the OIDC middleware for that.

var oidcOptions = new OpenIdConnectOptions
{
   AuthenticationScheme = "oidc",
   SignInScheme = "cookies",

   Authority = Clients.Constants.BaseAddress,

   ClientId = "mvc.hybrid",
   ClientSecret = "secret",
   ResponseType = "code id_token",
   SaveTokens = true,

   TokenValidationParameters = new TokenValidationParameters
   {
      NameClaimType = JwtClaimTypes.Name,
      RoleClaimType = JwtClaimTypes.Role,
   },

   Events = new OpenIdConnectEvents
   {
       OnTicketReceived = e =>
       {
           ClaimsPrincipal p = TransformClaims(e.Ticket.Principal);
           e.Ticket = new AuthenticationTicket(
            p,
            e.Ticket.Properties,
            e.Ticket.AuthenticationScheme);

        return Task.CompletedTask;
    }
  }
};
Prefigure answered 21/8, 2016 at 10:6 Comment(3)
This looks like it should work, but when I tried it the original claims were still in place. If instead of replacing the e.Ticket with a new AuthenticationTicket you replace e.Principal with the new Principal it does work.Urinate
@PiersLawson Have you made any conclusions if replacing e.Principal instead of e.Ticket is ok to do?Participation
This answer worked well for me using AspNetCore 1.1.2 authenticating against Identity Server 3.Alienism
G
1

Thanks to the answers in this thread, I was able to get this working for myself too. What isn't addressed here is that if claims need to be modified which requires a service. In my case, I needed to build the service provider in order to get the proper dependencies in order to transform the claims. (I also was able to delete claims without a transformation - showing that code here as well).

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddAuthentication(options =>
        {
            // set options
        })
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
        {
            // set options
        })
        .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            // options such as Authority, ClientId, etc set here
            options.Authority = "your-value";
            options.ClientId = "your-value";
            // ...

            // remove automatically mapped claims we do not need, keeps the authentication cookie smaller
            options.ClaimActions.DeleteClaim("sid");
            options.ClaimActions.DeleteClaim("idp");
            options.ClaimActions.DeleteClaim("s_hash");
            options.ClaimActions.DeleteClaim("auth_time");

            options.Events.OnTicketReceived = async context =>
            {
                // Build the service provider and necessary dependencies
                // in order to enhance our claims once we receive it initially
                ServiceProvider serviceProvider = services.BuildServiceProvider();
                ICustomProvider customProvider = serviceProvider.GetService<ICustomProvider>();
                EnhanceClaimsTransformation claimsTransformation = new EnhanceClaimsTransformation(customProvider);

                context.Principal = await claimsTransformation.TransformAsync(context.Principal);
                await Task.CompletedTask;
            };
        });
}

EnhanceClaimsTransformation (ICustomProvider is registered in dependency injection in ConfigureServices)

Do note that in this code we need to make a clone of the principal to actually add claims to it.

public class EnhanceClaimsTransformation : IClaimsTransformation
{
    private readonly ICustomProvider _customProvider;

    public EnhanceClaimsTransformation(ICustomProvider customProvider)
    {
        _customProvider = customProvider;
    }

    /// <summary>
    /// Upon authentication, we transform the claims in order to enhance
    /// the claims with user-enhanced values.
    /// </summary>
    /// <param name="principal"></param>
    /// <returns></returns>
    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        // https://gunnarpeipman.com/aspnet-core-adding-claims-to-existing-identity/
        ClaimsPrincipal clone = principal.Clone();
        ClaimsIdentity claimsIdentity = (ClaimsIdentity)clone.Identity;

        Response response = await _customProvider.Find(principal.Identity.Name, CancellationToken.None);

        // Setting claims values
        claimsIdentity.AddClaims(new List<Claim>
        {
            new Claim("Datapoint1", response.Datapoint1),
            new Claim("Datapoint2", response.Datapoint2),
            // ...
        });        

        return clone;
    }
}
Goiter answered 4/3, 2022 at 15:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.