## Documentation

- About
- Read First
  - What is IdentityServer
  - Installation Scenarios
- Migrations
- Bootstrapping
- Configuration & Integration
  - AdminUI Settings
  - Configuring AdminUI
  - Configuring AdminUI (NuGet)
  - Integrating with Identity Server
  - Integrating with Identity Server Custom Schema
  - Integrating with Quickstarts
  - Enabling Logging
- Installation
  - IIS
  - Azure
    - Install in Azure
    - Create Azure Web App
  - Docker
  - NuGet
- Custom Database Connections
- Custom Identity Store
  - Overview
  - Partial Implementation
  - Full Implementation
  - Tenancy With Entity Framework
  - NoSql Implementation (MongoDB)
- Custom CSP
- Users
  - Searching for Users
  - Adding Users
  - Editing Users
  - Deleting Users
  - Merge Users
  - Set User Password
  - Session Management
  - User Reports
  - Bulk User Import
- Roles
  - Creating Roles
  - Managing Roles
  - Managing Users in a Role
  - Managing Role Claims
- Claim Types
- Clients
  - Searching for Clients
  - Adding Clients
  - Adding a SAML Client
  - Adding a WS-Fed Client
  - Editing Clients
  - Deleting Clients
  - Clone Client
- Import and Export
  - Exporting Clients
  - Importing Clients
  - Importing Client Configuration with Secrets
  - Importing SAML Clients with Certificates
  - Import and Export Via the API
- Dynamic Authentication Providers
  - What is Dynamic Authentication
  - Enabling or disabling Dynamic Authentication
  - Add a Dynamic Provider
    - First Steps
    - Adding OIDC Provider
    - Adding SAML Provider
  - Edit a Dynamic Provider
    - Accessing edit view
    - Editing a OIDC Provider
    - Editing a SAML Provider
  - Deleting a Provider
  - Searching for Provider
- Resources
  - Searching for Resources
  - Creating Resources
  - Editing resources
- Webhooks
  - Webhooks
  - Endpoint Configuration
- MCP
  - Overview
  - Default Server Prompt Instructions
- User Settings Endpoints
- Access Policy
- Keys
- Auditing
- FAQs
- Troubleshooting
  - Unable to start AdminUI
  - Unable to contact IdentityServer
  - Unable to contact API
  - Unable to log in

### Tenancy With Entity Framework

- -Tenancy Example using Entity Framework
- -Prerequisites
- -Database
- -Models
- -Creating and Running Migrations
- -Updating the stores & factory
- -Mapping the Tenancy field

This section uses the code from full override as a starting point.

You can find the finished code on our GitHub we recommend reading the previous section to understand the code written so far.

## Tenancy Example using Entity Framework

In this documentation you will be learning how to make a non-AdminUI field (one that is not on ISSOUser) editable within AdminUI using claims.

The outcome of this documentation is to take an example user like the one below - one that has TenantName on it which is linked to the tenants table (fig2)

And make them editable in AdminUI by using claims:

## Prerequisites

There are 3 NuGet packages and a few external dependencies before you get started

- Nuget Packages
  - EntityFramework
  - EntityFramework.SqlServer
  - EntityFramework.Design
- A SqlServer database
- An IdentityServer that can access your user store to login

## Database

First you must create a database model and be able to scaffold and seed a database with some sample data to get AdminUI. In this section you'll be using EntityFramework to do so.

### Models

To get started you will create the models that represent your Identity model:

#### Tenant User

`TenantUser` is the model that will represent users in your system. This model is based off of `ISSOUser` but does not implement all of the interface. This is for 2 reasons:

1. We want to include the roles a user belongs to in the model
2. The claims collection needs a concrete type for Entity Framework to work against and `ISSOUser` expects a collection of `ISSOClaim`

    public class TenantUser
    {
        [Key]
        public string UserId { get; set; }
        public string UserName { get; set; }
        public string Email { get; set; }

        [ConcurrencyCheck]
        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; }
        public string Password { get; set; }

        public virtual ICollection<TenantRole> Roles { get; set; }

        public virtual ICollection<TenantClaim> Claims { get; set; }

        public virtual Tenant? Tenant { get; set; }
    }

#### Tenant Role

Describes roles that a user can belong to - including the Users collection on this allows EF to create a many-many connection between the Users and Roles tables.

    public class TenantRole : ISSORole
    {
        [Key]
        public string Id { get; set; }
        public string Description { get; set; }
        public bool NonEditable { get; set; }
        public string Name { get; set; }

        public virtual ICollection<TenantUser> Users { get; set; }
    }

#### Tenant Claim

Describes claims that a user has

    public class TenantClaim : ISSOClaim
    {
        [Key]
        public int Id { get; set; }
        public string ClaimType { get; set; }
        public string ClaimValue { get; set; }
    }

#### Tenant

The table representing tenants a user can belong to

    public class Tenant
    {
        [Key]
        public string Name { get; set; }
    }

#### Tenant ClaimType

