Where to filter Identity 2.0 claim ticket in a Web

2019-08-10 03:53发布

问题:

ASP.NET apps using OWIN permit multiple Identity sources (Facebook, Google, etc.). Most of the provider-specifc information those sources provide is irrelevant to my app, potentially even large, and I don't want it in my cookies all session. My app is primarily WebAPI, but I suspect the question applies equally to MVC and WebForms.

For now, all I need is an integer account ID. Where/when should I reconstruct the identity, after external authentication?

For example, here is one way I could filter claims:

public ReplaceExistingClaims(ClaimsIdentity identity) {
{
    Claim customClaim = GetCustomClaimFromDbForIdentity(identity);
    foreach (Claim claim in ClaimsIdentity.Claims) ClaimsIdentity.RemoveClaim(claim);
    ClaimsIdentity.AddClaim(customClaim);
}

And following are two different places I could inject those claims changes:

var facebookAuthenticationOptions = new FacebookAuthenticationOptions
{
    Provider = new FacebookAuthenticationProvider
    {
        OnAuthenticated = context =>
        {
            ReplaceExistingClaims(context.Identity);
            return Task.FromResult(0);
        }
    }
};

Above, I know I can hook an individual provider from Startup IF it provides an Authenticated event. I have two conceptual problems with this. One: it requires me to write and wire up my code separately for each provider I plug in. Two: there is no requirement for providers to provide this event. Both of these make me feel like there must be a different intended insertion point for my code.

public ActionResult ExternalLoginCallback(string returnUrl)
{
    ReplaceExistingClaims((ClaimsIdentity)User.Identity);
    new RedirectResult(returnUrl);
}

Above, I know I can put code in ExternalLoginCallback. But this happens too late for two reasons. One: The user has already been issued a ticket I consider invalid, but the default [Authorized] considers valid because it's signed by me, and now they are making requests to my site with it. There could even be race conditions here. Two: There is no guarantee the browser will visit this redirect, and I'd prefer from a design perspective if it didn't have to, e.g. to simplify my WebAPI client code.

To the best of my knowledge, the best solution will meet these requirements:

  1. same code applies to all providers
  2. client receives my custom ticket from my server (e.g. without image claims)
  3. client never receives another ticket format from my server
  4. the authentication process requires the minimum possible HTTP round-trips
  5. token-refresh and other core identity features are still available
  6. once a user is [Authorize]d, no further account transformation is necessary
  7. database/repository access is feasible during ticket generation

Some pages I'm researching, for my own notes:

  • How do I access Microsoft.Owin.Security.xyz OnAuthenticated context AddClaims values?
  • https://katanaproject.codeplex.com/SourceControl/latest#src/Microsoft.Owin.Security.Facebook/FacebookAuthenticationHandler.cs
  • https://katanaproject.codeplex.com/workitem/82
  • https://www.simple-talk.com/dotnet/.net-framework/creating-custom-oauth-middleware-for-mvc-5/

回答1:

You have to implement DelegationHandler and put all your authentication routines in it.

Register at Application start (DI usage is enabled):

private static void RegisterHandlers(HttpConfiguration config)
{
    var authHandler = new MyFacebookAuthHandler();
    config.MessageHandlers.Add(authHandler);
}

And this is an example of implementation:

public class MyFacebookAuthHandler : DelegationHandler
{
    public override sealed Task<HttpResponseMessage> OnSendAsync(HttpRequestMessage request,
                                                                 CancellationToken cancellationToken)
    {
        try
        {
            // Process credentials
            // Probably you have to save some auth information to HttpContext.Current
            // Or throw NotAuthorizedException
        }
        catch(NotAuthorizedException ex)
        {
            return request.CreateErrorResponse(HttpStatusCode.Unauthorized, ex).ToCompletedTask();
        }
        catch (Exception ex)
        {
            return request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex).ToCompletedTask();
        }

        return base.OnSendAsync(request, cancellationToken);
    }
}


回答2:

The ClaimsAuthenticationManager class is specifically for this.

https://msdn.microsoft.com/en-us/library/system.security.claims.claimsauthenticationmanager(v=vs.110).aspx

Code sample from that reference:

class SimpleClaimsAuthenticatonManager : ClaimsAuthenticationManager
{
    public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
    {
        if (incomingPrincipal != null && incomingPrincipal.Identity.IsAuthenticated == true)
        {
            ((ClaimsIdentity)incomingPrincipal.Identity).AddClaim(new Claim(ClaimTypes.Role, "User"));
        }
        return incomingPrincipal; 
    }
}