ApiKey AuthSession Caching

I am trying to subclass the ApiKeyAuthProvider to do more sophisticated auth provision and usage tracking.
My issue is around different APIs called requiring the new functionality, as opposed to previous functionality (current denoted by attached Attribute)

To do what I am doing, I need the Session NOT to be cached when the new functionality, but remain cached as part of existing functionality.

The issue I have is that the SessionCacheDuration value is based on the SS instance, and not specific to each authorisation request. Thus, although setting it to NULL stops the caching, it stops it permanently for all calls.

Unfortunately there is no ability to override the session caching functionality in a descendant class.

The overridden OnAuthenticated doesn’t seem to be called, so I can’t use it to remove the cached session.

In reviewing the underlying code, I found my best approach was to replace the entire ApiKeyAuthProvider and then add two virtual functions CacheSession() and CachedSession() used within PreAuthenticateWithApiKey.

I could then in my descendant version, override either or both of these to manage caching on a case by case basis.

This works fine, except we’re still using v5.7 of ServiceStack, and when we migrate to newer versions, I’ll have to re-do this work on a newer version of the provider.

Below is what I did, and poerhaps similar could be implemented in the said provider.

Any other suggestions greatly appreciated.

       public void PreAuthenticateWithApiKey(IRequest req, IResponse res, ApiKey apiKey)
    {
        if (RequireSecureConnection && !req.IsSecureConnection)
            throw HttpError.Forbidden(ErrorMessages.ApiKeyRequiresSecureConnection.Localize(req));

        ValidateApiKey(req, apiKey);

        var apiSessionKey = GetSessionKey(apiKey.Id);
        if (CachedSession(req, apiSessionKey))
        {
            req.Items[Keywords.ApiKey] = apiKey;
            return;
        }
        //Need to run SessionFeature filter since its not executed before this attribute (Priority -100)			
        SessionFeature.AddSessionIdToRequestFilter(req, res, null); //Required to get req.GetSessionId()

        using (var authService = HostContext.ResolveService<AuthenticateService>(req))
        {
            var response = authService.Post(new Authenticate
            {
                provider = Name,
                UserName = "ApiKey",
                Password = apiKey.Id,
            });
        }
        CacheSession(req, apiSessionKey);
    }

    public virtual bool CachedSession(IRequest req, string cacheKey)
    {
        if (SessionCacheDuration != null)
        {
            var session = req.GetCacheClient().Get<IAuthSession>(cacheKey);

            if (session != null)
                session = HostContext.AppHost.OnSessionFilter(req, session, session.Id);

            if (session != null)
            {
                req.Items[Keywords.Session] = session;
                return true;
            }
        }
        return false;
    }

    public virtual void CacheSession(IRequest req, string cacheKey)
    {
        if (SessionCacheDuration != null)
        {
            var session = req.GetSession();
            req.GetCacheClient().Set(cacheKey, session, SessionCacheDuration);
        }

    }

I’ve refactored the caching strategy into overridable HasCachedSessionAsync() and CacheSessionAsync() as similarly done in your code example in this commit.

Feel free to submit Pull Requests for any code changes like this you’d like to propose in future.

This change is available in the latest ServiceStack v5.10.5 that’s now available on MyGet.

Alternatively you should be able to maintain your own custom version of the entire ApiKeyAuthProvider implementation. With the upgrade to Async Auth Providers in the latest release, after upgrading you’ll need to change your base class to inherit from AuthProviderSync if you want to maintain the previous sync APIs, alternatively you could reapply your changes to the latest async ApiKeyAuthProvider.

Thanks and appreciated for the quick response and update. Would love to move to the lastest version asap, but unfortunately, other priorities.

@mythz

We are having a followup issue with our sub-classed ApiKeyAuthProvider.

The services GetApiKeyService and RegenerateApiKeyService use an internal AssertValidApiKeyRequest Extension method that does a hard-coded typecast to ApiKeyAuthProvider, which fails in our case.

public static class ApiKeyAuthProviderExtensions
{
    public static ApiKey GetApiKey(this IRequest req)
    {
        if (req == null)
            return null;

        return req.Items.TryGetValue(Keywords.ApiKey, out var oApiKey)
            ? oApiKey as ApiKey
            : null;
    }

    internal static ApiKeyAuthProvider AssertValidApiKeyRequest(this IRequest req)
    {
        var apiKeyAuth = (ApiKeyAuthProvider)AuthenticateService.GetAuthProvider(AuthenticateService.ApiKeyProvider);
        if (apiKeyAuth.RequireSecureConnection && !req.IsSecureConnection)
            throw HttpError.Forbidden(ErrorMessages.ApiKeyRequiresSecureConnection.Localize(req));

        return apiKeyAuth;
    }
}

