Identity Server 4 and RavenDb 5: Resource store
Tags: ravendb, identityserver4, dotnet, aspnetcore
Published: 2020-11-20 15:02:29

When deploying an identity server to production, it's recommended to port `Identity Resources`, `Api Scopes` and `Clients` to a database.

This was my strategy:
1. Identity configuration, is prepared at runtime, by a specific class named `IdentityClientAndResourcesSeedData`.
2. In host startup, I execute an method that initialises database with initial data.
3. Seed class deletes old data, and recreates. Doing so, if configuration changed in `appSettings.json`, changes will be applied into database.
4. Creating custom data store classes.
5. Modifying Identity Server registration: stores included.
6. Modifying RavenDB conventions, to deal with `IdentityResources.OpenId`, `IdentityResources.Profile`, `IdentityResources.Email`.

# 1. Identity configuration class:
```csharp
public class IdentityClientAndResourcesSeedData
    {
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Email()
            };
        }

        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("myApi", "API BACKEND")
                {
                    Scopes = new List<string>()
                    {
                        "myApi"
                    }
                }
            };
        }

        public static IEnumerable<ApiScope> GetApiScopes()
        {
            return new[]
            {
                new ApiScope(name: "myApi.access", displayName: "Acessar API")
            };
        }

        // clients want to access resources (aka scopes)
        public static IEnumerable<Client> GetMainClients(IConfiguration configuration)
        {
            var clientList = new List<Client>();

            /* Config MVC Client */
            var mvcClientConfig = new IdentityServerClientConfig();
            configuration.Bind("IdentityServerClients:MvcClient", mvcClientConfig);

            clientList.Add(
                // OpenID Connect hybrid flow client (MVC)
                new Client
                {
                    ClientId = mvcClientConfig,
                    ClientName = MVCClient",
                    AllowedGrantTypes = GrantTypes.Code,

                    ClientSecrets = {new Secret(mvcClientConfig.ClientSecret.Sha256())},

                    RedirectUris = {$"{mvcClientConfig.ClientUrl}/signin-oidc"},
                    PostLogoutRedirectUris =
                        {$"{mvcClientConfig.ClientUrl}/signout-callback-oidc"},
                    RequireConsent = false,
                    RequirePkce = false,

                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.Email,
                        "myApi.access",
                        "offline_access"
                    },
                    AllowOfflineAccess = true
                }
            );

// ... insert other necessary clients here

            return clientList;
        }
    }
```

# 2. Program.cs:
```csharp
var hostBuilded = CreateWebHostBuilder(args)
                    .Build();

// Tip from: https://dotnetthoughts.net/seed-database-in-aspnet-core/
using (var scope = hostBuilded.Services.CreateScope())
{
   Log.Information("DATA SEED: will start!");
   DataSeeder.Initialize(scope.ServiceProvider).Wait();
   Log.Information("DATA SEED: ended.");
}

hostBuilded.Run();
```

# 3. Seed:

```csharp
public static class DataSeeder
    {
        public static async Task Initialize(IServiceProvider serviceProvider)
        {
            var configuration = serviceProvider.GetRequiredService<IConfiguration>();
            using var dbSession = serviceProvider.GetRequiredService<IAsyncDocumentSession>();

            // 1. Identity Resources
            var identityResourcesToSeed =
                IdentityClientAndResourcesSeedData.GetIdentityResources();
            
            foreach (var item in identityResourcesToSeed)
            {
                var preExistingItem = await dbSession.Query<IdentityResource>()
                    .Where(wh => wh.Name == item.Name)
                    .FirstOrDefaultAsync();
                
                if (preExistingItem != null)
                {
                    // deletes
                    dbSession.Delete(preExistingItem);
                }

                await dbSession.StoreAsync(item);
            }
            
            // 2. Api Resources
            var apiResourcesToSeed =
                IdentityClientAndResourcesSeedData.GetApiResources();

            foreach (var item in apiResourcesToSeed)
            {
                var preExistingItem = await dbSession.Query<ApiResource>()
                    .Where(wh => wh.Name == item.Name)
                    .FirstOrDefaultAsync();
                
                if (preExistingItem != null)
                {
                    // deletes
                    dbSession.Delete(preExistingItem);
                }

                await dbSession.StoreAsync(item);
            }
            
            // 3. Api Scopes
            var apiScopesToSeed =
                IdentityClientAndResourcesSeedData.GetApiScopes();

            foreach (var item in apiScopesToSeed)
            {
                var preExistingItem = await dbSession.Query<ApiScope>()
                    .Where(wh => wh.Name == item.Name)
                    .FirstOrDefaultAsync();
                
                if (preExistingItem != null)
                {
                    // deletes
                    dbSession.Delete(preExistingItem);
                }

                await dbSession.StoreAsync(item);
            }
            
            // 4. Identity Clients
            var mainClientsToSeed =
                IdentityClientAndResourcesSeedData.GetMainClients(configuration);
            
            foreach (var item in mainClientsToSeed)
            {
                var preExistingItem = await dbSession.Query<Client>()
                    .Where(wh => wh.ClientId == item.ClientId)
                    .FirstOrDefaultAsync();
                
                if (preExistingItem != null)
                {
                    // deletes
                    dbSession.Delete(preExistingItem);
                }

                await dbSession.StoreAsync(item);
            }

            await dbSession.SaveChangesAsync();
        }
    }
```