ClaimTypes represent the `ClaimType` field on a `TenantClaim`. The main thing to note is your `AllowedValues` collection - this is how we will translate the `Tenant` table to a selectable list of values

    public class TenantClaimType
    {
    [Key]
        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<TenantEnumClaimTypeValue> AllowedValues { get; set; }
    }

#### Tenant Enum Claim Type Allowed Values

This table ties allowed values to particular claim types.

    public class TenantEnumClaimTypeValue
    {
        public string ClaimTypeId { get; set; }
        public string Value { get; set; }
    }

#### Create DBContext for Identity

Your next step is to build a DbContext - a DbContext in EntityFramework is the primary class for interacting with your database, it has knowledge of all your tables and the relationships between them.

As you're using Entity Framework to build the database (known as Code-First Entity Framework) you can let the models define most of the relationships, however a limitation in EntityFramework is that you cannot define composite keys at the model level, so you need to add an "OnModelCreating" override that defines the `TenantEnumClaimTypeValue` as a composite key of the claimType it belongs to and it's value

    public class CustomIdentityDb : DbContext
    {
        public CustomIdentityDb(DbContextOptions<CustomIdentityDb> options) : base(options){}

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<TenantEnumClaimTypeValue>(enumClaimTypeValue =>
            {
                enumClaimTypeValue.HasKey(ctv => new { ctv.ClaimTypeId, ctv.Value });
            });
        }

        public DbSet<TenantUser> Users { get; set; }

        public DbSet<TenantRole> Roles { get; set; }

        public DbSet<TenantClaim> UserClaims { get; set; }

        public DbSet<TenantClaimType> ClaimTypes { get; set; }
        public DbSet<TenantEnumClaimTypeValue> EnumClaimTypeAllowedValues { get; set; }

        public DbSet<Tenant> Tenants { get; set; }
    }

### Creating and Running Migrations

Now that your database has been defined in code, you'll need to get it up and running.

To do this, the Entity Framework tooling needs to know where your database in and what database engine to use. So in your startup register the DbContext with `.AddDbContext` and use the `OptionsBuilder` to register a SqlServer database.

    builder.Services.AddDbContext<CustomIdentityDb>(options =>
    {
        options.UseSqlServer(@"Server=localhost,1433;User Id=sa;Password=Password123!;Database=CustomIdentityDb;");
    });

In our sample the connection string is hardcoded for ease but it is also possible to get it from config if you please.

From a terminal or command line, navigate to your project and run the following

`dotnet ef migrations add InitialCreate` - This is what creates the migrations. After this command is run you will have a new `Migrations` folder in your project.

Secondly run `dotnet ef database update` - This command connects your project to the database server and uses the migrations previously generated to generate a database.

Afterwards you can navigate to your database to confirm that it has been created successfully.

#### Seeding the database

Provided in the `Program.cs` of our sample solution is a commented out block of code to seed the database which does the following:

- Maps an HttpGet to the base path
- Injects the new DbContext you just created
- Creates an Admin role
- Creates 2 Tenants,
- Assigns the Admin Role and 1 Tenant to a new User - this is the use you will use to login to AdminUI
- It then creates a ClaimType that represents tenants.
  - An important note is that this code does not populate the allowed values here as you will be filling that collection at a later time
  - It is also required, meaning we will see it when editing user details and it is reserved, meaning no one can edit it from within AdminUI.

If you would like to use the code snippet, put it above the `app.Run` call, comment out the `app.useAdminUI` call, uncomment the seed block and run your solution.

### Updating the stores & factory

The next step is to write your services to inject your context so they can make database calls. You will also need to update your factory to

