Auth Service Saying Not Authenticated when using Permanent Sessions

1.5k views Asked by At

When using temporary sessions it works fine. Log into the auth service and calling /auth without any parameters and it shows the display name, session id, etc.

When I log in with RememberMe=true, that call returns the session information properly. But on subsequent calls to /auth without any parameters, ServiceStack returns a 401 not authenticated. The session object's IsAuthenticated property is true and actually exists. My code checks for this and if it's false, forwards the user to the login page which doesn't happen so I know the user really is authenticated.

I am not doing anything different. How can I authenticate with a permanent session and get subsequent calls to /auth to acknowledge that I am logged in?

If it helps I'm using a CustomCredentialsProvider.

Update:

AppHost code:

    public override void Configure(Funq.Container container)
    {
        //Set JSON web services to return idiomatic JSON camelCase properties
        ServiceStack.Text.JsConfig.EmitCamelCaseNames = true;

        Config.RestrictAllCookiesToDomain = ConfigurationManager.AppSettings["cookieDomain"];

        Plugins.Add(new AuthFeature(() => new CustomUserSession(),
            new IAuthProvider[] { 
                    new CustomCredentialsProvider() 
                        { SessionExpiry = 
                            TimeSpan.FromMinutes(Convert.ToDouble(ConfigurationManager.AppSettings["SessionTimeout"])) 
                        }, 
                }) //end IAuthProvider
                {
                    IncludeAssignRoleServices = false,
                    IncludeRegistrationService = false,
                    HtmlRedirect = ConfigurationManager.AppSettings["mainSiteLink"] + "Login.aspx"
                } //end AuthFeature initializers
                );//end plugins.add AuthFeature

        Plugins.Add(new PostmanFeature() { EnableSessionExport = true });// this is only for when we want the feature and it's NOT in DebugMode
        Plugins.Add(new SwaggerFeature());
        Plugins.Add(new CorsFeature(allowedOrigins: "*",
                                    allowedMethods: "GET, POST, PUT, DELETE, OPTIONS",
                                    allowedHeaders: "Content-Type, Authorization, Accept",
                                    allowCredentials: true));


        container.Register<IRedisClientsManager>
            (c => new PooledRedisClientManager(2, ConfigurationManager.AppSettings["redisIpPort"]));
        container.Register<ICacheClient>(c => c.Resolve<IRedisClientsManager>().GetCacheClient());

        container.Register<ISessionFactory>(c => new SessionFactory(c.Resolve<ICacheClient>()));


        var userRep = new InMemoryAuthRepository();
        container.Register<IUserAuthRepository>(userRep);

        //Set MVC to use the same Funq IOC as ServiceStack
        ControllerBuilder.Current.SetControllerFactory(new FunqControllerFactory(container));

#if DEBUG
        Config.DebugMode = true;

        typeof(Authenticate).AddAttributes
            (
                new RestrictAttribute
                    (RequestAttributes.HttpGet | RequestAttributes.HttpPost)
            );

#else
        typeof(Authenticate).AddAttributes(new RestrictAttribute(RequestAttributes.HttpPost));
#endif

        RegisterTypedRequestFilter<Authenticate>((req, res, dto) =>
            {
                if (dto.UserName != null && dto.UserName != string.Empty
                    && dto.Password != null && dto.Password != string.Empty)
                    if(dto.RememberMe == null)
                        dto.RememberMe = false; 
            });

        RegisterTypedResponseFilter<AuthenticateResponse>((req, res, dto) =>
            {
                var appSettings = new ServiceStack.Configuration.AppSettings();
                dto.UserId = AppHostBase.Instance.TryResolve<ICacheClient>().SessionAs<CustomUserSession>().UserId.ToString();
                dto.Meta = new Dictionary<string, string>();
                dto.Meta.Add("ExpiresMinutes", appSettings.Get("SessionTimeout"));
            });
    }

    public static void Start()
    {
        Licensing.RegisterLicense(licenceKey);
        new ServiceStackAppHost().Init();
    }

Initial request headers:

https://****.com/api2/auth?username=user&password=passwordmberme=true

  • GET /api2/auth?username=user&password=password&rememberme=true HTTP/1.1
  • Accept: text/html, application/xhtml+xml, /
  • Accept-Language: en-US
  • User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
  • Accept-Encoding: gzip, deflate
  • Host: propel.zola360.com
  • DNT: 1
  • Connection: Keep-Alive
  • Cookie: ss-pid=P2hslABCmSs7pomRqNz5; ss-opt=perm; X-UAId=