Secondly, trying to instantiate our own versions of these classes, causes an exception:
System.Reflection.AmbiguousMatchException: ‘Could not register Request ‘ServiceStack.GetApiKeys’ with service ‘Solcast.ServiceStack.Auth.GetApiKeysService’ as it has already been assigned to another service.
Each Request DTO can only be handled by 1 service.’

I found it was auto-wire registering, but without the atRest urls, before the Register method of the service.

I had to change the

        foreach (var registerService in ServiceRoutes)
        {
            appHost.RegisterService(registerService.Key, registerService.Value);
        }

to

        foreach (var registerService in ServiceRoutes)
        {
            foreach(var route in registerService.Value)
                appHost.Routes.Add(registerService.Key, route, null);
        }

as well as change the typeof() from the service to the RequestDto.

This has allowed me to include these services.

Perhaps there should be an IApiKeyAuthProvider interface to use and then I wouldn’t have had to do this work around.

We are pushing to get to your latest version, but its slow going with other priorities

Hey @steve.solcast,

For the extension method, is also opinionated that the connection must be secure, which depending on your hosting setup, say behind a CDN TLS termination point, also will fail so likely better to create one that suits your situation with the type cast as well, though the use of apiKeyAuth.RequireSecureConnection is customizable here so I can see the typecast making it less flexible.

Not sure what changes to ServiceRoutes was in your current version but in the latest, it is public and values can be overridden, so if you need to registered different endpoints (URLs and DTO types), your sub class constructor should be able to change these and follow the normal register process. Or was there another Plugin registering this as well?

If other priorities are hampering upgrading, it might be easier to have your own stand alone ApiKeyAuthProvider, you can base your own custom one off existing source as needed.

Hey Darren,

Yes, we have our own version of the ApiKeyAuthProvider, and this is what is causing the issues.

It was implemented to virtual-ise the Session Caching methods so our real sub-classes auth provider could separately control the caching

I grabbed your ApiKeyAuthProvider source for the v5.7 version we are running and went from there.

Within this source I has deleted the two service class (GetApiKeysService and RegenerateApiKeyService) as i thought e SS ones would still work.

This is where they broke, as they had the hardcoded type casting to your provider and not support any sub-classing of the provider.

When I added them back to mine, it seems like the [DefaultRequest] attribute (or something) was causing the RegisterAutoWiredTypes to register the services but without the paths defined in the ServiceRoutes, since it knows nothing about them. And there are no routes on the RequestDTOs.

So the attempt by Register to register the services failed, since they’d already been registered.

Changing the register to only add the Routes solved this issue and allowed us to move forward.

But, as I say, The existing Services for APIKeys should work, regardless of the subclassing of the ApiKeyAuthProvider and they do not.

Any way to upload the apikeyauthprovider cs file to you so you can try it out?

If you have your own version of the ApiKeyAuthProvider, there shouldn’t be any need to to use inheritance, just make it your own with changes?

It sounds like you might have copied the service but are still reusing the ServiceStack DTOs. Change them to use your own, check the using statements as if you have created your own GetApiKeys etc DTOs, you still might have spots using the ServiceStack built in ones.

Since the copy is in your assemblies, this would make sense as well, since now your AppHost will scan and find these Services and try and register, hence the double registration. The original DTOs are not declared with a Route.

Your custom DTOs could use a route and skip registration in your custom AuthProvider, otherwise you’ll likely need to place your custom Auth provider, services and DTOs in a separate assemby/project that isn’t being scanned by your AppHost.

Sharing code is probably easiest via GitHub or a Gist, remember to remove all sensitive information before posting any code publicly.

1 Like

The reason for inheritance is to support either/or scenarios.

We wish to use APIKeys in there current form for much of the existing environment.

Under certain scenarios, we want extended functionality.
(This is currently done, based on an different Authorize attribute)

So the sub-class checks for the certain scenarios. If it is, it runs its own code, if not it simply calls the base class functionality and continues.

Thus 95% of all the features of the APIKey provider (including key generation etc) should continue to be the default underlying class supplied by ServcieStack, as we have no reason to change it.

So, to me, just like AuthRepositories support an IManageApiKeys interface for the necessary functionality, perhaps the ApiKeyAuthProvider should have an IAuthApiKeys interface to support the apikey services, and then any type casting would be to an interface and not a discrete class

Ah right, I misunderstood. Feel free to post link to any shared code as it is hard to follow without looking at concrete implementations. Any additional details of the use case (eg auth provision and usage tracking) and approach would also help as myself and others might be able to propose alternate options.

From what I understand at present, given the level of customization you might need + older version + size of the services trying to reuse and that custom services can be registered, the path I would recommend would be to use your own GetApiKeysService and RegenerateApiKeysService and when you upgrade you should be able to just use an inherited CustomApiKeyProvider : ApiKeyAuthProvider with default services and use the newly virtual CacheSessionAsync and HasCacheSessionAsync. That should allow you to just migrate your custom code and delete a lot of the rest.