Strange behavior with AuthenticateAttribute

Preface

We just upgraded our license from 4.0.33 to 4.5.12 and this seems to be the only problem I’m encountering. The documentation hasn’t seemed to change much on doing custom authentication. When looking at this part of the documentation my code looks like it should be able to stay much the same.

The Problem

It seems that when using a CustomCredentialProvider inheriting from CredentialsAuthProvider, the session information doesn’t pass the test for the AuthenticateAttribute to show as authenticated and kicks back a 401 even though I’m logged in.

The Code

Here’s the code for CustomCredentialsAuthProvider.cs

public class CustomCredentialsAuthProvider : CredentialsAuthProvider
{
    private readonly IDbConnectionFactory _connectionFactory;
    private readonly LogProvider _logger;

    public CustomCredentialsAuthProvider(IDbConnectionFactory connectionFactory,
        IAppSettings appSettings) : base(appSettings)
    {
        _connectionFactory = connectionFactory;
        _logger = new LogProvider(_connectionFactory.OpenDbConnection());
    }

    public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
    {
        userName.ThrowIfNullOrEmpty();
        password.ThrowIfNullOrEmpty();

        using (var db = _connectionFactory.Open())
        {
            //Custom authentication logic goes here
            var user = db.FirstOrDefault<DashboardUser>(u => u.UserName == userName && u.Active);

            var success = user != null && BCrypt.Net.BCrypt.Verify(password, user.Password);

            if (!success)
                if (user != null)
                    _logger.AddLogMessage(user.Id, user.UserName, user.ReferenceLocationId,
                        Tokens.LoggingEvent.Login, "Unsuccessful login attempt.");
                else
                    _logger.AddLogMessage(0, userName, 0, Tokens.LoggingEvent.Login,
                        string.Format("Unsuccessful login attempt with UserName: {0}", userName));

            return success;
        }
    }

    public override IHttpResult OnAuthenticated(IServiceBase authService, IAuthSession session, IAuthTokens tokens,
        Dictionary<string, string> authInfo)
    {
        using (var db = _connectionFactory.Open())
        {
            //Fill the IAuthSession with data
            var user = db.FirstOrDefault<DashboardUser>(u => u.UserName == session.UserAuthName);
            var userSession = (CustomUserSession) session;
            var roles = userSession.Roles ?? (userSession.Roles = new List<string>());
            var permissions = userSession.Permissions ?? (userSession.Permissions = new List<string>());

            session.PopulateWithNonDefaultValues(user);

            if (user.CanManageUsers) permissions.Add(Tokens.Permissions.CanManageUsers);

            //Important! Save the session data.
            authService.SaveSession(session, SessionExpiry);

            //log login
            _logger.AddLogMessage(userSession, Tokens.LoggingEvent.Login, "Successful login.");
        }

        return null;
    }
}

And here’s the code registering it in my AppHost.cs

 private void ConfigureAuth(Container container)
{
    container.Register<IAppSettings>(new AppSettings());
    container.RegisterAutoWired<CustomCredentialsAuthProvider>();

    //Default route: /auth/{provider}
    Plugins.Add(new AuthFeature(() => new CustomUserSession(),
        new IAuthProvider[] {container.Resolve<CustomCredentialsAuthProvider>()}));

    //Default route: /register
    //Plugins.Add(new RegistrationFeature()); 
}

This is the method I’m trying to call in my Services

[Authenticate]
public ResponseStatus Any(RegisterComputer request)
{
    var locationId = UserSession.ReferenceLocationId;

    var location = Db.First<Location>(x => x.RefLocationID == locationId);
    Cache.Set(Tokens.CacheItems.LocationCacheKey, location, TimeSpan.FromHours(1));

    if (UserSession.CanManageUsers && //they are admin
        DateTime.UtcNow <= location.ComputerRegistrationExpiration)
    {
        Logger.AddLogMessage(UserSession, Tokens.LoggingEvent.ComputerRegister, "Computer registered.");
        Response.SetCookie(Tokens.Cookies.ComputerRegistrationKey, location.ComputerRegistrationKey.ToString(),
            DateTime.MaxValue);
    }

    return new ResponseStatus();
}

Conclusion

This all looks fine, but doesn’t seem to work anymore. It will authenticate me using the provider but the first call to anything with the AuthenticateAttribute will always result in 401. I tried using the debug symbols to see if I could step into the AuthenticateAttribute but can’t seem to get that to work. Any guidance would be appreciated.

Can you post your code for CustomUserSession as well as the raw HTTP Request Headers for the first call that authenticates + the subsequent call returning 401.

Note: AuthProivders are singleton instances, keeping an open DB connection for each class that needs logging for the lifetime of the App could result in a lot of open db connections. I’d just be passing the IDbConnectionFactory to the LogProvider and resolve a db connection each time you log, most ADO.NET providers implement connection pooling so it would be more efficient than holding lots of DB connections open.

Thanks for your response! I’ll fix that db connection leak right now.

Also of note, UserSession is just SessionAs() in the AbstractBaseService

Here is CustomUserSession.cs

public class CustomUserSession : AuthUserSession
{
    public int ReferenceLocationId { get; set; }
    public bool CanManageUsers { get; set; }
}

These headers are from Postman

Headers for call to /auth