Initial response headers:

  • HTTP/1.1 200 OK
  • Cache-Control: private
  • Content-Type: text/html
  • Content-Encoding: gzip
  • Vary: Accept-Encoding
  • Server: Microsoft-IIS/7.5
  • X-Powered-By: ServiceStack/4.033 Win32NT/.NET
  • Access-Control-Allow-Origin: *
  • Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
  • Access-Control-Allow-Headers: Content-Type, Authorization, Accept
  • Access-Control-Allow-Credentials: true
  • X-AspNet-Version: 4.0.30319
  • Set-Cookie: ss-id=pojZkNAdMcEcACDREcRM; domain=.zola360.com; path=/; HttpOnly
  • Set-Cookie: ss-opt=perm; domain=.zola360.com; expires=Mon, 13-Nov-2034 16:11:09 GMT; - path=/; HttpOnly
  • Set-Cookie: X-UAId=; domain=.zola360.com; expires=Mon, 13-Nov-2034 16:11:09 GMT; path=/; HttpOnly
  • Set-Cookie: 47=0; domain=.zola360.com; path=/
  • Set-Cookie: UserId=47; domain=.zola360.com; path=/
  • X-Powered-By: ASP.NET
  • Date: Thu, 13 Nov 2014 16:11:09 GMT
  • Content-Length: 4129

Initial response body:

{"userId":"47","sessionId":"PKrITmRawxAtnaABCDgN","userName":"user","responseStatus":{},"meta":{"ExpiresMinutes":"360"}}

Subsequent call to /auth request:

  • GET /api2/auth HTTP/1.1
  • Accept: text/html, application/xhtml+xml, /
  • Accept-Language: en-US
  • User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
  • Accept-Encoding: gzip, deflate
  • Host: propel.zola360.com
  • DNT: 1
  • Connection: Keep-Alive
  • Cookie: ss-pid=cvgslABCmSs6pomYdLu0; ss-opt=perm; X-UAId=; ss-id=lYWZkFAdMcZcABCDcRM; 47=0; UserId=47

Subsequent call to /auth response

  • HTTP/1.1 401 Not Authenticated
  • Cache-Control: private
  • Content-Type: text/html
  • Vary: Accept
  • Server: Microsoft-IIS/7.5
  • X-Powered-By: ServiceStack/4.033 Win32NT/.NET
  • Access-Control-Allow-Origin: *
  • Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
  • Access-Control-Allow-Headers: Content-Type, Authorization, Accept
  • Access-Control-Allow-Credentials: true
  • X-AspNet-Version: 4.0.30319
  • X-Powered-By: ASP.NET
  • Date: Thu, 13 Nov 2014 16:11:23 GMT
  • Content-Length: 9731

Subsequent call to /auth body:

{"responseStatus":{"errorCode":"Not Authenticated","message":"Not Authenticated","stackTrace":"[Authenticate: 11/13/2014 3:27:49 PM]:\n[REQUEST: {}]\nServiceStack.HttpError: Not Authenticated\r\n at ServiceStack.Auth.AuthenticateService.Post(Authenticate request)\r\n at lambda_method(Closure , Object , Object )\r\n at ServiceStack.Host.ServiceRunner`1.Execute(IRequest request, Object instance, TRequest requestDto)","errors":[]}}

Update I crafted a small Python3 script to authenticate myself and call some other web service. After authentication using RememberMe=true, the cookies come back as expected: ss-id/pid are set fine and ss-opt=perm. I figured I would print the header cookie and just paste it into a header of another request to call a different service marked with [Authenticate]. It didn't work. So I tried something silly and pasted the ss-pid cookie value into the ss-id one. It worked.

Here's the failing cookie string (session redacted :)):

cookie = "ss-id=ss-ID-session-cookie; domain=.zola360.com; path=/; HttpOnly, ss- pid=ss-PID-session-cookie; domain=.zola360.com; expires=Tue, 14-Nov-2034 01:34:25 GMT; path=/; HttpOnly, ss-opt=perm; domain=.zola360.com; expires=Tue, 14-Nov-2034 01:34:25 GMT; path=/; HttpOnly, X-UAId=; domain=.zola360.com; expires=Tue, 14-Nov-2034 01:34:25 GMT; path=/; HttpOnly, 47=0; domain=.zola360.com; path=/, UserId=47; domain=.zola360.com; path=/"

