UserAuthId null after upgrading Service Stack from 5.5 to 6.9

Hi,
We have a Self-hosted Service Stack application with a CustomCredentialAuthProvider implementation using custom AuthUserSession and MemoryCacheClient. The authentication call is successful, but the UserAuthId is always “null”.
Authentication Configuration -

 Plugins.Add(new SessionFeature());
  container.Register<ICacheClient>(new MemoryCacheClient());

  Plugins.Add(new AuthFeature(() => new CustomUserSession(),
      new IAuthProvider[]
      {
          new CustomAuthProvider {SessionExpiry = SessionExpiryTimeout}
      })
  { IncludeAssignRoleServices = false, HtmlRedirect = "./#/login" });

This works fine in v5.5 and is an issue only with v6.9. Is there any additional confguration required to ensure UserAuthId is populated in CustomUserSession object?

As this is the first I’ve heard of this issue and you’re using a CustomAuthProvider I’m assuming the issue may lay somewhere in there. Would need a repro to be able to be able to identify the issue.

Here is the AuthProvider implementation -

public class CustomAuthProvider : CredentialsAuthProvider
{
	public override async Task<object> AuthenticateAsync(IServiceBase authService, IAuthSession session, Authenticate request, CancellationToken cancellationToken = default)
	{
		object result = new ServiceStack.AuthenticateResponse();
		
		var DRUserAcctIsLocked = ResultCode.RESULT.SUCCESS;
		try
		{
			Session.ClientMachine = authService.Request?.RemoteIp ?? string.Empty;

			if (request?.Meta != null && request.Meta.Any())
			{
				if (request.Meta.ContainsKey("ApplicationName"))
				{
					Session.ClientAppName = request.Meta["ApplicationName"];
				}

				if (request.Meta.ContainsKey("ApplicationVersion"))
				{
					Session.ClientAppVersion = request.Meta["ApplicationVersion"];
				}

				if (request.Meta.ContainsKey("ApiToken"))
				{
					var token = request.Meta["ApiToken"];

					if (token.IsNotNullOrEmpty() && ValidateToken(token, request.UserName))
					{
						Session.ClientApp = "CaptureServer";
					}
				}
			}

			
				result = await base.AuthenticateAsync(authService, session, request, cancellationToken);
								 

				var docRecordSession = authService.GetSession() as CustomUserSession ;

				if (result is ServiceStack.AuthenticateResponse response && docRecordSession != null)
				{
					LogLoginAttempt(authService, true, request.UserName, string.Empty, docRecordSession);
					response.DisplayName = docRecordSession.DisplayName;
					response.UserName = docRecordSession.UserName;
				}
				else
					LogLoginAttempt(authService, false, request.UserName, "Authentication Failed", null);

				ManageSession(authService, authService.GetSession() as CustomUserSession , request);
			
		}
		catch (Exception ex)
		{
			LogLoginAttempt(authService, false, request.UserName, ex.Message, null);
			((ServiceStack.AuthenticateResponse) result).ResponseStatus = new ResponseStatus
			{
				ErrorCode = ex.ToErrorCode(),
				Message = ex.Message
			};
		}

		return result;
	}
}
public class CustomUserSession : AuthUserSession
{
    [DataMember]
    public string SessionID { get; set; }
    [DataMember]
    public WindowsIdentity WindowsIdentity { get; set; }
    [DataMember]
    public UserDescriptor DRUser { get; set; }
    [DataMember]
    public UserRole Role { get; set; }
    [DataMember]
    public UserProfileData Profile { get; set; }
    [DataMember]
    public WebUserProfile WebUserProfile { get; set; }
    [DataMember] 
    public string ShortUserName { get; set; }
    [DataMember] 
    public string ClientAppName { get; set; }
    [DataMember] 
    public string ClientApp { get; set; }
}

The UserAuthId is typically populated from your registered AuthRepository, what Auth Repository are you using?

Your best bet is debugging into the framework, if you can try put a breakpoint on this line and find out what the userAuth.Id is:

Hi,
We are using InMemoryAuthRepository and userAuth.Id value is “0” and session.UserAuthId is set to null.

In version 5.5, session.UserAuthId value is set to 0.

Is userAuth otherwise populated or an empty instance?

It is slightly populated, mostly nulls.

Then it should be getting populated from the InMemoryAuthRepository, so we’d need to track down why UserAuth.Id is null.

Try putting a breakpoint on InMemoryAuthRepository.GetById to check whether the Id is populated there:

