Overview
If you have a requirement to use NoSQL database providers such as MongoDB to manage your users within IdentityServer, you can leverage AdminUI's Custom Identity Store interfaces to achieve this.
This document will outline the basic implementations needed to achieve this
integration within MongoDB using the official MongoDB.Driver
package within
our solution.
MongoDB Configuration
For our database, you will be using a locally hosting MongoDB database. To make things easier you can place MongoDB configuration in your appsettings.json
file.
"IdentityDatabase": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "AdminUIUserStore",
"UsersCollectionName": "Users",
"RolesCollectionName": "Roles",
"ClaimTypesCollectionName": "ClaimTypes"
},
You can also create an object to deserialize these configuration values:
public class IdentityStoreDatabaseSettings
{
public string ConnectionString { get; set; } = null!;
public string DatabaseName { get; set; } = null!;
public string UsersCollectionName { get; set; } = null!;
public string RolesCollectionName { get; set; } = null!;
public string ClaimTypesCollectionName { get; set; } = null!;
}
To access this configuration object, add it to your service collection in the Program.cs
class.
builder.Services.Configure<IdentityStoreDatabaseSettings>(builder.Configuration.GetSection("IdentityDatabase"));
Implementing the entity models
The first step to achieving NoSQL integration with AdminUI is to create
concrete implementations of our new model interfaces found within the Rsk.CustomIdentity
NuGet package.
These interfaces are as follows:
- ISSOUser
- ISSORole
- ISSOClaim
- ISSOClaimType
ISSOUser Implementation
First, create an implementation of the ISSOUser
interface.
public class CustomSSOUser : ISSOUser
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string Email { get; set; }
public string ConcurrencyStamp { get; set; }
public bool TwoFactorEnabled { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool IsBlocked { get; }
public bool IsDeleted { get; set; }
public bool LockoutEnabled { get; set; }
public DateTimeOffset? LockoutEnd { get; }
[BsonIgnoreIfNull]
public ICollection<ISSOClaim> Claims { get; set; } = new List<ISSOClaim>();
//Not part of the ISSOUser interface. Included to make querying user roles easier.
[BsonIgnoreIfNull]
public ICollection<ISSORole> Roles { get; set; } = new List<ISSORole>();
}
The fields contained within this interface are the foundations of a user within
AdminUI. If you have any additional fields you wish to manage within AdminUI,
you'll need to translate them to claims and place them in the Claims
collection
on the user class.
There are a few MongoDB specific attributes on our CustomSSOUser
class.
Firstly, the [BsonId]
attribute specifies the field to be used as the primary key for the object.
Below the [BsonId]
attribute, we have the [BsonRepresentation(BsonType.ObjectId)]
attribute.
This attribute is used to specify that the Id field on our object is to be seen (or represented) within MongoDB as an ObjectId
type.
The benefit of having the Id
field represented as an ObjectId
, is that we can offload the generating of unique ID's for the user object to MongoDB.
The [BsonIgnoreIfNull]
attribute specifies that if the Claims
or Roles
collections on the user object are null,
serializing/deserializing shouldn't be attempted. This helps to avoid any unwanted serialization issues within the MongoDB driver.
ISSORole Implementation
Write an implementation of the ISSORole
interface.
public class CustomSSORole : ISSORole
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string Description { get; set; }
public bool NonEditable { get; set; }
public string Name { get; set; }
}
ISSOClaimType Implementation
Now, the implementation of the ISSOClaimType
interface
public class CustomSSOClaimType : ISSOClaimType
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string Name { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
public bool IsRequired { get; set; }
public bool IsReserved { get; set; }
public SSOClaimValueType ValueType { get; set; }
public string RegularExpressionValidationRule { get; set; }
public string RegularExpressionValidationFailureDescription { get; set; }
public bool IsUserEditable { get; set; }
public ICollection<string> AllowedValues { get; set; }
}
ISSOClaim Implementation
And finally, the implementation of the ISSOClaim
interface
public class CustomSSOClaim : ISSOClaim
{
[BsonId]
public int Id { get; set; }
public string ClaimType { get; set; }
public string ClaimValue { get; set; }
}
MongoDB Driver Class Mapping
You must also register the class mapping within the MongoDB driver for the implementations.
To do this, update your Program.cs
class with the following:
BsonClassMap.RegisterClassMap<CustomSSOUser>();
BsonClassMap.RegisterClassMap<CustomSSORole>();
BsonClassMap.RegisterClassMap<CustomSSOClaim>();
BsonClassMap.RegisterClassMap<CustomSSOClaimType>();
It's not a requirement for this code to be in the Program.cs
class, you could also place it in the stores.
However, the registration of the class maps must be present before any calls to the database wherein deserialization would take place.
Store Implementations
Now you must create implementations of the stores. This will mean creating implementations of the following interfaces:
- ISSOUserStore
- ISSORoleStore
- ISSOClaimTypeStore
This will be the stores that are called withing AdminUI's service layer and contain all the methods AdminUI needs to work.
ISSOUserStore Implementation
Start by implementing the ISSOUserStore
. This is the largest store in terms of implemented methods.
// Implementation shortened for brevity
public class UserStore : ISSOUserStore
{
private readonly IMongoCollection<CustomSSOUser> dbUsers;
private readonly PasswordHasher<CustomSSOUser> passwordHasher;
public UserStore(IOptions<IdentityStoreDatabaseSettings> dbSettings)
{
var mongoClient = new MongoClient(
dbSettings.Value.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase(
dbSettings.Value.DatabaseName);
dbUsers = mongoDatabase.GetCollection<CustomSSOUser>(
dbSettings.Value.UsersCollectionName);
passwordHasher = new PasswordHasher<CustomSSOUser>();
}
public async Task<ISSOUser> CreateUser(ISSOUser user)
{
var newUser = new CustomSSOUser
{
UserName = user.UserName,
Email = user.Email,
ConcurrencyStamp = Guid.NewGuid().ToString(),
TwoFactorEnabled = false,
FirstName = user.FirstName,
LastName = user.LastName,
IsDeleted = false,
LockoutEnabled = false,
};
foreach (var claim in user.Claims)
{
newUser.Claims.Add(claim);
}
await dbUsers.InsertOneAsync(newUser);
var createdUserQuery = await dbUsers.FindAsync(u => u.UserName == newUser.UserName);
return await createdUserQuery.FirstOrDefaultAsync();
}
You can see how to initialise the dbUsers
collection using the injected IdentityStoreDatabaseSettings
configured
in the Program.cs
file. In production, you may want to use a factory to abstract out this functionality.
ISSORoleStore Implementation
Next, implement the ISSORoleStore
.
//Implementation shortened for brevity
public class RoleStore : ISSORoleStore
{
private readonly IMongoCollection<CustomSSOUser> dbUsers;
private readonly IMongoCollection<CustomSSORole> dbRoles;
public RoleStore(IOptions<IdentityStoreDatabaseSettings> dbSettings)
{
var mongoClient = new MongoClient(
dbSettings.Value.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase(
dbSettings.Value.DatabaseName);
dbUsers = mongoDatabase.GetCollection<CustomSSOUser>(
dbSettings.Value.UsersCollectionName);
dbRoles = mongoDatabase.GetCollection<CustomSSORole>(
dbSettings.Value.RolesCollectionName);
}
public async Task<ISSORole> CreateRole(ISSORole role)
{
await dbRoles.InsertOneAsync(new CustomSSORole
{
Name = role.Name,
Description = role.Description,
NonEditable = role.NonEditable
});
var rolesInsertedName = await dbRoles.FindAsync(r => r.Name == role.Name);
return await rolesInsertedName.FirstOrDefaultAsync();
}
ISSOClaimTypeStore Implementation
Finally, implement the ISSOClaimTypeStore
//Implementation shortened for brevity
public class ClaimTypeStore : ISSOClaimTypeStore
{
private readonly IMongoCollection<CustomSSOClaimType> dbClaimTypes;
public ClaimTypeStore(IOptions<IdentityStoreDatabaseSettings> dbSettings)
{
var mongoClient = new MongoClient(
dbSettings.Value.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase(
dbSettings.Value.DatabaseName);
dbClaimTypes = mongoDatabase.GetCollection<CustomSSOClaimType>(
dbSettings.Value.ClaimTypesCollectionName);
}
public async Task<ISSOClaimType> CreateClaimType(ISSOClaimType claimType)
{
var newClaimType = new CustomSSOClaimType
{
Name = claimType.Name,
DisplayName = claimType.DisplayName,
Description = claimType.Description,
IsRequired = claimType.IsRequired,
IsReserved = claimType.IsReserved,
ValueType = claimType.ValueType,
RegularExpressionValidationRule = claimType.RegularExpressionValidationRule,
RegularExpressionValidationFailureDescription = claimType.RegularExpressionValidationFailureDescription,
IsUserEditable = claimType.IsUserEditable,
AllowedValues = claimType.AllowedValues
};
await dbClaimTypes.InsertOneAsync(newClaimType);
var claimTypeQuery = await dbClaimTypes.FindAsync(ct => ct.Name == claimType.Name);
return await claimTypeQuery.FirstOrDefaultAsync();
}
ISSOStoreFactory Implementation
Next, you will create an implementation of the ISSOStoreFactory
. This is the factory used by the AdminUI service layer to create the instances
of the stores you have already written.
public class CustomSSOStoreFactory : ISSOStoreFactory
{
private readonly IOptions<IdentityStoreDatabaseSettings> dbSettings;
public CustomSSOStoreFactory(IOptions<IdentityStoreDatabaseSettings> dbSettings)
{
this.dbSettings = dbSettings ?? throw new ArgumentNullException(nameof(dbSettings));
}
public ISSOUserStore CreateUserStore()
{
return new UserStore(dbSettings);
}
public ISSORoleStore CreateRoleStore()
{
return new RoleStore(dbSettings);
}
public ISSOClaimTypeStore CreateClaimTypeStore()
{
return new ClaimTypeStore(dbSettings);
}
}
Summary
In this document you have created a NoSQL implementation of an Identity Store for AdminUI using the
Rsk.CustomIdentity
and MongoDB.Driver
NuGet packages.
The full source code for this sample can be found on our GitHub page