# 4. Creating ClientStore and Identity Store 

These stores will be the layer between Identity Server and RavenDB database.

### 4.1 Client Store
```csharp
public class ClientStore : IClientStore
    {
        private readonly IAsyncDocumentSession _dbSession;

        public ClientStore(
            IAsyncDocumentSession dbSession
            )
        {
            _dbSession = dbSession;
        }
        
        public async Task<Client> FindClientByIdAsync(string clientId)
        {
            var clientFound =
                await _dbSession.Query<Client>()
                    .Where(wh => wh.ClientId == clientId)
                    .FirstOrDefaultAsync();

            return clientFound;
        }
    }
```

### 4.2 Resource Store

```csharp
public class ResourceStore : IResourceStore
    {
        private readonly IAsyncDocumentSession _dbSession;

        public ResourceStore(
            IAsyncDocumentSession dbSession
            )
        {
            _dbSession = dbSession;
        }
        
        public async Task<IEnumerable<IdentityResource>> FindIdentityResourcesByScopeNameAsync(
            IEnumerable<string> scopeNames)
        {
            if (scopeNames == null) throw new ArgumentNullException(nameof(scopeNames));

            var _identityResources =
                await _dbSession.Query<IdentityResource>().ToListAsync();
            
            var identity = from i in _identityResources
                where scopeNames.Contains(i.Name)
                select i;

            return identity;
        }

        public async Task<IEnumerable<ApiScope>> FindApiScopesByNameAsync(IEnumerable<string> scopeNames)
        {
            if (scopeNames == null) throw new ArgumentNullException(nameof(scopeNames));

            var _apiScopes =
                await _dbSession.Query<ApiScope>().ToListAsync();
            
            var query =
                from x in _apiScopes
                where scopeNames.Contains(x.Name)
                select x;
            
            return query;
        }

        public async Task<IEnumerable<ApiResource>> FindApiResourcesByScopeNameAsync(IEnumerable<string> scopeNames)
        {
            if (scopeNames == null) throw new ArgumentNullException(nameof(scopeNames));

            var allData =
                await _dbSession.Query<ApiResource>().ToListAsync();

            var query = from a in allData
                where a.Scopes.Any(x => scopeNames.Contains(x))
                select a;

            return query;
        }

        public async Task<IEnumerable<ApiResource>> FindApiResourcesByNameAsync(
            IEnumerable<string> apiResourceNames
            )
        {
            if (apiResourceNames == null) throw new ArgumentNullException(nameof(apiResourceNames));

            var allData =
                await _dbSession.Query<ApiResource>().ToListAsync();

            var query = from a in allData
                where apiResourceNames.Contains(a.Name)
                select a;

            return query;
        }

        public async Task<Resources> GetAllResourcesAsync()
        {
            var allApiResources =
                await _dbSession.Query<ApiResource>().ToListAsync();
            var allApiScopes =
                await _dbSession.Query<ApiScope>().ToListAsync();
            var allIdentityResources =
                await _dbSession.Query<IdentityResource>().ToListAsync();

            return new Resources(allIdentityResources, allApiResources, allApiScopes);
        }
    }
```


# 5. Startup.cs > Registering Identity Server resource and client stores

```csharp
var builder = services.AddIdentityServer()
// Add Client Store and Resource Store implementations
   .AddClientStore<ClientStore>()
   .AddResourceStore<ResourceStore>()
   // Disable InMemory additions if they were being used.
   // .AddInMemoryIdentityResources( IdentityDevelopmentConfig.GetIdentityResources())
   // .AddInMemoryApiResources(IdentityDevelopmentConfig.GetApiResources())
   // .AddInMemoryApiScopes(IdentityDevelopmentConfig.GetApiScopes())
   // .AddInMemoryClients(IdentityDevelopmentConfig.GetMainClients(configuration))
  .AddAspNetIdentity<AppUser>();
```

