---
title: Custom Identity Store using MongoDB
---

## 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. 

```language-json
"IdentityDatabase": {
    "ConnectionString": "mongodb://localhost:27017",
    "DatabaseName": "AdminUIUserStore",
    "UsersCollectionName": "Users",
    "RolesCollectionName": "Roles",
    "ClaimTypesCollectionName": "ClaimTypes"
  },
```

You can also create an object to deserialize these configuration values: 

```language-csharp
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. 

```language-csharp
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. 

```language-csharp
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

```language-csharp
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 

```language-csharp
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: 

```language-csharp
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.

```language-csharp
// 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``.

```language-csharp
//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``

```language-csharp
//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.

```language-csharp
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](https://github.com/RockSolidKnowledge/Samples.CustomIdentityStore/tree/main/Duende/NoSQL%20Starting%20Point)




