In this document

Introduction

Identity Server is an open source OpenID Connect and OAuth 2.0 framework. It can be used to make your application an authentication / single sign on server. It can also issue access tokens for 3rd party clients. This document describes how you can integrate IdentityServer to your project.

Startup Project

This document assumes that you have created an ASP.NET Core based project (including module zero) from startup templates and made it working. I created an ASP.NET Core MVC startup project for demonstration.

Installation

There are two nuget packages:

Since EF Core package already depends on the first one, you can only install Abp.ZeroCore.IdentityServer4.EntityFrameworkCore package to your project. Install it to the project contains your DbContext (.EntityFrameworkCore project for default templates):

Install-Package Abp.ZeroCore.IdentityServer4.EntityFrameworkCore

Then you can add dependency to your module (generally, to your EntityFrameworkCore project):

[DependsOn(typeof(AbpZeroCoreIdentityServerEntityFrameworkCoreModule))]
public class MyModule : AbpModule
{
    //...
}

Configuration

Configuring and using IdentityServer4 with Abp.ZeroCore is similar to independently use IdentityServer4. You should read it's own documentation to understand and use it. In this document, we only show additional configuration needed to integrate to Abp.ZeroCore.

Startup Class

In the ASP.NET Core Startup class, we should add IdentityServer to service collection and to ASP.NET Core middleware pipeline. Highlighted the differences from standard IdentityServer4 usage:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        //...
        services.AddIdentityServer()
            .AddTemporarySigningCredential()
            .AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources())
            .AddInMemoryApiResources(IdentityServerConfig.GetApiResources())
            .AddInMemoryClients(IdentityServerConfig.GetClients())
            .AddAbpPersistedGrants<YourDbContext>()
            .AddAbpIdentityServer<User>();
        //...
    }

    public void Configure(IApplicationBuilder app)
    {
        //...
        app.UseIdentityServer();
        //...
    }
}

I added services.AddIdentityServer() just after IdentityRegistrar.Register(services) and added app.UseIdentityServer() just afterAuthConfigurer.Configure in the startup project.

AddAbpIdentityServer<User>() is the main method that integrates IdentityServer4 with Abp.ZeroCore and Microsoft ASP.NET Core Identity. It gets your User class a generic parameter. Add this just after adding Microsoft ASP.NET Core Identity.

IdentityServerConfig Class

We have used IdentityServerConfig class to get identity resources, api resources and clients. You can find more information about this class in it's own documentation. For the simplest case, it can be a static class like below:

public static class IdentityServerConfig
{
    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource("default-api", "Default (all) API")
        };
    }

    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResources.Email(),
            new IdentityResources.Phone()
        };
    }

    public static IEnumerable<Client> GetClients()
    {
        return new List<Client>
        {
            new Client
            {
                ClientId = "client",
                AllowedGrantTypes = GrantTypes.ClientCredentials.Union(GrantTypes.ResourceOwnerPassword),
                AllowedScopes = {"default-api"},
                ClientSecrets =
                {
                    new Secret("secret".Sha256())
                }
            }
        };
    }
}

DbContext Changes

AddAbpPersistentGrants() method is used to save consent responses to the persistent data store. In order to use it, YourDbContext must implement IAbpPersistedGrantDbContext interface as shown below:

public class YourDbContext : AbpZeroDbContext<Tenant, Role, User, YourDbContext>, IAbpPersistedGrantDbContext
{
    public DbSet<PersistedGrantEntity> PersistedGrants { get; set; }

    public YourDbContext(DbContextOptions<YourDbContext> options)
        : base(options)
    {
    }

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

        modelBuilder.ConfigurePersistedGrantEntity();
    }
}

IAbpPersistedGrantDbContext defines PersistedGrants DbSet. We also should call modelBuilder.ConfigurePersistedGrantEntity() extension method as shown above in order to configure EntityFramework for PersistedGrantEntity.

Notice that this change in YourDbContext cause a new database migration. So, remember to use "Add-Migration" and "Update-Database" commands to update your database.

