How to add a database retrieved value custom claim to JWT Token using ServiceStack

Viewed 569

In AppHost.Configure I have the following code:

Plugins.Add(new AuthFeature(() => new AuthUserSession(),
    new IAuthProvider[] {
        new JwtAuthProvider
        {
            HashAlgorithm = "HS256",
            RequireSecureConnection = requireSecureConnection,
            AuthKeyBase64 = _configuration["AuthSettings:JwtAuthKeyBase64"],//Settings.Value.JwtAuthKeyBase64,
            ExpireTokensIn        = TimeSpan.FromHours(_configuration["AuthSettings:ExpireTokensIn"].ToDouble()),  // JWT Token Expiry
            ExpireRefreshTokensIn = TimeSpan.FromHours(_configuration["AuthSettings:ExpireRefreshTokensIn"].ToDouble()), // Refresh Token Expiry,
            CreatePayloadFilter = (payload,session) => {
                    payload["ZipCode"] = "value_from_database_for_user";
            }
        },
        new CustomCredentialsAuthProvider(), //HTML Form post of User/Pass
    }));

The above code is standard ServiceStack JwtAuthProvider code.

You can see in the above code that the implementation for the anonymous function bound to CreatePayloadFilter would like to retrieve a value from the database, the user's ZipCode is the value and add that as a custom claim to the token.

For many obvious reasons, implementing the retrieval of the user's ZipCode in the AppHost is not easy, elegant or system/architecturally sound. Also it is not even possible, as I will not have the UserId, AppHost is just startup configuration code ran when the service starts.

You can also see in the above code that I have implemented a CustomCredentialsAuthProvider, I can load session data for the logged in user in CustomCredentialsAuthProvider and ServiceStack will map the session values to hydrate the appropriate JWT claims but I cannot add a custom claim via the ServiceStack session object, here is the implementation of CustomCredentialsAuthProvider:

public class CustomCredentialsAuthProvider : CredentialsAuthProvider
{
    public override bool TryAuthenticate(IServiceBase authService,
        string userName, string password)
    {
        return true;
        //throw new NotImplementedException();
    }

    public override IHttpResult OnAuthenticated(IServiceBase authService,
        IAuthSession session, IAuthTokens tokens,
        Dictionary<string, string> authInfo)
    {
        //Fill IAuthSession with data you want to retrieve in the app eg:
        session.FirstName = "some_firstname_from_db";
        session.LastName = "some_lastname_from_db";
        session.Roles = new List<string> {"role1", "role2"};
        session.Permissions = new List<string> { "permission1", "permission2" };
        session.Email = "test@test.com";
        session.AuthProvider = "credentials";

        //...

        //Call base method to Save Session and fire Auth/Session callbacks:
        return base.OnAuthenticated(authService, session, tokens, authInfo);

        //Alternatively avoid built-in behavior and explicitly save session with
        //authService.SaveSession(session, SessionExpiry);
        //return null;
    }
}

How can I add a custom claim to the ServiceStack framework created JWT, the value of that claim coming from the database and this implementation of this code not be function binding in AppHost.cs where I do not even have the UserId to retrieve the value anyways?

2 Answers

It sounds like you want to use a Custom UserSession to hold the additional metadata, you can tell ServiceStack to use your Custom Session when you register the AuthFeature, e.g:

Plugins.Add(new AuthFeature(() => new CustomUserSession(), ...)

After which you can cast the UserSession to your CustomUserSession to access the additional properties.

To add additional metadata to the JWT Token you'd use CreatePayloadFilter to add the data to the JWT Token and a corresponding PopulateSessionFilter to populate your Custom UserSession with the additional data.

I wanted to provide my final code for this even though @mythz provided the answer:

The CustomerUserSession:

[DataContract]
public class CustomUserSession: AuthUserSession
{
    [DataMember]
    public string ZipCode { get; set; }
}

The CustomCredentialsAuthProvider because I have a legacy db with all account info:

public override bool TryAuthenticate(IServiceBase authService,
        string userName, string password)
    {
        return true;
    }

    public override IHttpResult OnAuthenticated(IServiceBase authService,
        IAuthSession session, IAuthTokens tokens,
        Dictionary<string, string> authInfo)
    {
        var customUserSession = (CustomUserSession) session;
        //Fill IAuthSession with data you want to retrieve in the app eg:
        customUserSession.FirstName = "some_firstname_from_db";
        customUserSession.LastName = "some_lastname_from_db";
        customUserSession.Roles = new List<string> {"role1", "role2"};
        customUserSession.Permissions = new List<string> { "permission1", "permission2" };
        customUserSession.Email = "test@test.com";
        customUserSession.AuthProvider = "credentials";
        customUserSession.CreatedAt = DateTime.UtcNow;
        customUserSession.ZipCode = "92123";


        //Call base method to Save Session and fire Auth/Session callbacks:
        return base.OnAuthenticated(authService, session, tokens, authInfo);
    }

And my AppHost adding of the ServiceStack AuthFeature:

Plugins.Add(new AuthFeature(() => new CustomUserSession(),
    new IAuthProvider[] {
        new JwtAuthProvider
        {
            HashAlgorithm = "HS256",
            RequireSecureConnection = requireSecureConnection,
            AuthKeyBase64 = _configuration["AuthSettings:JwtAuthKeyBase64"],//Settings.Value.JwtAuthKeyBase64,
            ExpireTokensIn        = TimeSpan.FromHours(_configuration["AuthSettings:ExpireTokensIn"].ToDouble()),  // JWT Token Expiry
            ExpireRefreshTokensIn = TimeSpan.FromHours(_configuration["AuthSettings:ExpireRefreshTokensIn"].ToDouble()), // Refresh Token Expiry,
            CreatePayloadFilter = (payload,session) => {
                    payload["zipCode"] = ((CustomUserSession)session).ZipCode;
            },
            PopulateSessionFilter = (session, token, req) => {
                ((CustomUserSession) session).ZipCode = token["zipCode"];
            }
        },
        new CustomCredentialsAuthProvider() //HTML Form post of User/Pass
    }));
Related