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:
- Identity configuration, is prepared at runtime, by a specific class named
IdentityClientAndResourcesSeedData
. - In host startup, I execute an method that initialises database with initial data.
- Seed class deletes old data, and recreates. Doing so, if configuration changed in
appSettings.json
, changes will be applied into database. - Creating custom data store classes.
- Modifying Identity Server registration: stores included.
- Modifying RavenDB conventions, to deal with
IdentityResources.OpenId
,IdentityResources.Profile
,IdentityResources.Email
.
1. Identity configuration class:
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:
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:
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
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
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
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:
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
var builder = services.AddIdentityServer(
config =>
{
// ...
.AddClientStore<ClientStore>()
.AddResourceStore<ResourceStore>()
.AddPersistedGrantStore<PersistedGrantStore>()
.AddAspNetIdentity<AppUser>();
// ...
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/
Top comments (0)