Populating the PrimaryIdentity in WCF

2019-04-01 00:39发布

问题:

I'm using simple HTTP headers to pass a token to a WCF service for authentication (The WCF service is required to use the basicHTTPBinding, so I unfortunately cannot use the canned ws-security implementation). I would like to populate the PrimaryIdentity object so the WCF services can examine it to determine the authenticated user.

The issue is that the OperationContext.Current.ServiceSecurityContext.PrimaryIdentity property is read-only at the time I'm trying to populate it. I've tried using SecurityTokenAuthenticators and IAuthorizationPolicy objects to set the identity info, but that route seems to require the use of message-level security (such as always sending in a username and password), which isn't what I want.

Can anyone shed light on how I could set the PrimaryIdentity field?

回答1:

PrimaryIdentity is not intended to be populated by you - it's the WCF runtime's job to determine who's calling, and set the PrimaryIdentity accordingly.

So if you're calling with Windows credentials, then you'll get a WindowsIdentity stored there; if you're using ASP.NET membership providers, you'll get that caller stored into the PrimaryIdentity.

The only way you could actually set this is by creating your own custom server-side authentication mechanism and plug that into WCF.

See:

  • WCF Authentication Service Overview
  • Fundamentals of WCF Security - Authentication, Authorization, Identities
  • WCF Custom Authentication and Impersonation


回答2:

You can create a class that implments IAuthorizationPolicy. WCF parses all the identity tokens (X509, WS-Security Username/pass, etc.) and puts them in the evaluationContext.Properties["Identities"] which you get in the Evaluate function. This will return a List to you. If you replace this list with a list containing your own class that implements IIdentity then WCF will read that into ServiceSecurityContext.Current.PrimaryIdentity. please ensure that the list only contains one item otherwise you will confuse WCF and PrimaryIdentity.Name will be the empty string.

var myIdentity = new MyIdentity("MyId", otherstuff);
evaluationContext.Properties["Identities"] = new List<IIdentity> {myIdentity};

Also, you may want to process/read any of the tokens before you replace them.

var identities = evaluationContext.Properties.ContainsKey("Identities")
                                 ? (List<IIdentity>) evaluationContext.Properties["Identities"]
                                 : new List<IIdentity>();

var genericIdentity = identities.Find(x => x.AuthenticationType == "MyUserNamePasswordValidator");

genericIdentity.Name --> contains the username from WSS username token.

You would need a UsernamePasswordValidator (http://msdn.microsoft.com/en-us/library/system.identitymodel.selectors.usernamepasswordvalidator.aspx) if you are using WS-Security Username token and do not want any default WCF validation. Since, we have a DataPower device that validates the token before the message gets to our service, we don't need to validate the username and password. In our case it just returns true.



回答3:

This works for me... First setup the authentication behavior of the host (here shown through code, but can also be done in config):

ServiceAuthorizationBehavior author = Description.Behaviors.Find<ServiceAuthorizationBehavior>();
author.ServiceAuthorizationManager = new FormCookieServiceAuthorizationManager();
author.PrincipalPermissionMode = PrincipalPermissionMode.Custom;
author.ExternalAuthorizationPolicies = new List<IAuthorizationPolicy> { new CustomAuthorizationPolicy() }.AsReadOnly();

And then the helper classes

  internal class FormCookieServiceAuthorizationManager : ServiceAuthorizationManager
  {
     public override bool CheckAccess(OperationContext operationContext)
     {
        ParseFormsCookie(operationContext.RequestContext.RequestMessage);
        return base.CheckAccess(operationContext);
     }
     private static void ParseFormsCookie(Message message)
     {
        HttpRequestMessageProperty httpRequest = message.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
        if (httpRequest == null) return;

        string cookie = httpRequest.Headers[HttpRequestHeader.Cookie];
        if (string.IsNullOrEmpty(cookie)) return;

        string regexp = Regex.Escape(FormsAuthentication.FormsCookieName) + "=(?<val>[^;]+)";
        var myMatch = Regex.Match(cookie, regexp);
        if (!myMatch.Success) return;

        string cookieVal = myMatch.Groups["val"].ToString();
        FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(cookieVal);
        Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(authTicket.Name), new string[0]);
     }
  }
  internal class CustomAuthorizationPolicy : IAuthorizationPolicy
  {
     static readonly string _id = Guid.NewGuid().ToString();
     public string Id
     {
        get { return _id; }
     }

     public bool Evaluate(EvaluationContext evaluationContext, ref object state)
     {
        evaluationContext.Properties["Principal"] = Thread.CurrentPrincipal;
        evaluationContext.Properties["Identities"] = new List<IIdentity> { Thread.CurrentPrincipal.Identity };
        return true;
     }

     public ClaimSet Issuer
     {
        get { return ClaimSet.System; }
     }
  }

And for when AspNetCompatibility is set, then FormCookieServiceAuthorizationManager is slightly simpler:

 internal class FormCookieServiceAuthorizationManager : ServiceAuthorizationManager
 {
    public override bool CheckAccess(OperationContext operationContext)
    {
       Thread.CurrentPrincipal = HttpContext.Current.User;
       return base.CheckAccess(operationContext);
    }
 }