Different authentication behavior between 4.0.42 und 4.5

A customer of us managed to authenticate against our API from an existing session. That means that the request against the auth endpoint contains the ss-id of an existing session. With 4.0.42 this seemed to work though. Now on 4.5 we get only every other request to the Authenticate method of our AuthProvider implementation. If you do 5 auth requests in a row without cleaning the cookies or logging out, the requests 1, 3, and 5 will go through to the Authenticate method, 2 and 4 will not.

The requests we don’t get are shown to the customer as success and get an own session ID, but any further requests for this session are failing on our IsAuthorized method because the server side data is missing.

This looks like a bug to me. Has there anything changed in this area that can cause this behavior?

Thanks,
Joerg

That does seem odd, it should generate new cookies on each auth attempt. Could you also send the raw HTTP Request/Response Headers for requests 1-3 (using something like Fiddler or Chrome’s built-in WebInspector if they’re logging in via the Web)

Also can you try disabling GenerateNewSessionCookiesOnAuthentication and let me know if it goes through each time:

Plugins.Add(new AuthFeature(...) {
    GenerateNewSessionCookiesOnAuthentication = false
});

This setting works, as the first session is reused for all further requests. My Authenticate method is only called once. For our case this is sufficient, as it matches the behavior of the older version.

It doesn’t take the data provided in the auth call into account though. I.e. when I login as user A and then do the second login as user B all my operations will be done as user A.

Here are the HTTP Request and Response Headers without the GenerateNewSessionCookiesOnAuthentication setting:

First auth request:

POST http://vidrn301.vi.lan/D1IMAppServer-7.1/auth/apphost?format=json HTTP/1.1
Content-Type: text/plain; charset=utf-8
Host: vidrn301.vi.lan
Content-Length: 53
Expect: 100-continue
Connection: Keep-Alive

HTTP/1.1 200 OK
Cache-Control: private
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/10.0
X-StackifyID: V1|750c53bf-767e-42c9-a0e4-6658239e2bf1|
X-String-Rev: 636167218009971028
X-Content-Type-Options: nosniff
Set-Cookie: ss-id=HLpDOLZvFnXsUNiMSKmx; path=/; HttpOnly
Set-Cookie: ss-pid=li8ZjbyAhJux4fmnXNge; expires=Sun, 07-Dec-2036 15:36:42 GMT; path=/; HttpOnly
Set-Cookie: ss-opt=temp; expires=Sun, 07-Dec-2036 15:36:42 GMT; path=/; HttpOnly
X-Powered-By: ASP.NET
Date: Wed, 07 Dec 2016 15:36:42 GMT
Content-Length: 773

Second auth request:

POST http://vidrn301.vi.lan/D1IMAppServer-7.1/auth/apphost?format=json HTTP/1.1
Content-Type: text/plain; charset=utf-8
Host: vidrn301.vi.lan
Cookie: ss-id=HLpDOLZvFnXsUNiMSKmx; ss-pid=li8ZjbyAhJux4fmnXNge; ss-opt=temp
Content-Length: 53
Expect: 100-continue

HTTP/1.1 200 OK
Cache-Control: private
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/10.0
X-StackifyID: V1|8bdcad64-4d86-4d4f-9283-d55cff803a82|
X-String-Rev: 636167218009971028
X-Content-Type-Options: nosniff
Set-Cookie: ss-id=LyEWV2wsgE0mbpiZjlkX; path=/; HttpOnly
Set-Cookie: ss-pid=0gbalHwwoqStI71xZp9s; expires=Sun, 07-Dec-2036 15:36:43 GMT; path=/; HttpOnly
Set-Cookie: ss-opt=temp; expires=Sun, 07-Dec-2036 15:36:43 GMT; path=/; HttpOnly
X-Powered-By: ASP.NET
Date: Wed, 07 Dec 2016 15:36:42 GMT
Content-Length: 73

Third auth request:

POST http://vidrn301.vi.lan/D1IMAppServer-7.1/auth/apphost?format=json HTTP/1.1
Content-Type: text/plain; charset=utf-8
Host: vidrn301.vi.lan
Cookie: ss-id=LyEWV2wsgE0mbpiZjlkX; ss-pid=0gbalHwwoqStI71xZp9s; ss-opt=temp
Content-Length: 53
Expect: 100-continue

HTTP/1.1 200 OK
Cache-Control: private
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/10.0
X-StackifyID: V1|0c4dbff7-45f3-4f69-beab-9963103f6d37|
X-String-Rev: 636167218009971028
X-Content-Type-Options: nosniff
Set-Cookie: ss-id=xJAhAA1ME0FzO3mcTmDP; path=/; HttpOnly
Set-Cookie: ss-pid=XbHG7Eop4YVpd1nFOTi8; expires=Sun, 07-Dec-2036 15:36:43 GMT; path=/; HttpOnly
Set-Cookie: ss-opt=temp; expires=Sun, 07-Dec-2036 15:36:43 GMT; path=/; HttpOnly
X-Powered-By: ASP.NET
Date: Wed, 07 Dec 2016 15:36:42 GMT
Content-Length: 773