Access-Control-Allow-Credentials →true
Access-Control-Allow-Headers →Content-Type,Authorization,X-ApiKey,X-Auth,X-Requested-With,X-ComputerTokenBypass
Access-Control-Allow-Methods →GET, POST, PUT, DELETE, OPTIONS
Cache-Control →private
Content-Length →78
Content-Type →application/json; charset=utf-8
Date →Tue, 29 Aug 2017 18:39:53 GMT
Server →Microsoft-IIS/10.0
Set-Cookie →ss-id=l1puiOrkyOmjdx8sCQ1I; path=/; HttpOnly
Set-Cookie →ss-pid=ZVNE1b7MFqO0IdOm7r2z; expires=Sat, 29-Aug-2037 18:39:52 GMT; path=/; HttpOnly
Set-Cookie →ss-opt=perm; expires=Sat, 29-Aug-2037 18:39:52 GMT; path=/; HttpOnly
Vary →Accept
X-AspNet-Version →4.0.30319
X-Powered-By →ServiceStack/4.512 NET45 Win32NT/.NET
X-Powered-By →ASP.NET
X-SourceFiles →=?UTF-8?B?QzpcRGV2XERpZXRTeXN0ZW1zXERpZXRTeXN0ZW1zLlNlcnZpY2VzLlBvcnRhbERhc2hib2FyZC5XZWJcYXV0aA==?=

For call to /servicewithauthenticateattribute

Access-Control-Allow-Credentials →true
Access-Control-Allow-Headers →Content-Type,Authorization,X-ApiKey,X-Auth,X-Requested-With,X-ComputerTokenBypass
Access-Control-Allow-Methods →GET, POST, PUT, DELETE, OPTIONS
Cache-Control →private
Content-Length →0
Date →Tue, 29 Aug 2017 18:40:26 GMT
Server →Microsoft-IIS/10.0
Vary →Accept
WWW-Authenticate →credentials realm="/auth/credentials"
X-AspNet-Version →4.0.30319
X-Powered-By →ServiceStack/4.512 NET45 Win32NT/.NET
X-Powered-By →ASP.NET
X-SourceFiles →=?UTF-8?B?QzpcRGV2XERpZXRTeXN0ZW1zXERpZXRTeXN0ZW1zLlNlcnZpY2VzLlBvcnRhbERhc2hib2FyZC5XZWJccmVnaXN0ZXJ3b3Jrc3RhdGlvbg==?=

It doesn’t look like the Cookies aren’t being sent in the second request which explains why it’s returning a 401.

The issue then becomes why aren’t they being sent as all clients should be sending Cookies they’re instructed to send with subsequent requests. Given the expiry date hasn’t elapsed, common reasons why Cookies aren’t sent, is the request is to a different domain or sub domain, if you’re switching from http to https, if you’re making an ajax request and are not explicitly requesting fetch to include credentials.

Is there a reason why postman wouldn’t send the cookies? I mean my application does. I’ve written A LOT of code around making sure of that.

Don’t know what’s causing Postman not to send them, but we have a feature in our Postman plugin that allows you to export your authenticated session cookies with /postman?exportSession=true which redirects you to a URL you can copy in Postman to make authenticated requests. You’ll need to explicitly enable this feature with:

Plugins.Add(new PostmanFeature { 
    EnableSessionExport = true
});

I’m guessing I should just revert my packages back to 4.0.33. I mean prior version not broken, this version broken. I don’t really get it. I’m trying to stand up a spike to specifically target the AuthenticateAttribute now using nothing but a CustomCredentialsProvider and CustomSession. I pretty much try to circumvent all other auth done by ServiceStack seeing as how these are very old systems that have authentication all ready.

You could look to see if there’s any differences between the raw HTTP Response v4.0.33 returns vs v4.5.12 to see if there’s something new that’s triggering different behavior in Postman.

It’s not just Postman that behaves differently. It’s the entirety of my code base. It’s all broken because ServiceStack is the backbone of communication throughout all our systems. This is my 3rd attempt at upgrading now and this one isn’t going so well either. I may just have to be happy with old versions.

ServiceStack is returning the Session Cookies, the issue with the HTTP headers posted above is the client not re-sending the cookies. If the Cookies were being sent and ServiceStack was still returning a 401, then you could then check the CacheClient to see it has an Authenticated Session at the Session Cookies. If there is an Authenticated Session then GetSession() or SessionAs<T>() will return it.

Also note that each Auth Provider determines when a Session is Authenticated by implementing IsAuthorized(), you could override it to see if it’s returning false when you think the Session should be Authenticated.

Maybe just download this zip file here extract it and run. It’s a bare bones ServiceStack project. I only implemented CustomCredentialsAuthProvider and CustomUserSession. When you run it, POST to /auth, then post to /hello/world and you’ll be met with a 401. Cookies or no cookies. But definitely with cookies.

Let me stop for a moment and thank you for all of your help. It’s been amazing and I am very appreciative of it.

I might mis-understand what is going on or what is supposed to be happening. In old versions all I needed was the CustomCredentialsAuthProvider and if authentication went through there successfully the AuthenticateAttribute would work as designed. Is there something else I need to be doing in later versions of the framework to let it know that I am authenticated? Do I need to implement IsAuthorized on it now? Has it changed? Like I said. It used to work just fine.

1 Like

The issue is because you’re overriding the Session Id that the Session is stored against, this should be using the Session Cookie Id, so it will work if you remove:

//userSession.Id = "1";

Also you can go to /auth to check if ServiceStack thinks you’re authenticated or not.

Excellent I will try that out. In my CustomCredentialsAuthProvider I have this line:

session.PopulateWithNonDefaultValues(user);

That is what would emulate that session.Id being set to “1”

Ok so you’ll just need to preserve and restore the id, e.g:

var hold = session.Id;
session.PopulateWithNonDefaultValues(user);
session.Id = hold;

Yep, just got that implemented. That was a strange one. The weirdest part is all of this used to work. Just got that implemented in 1 of my 4 ServiceStack projects and it works swimmingly. Thanks for everything!

1 Like