Creating Your Own IdentityServer4 Storage Library

Scott Brady
Scott Brady
Identity Server

Over the years I’ve experienced many opinions about the default IdentityServer4 storage libraries; however, no matter your views on entity framework, clustered indexes, and varchar lengths, if you have concerns with the defaults then my advice is always the same: If you have database expertise in-house, use it and create your own storage layer.

Creating your own IdentityServer4 persistence store is very simple. There are only a handful of interfaces to implement, each with just a few read and write methods. They are not full repository layers, nor do they dictate database type or structure.

So, let’s take a look and see what’s involved with implementing our own IdentityServer4 storage library.

IdentityServer4 Entity Framework Library

The IdentityServer4 Entity Framework library is designed to work across a multitude of different database providers. It relies on the Entity Framework relational library, which might restrict the database providers it can support and is tested against SQL Server, MySQL, SQLite, and PostgreSQL.

As a result, it is not optimized for any one database provider and can suffer as a result. Despite this, Rock Solid Knowledge has customers using this library in production, with one customer having over 20 million users. So, unless you are hammering the introspection endpoint like a lunatic, then this library will most probably serve you well, despite your DBAs insistence.

The IdentityServer4 Storage Interfaces

As of IdentityServer4 v2.3, the storage interfaces and entities for IdentityServer4 can now be found in the IdentityServer4.Storage library. Otherwise, they can be found in the IdentityServer4 core library.

Let’s take a look at the IdentityServer4 storage interfaces, dealing with Clients, Resources, Scopes, and temporary data.

Client Store (IClientStore)

Probably the hardest store to deal with is the IClientStore. This is due to the large size of the Client entity and its many collections. However, once you have settled on a schema, the client store itself is very simple, with only one method to implement: FindClientByIdAsync.

public interface IClientStore {
    Task<Client> FindClientByIdAsync(string clientId);
}

The FindClientByIdAsync method should return the full client entity and all associated collections.

A Client also has a list of allowed scopes. It is up to you if you want this to be independent of or linked to the identity and API scopes that we’ll see shortly.

CORS (ICorsPolicyService)

When you implement your own IClientStore, you’ll also need to implement your own ICorsPolicyService. This interface needs to be able to use your client store of choice and load in all of the AllowedCorsOrigins to facilitate CORS origin checks.

public interface ICorsPolicyService {
    Task<bool> IsOriginAllowedAsync(string origin);
}

Resource Store (IResourceStore)

To store identity resources and API resources, we have the resource store. This interface has more methods than any of the other stores:

public interface IResourceStore {
    Task<IEnumerable<IdentityResource>> FindIdentityResourcesByScopeAsync(IEnumerable<string> scopeNames);
    Task<IEnumerable<ApiResource>> FindApiResourcesByScopeAsync(IEnumerable<string> scopeNames);
    Task<ApiResource> FindApiResourceAsync(string name);
    Task<Resources> GetAllResourcesAsync();
}

This interface handles the conversion of scopes received from authorization and token requests, into their respective resource models within IdentityServer. So, don’t forget, that means the resource name for identity resources, but the individual API scopes on an API resource. After all, an API resource models the API itself, which can in turn have many scopes, each representing a delegable permission on that API.

Persisted Grants

With persisted grants we have two options: implement the IPersistedGrantStore and handle the storage of authorization codes, refresh tokens, reference tokens, and consent all at once, or implement each of these individually using the IAuthorizationCodeStore, IRefreshTokenStore, IReferenceTokenStore, and IUserConsentStore.

IPersistedGrantStore

The default implementations of IAuthorizationCodeStore, IRefreshTokenStore, IReferenceTokenStore, and IUserConsentStore all utilise the IPersistedGrantStore. This one size fits all store accepts serialized data that can later be retrieved by key. This key is either something that is known to client applications (e.g. an authorization code) or something that can always evaluate into the same for the incoming client application (e.g. consent).

Persisted grants can be given an expiry by IdentityServer, and it is up to you to clean up expired grants lest your database start groaning with the strain.

Since keys can be something sensitive such as a refresh token value, then it should be stored in a hashed format.

public class PersistedGrant {
    public string Key { get; set; }
    public string Type { get; set; }
    public string SubjectId { get; set; }
    public string ClientId { get; set; }
    public DateTime CreationTime { get; set; }
    public DateTime? Expiration { get; set; }
    public string Data { get; set; }
}
public interface IPersistedGrantStore {
    Task StoreAsync(PersistedGrant grant);
    Task<PersistedGrant> GetAsync(string key);
    Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId);
    Task RemoveAsync(string key);
    Task RemoveAllAsync(string subjectId, string clientId);
    Task RemoveAllAsync(string subjectId, string clientId, string type);
}
Serialization