As an example the User store will now look like:

    public class UserStore : ISSOUserStore
    {
        private readonly CustomIdentityDb _identityContext;

        public UserStore(CustomIdentityDb identityContext)
        {
            _identityContext = identityContext ?? throw new ArgumentNullException(nameof(identityContext));
        }

        ...

and the factory:

    public class CustomSSOStoreFactory : ISSOStoreFactory
    {
        private readonly CustomIdentityDb _identityDb;

        public CustomSSOStoreFactory(CustomIdentityDb identityDb)
        {
            _identityDb = identityDb ?? throw new ArgumentNullException(nameof(identityDb));
        }

        public ISSOUserStore CreateUserStore()
        {
            return new UserStore(_identityDb);
        }

        ...

### Mapping the Tenancy field

Now you will write the stores so they can make the conversion from field to claim happen. As a brief behind the scenes explanation when AdminUI gets a User it will get a list of claims for that user. This is a ClaimType-ClaimValue pair. AdminUI will then get all ClaimTypes and match that Claim's `ClaimType` to a ClaimType's `Name`

The rest of the guide will not take you over all of the store methods, but the essential ones crucial to the process described above, starting with getting the Claim Types.

#### ClaimType Store

The main method to focus on in the ClaimType store is the GetAllClaimTypes method. This method:

- Gets all ClaimTypes from the database and maps them to our store level objects (something which implements ISSOClaimType)
- Finds the claim type that represents Tenant (This was added upon seed)
- Populates the Allowed Values collection on that object by calling off to the Tenants table and getting all the names from that table
- Returns the ClaimTypes

    public async Task<IEnumerable<ISSOClaimType>> GetAllClaimTypes()
    {
        var ctList = _identityContext.ClaimTypes.Include(claimType => claimType.AllowedValues)
            .Select(dbCt => dbCt.ToStoreClaimType());

        var tenantCt = await ctList.FirstOrDefaultAsync(ctList => ctList.Id == TenantClaimTypeConsts.TenantCTId);
        tenantCt.AllowedValues = _identityContext.Tenants.Select(x => x.Name).ToList();

        return await ctList.ToListAsync();
    }

#### User Store

When Creating or Updating a user you must map the ClaimType that you received from the ClaimStore back from a Claim on the User to a field, for example your create should do the following:

- Map the incoming user to something that database can read
- Hash password if needed
- Grab the value of the Tenant claim from the passed in user
- Validate that the claim value matches a value in the tenants table
- Updates the users tenants field with that value
- Add, Saves and Returns

C#

    public async Task<ISSOUser> CreateUserWithPassword(ISSOUser user, string password)
    {
        var dbUser = user.ToDbUser();
        dbUser.ConcurrencyStamp = Guid.NewGuid().ToString();
        dbUser.Password = _passwordHasher.HashPassword(dbUser, password);

        var tenantName =  user.Claims.FirstOrDefault(claim => claim.ClaimType == TenantClaimTypeConsts.TenantCTName)?.ClaimValue;
        dbUser.Tenant = await _identityContext.Tenants.FirstOrDefaultAsync(tenant => tenant.Name == tenantName);

        _identityContext.Users.Add(dbUser);
        await _identityContext.SaveChangesAsync();

        var userInDb = await _identityContext.Users.FirstOrDefaultAsync(dbUsers => dbUsers.UserName == user.UserName);
        return userInDb.ToStoreUser();
    }

C#

    public async Task<ISSOUser> UpdateUser(ISSOUser user)
    {
        var dbUser = await _identityContext.Users.Include(dbUser => dbUser.Claims).Include(dbUser => dbUser.Roles).Include(dbUser => dbUser.Tenant).FirstOrDefaultAsync(dbUser => dbUser.UserId == user.Id);
        dbUser.Email = user.Email;
        dbUser.FirstName = user.FirstName;
        dbUser.UserName = user.UserName;
        dbUser.TwoFactorEnabled = user.TwoFactorEnabled;
        dbUser.LastName = user.LastName;
        dbUser.ConcurrencyStamp = Guid.NewGuid().ToString();

        var tenantClaim = user.Claims.FirstOrDefault(userClaim => userClaim.ClaimType == TenantClaimTypeConsts.TenantCTName)?.ClaimValue;
        var dbTenant = _identityContext.Tenants.FirstOrDefault(tenant => tenant.Name == tenantClaim);

        dbUser.Tenant = dbTenant;

        await _identityContext.SaveChangesAsync();

        return dbUser.ToStoreUser();
    }

Then implement the UserStores Find methods such that when the ClaimType is encountered, you map the `Tenant` field to a new Claim that has the TenantClaimType as the ClaimType. For example the FindUserById method looks like this:

        public async Task<ISSOUser> FindUserById(string userId)
        {
            var dbUser = await _identityContext.Users.Include(user => user.Claims).Include(user => user.Roles).Include(user => user.Tenant).FirstOrDefaultAsync(user => user.UserId == userId);
            if (dbUser == null)
            {
                throw new UserNotFoundException(userId);
            }

            var storeUser = dbUser.ToStoreUser();
            storeUser.Claims.Add(new SSOClaim(TenantClaimTypeConsts.TenantCTName, dbUser.Tenant.Name));

            return storeUser;
        }

#### IdentityServer Setup

Your IdentityServer setup might not need changing but if you are starting from the Samples you will need to add a profile service like below

    public class TenantProfileService : IProfileService
    {
        private readonly CustomIdentityDb _dbContext;
        public TenantProfileService(CustomIdentityDb dbContext)
        {
            _dbContext = dbContext;
        }
        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var userId = context.Subject.FindFirstValue(JwtClaimTypes.Subject);
            var user = await _dbContext.Users.Include(user => user.Roles).FirstOrDefaultAsync(tenantUser => tenantUser.UserId == userId);

            if (!string.IsNullOrWhiteSpace(user?.UserName))
            {
                context.IssuedClaims.Add(new Claim("name", user.UserName));
            }
            var userRoles = user.Roles;
            foreach (var role in userRoles)
            {
                context.IssuedClaims.Add(new Claim("role", role.Name));
            }
        }
        public Task IsActiveAsync(IsActiveContext context)
        {
            context.IsActive = true;
            return Task.CompletedTask;
        }
    }

Previous Next