# 6. RavenDB registration > Override DocumentStore to correctly define a collection name to IdentityResources types:

```csharp
options.BeforeInitializeDocStore += docStoreOverride =>
                    {
                        
                        docStoreOverride.Conventions.FindCollectionName = type =>
                        {
                            var identityResourcesTypes = new Type[]
                            {
                                typeof(IdentityResources.OpenId),
                                typeof(IdentityResources.Profile),
                                typeof(IdentityResources.Email)
                            };
                            
                            if (identityResourcesTypes.Contains(type))
                                return "IdentityResources";
                            
                            return DocumentConventions.DefaultGetCollectionName(type);
                        };
                    };
```

These steps should work.
They cover the changes will need to do to make RavenDB the official data store for your identity server resources and clients.  

💡 The Data Seed implementation used in this tutorial is very useful for another scenarios.

If you have any problems let me know in comments. :)

## Edit: 11/27/2020 - Persisted grant store implemented

```csharp
var builder = services.AddIdentityServer(
config =>
   {
// ...
       .AddClientStore<ClientStore>()
       .AddResourceStore<ResourceStore>()
       .AddPersistedGrantStore<PersistedGrantStore>()
       .AddAspNetIdentity<AppUser>();
// ...
```

```csharp
public class PersistedGrantStore : IPersistedGrantStore
{
    private readonly IAsyncDocumentSession _dbSession;

    public PersistedGrantStore(
        IAsyncDocumentSession dbSession
        )
    {
        _dbSession = dbSession;
    }
    public async Task StoreAsync(PersistedGrant grant)
    {
        await _dbSession.StoreAsync(grant);

        await _dbSession.SaveChangesAsync();
    }

    public Task<PersistedGrant> GetAsync(string key)
    {
        return _dbSession.Query<PersistedGrant>()
            .Where(wh => wh.Key == key)
            .FirstOrDefaultAsync();
    }

    public Task<IEnumerable<PersistedGrant>> GetAllAsync(PersistedGrantFilter filter)
    {
        var qry = _dbSession.Query<PersistedGrant>();

        if (filter.Type.IsNullOrEmpty() == false)
            qry = qry.Where(wh => wh.Type == filter.Type);

        if (filter.ClientId.IsNullOrEmpty() == false)
            qry = qry.Where(wh => wh.ClientId == filter.ClientId);
        
        if (filter.SessionId.IsNullOrEmpty() == false)
            qry = qry.Where(wh => wh.SessionId == filter.SessionId);
        
        if (filter.SubjectId.IsNullOrEmpty() == false)
            qry = qry.Where(wh => wh.SubjectId == filter.SubjectId);

        return Task.FromResult(qry.AsEnumerable());
    }

    public async Task RemoveAsync(string key)
    {
        var objToDelete = 
            await this.GetAsync(key);
        _dbSession.Delete(objToDelete);
        
        await _dbSession.SaveChangesAsync();
    }

    public async Task RemoveAllAsync(PersistedGrantFilter filter)
    {
        var qry = _dbSession.Query<PersistedGrant>();

        if (filter.Type.IsNullOrEmpty() == false)
            qry = qry.Where(wh => wh.Type == filter.Type);

        if (filter.ClientId.IsNullOrEmpty() == false)
            qry = qry.Where(wh => wh.ClientId == filter.ClientId);
        
        if (filter.SessionId.IsNullOrEmpty() == false)
            qry = qry.Where(wh => wh.SessionId == filter.SessionId);
        
        if (filter.SubjectId.IsNullOrEmpty() == false)
            qry = qry.Where(wh => wh.SubjectId == filter.SubjectId);

        var grantsToRemove = await qry.ToListAsync();

        foreach (var grant in grantsToRemove)
        {
            _dbSession.Delete(grant);    
        }
        
        await _dbSession.SaveChangesAsync();
    }
}
```


### References:
https://github.com/IdentityServer/IdentityServer4/blob/18897890ce2cb020a71b836db030f3ed1ae57882/src/IdentityServer4/src/Stores/InMemory/InMemoryResourcesStore.cs
http://docs.identityserver.io/en/latest/topics/deployment.html
https://ravendb.net/docs/article-page/5.0/NodeJs/client-api/session/configuration/how-to-customize-collection-assignment-for-entities#session-how-to-customize-collection-assignment-for-entities
https://dotnetthoughts.net/seed-database-in-aspnet-core/