views:

773

answers:

2

I have a working custom UserNamePasswordValidator that calls into my Oracle DB.

This class derives from System.IdentityModel.Selectors.UserNamePasswordValidator and the Validate() method returns void.

I load my User object from the database, and once the password is validated, I want to stash my "User" object so the service can access it when going about its business. In ASP.NET / Java land I would stash it into a session, or perhaps my overall Controller class. How do I do this from the Validator in WCF?

Or, in other words, what is the best practice in WCF land to set a custom User domain object for the service.

Update: This is how I've worked around it. I cache the User object during the validator, then access it later in the AuthorizatinPolicy step.

  // this gets called after the custom authentication step where we loaded the User
  public bool Evaluate(EvaluationContext evaluationContext, ref object state)
  {
     // get the authenticated client identity
     IIdentity client = GetClientIdentity(evaluationContext);

     User user;
     OraclePasswordValidator.users.TryGetValue(client.Name, out user);
     if(user != null) {
        // set the custom principal
        evaluationContext.Properties["Principal"] = user;
        return true;
     }

     return false;
  }
+1  A: 

I have exactly the same issue.

I am using an API to connect to my underlying Oracle Database, and I "validate" logon details by opening a connection.

I then want to store this connection somewhere (easy enough, I will create a connection pool for all the different users), but also create a custom Identity and Principal representing this user, so that once it gets to my custom IAuthorizationPolicy, it doesn't need to reload this information.

I have done a lot of searching and not found anything so my plan is to do this:

  1. Validate login details in custom UserNamePasswordValidator by opening API connection.

  2. Store opened connection in connection pool under the user name.

  3. When my custom IAuthorizationPolicy.Evaluate() is called, I will look at the generic identity provided:

    IIdentity GetClientIdentity(EvaluationContext evaluationContext)
    {
        object obj;
        if (!evaluationContext.Properties.TryGetValue("Identities", out obj))
            throw new Exception("No Identity found");
    
    
    
       IList<IIdentity> identities = obj as IList<IIdentity>;
       if (identities == null || identities.Count <= 0)
          throw new Exception("No Identity found");
    
    
       return identities[0];
    
    }

(sorry I can't get rid of this poor HTML escaping)

  1. I then grab a connection from the pool based on the IIdentity.Name, use this connection to load up user-specific data from the database and store this in a custom Identity and Principal which I set in the EvaluationContext:

    public bool Evaluate(EvaluationContext evaluationContext, ref object state)
    {
        IIdentity identity = GetClientIdentity(evaluationContext);
        if (identity == null)
            throw new Exception();
    
    
    
        // These are my custom Identity and Principal classes
        Identity customIdentity = new Identity();
        Principal customPrincipal = new Principal(customIdentity);
        // populate identity and principal as required
        evaluationContext.Properties["Principal"] = customPrincipal;
        return true;
    }
    

Then I should have access to my custom identity and principal whenever I need it by using System.Threading.Thread.CurrentPrincipal or CurrentIdentity.

Hope this helps in some way; I'm not sure it's the best way to go about it, but it's the best I've come up with so far...

Steve

Steve
I used a similar approach except I cache the User object (IPrincipal) instead of the database connection. I tried stashing it to Thread.CurrentPrincipal in the validator, but it gets overwritten with a GenericPrincipalin before the AuthorizationPolicy.Evaluate() call. AuthorizationPolicy.Evaluate is called after I've already loaded my User object in the password validator. I think this was shortsighted by WCF the designers. So the basic idea is where in the Thread do we stash it? Why can't I just do this in the validator and have WCF leave it alone?>> Thread.CurrentPrincipal = user;
mrjoltcola
+1  A: 

I'm not a WCF expert, but from what I've read and implemented so far, the 'correct' way to do this would be to use the Validator to authenticate the user, and then implement an IAuthorizationPolicy to do the actual authorization. So it would be in the authorization policy that you'll set your custom principal on the current thread.

To be able to forward information from the username/password validation, you can implement a security token authenticator that inherits from UserNameSecurityTokenAuthenticator. The SecurityTokenAuthenticator will first call the validator and if validation succeeds, it can add your custom authorization policy and send userinfo to the policy through the constructor. Something a long the lines of this:

public class CustomUsernameSecurityTokenAuthenticator : UserNameSecurityTokenAuthenticator
{
    protected override bool CanValidateTokenCore(System.IdentityModel.Tokens.SecurityToken token)
    {
        return (token is UserNameSecurityToken);
    }

    protected override ReadOnlyCollection<IAuthorizationPolicy> ValidateTokenCore(SecurityToken token)
    {
        var authorizationPolicies = new List<IAuthorizationPolicy>();

        try
        {
            var userNameToken = token as UserNameSecurityToken;
            new CustomUserNameValidator().Validate(userNameToken.UserName, userNameToken.Password);

            var claims = new DefaultClaimSet(ClaimSet.System, new Claim(ClaimTypes.Name, userNameToken.UserName, Rights.PossessProperty));

            authorizationPolicies.Add(new CustomAuthorizationPolicy(claims));
        }
        catch (Exception)
        {
            authorizationPolicies.Add(new InvalidAuthorizationPolicy());
            throw;
        }
        return authorizationPolicies.AsReadOnly();
    }
}

There's an article here that describes a bit more around the involved classes; http://blogs.msdn.com/card/archive/2007/10/04/how-identity-providers-can-show-custom-error-messages-in-cardspace.aspx

Kjetil Klaussen
I'm going to look closer at your answer and give it a try, and update the question. Caching the User (Principal) into a map solved the problem immediately, and I haven't had time to revisit that section of code yet, but yours is the first plausible explanation I've seen.
mrjoltcola