Custom Auth Scenario

Hi, sorry. I need to clarify my understanding of ServiceStack Auth again. Got myself in a twist the last few days.

I am retrofitting ServiceStack to an existing app, that we are hollowing out, and I just need some concrete pointers to straighten out my way forward.
I have a front end web server serving server-side razor and client JSApp code (reactjs), and a bunch of backend APIS services, also SS services. I want the frontend server to be the AutnN server and generate JWT tokens given username+password creds, and then return me the JWT so that the JSApp can access the backend API with the JWT. Backends just use the JwtAuthProviderReader to verify the token, and use the claims within it.

I already have my own username+password tables and user profiles tables in a production database, that I need to maintain. I don’t want sessions. Just JWT. Ideally, I could do without any CustomAuthUserSession as well if possible, and just leave everything I need in the JWT, or look it up from existing tables if I do need it.

So, I am struggling to find the right combo of custom pieces that I need on the frontend server.
I think that all I need is this:

  • AuthFeature + JwtAuthProvider + CustomCredentalsAuthProvider
    Where my CustomCredentialsAuthProvider does my username+password lookup.

Is that all I should need?

  1. Do I actually need a CustomAuthUserSession as well? If, so to do what?
  2. Do I actually need a CustomAuthRepository at all? given that I already have user profile info in existing production tables?
  3. Do I need to store sessions or AuthUser stuff at all in our repository?

I know that there must be a handy SS sample that demonstrates this, but not sure which one it is these days.

You should be able to get by with a Custom AuthProvider that authenticates against your own table and populates the Authenticated User Session.

If you also want refresh token support you’ll need to implement IUserSessionSource.

Hey @mythz I finally got there. One last problem.

I implemented IUserSessionSourceAsync (v.5.10.0 does not have IUserSessionSource) on my CustomCredentialAuthProvider and also implemented it on a standalone class that I registered in the container explicitly.
Neither approach worked, perhaps there is a property on the AuthFeature or on the JwtAuthProvider that is getting in the way?

do you know how to troubleshoot this?

Here is my AuthFeature:

var container = appHost.GetContainer();
            appHost.Plugins.Add(new AuthFeature(() => new AuthUserSession(),
                new IAuthProvider[]
                {
                    new JwtAuthProvider
                    {
                        HashAlgorithm = @"RS256",
                        PrivateKeyXml = appHost.AppSettings.GetString(@"JwtPrivateKeyXml"),
                        EncryptPayload = false,
                        ExpireTokensIn = JwtTokenExpiry,
                        ExpireRefreshTokensIn = JwtRefreshTokenExpiry,
                        UseTokenCookie = false,
                        CreatePayloadFilter = (payload, session) =>
                        {
                            payload[@"sub"] = session.UserAuthId;
                            payload[@"roles"] = session.Roles.ToJson();
                            payload.Remove(@"preferred_username");
                        },
                        PopulateSessionFilter = (session, payload, req) =>
                        {
                            session.Roles = payload[@"roles"].FromJson<List<string>>();
                        },
                        SetBearerTokenOnAuthenticateResponse = true
                    },
                    new PlaybooksCredentialsAuthProvider(container.Resolve<ILogger>(),
                        container.Resolve<IUsersStorage>())
                })
            {
                MaxLoginAttempts = MaxLoginAttempts,
                SaveUserNamesInLowerCase = true,
                ValidateUniqueUserNames = true,
                ValidateUniqueEmails = true,
                IsValidUsernameFn = username =>
                    username.EqualsOrdinal(AuthConstants.PrivilegedAccountName) || Validations.Email.Matches(username),
                IncludeRolesInAuthenticateResponse = true,
                IncludeAssignRoleServices = false,
                IncludeRegistrationService = false
            });

If this API returns your IOC or AuthProvider instance then it’s properly registered:

var userSessionSource = AuthenticateService.GetUserSessionSourceAsync();

Exactly what doesn’t work?

I presumed that my custom IUserSessionSourceAsync implementation would have been called during an authentication flow to produce a refresh token in the Autheticate response, but it is not called at all? and there is no refreshToken in that response.

Is that the wrong assumption?

I can confirm that calling AuthenticateService.GetUserSessionSourceAsync() does return my custom implementation.

So I guess I dont understand who calls it when, to do what?

If it’s configured correctly it should only generate the tokens on authentication, does your Custom AuthProvider call base.OnAuthenticatedAsync() or the alternative to save your session and mark the authentication request?

session.IsAuthenticated = true;
await authService.SaveSessionAsync(session, SessionExpiry, token);
authService.Request.Items[Keywords.DidAuthenticate] = true;
return null;