The breakpoint there is never reached.

The UserAuthRepository is inherits from InMemoryAuthRepository and does not explicitly instantiate Id. I am still wrapping my head around how this was implemented but it looks like it should do so automatically or does it need to do so explicitly?

Just to clarify, you are using a custom AuthRepository? Are you able to share it, if we can reproduce the issue we will be able to help resolve the problem you are having.

I’ll see if I can get it formatted to share shortly.

I have been digging more and, if I understand this right, it looks like we assumed that everything we did not define would be handled automatically. So our Custom UserAuth adds some properties:

public class CustomUserAuth : UserAuth
{
	public WindowsIdentity Identity { get; set; }
	public UserDescriptor DRUser { get; set; }
	public UserRole Role { get; set; }
	public string SessionId { get; set; }
	public User User { get; set; }
	public UserProfileData UserProfile { get; set; }
	public WebUserProfile WebUserProfile { get; set; }
	public string ShortUserName { get; set; }
	public string ClientAppName { get; set; }
	public string ClientApp { get; set; }
}

And when the user is declared, we assume the id is handled.

UserAuth = new CustomUserAuth
{
	UserName = user,
	DisplayName = displayName,
	Identity = Identity,
	SessionId = SessionToken,
	User = LoggedInUser,
	Role = UserRole,
	UserProfile = UserProfile,
	WebUserProfile = WebUserProfile,
	ShortUserName = Environment.UserName,
	ClientAppName = Session.ClientAppName,
	ClientApp = Session.ClientApp
};

and there is no attempt to initiate the Id.

Here is the Custom User Auth Repository

using System;
using System.DirectoryServices.AccountManagement;
using App.Interfaces;
using ServiceStack.Auth;
using System.Linq;
using System.Security.Principal;
using App.Security;
using ServiceStack;
using App.Logging;
using System.Threading.Tasks;
using System.Threading;

namespace App.Services
{
    public class CustomUserAuthRepository : InMemoryAuthRepository
    {
        private readonly IDocumentManagerFactory _factory;

        public CustomUserAuthRepository(IDocumentManagerFactory factory)
        {
            _factory = factory;
        }

        public override bool TryAuthenticate(string userName, string password, out IUserAuth userAuth)
        {
            var dm = _factory.GetDocumentManager(true);
            
            var authenticator = new Authenticator(dm);

            authenticator.Authenticate(userName, password);
             
            var result = authenticator.Result;
            userAuth = authenticator.UserAuth;

            if (userAuth != null)
            {
                SaveUserAuth(userAuth);

                if (result && authenticator.UserRole != null && authenticator.UserRole.RoleNames.Any())
                    userAuth.Roles.AddRange(authenticator.UserRole.RoleNames);
            }

            _factory.ReleaseDocumentManager(dm);
            
            return result;
        }

        public override async Task<IUserAuth> TryAuthenticateAsync(string userName, string password, CancellationToken token = default)
        {
            var dm = _factory.GetDocumentManager(true);

            var authenticator = new Authenticator(dm);

            await authenticator.AuthenticateAsync(userName, password);

            var result = authenticator.Result;
            var userAuth = authenticator.UserAuth;

            if (userAuth != null)
            {
                SaveUserAuth(userAuth);

                if (result && authenticator.UserRole != null && authenticator.UserRole.RoleNames.Any())
                    userAuth.Roles.AddRange(authenticator.UserRole.RoleNames);
            }

            _factory.ReleaseDocumentManager(dm);

            return userAuth;
        }

        private class Authenticator
        {
            private readonly IDocumentServicesProvider _service;

            public Authenticator(IDocumentServicesProvider service)
            {
                _service = service;
            }

            public bool Result { get; private set; }

            public CustomUserAuth UserAuth { get; private set; }

            private User LoggedInUser { get; set; }

            internal UserRole UserRole { get; set; }	

            private UserProfileData UserProfile { get; set; }

            private WebUserProfile WebUserProfile { get; set; }

            private WindowsIdentity Identity { get; set; }

            private string SessionToken { get; set; }
            public void Authenticate(string userName, string password)
            {
                Result = false;
                UserAuth = null;

                var isWindowsUser = userName.Contains('\\') || userName.Contains('@');

                if (isWindowsUser)
                {
                    LogonWindowsUser(userName, password);
                    return;
                }

                LogonCustomUser(userName, password);

                if (!Result)
                {
                    LogonWindowsUser(userName, password);
                }
            }

