4
votes

I'm still learning the Identity Framework and am pretty lost in trying to setup authentication in my .Net Core 2 MVC application. Any suggestions are appreciated since I'm not even sure what I'm doing is correct.


I have a requirement to integrate an OpenID Connect identity provider for authentication and use a secondary data source for authorization. Inconveniently I cannot use any claim from the OIDC IdP except for the name claim. The rest of the user claims must come from the secondary data source (which is connected to the Identity Framework through a custom UserStore and User entity).

I am using the OpenId Connect provider to handle the authentication. This is working fine and gives me the first Identity (which I can only use one Claim from). My confusion starts when I need to fetch the second Identity of the user, add it to the principal, and set it as the default Identity. This second Identity provides all of the user claims, including role.

My understanding of identity framework is that I should have a single ClaimsPrincipal with two identities so that I can plug into the rest of Identity Framework. However with two identities the default ClaimsPrincipal will automatically select the first Identity (which is the one I can't use), therefor it seems I should create a custom ClaimsPrincipal and set the PrimaryIdentitySelector so that my second Identity is the primary.

public class MyClaimsPrincipal : ClaimsPrincipal
{
    private static readonly Func<IEnumerable<ClaimsIdentity>, ClaimsIdentity> IdentitySelector = SelectPrimaryIdentity;

    /// <summary>
    /// This method iterates through the collection of ClaimsIdentities and chooses an identity as the primary.
    /// </summary>
    private static ClaimsIdentity SelectPrimaryIdentity(IEnumerable<ClaimsIdentity> identities)
    {
        // Find and return the second identity
    }
}

Once I get the validated token from the OIDC IdP, I fetch the second identity, create a new MyClaimsPrincipal, add the two Identities to the new principal. After that I'm not sure what to do with this new principal.

I've tried to sign the user in via the SignInManager, setting the User on the HTTP context explicitly, and using middleware to convert ClaimsPrincipals to MyClaimsPrincipals but all of these seem to do nothing. I think I am missing the point.

Some specific questions:

  • Is this the best way to do this? Being generally confused about all of this makes it hard to tell if I'm even on the right track.
  • Once I've created a custom principal, how to I "set" it into the HTTP context so that it's persistent?
  • How does Cookie authentication work with OpenId Connect authentication? It seems OIDC somehow passes the user into Cookie authentication, and that adding cookie authentication is required for OIDC authentication to work.
1

1 Answers

9
votes

An important thing to know when using the OpenID Connect scheme is that the scheme will never work on its own. In pretty much every example you can find you will see it combined with the cookie scheme. The reason for this is very simple: OIDC is for authenticating the user with an external authentication provider. But that authentication is only temporary. In order to store it locally within your application, you need to sign in your user. This is usually done with the cookie authentication scheme (although it could be done in other ways).

The authentication flow for an application that uses OIDC and cookies usually works like this:

  1. User accesses your application.
  2. Authentication: The cookie scheme, the default authentication scheme, will attempt to authenticate the user. If there is no cookie, the handler will challenge the authentication.
  3. Challenge: The OIDC scheme, the default challenge scheme, will challenge the user and redirect to the external authentication provider.
  4. The user will authenticate with the external authentication provider and will get redirected back to the application.
  5. Challenge callback: The OIDC scheme will take the response from the external authentication provider, complete the challenge and create a claims principal.
  6. Sign-in: The OIDC scheme will sign in that principal with its configured sign-in scheme (the cookie scheme).
  7. The cookie scheme will sign in the user and create a cookie that is persisted in the user’s browser.
  8. On a subsequent request to your application, the user will include a valid cookie, so the cookie schem can successfully authenticate the user, without having to challenge the OIDC scheme again.

So assuming that everything worked properly, the OIDC scheme will not be involved again in the authentication. Instead, the identity from the cookie will be used every time.

You can use this for your purpose to expand the principal that the OIDC scheme created with additional claims, before it is signed in and persisted by the cookie scheme. You could do this using a custom sign-in scheme that sits between the OIDC and cookie schemes, or you could simply attach to an authentication event of the OIDC scheme that is invoked after the challenge is completed but before the sign-in occurs.

You can use the TicketReceived event for this purpose:

public void ConfigureServices(IServiceCollection services)
{
    // …

    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
        .AddCookie()
        .AddOpenIdConnect(options =>
        {
            // …

            options.Events.OnTicketReceived = OnOpenIdConnectTicketReceived;
        });
}

public static Task OnOpenIdConnectTicketReceived(TicketReceivedContext context)
{
    if (context.Principal.Identity is ClaimsIdentity identity)
    {
        identity.AddClaim(new Claim("foo", "bar"));
    }

    return Task.CompletedTask;
}