IdentityServer4 will continue to work even if you don't call AddAbpPersistedGrants<YourDbContext>() extension method, but user consent responses will be stored an in-memory data store in that case (which is cleared when you restart your application).

JWT Authentication Middleware

If we want to authorize clients against the same application we can use IdentityServer authentication middleware for that.

First, install IdentityServer4.AccessTokenValidation package from nuget to your project:

Install-Package IdentityServer4.AccessTokenValidation

Then we can add the middleware to Startup class as shown below:

app.UseIdentityServerAuthentication(
    new IdentityServerAuthenticationOptions
    {
        Authority = "http://localhost:62114/",
        RequireHttpsMetadata = false,
        AutomaticAuthenticate = true,
        AutomaticChallenge = true
    });

I added this just after app.UseIdentityServer() in the startup project.

Test

Now, our identity server is ready to get requests from clients. We can create a console application to make requests and get responses.

  • Create a new Console Application inside your solution.
  • Add IdentityModel nuget package to the console application. This package is used to create clients for OAuth endpoints.

While IdentityModel nuget package is enough to create a client and consume your API, I want to show to use API in more type safe way: We will convert incoming data to DTOs returned by application services.

  • Add reference to Application layer from the console application. This will allow us to use same DTO classes returned by the application layer in the client side.
  • Add Abp.Web.Common nuget package. This will allow us to use AjaxResponse class defined in ASP.NET Boilerplate class. Otherwise, we will deal with raw JSON strings to handle the server response.

Then we can change Program.cs as shown below:

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Abp.Application.Services.Dto;
using Abp.Json;
using IdentityModel.Client;
using Abp.MultiTenancy;
using Abp.Web.Models;
using IdentityServerIntegrationDemo.Users.Dto;
using Newtonsoft.Json;

namespace IdentityServerIntegrationDemo.ConsoleApiClient
{
    class Program
    {
        static void Main(string[] args)
        {
            RunDemoAsync().Wait();
            Console.ReadLine();
        }

        public static async Task RunDemoAsync()
        {
            var accessToken = await GetAccessTokenViaOwnerPasswordAsync();
            await GetUsersListAsync(accessToken);
        }

        private static async Task<string> GetAccessTokenViaOwnerPasswordAsync()
        {
            var disco = await DiscoveryClient.GetAsync("http://localhost:62114");

            var httpHandler = new HttpClientHandler();
            httpHandler.CookieContainer.Add(new Uri("http://localhost:62114/"), new Cookie(MultiTenancyConsts.TenantIdResolveKey, "1")); //Set TenantId
            var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret", httpHandler);
            var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync("admin", "123qwe");

            if (tokenResponse.IsError)
            {
                Console.WriteLine("Error: ");
                Console.WriteLine(tokenResponse.Error);
            }

            Console.WriteLine(tokenResponse.Json);

            return tokenResponse.AccessToken;
        }

        private static async Task GetUsersListAsync(string accessToken)
        {
            var client = new HttpClient();
            client.SetBearerToken(accessToken);

            var response = await client.GetAsync("http://localhost:62114/api/services/app/user/getUsers");
            if (!response.IsSuccessStatusCode)
            {
                Console.WriteLine(response.StatusCode);
                return;
            }

            var content = await response.Content.ReadAsStringAsync();
            var ajaxResponse = JsonConvert.DeserializeObject<AjaxResponse<PagedResultDto<UserListDto>>>(content);
            if (!ajaxResponse.Success)
            {
                throw new Exception(ajaxResponse.Error?.Message ?? "Remote service throws exception!");
            }

            Console.WriteLine();
            Console.WriteLine("Total user count: " + ajaxResponse.Result.TotalCount);
            Console.WriteLine();
            foreach (var user in ajaxResponse.Result.Items)
            {
                Console.WriteLine($"### UserId: {user.Id}, UserName: {user.UserName}");
                Console.WriteLine(user.ToJsonString(indented: true));
            }
        }
    }
}

Before running this application, ensure that your web project is up and running, because this console application will make request to the web application. Also, ensure that the requesting port (62114) is the same of your web application.

Source Code

You can see source code of this tutorial here: https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/IdentityServerDemo. This commit shows all changes done to the startup project.