By default, persisted grants are serialized into JSON using the IPersistentGrantSerializer interface. If this is not to your liking, this is again something that can be overridden and then automatically used by the default IdentityServer stores.

Individual Stores

Otherwise, if you’re finding yourself dealing with millions of reference tokens and your current store is becoming a bottleneck, you can implement these stores one at a time, storing each one differently, maybe some using the IPersistedGrantStore, and some not.

public interface IAuthorizationCodeStore {
	Task<string> StoreAuthorizationCodeAsync(AuthorizationCode code);
	Task<AuthorizationCode> GetAuthorizationCodeAsync(string code);
	Task RemoveAuthorizationCodeAsync(string code);
}
public interface IRefreshTokenStore {
	Task<string> StoreRefreshTokenAsync(RefreshToken refreshToken);
	Task UpdateRefreshTokenAsync(string handle, RefreshToken refreshToken);
	Task<RefreshToken> GetRefreshTokenAsync(string refreshTokenHandle);
	Task RemoveRefreshTokenAsync(string refreshTokenHandle);
	Task RemoveRefreshTokensAsync(string subjectId, string clientId);
}
public interface IReferenceTokenStore {
	Task<string> StoreReferenceTokenAsync(Token token);
	Task<Token> GetReferenceTokenAsync(string handle);
	Task RemoveReferenceTokenAsync(string handle);
	Task RemoveReferenceTokensAsync(string subjectId, string clientId);
}
public interface IUserConsentStore {
	Task StoreUserConsentAsync(Consent consent);
	Task<Consent> GetUserConsentAsync(string subjectId, string clientId);
	Task RemoveUserConsentAsync(string subjectId, string clientId);
}

If you go the route of using an individual store, then you’ll need to also create an implementation of IPersistedGrantService that is aware of them, since the default implementation only uses IPersistedGrantStore. However, this service is only used by the DefaultIdentityServerInteractionService’s method GetAllUserConsentsAsync, which even then, is only used in the QuickStart UI’s grants page.

Device Flow (IDeviceFlowStore)

The storage of device flow requests is again relatively simple, but unlike the other temporary data stores, it must be searchable by two different items: a device code, and a user code.

public interface IDeviceFlowStore {
	Task StoreDeviceAuthorizationAsync(string deviceCode, string userCode, DeviceCode data);
	Task<DeviceCode> FindByUserCodeAsync(string userCode);
	Task<DeviceCode> FindByDeviceCodeAsync(string deviceCode);
	Task UpdateByUserCodeAsync(string userCode, DeviceCode data);
	Task RemoveByDeviceCodeAsync(string deviceCode);
}

This store can again take advantage of the IPersistentGrantSerializer to simplify storage.

Registering your Custom Implementations

To register our store, there are some extensions on IIdentityServerBuilder than we can use; otherwise, we have to register them ourselves. By default, these stores are registered with the transient lifetime.

services.AddIdentityServer()
    // existing registrations
    .AddClientStore<MyCustomClientStore>()
    .AddCorsPolicyService<MyCustomCorsPolicyService>()
    .AddResourceStore<MyCustomResourcesStore>()
    .AddPersistedGrantStore<MyCustomPersistedGrantStore>()
    .AddDeviceFlowStore<MyCustomDeviceFlowStore>(); 

// For manual temp data stores
services.AddTransient<IAuthorizationCodeStore, MyCustomAuthorizationCodeStore>();
services.AddTransient<IRefreshTokenStore, MyCustomRefreshTokenStore>();
services.AddTransient<IReferenceTokenStore, MyCustomReferenceTokenStore>();
services.AddTransient<IUserConsentStore, MyCustomUserConsentStore>();

Bonus: Key & Message Stores

Less common, but still in scope are the remaining stores of ISigningCredentialStore, IValidationKeysStore, IMessageStore, and IConsentMessageStore.

ISigningCredentialStore, and IValidationKeys respectively handle the loading of a private key for signing tokens, and public keys to verify them. By default, keys are loaded in from an x509 cert, or from the certificate store, and then stored in-memory.

IMessageStore, and IConsentMessage store handle the persistence of data between IdentityServer protocol endpoints and your UI. Usage of these is handled by the IdentityServer interaction service, allowing errors to be loaded in by ID, and consent response information back to IdentityServer. Personally, I’ve never seen custom implementations of these two in the wild.