MultiTenancy Sessions

Hi, I’ve successfully implemented multi-tenancy which works well. I am using a single instance web-app which correctly resolves the db-connection for general use and Authentication based on the request domain. Suddenly I thought how can I eliminate cross-tenant session-spoofing?

Thinking about this further, I think the below applies to any SS services that share a ICacheClient Redis instance (can be on different web apps).

Scenario:

Person A is permitted to access all resources on tenant1.acme.com, but nothing on tenant2.acme.com

Person A authenticates successfully onto tenant1.acme.com - ss cookies stored on browser and session stored on Redis.
Person A can then go to tenant2.acme.com and (using dev tools or fiddler) replace the cookies with the session cookies retrieved from “tenant1”.

In this scenario any [Authenticate] attribute-protected services will look up the urn:iauthsession session from the ICacheClient, and as there is no knowledge of which tenant generated this session, they are granted access.

  • Is my reasoning correct?
  • If so, where would be the best place to inject information into the session to indicate which Tenant generated it (can’t see hooks in AuthProvider) and best place to extend authentication / required permission attributes to check that the session was generated by the current Tenant?

G

Cookies aren’t shared across sub domains by default.

But yeah they could set their own cookies on the request to validate as an Authenticated user for a different site. But wouldn’t the User Id be unique so that even though they were Authenticated they wouldn’t be able to access any info because they’d just be a non-existent user in another tenants database?

Either way you could protect against this with a Custom Attribute that extends AuthenticateAttribute that also checks that the user is in the right tenant by verifying it against a property on a Custom UserSession after authenticating. You can check out the RequiredRoleAttribute for an example of an attribute that extends [Authenticate] to apply additional checks after calling base.ExecuteAsync() to validate their an Authenticated User.

Another solution would be to keep the sessions isolated so that the session cookie wont resolve to an Authenticated User in a different site. You should be able to do this by overriding GetCacheClient() in your AppHost and returning a client with a prefix, e.g:

public override ICacheClient GetCacheClient(IRequest req) 
{
    var tenantId = GetTenentId(req);
    return base.GetCacheClient(req).WithPrefix(tenantId + ":");
}

But you’d want to use the latest ServiceStack v5 on MyGet as it includes updating some calls that weren’t resolving the ICacheClient from the AppHost.

Love that “with prefix” solution - thanks. As each tenant’s userid is just an identity column, there is scope for collision.

1 Like

This works perfectly for the urn:authsession, but am afraid the addition of a prefix breaks RedisMqServer.

If I remove the prefix, MQ works fine, but the addition of a prefix causes messages to go straight to the outq without being processed by the handler.

fwiw I want to keep current mq “no prefix” behaviour - I don’t want Mq messages going into a tenant-specific (prefixed) key area, as I already have code to handle this.

update This is doesn’t happen if I remove the [RequiredPermission] attribute from the MQ handler. Unfortunately I need this here to protect the service.

You should be able to return an unprefixed CacheClient in MQ Requests by:

public override ICacheClient GetCacheClient(IRequest req) 
{
    if (req is BasicRequest)
        return base.GetCacheClient(req);

    var tenantId = GetTenentId(req);
    return base.GetCacheClient(req).WithPrefix(tenantId + ":");
}

Yeah, did try that but same behaviour. Also same behaviour when I downgrade the [RequiresPermission] attr to just [Authenticate]. I used Redis Monitor, and with the conditional prefix approach, there are calls to the non-prefixed session key:

1510907491.199955 [0 127.0.0.1:28894] “GET” “urn:iauthsession:GmZwU3iGTxVmZhMVw6Cd”

Re. the overriding AuthenticateAttribute, this won’t be possible as this is where the problem lies.

In AuthenticateAttribute.cs I believe the line below will lead to httpReq.GetCacheClient() which will evaluate to the the non-prefixed version.

update I have just re-read the messaging docs and realised that the [Authenicate] at the Service level isn’t evaluated for MQ, so I have removed the [RequiredPermission] from that method and added conditional prefix and all working. Thanks

G

You can override the OnSessionFilter in your AppHost for that.

But I’m not seeing where it doesn’t call AppHost.GetCacheClient() as req.GetSession() as it calls IRequest.GetCacheClient()

Which is what calls AppHost.GetCacheClient(). Did you clear your NuGet cache to get the latest ServiceStack v5?

$ nuget locals all -clear

Yes, cleared all caches. Yes it’s calling AppHost.GetCacheClient(req), but the IRequest will be a BaseRequest, which will result in the non-prefixed ICacheClient being returned.

Right, it’s up to your App to return the correctly configured ICacheClient. If you want to the MQ Request to resolve a tenant UserSession it needs to return the correct tenant Id. Not sure how you’re expecting it to work otherwise.

Note: another way to specify the UserSession for the request is to inject the UserSession:

req.Items[Keywords.Session] = usersSession;