Search code examples
wifclaims-based-identitythinktecture-ident-model

Authenticate - provide login email address to lookup user identity


I am using Thinktecture AuthenticationConfiguration to provide an end point for signing tokens on my API:

var authConfig = new AuthenticationConfiguration
{
    EnableSessionToken = true,
    SendWwwAuthenticateResponseHeaders = true,
    RequireSsl = false,
    ClaimsAuthenticationManager = new ClaimsTransformation(),
    SessionToken = new SessionTokenConfiguration
    {
        EndpointAddress = "/api/token",
        SigningKey = signingKey,
        DefaultTokenLifetime = new TimeSpan(1, 0, 0)
    }
};

var userCredentialsService = new CredentialsService(credentialStore);
authConfig.AddBasicAuthentication(userCredentialsService.Validate);

And authenticating users with CredentialsService:

public class CredentialsService
{
    public bool Validate(string username, string password)
    {
        return username == password; 
    }
}

The above works, and no its certainly not used in production, but on returning true i will get a token in which contains a claim with the username.

In my scenario I have a user id (an integer) which can never change and I would like this to be in my claim. So the user would pass an email address to the service endpoint in the header as basic authentication, and then if valid go ahead and sign with the id as the claim (but not the email address as the claim):

public class CredentialsService
{
    public bool Validate(string emailAddress, string password)
    {
        // map from the provided name, to the user id
        var details = MySqlDb.ReadBy(emailAddress);

        var id = details.Id; // this is the actual identity of the user
        var email = details.EmailAddress;
        var hash = details.Hash;

        return PasswordHash.ValidatePassword(password,hash);          
    }
}

I appreciate this will need a second lookup to a sql server database to transform the emailAddress in to a userId, is there a way for me to insert this in to the pipeline flow before CredentialsService is called?

Or am i going about it the wrong way, and just stick with the username that was signed in as, then use a claims transformation based on the username to enrich with the integer identity - but then what if they changed the username?


Solution

  • Ok, I managed to solve this by taking a look at the awesome thinktecture source and overriding BasicAuthenticationSecurityTokenHandler to give a derived class which has a second delegate returning a Claim[] ready to be signed:

    public class BasicAuthSecurityTokenHandlerWithClaimsOutput : BasicAuthenticationSecurityTokenHandler
    {       
        public BasicAuthSecurityTokenHandlerWithClaimsOutput(ValidateUserNameCredentialDelegate validateUserNameCredential, GetClaimsForAuthenticatedUser getClaimsForAuthenticatedUser)
            : base()
        {
            if (validateUserNameCredential == null)
            {
                throw new ArgumentNullException("ValidateUserNameCredential");
            }
    
            if (getClaimsForAuthenticatedUser== null)
            {
                throw new ArgumentNullException("GetClaimsForAuthenticatedUser");
            }
    
            base.ValidateUserNameCredential = validateUserNameCredential;
            _getClaimsForAuthenticatedUser = getClaimsForAuthenticatedUser;
        }
    
        public delegate Claim[] GetClaimsForAuthenticatedUser(string username);
        private readonly GetClaimsForAuthenticatedUser _getClaimsForAuthenticatedUser;
    
        public override ReadOnlyCollection<ClaimsIdentity> ValidateToken(SecurityToken token)
        {
            if (token == null)
            {
                throw new ArgumentNullException("token");
            }
    
            if (base.Configuration == null)
            {
                throw new InvalidOperationException("No Configuration set");
            }
    
            UserNameSecurityToken unToken = token as UserNameSecurityToken;
            if (unToken == null)
            {
                throw new ArgumentException("SecurityToken is not a UserNameSecurityToken");
            }
    
            if (!ValidateUserNameCredentialCore(unToken.UserName, unToken.Password))
            {
                throw new SecurityTokenValidationException(unToken.UserName);
            }
    
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, unToken.UserName),
                new Claim(ClaimTypes.AuthenticationMethod, AuthenticationMethods.Password),
                AuthenticationInstantClaim.Now
            };
    
            var lookedUpClaims = _getClaimsForAuthenticatedUser(unToken.UserName);
    
            claims.AddRange(lookedUpClaims);
    
            if (RetainPassword)
            {
                claims.Add(new Claim("password", unToken.Password));
            }
    
            var identity = new ClaimsIdentity(claims, "Basic");
    
            if (Configuration.SaveBootstrapContext)
            {
                if (this.RetainPassword)
                {
                    identity.BootstrapContext = new BootstrapContext(unToken, this);
                }
                else
                {
                    var bootstrapToken = new UserNameSecurityToken(unToken.UserName, null);
                    identity.BootstrapContext = new BootstrapContext(bootstrapToken, this);
                }
            }
    
            return new List<ClaimsIdentity> {identity}.AsReadOnly();
        }
    }
    

    I then added a second helper method to make it easier to wire up:

    public static class BasicAuthHandlerExtensionWithClaimsOutput
    {
        public static void AddBasicAuthenticationWithClaimsOutput(
            this AuthenticationConfiguration configuration,
            BasicAuthenticationSecurityTokenHandler.ValidateUserNameCredentialDelegate validationDelegate,
            BasicAuthSecurityTokenHandlerWithClaimsOutput.GetClaimsForAuthenticatedUser getClaimsForAuthenticatedUserDelegate,
            string realm = "localhost", bool retainPassword = false)
        {
            var handler = new BasicAuthSecurityTokenHandlerWithClaimsOutput(validationDelegate, getClaimsForAuthenticatedUserDelegate);
            handler.RetainPassword = retainPassword;
    
            configuration.AddMapping(new AuthenticationOptionMapping
            {
                TokenHandler = new SecurityTokenHandlerCollection { handler },
                Options = AuthenticationOptions.ForAuthorizationHeader(scheme: "Basic"),
                Scheme = AuthenticationScheme.SchemeAndRealm("Basic", realm)
            });
        }
    }
    

    Hope this helps others, please let me know if i have done something horrific!