            public async Task<UserAuth> AuthenticateAsync(string userName, string password)
            {
                Result = false;
                UserAuth = null;

                var isWindowsUser = userName.Contains('\\') || userName.Contains('@');

                if (isWindowsUser)
                {
                    await LogonWindowsUserAsync(userName, password);
                    return UserAuth;
                }

                await LogonCustomUserAsync(userName, password);

                if (!Result)
                {
                    await LogonWindowsUserAsync(userName, password);
                }

                return UserAuth;
            }

            private void LogonWindowsUser(string username, string password)
            {
                var token = IntPtr.Zero;
                try
                {
                    GetUserAndDomainNames(username, out username, out var domainName);

                    AdvApi.LogonUser(username, domainName, password, LogonType.NETWORK,
                        LogonProvider.DEFAULT, ref token);

                    if (token != IntPtr.Zero)
                    {
                        AdvApi.ImpersonateLoggedOnUser(token);

                        var user = WindowsUserNameHelper.GetUserPrincipalName();
                        var displayName = WindowsUserNameHelper.GetUserDisplayName();

                        if (string.IsNullOrEmpty(user))
                            user = WindowsUserNameHelper.GetUserName();

                        if (user != null)
                        {
                            LoggedInUser = new User(user)
                            {
                                Type = UserType.WindowsUser,
                            };

                            Identity = new WindowsIdentity(token);

                            AdvApi.RevertToSelf();

                            StartSession();

                            if (string.IsNullOrEmpty(SessionToken) && UserRole == null)
                            {
                                Result = false;
                                return;
                            }

                            UserAuth = new CustomUserAuth
                            {
                                UserName = user,
                                DisplayName = displayName,
                                Identity = Identity,
                                SessionId = SessionToken,
                                User = LoggedInUser,
                                Role = UserRole,
                                UserProfile = UserProfile,
                                WebUserProfile=WebUserProfile,
                                ShortUserName = Environment.UserName,
                                ClientAppName = Session.ClientAppName,
                                ClientApp = Session.ClientApp
                            };

                            try
                            {
                                UserAuth.Email = UserPrincipal.Current.EmailAddress;
                            }
                            catch (Exception ex)
                            {
                                if (UserAuth.Email == null)
                                {
                                    UserAuth.Email = "";
                                }
                                LogMain.Main.LogException(ex);
                            }

                            Result = true;               
                        }
                    }
                    else
                    {
                        Result = false;
                    }
                }
                finally
                {
                    if (token != IntPtr.Zero)
                    {
                        AdvApi.CloseHandle(token);
                        AdvApi.RevertToSelf();
                    }
                }
            }
            private async Task LogonWindowsUserAsync(string username, string password)
            {
                var token = IntPtr.Zero;
                try
                {
                    GetUserAndDomainNames(username, out username, out var domainName);

                    await Task.Run(() => AdvApi.LogonUser(username, domainName, password, LogonType.NETWORK,
                                        LogonProvider.DEFAULT, ref token));

                    if (token != IntPtr.Zero)
                    {
                        AdvApi.ImpersonateLoggedOnUser(token);

                        var user = WindowsUserNameHelper.GetUserPrincipalName();
                        var displayName = WindowsUserNameHelper.GetUserDisplayName();

                        if (string.IsNullOrEmpty(user))
                            user = WindowsUserNameHelper.GetUserName();

                        if (user != null)
                        {
                            LoggedInUser = new User(user)
                            {
                                Type = UserType.WindowsUser,
                            };

                            Identity = new WindowsIdentity(token);

                            AdvApi.RevertToSelf();

                            StartSession();

                            if (string.IsNullOrEmpty(SessionToken) && UserRole == null)
                            {
                                Result = false;
                                return;
                            }

                            UserAuth = new CustomUserAuth
                            {
                                UserName = user,
                                DisplayName = displayName,
                                Identity = Identity,
                                SessionId = SessionToken,
                                User = LoggedInUser,
                                Role = UserRole,
                                UserProfile = UserProfile,
                                WebUserProfile = WebUserProfile,
                                ShortUserName = Environment.UserName,
                                ClientAppName = Session.ClientAppName,
                                ClientApp = Session.ClientApp
                            };

                            try
                            {
                                UserAuth.Email = UserPrincipal.Current.EmailAddress;
                            }
                            catch (Exception ex)
                            {
                                if (UserAuth.Email == null)
                                {
                                    UserAuth.Email = "";
                                }
                                LogMain.Main.LogException(ex);
                            }

                            Result = true;
                        }
                    }
                    else
                    {
                        Result = false;
                    }
                }
                finally
                {
                    if (token != IntPtr.Zero)
                    {
                        AdvApi.CloseHandle(token);
                        AdvApi.RevertToSelf();
                    }
                }
            }