And simply pasting the ss-pid value into ss-id works:

cookie = "ss-id=ss-PID-session-cookie; domain=.zola360.com; path=/; HttpOnly, ss- pid=ss-PID-session-cookie; domain=.zola360.com; expires=Tue, 14-Nov-2034 01:34:25 GMT; path=/; HttpOnly, ss-opt=perm; domain=.zola360.com; expires=Tue, 14-Nov-2034 01:34:25 GMT; path=/; HttpOnly, X-UAId=; domain=.zola360.com; expires=Tue, 14-Nov-2034 01:34:25 GMT; path=/; HttpOnly, 47=0; domain=.zola360.com; path=/, UserId=47; domain=.zola360.com; path=/"

And the Python3 script I used:

import httplib2 as http
import json

try:
    from urlparse import urlparse
except ImportError:
    from urllib.parse import urlparse

headers = {
    'Accept': 'application/json',
    'Content-Type': 'application/json; charset=UTF-8'
}

uri = 'https://mysite.com'
path = '/api2/auth/credentials'

target = urlparse(uri+path)
method = 'POST'
body = '{"username": "username", "password": "password", "RememberMe": "true"}'.encode()

h = http.Http()

response, content = h.request(target.geturl(), method, body, headers)

#save the cookie and use it for subsequent requests
cookie = response['set-cookie']

print(cookie)

path2 = '/api2/time/start'
target2 = urlparse(uri+path2)

headers['cookie'] = cookie

response, content = h.request(target2.geturl(), 'GET', body, headers)

# assume that content is a json reply
# parse content with the json module
data = json.loads(content.decode())

print(data)

It seems that something still looks at the value of ss-id even if ss-opt=perm.

1

There are 1 answers

3
mythz On BEST ANSWER

When Authenticating with GET /api2/auth?username=user&password=... it is sent with your permanent cookie ss-pid, i.e:

Cookie: ss-pid=P2hslABCmSs7pomRqNz5; ss-opt=perm; X-UAId=

The rememberme=true option tells ServiceStack to maintain the users session against the permanent ss-pid cookie. This option is maintained in the users ss-opt=perm cookie, which the HTTP Response tells the client to add with:

Set-Cookie: ss-opt=perm; domain=.zola360.com; expires=Mon, 13-Nov-2034 16:11:09 GMT; - path=/; HttpOnly

Although not important in this case, since the temporary session ss-id was missing from the Request, ServiceStack tells the client to add a new one with:

Set-Cookie: ss-id=pojZkNAdMcEcACDREcRM; domain=.zola360.com; path=/; HttpOnly

The issue is with the subsequent request to GET /api2/auth where the client is not re-sending the ss-pid cookie it originally authenticated with (i.e. P2hslABCmSs7pomRqNz5 vs cvgslABCmSs6pomYdLu0):

Cookie: ss-pid=cvgslABCmSs6pomYdLu0; ss-opt=perm; X-UAId=; ss-id=lYWZkFAdMcZcABCDcRM; 47=0; UserId=47

Which ServiceStack doesn't know about (i.e. doesn't maintain any session against) which is why it returns with a 401 Not Authenticated as expected.

HTTP Client should be configured to resend Cookies

It's not clear what HTTP Client you're using but it should be configured to re-send cookies which is normally the default behavior. Ajax will send both the permanent ss-pid cookies and the temporary ss-id only for that browser session, e.g. the temporary ss-id cookie will be discarded when the browser is closed and making a new request will receive a new ss-id cookie.

With the C# Service Clients, it only resends permanent cookies so the client needs to be authenticated with RememberMe = true, e.g:

var client = JsonServiceClient(BaseUrl);
var authResponse = client.Send(new Authenticate
{
    provider = "credentials",
    UserName = "user",
    Password = "p@55word",
    RememberMe = true,
});

authResponse.PrintDump();

Once Authenticated the same authenticated client instance can be used to access a protected record multiple times as seen in this Auth Test:

for (int i = 0; i < 500; i++)
{
    var response = client.Send<SecureResponse>(new Secured { Name = "test" });
    Console.WriteLine("loop : {0}", i);
}