The different content length comes from our own AuthenticateResponse which is only sent when the Authenticate function is been called.

I have built a simple test case showing this behavior: https://github.com/joero74/ServiceStack-Auth-Problem

Please note that normally you’d inherit an existing AuthProvider like CredentialsAuthProvider, this makes sure that the necessary Auth events that’s in OnAuthenticated() are called. Overriding Authenticate() effectively replaces the built-in implementation so it’s an advanced feature where you need to know what you’re doing. Your default implementation has a lot missing, normally you’d have a Username or Email, Password and UserAuthId.

The issue here is that your custom HelloAuthProvider is incompatible with the default GenerateNewSessionCookiesOnAuthentication behavior.

public class HelloAuthProvider : AuthProvider
{
    private readonly ConcurrentDictionary<string, bool> _sessions = new ConcurrentDictionary<string, bool>();

    public HelloAuthProvider()
    {
        Provider = "apphost";
        AuthRealm = "/auth/" + Provider;
    }

    public override object Authenticate(IServiceBase authService, IAuthSession session, Authenticate request)
    {
        Console.WriteLine($$"Authenticate called: session.IsAuthenticated = {session.IsAuthenticated}");

        session.IsAuthenticated = true;

        _sessions[session.Id] = true;

        return new AuthenticateResponse
        {
            SessionId = session.Id
        };
    }

    public override bool IsAuthorized(IAuthSession session, IAuthTokens tokens, Authenticate request = null)
    {
        return session?.Id != null && _sessions.ContainsKey(session.Id);
    }
}

You’re using the session id to determine whether a session is authenticated, but by the time the [Authenticate] calls IsAuthorized() to verify the Session, the cookies/sessionId have already been regenerated where the sessionId that was authenticated is no longer the session that’s being validated. So this logic is incompatible with GenerateNewSessionCookiesOnAuthentication which you need to disable in order to get this to verify + validate successfully, i.e:

Plugins.Add(new AuthFeature(() => new AuthUserSession(), new IAuthProvider[] {
        new HelloAuthProvider(),
    }) {
    GenerateNewSessionCookiesOnAuthentication = false,
});

If you changed it to use the built-in CredentialsAuthProvider by adding it to AuthFeature, e.g:

public override void Configure(Funq.Container container)
{
    container.Register<IAuthRepository>(c =>
        new InMemoryAuthRepository());

    Plugins.Add(new AuthFeature(
        () => new AuthUserSession(),
        new IAuthProvider[]
            {
                new HelloAuthProvider(),
                new CredentialsAuthProvider(), 
            })
    {
        IncludeRegistrationService = true,
    });
}

Then moving the HelloService outside of the Program class so it can be reused, e.g:

[Route("/hello/{Name}")]
public class Hello : IReturn<HelloResponse>
{
    public string Name { get; set; }
}

public class HelloResponse
{
    public string Result { get; set; }
}

[Authenticate]
public class HelloService : Service
{
    public object Any(Hello request)
    {
        var session = base.SessionAs<AuthUserSession>();
        return new HelloResponse { 
            Result = "Hello, " + request.Name + ", sessionId: " + session.Id 
        };
    }
}

class Program { ... }

Will let you re-use the DTOs on the client, I’ve created a new client project that registers a new User and authenticates + calls the authenticated Hello Service 5 times in a row like your example, e.g:

static void Main(string[] args)
{
    var client = new JsonServiceClient("http://127.0.0.1:1337/");

    client.Post(new Register
    {
        FirstName = "Test",
        LastName = "User",
        UserName = "test",
        Password = "password",
        AutoLogin = false,
    });

    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine("Round {0}", i);

        try
        {
            client.Post(new Authenticate
            {
                provider = "credentials",
                UserName = "test",
                Password = "password"
            });

            var response = client.Get(new Hello { Name = $$"Round {i}" });
            Console.WriteLine("Success: " + response.Result);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Failed with {0}", ex);
        }
    }

    Console.ReadLine();
}

Which returns the expected response where each request is successful with a new sessionId:

Round 0
Success: Hello, Round 0, sessionId: QgW06KNY6qiTTLSCUI2Q
Round 1
Success: Hello, Round 1, sessionId: eENayX8mH2ibc2CBzMZ7
Round 2
Success: Hello, Round 2, sessionId: EAElu68VP7hUccxwS8qh
Round 3
Success: Hello, Round 3, sessionId: KO6KWZAHpCQqIpCix6M1
Round 4
Success: Hello, Round 4, sessionId: CW5yBs6ell9CoLST0UYY
2 Likes

Thanks for your explanation! I will try to fix our code that way.

Is there any AuthProvider base class doing the plumbing and doesn’t requiring user name and password? Background: we have our internal authentication system that is customer configurable. It can take user/password combinations, SSO, etc. The ServiceStack based API uses this system for authentication too, but should use ServiceStack’s session handling.

Only CredentialsAuthProvider expects a UserName/Password, there’s OAuthProvider for OAuth Providers and OAuth2Provider for OAuth2 Providers, all other non UserName/Passwords inherit from AuthProvider.