Im calling base

        public override Task<IHttpResult> OnAuthenticatedAsync(IServiceBase authService, IAuthSession session,
            IAuthTokens tokens, Dictionary<string, string> authInfo,
            CancellationToken token = new CancellationToken())
        {
            var account = this.storage.FindAccountByEmailAddress(session.UserAuthName);
            if (account.NotExists())
            {
                throw new ResourceNotFoundException(
                    Resources.PlaybooksCredentialsAuthProvider_UserNotFound.Format(session.UserAuthName));
            }

            var user = this.storage.GetUser(account.Id.ToIdentifier());
            var authUserSession = (AuthUserSession) session;
            authUserSession.UserAuthId = account.Id;
            authUserSession.UserAuthName = account.EmailAddress;
            authUserSession.UserName = authUserSession.UserAuthName;
            authUserSession.Roles = new List<string> {user.RoleCode};
            authUserSession.Email = authUserSession.UserAuthName;
            authUserSession.FirstName = user.Account.FirstName;
            authUserSession.LastName = user.Account.LastName;
            authUserSession.DisplayName = user.Account.FullName;
            authUserSession.TimeZone = user.Timezone;
            authUserSession.Company = user.OrganisationId;

            return base.OnAuthenticatedAsync(authService, session, tokens, authInfo, token);
        }

It also needs to be called over a secure connection (e.g. https) or configured with RequireSecureConnection:

new JwtAuthProvider {
    RequireSecureConnection = false
}

Yep, that is the case here. RequireSecureConnection=true, and HTTPS all the way.

my response to Authenticate looks like this right now:
(refreshToken is not there right now, but other than that, it is just the way I like it)

{
    "userId": "usr_0r3pWYhVkKiuQ175GMMyw",
    "sessionId": "lqI3miyQm7TSNrQZDjzP",
    "userName": "auser1@company.com",
    "displayName": "afirstname alastname",
    "bearerToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InljTCJ9.eyJpc3MiOiJzc2p3dCIsInN1YiI6InVzcl8wcjNwV1loVmtLaXVRMTc1R01NeXciLCJpYXQiOjE2MTExNjg1NjIsImV4cCI6MTYxMjM3ODE2MiwiZW1haWwiOiJhdXNlcjFAY29tcGFueS5jb20iLCJnaXZlbl9uYW1lIjoiYWZpcnN0bmFtZSIsImZhbWlseV9uYW1lIjoiYWxhc3RuYW1lIiwibmFtZSI6ImFmaXJzdG5hbWUgYWxhc3RuYW1lIiwicm9sZXMiOlsiU1RBTkRBUkQiXX0.ooacbTZd8E0RV3ku5bKMBFIStrP6yBU2c7ysKDkD1vjG2Nc44hOxDfftJzfXt9QXoR2sLecsepPZwhlp_7zNnuAqJEAchDwe3WnrvezybcmKmhynzzJOT-F2qmbTZmBp9kGP1pEf_-oYwfuYpbonBm7my--zOKhooSOgzwsqua1nsUWAlbO9vYhKFqwZGHjtPlhmfLjCr0aF3rydkUMYrYoOb_2xRXc2d0HiAlkzfcFKitinpHJ9saAYzMWEMunw4uaztPduQ5xcLQNJFORJ8_twL2aA_5McTMipElWo9RtOlOWAkQKWacvS561_SwNFTOOmTOVnRUjLDCS8ONbjmw",
    "profileUrl": "data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E %3Cstyle%3E .path%7B%7D %3C/style%3E %3Cg id='male-svg'%3E%3Cpath fill='%23556080' d='M1 92.84V84.14C1 84.14 2.38 78.81 8.81 77.16C8.81 77.16 19.16 73.37 27.26 69.85C31.46 68.02 32.36 66.93 36.59 65.06C36.59 65.06 37.03 62.9 36.87 61.6H40.18C40.18 61.6 40.93 62.05 40.18 56.94C40.18 56.94 35.63 55.78 35.45 47.66C35.45 47.66 32.41 48.68 32.22 43.76C32.1 40.42 29.52 37.52 33.23 35.12L31.35 30.02C31.35 30.02 28.08 9.51 38.95 12.54C34.36 7.06 64.93 1.59 66.91 18.96C66.91 18.96 68.33 28.35 66.91 34.77C66.91 34.77 71.38 34.25 68.39 42.84C68.39 42.84 66.75 49.01 64.23 47.62C64.23 47.62 64.65 55.43 60.68 56.76C60.68 56.76 60.96 60.92 60.96 61.2L64.74 61.76C64.74 61.76 64.17 65.16 64.84 65.54C64.84 65.54 69.32 68.61 74.66 69.98C84.96 72.62 97.96 77.16 97.96 81.13C97.96 81.13 99 86.42 99 92.85L1 92.84Z'/%3E%3C/g%3E%3C/svg%3E",
    "roles": [
        "STANDARD"
    ],
    "responseStatus": {}
}

My AuthFeature configuration is above. Is there something in there that is getting in the way? Custom Auth Scenario - #3 by jezzsantos

Yeah it looks like it’s still checking for IUserSessionSource even though it doesn’t use it, I’ve deprecated and changed it to check for IUserSessionSourceAsync in this commit.

But can work around it if wherever you’ve implemented IUserSessionSourceAsync to also implement:

class CustomCredentialAuthProvider : IUserSessionSource
{
    public IAuthSession GetUserSession(string userAuthId) => 
        throw new NotImplementedException();

    //...
}

OK, so I implemented both IUserSessionSource and IUserSessionSourceAsync in my CustomCredentialsAuthprovider and, I now get a called at IUserSessionSource.GetUserSession() AND there is now a value in the refreshToken in the response to Authenticate.

OK, thank you very much for that sleuthing.
I’ll upgrade on next version of ServiceStack to remove IUserSessionSource implementation.

1 Like