            private void GetUserAndDomainNames(string value, out string userName, out string domainName)
            {
                if (value.Contains("@"))
                {
                    var parts = value.Split('@');

                    userName = parts[0];
                    domainName = parts[1];
                    return;
                }
                if (value.Contains("\\"))
                {
                    var parts = value.Split('\\');
                    domainName = parts[0];
                    userName = parts[1];
                    return;
                }
                userName = value;
                domainName = GeneralHelper.GetDomainName();
            }

            private void LogonCustomUser(string userName, string password)
            {
                var ud = new UserDescriptor {UserName = userName, PasswordString = password};

                ud.HashPassword();                

                if (!ResultCode.Succeeded(_service.GetUser(ref ud)))
                {
                    return;
                }

                LoggedInUser = new User(userName) {Type = UserType.CustomUser};

                StartSession();

                if (string.IsNullOrEmpty(SessionToken) && UserRole == null)
                {
                    Result = false;
                    return;
                }

                UserAuth = new CustomUserAuth
                {
                    UserName = ud.UserName,
                    DisplayName = ud.FullName,
                    SessionId = SessionToken,
                    User = LoggedInUser,
                    Role = UserRole,
                    DRUser = ud,
                    UserProfile = UserProfile,
                    WebUserProfile=WebUserProfile,
                    ShortUserName = ud.UserName,
                    ClientApp = Session.ClientApp,
                    ClientAppName = Session.ClientAppName,
                    Email = ud.Email
                };

                Result = true;
            }

            private async Task LogonCustomUserAsync(string userName, string password)
            {
                var ud = new UserDescriptor { UserName = userName, PasswordString = password };

                ud.HashPassword();
                var retVal = await Task.Run(() => _service.GetUser(ref ud));
                if (!ResultCode.Succeeded(retVal))
                {
                    return;
                }

                LoggedInUser = new User(userName) { Type = UserType.CustomUser };

                StartSession();

                if (string.IsNullOrEmpty(SessionToken) && UserRole == null)
                {
                    Result = false;
                    return;
                }

                UserAuth = new CustomUserAuth
                {
                    UserName = ud.UserName,
                    DisplayName = ud.FullName,
                    SessionId = SessionToken,
                    User = LoggedInUser,
                    Role = UserRole,
                    DRUser = ud,
                    UserProfile = UserProfile,
                    WebUserProfile = WebUserProfile,
                    ShortUserName = ud.UserName,
                    ClientApp = Session.ClientApp,
                    ClientAppName = Session.ClientAppName,
                    Email = ud.Email
                };

                Result = true;
            }

            private void StartSession()
            {
                var nameDs = Role.GetUserRoleDs(LoggedInUser, true, Identity);

                Session.User = LoggedInUser.Name;
                LogMain.Main.LogDebug($"StartSession - User:{Session.User}; ClientApp:{Session.ClientApp}");
                if (Session.ClientApp.IsNullOrEmpty())
                {
                    Session.ClientApp = "App API";
                }
                
                var rval = _service.StartSession(nameDs, out var userRole, out var userProfile, out var webuserProfile);

                if (!ResultCode.Succeeded(rval))
                {
                    return;
                }

                if (userRole != null) UserRole = userRole;
                if (userProfile != null) UserProfile = userProfile;
                if (webuserProfile != null) WebUserProfile = webuserProfile;

                SessionToken = Session.ID;
            }
        }
    }
}
session.UserAuthId ??= (userAuth.Id != default ? userAuth.Id.ToString(CultureInfo.InvariantCulture) : null);

As the session code above from the previous responses shows, if the userAuth.Id is default, eg 0 or not instantiated/changed in anyway, the session.UserAuthId will be null. Since you have your own custom UserAuth, AuthRepository etc, the Id value should be set based on your customization. This will flow on to the session when it has a value. What value that should be is going to come down to your own system, usually this will be populated with a value that references the built in ServiceStack AuthRepository and backing database primary key.

Thank you for your assistance. Do you know if/where the documentation is on implementing custom auth repositories?

Our UserAuth Repository Docs contains the interfaces that need to be implemented along with the different AuthRepositories that are available, which you can find the source code for by searching for *AuthRepository files in github.com/ServiceStack/ServiceStack repo.