Anto Subash.

abp
identity-server
openiddict
migration
.NET Microservice with ABP -Part : 13

Migrating Identity Service to OpenIddict Module

Table of contents

Introduction

In this post we will see how to replace Identity server with OpenIddict in our microservice. We will use the same microservice we created in the previous posts. If you haven't read the previous post, you can read it here.

OpenIddict

OpenIddict aims at providing a versatile solution to implement OpenID Connect client, server and token validation support in any ASP.NET Core 2.1 (and higher) application. ASP.NET 4.6.1 (and higher) applications are also fully supported thanks to a native Microsoft.Owin 4.2 integration.

OpenIddict fully supports the code/implicit/hybrid flows, the client credentials/resource owner password grants and the device authorization flow.

OpenIddict natively supports Entity Framework Core, Entity Framework 6 and MongoDB out-of-the-box and custom stores can be implemented to support other providers.

Reason for the migration

The main reason for the migration is that the Identity server is not maintained anymore. The last release was in 2019. The latest version of ABP framework is using OpenIddict. So, it is better to use the latest version of the framework. you can read more about the migration here. you can learn more about the background of the migration here

Create a new temp project

We will create a temp project and copy the AuthServer project from the temp project to the solution. We did the same while creating the microservice. We will do the same here.

Create a new folder and create the solution using the following command.

1abp new Tasky -t app -u angular --separate-auth-server -dbms PostgreSQL

This will create a new solution with the AuthServer project. Copy the AuthServer project from the temp project to the solution. after copying the project, copy angular folder from the temp project to the solution. This will replace the existing angular folder.

Update the ABP packages

To update the ABP packages, use the following command.

1abp update

This will update all the ABP packages to the latest version. You can see the changes in the *.csproj files.

Update the .Net Version

Update the .Net version to 6.0 in the *.csproj files.

1<TargetFramework>net6.0</TargetFramework>

Update the tye.yaml file

Update the tye.yaml file to add the AuthServer project.

1- name: tasky-auth-server
2  project: apps/Tasky.AuthServer/Tasky.AuthServer.csproj
3  bindings:
4    - protocol: https
5      port: 7600

Configure the AuthServer

Add Auth Server to the solution

Add the AuthServer project to the solution.

1dotnet sln add .\apps\Tasky.AuthServer\Tasky.AuthServer.csproj

Add the Project references to the AuthServer project.

1dotnet add .\apps\Tasky.AuthServer\Tasky.AuthServer.csproj reference .\services\administration\src\Tasky.AdministrationService.EntityFrameworkCore\Tasky.AdministrationService.EntityFrameworkCore.csproj
2
3dotnet add .\apps\Tasky.AuthServer\Tasky.AuthServer.csproj reference .\services\identity\src\Tasky.IdentityService.EntityFrameworkCore\Tasky.IdentityService.EntityFrameworkCore.csproj
4
5dotnet add .\apps\Tasky.AuthServer\Tasky.AuthServer.csproj reference .\services\saas\src\Tasky.SaasService.EntityFrameworkCore\Tasky.SaasService.EntityFrameworkCore.csproj
6
7dotnet add .\apps\Tasky.AuthServer\Tasky.AuthServer.csproj reference .\shared\Tasky.Shared.Microservice.Hosting\Tasky.Shared.Microservice.Hosting.csproj

Update the TaskyAuthServerModule Dependency

We will update the TaskyAuthServerModule dependency in the AuthServer.csproj file:

1    typeof(AdministrationServiceEntityFrameworkCoreModule),
2    typeof(SaaSServiceEntityFrameworkCoreModule),
3    typeof(IdentityServiceEntityFrameworkCoreModule),
4    typeof(TaskyMicroserviceHosting)

Configure RabbitMQ

We will use RabbitMQ to publish the events. So, we need to configure RabbitMQ in the AuthServer project.

Add the following section to the appsettings.json file:

1  "RabbitMQ": {
2    "Connections": {
3      "Default": {
4        "HostName": "localhost"
5      }
6    },
7    "EventBus": {
8      "ClientName": "Tasky_AuthServer",
9      "ExchangeName": "Tasky"
10    }
11  }

Configure the Database

We will update the connection string in the appsettings.json file:

1  "ConnectionStrings": {
2    "SaaS": "User ID=postgres;Password=postgres;Host=localhost;Port=5432;Database=SaasService;Pooling=false;",
3    "IdentityService": "User ID=postgres;Password=postgres;Host=localhost;Port=5432;Database=IdentityService;Pooling=false;",
4    "Administration": "User ID=postgres;Password=postgres;Host=localhost;Port=5432;Database=AdministrationService;Pooling=false;"
5  },

Update CORS and Redirect URIs

We will update the CORS and Redirect URIs in the appsettings.json file:

1  "App": {
2    "SelfUrl": "https://localhost:44346",
3    "ClientUrl": "http://localhost:4200",
4    "CorsOrigins": "http://localhost:4200,http://localhost:3000,https://localhost:7001,https://localhost:7002,https://localhost:7003,https://localhost:7004,https://localhost:7005",
5    "RedirectAllowedUrls": "http://localhost:4200,http://localhost:3000,https://localhost:7001"
6  },

Update the port number in the AuthServer project

We will update the port number in the AuthServer project to 7600 in the AuthServer. you can do this by updating the launchSettings.json file.

1  "applicationUrl": "https://localhost:7600;http://localhost:7600"

Note: after updating the .Net version, you may get the error from non abp packages. You can update the non abp packages to the latest version.

Build the solution to make sure everything is working fine.

Replace the IdentityServer with OpenIddict

Search for IdentityServer in the solution and replace it with OpenIddict in the *.csproj files and *.cs files.

Update the IdentityServiceDbContext

Update the IdentityServiceDbContext class to inherit from IOpenIddictDbContext. This will add the required tables for the OpenIddict and remove the tables for the IdentityServer.

1    public DbSet<OpenIddictApplication> Applications { get; set; }
2    public DbSet<OpenIddictAuthorization> Authorizations { get; set; }
3    public DbSet<OpenIddictScope> Scopes { get; set; }
4    public DbSet<OpenIddictToken> Tokens { get; set; }

Add the migrations

After updating the IdentityServiceDbContext class, add the migrations using the following command.

1dotnet ef migrations add Init-OpenIddict

This will add the migrations for the OpenIddict. Now, we can run the migrations using the following command.

1dotnet ef database update

This will remove the tables for the IdentityServer and add the tables for the OpenIddict.

Update the data seeder

We will need to update the data seeder in the Tasky.DbMigrator.

Create a new class OpenIddictDataSeeder and add the following code.

1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Threading.Tasks;
5using Microsoft.Extensions.Configuration;
6using OpenIddict.Abstractions;
7using Volo.Abp.Authorization.Permissions;
8using Volo.Abp.DependencyInjection;
9using Volo.Abp.Guids;
10using Volo.Abp.MultiTenancy;
11using Volo.Abp.OpenIddict.Applications;
12using Volo.Abp.PermissionManagement;
13using Volo.Abp.Uow;
14using Microsoft.Extensions.Localization;
15using Volo.Abp;
16using JetBrains.Annotations;
17
18namespace Tasky.DbMigrator;
19
20public class OpenIddictDataSeeder : ITransientDependency
21{
22    private readonly IConfiguration _configuration;
23    private readonly ICurrentTenant _currentTenant;
24    private readonly IGuidGenerator _guidGenerator;
25    private readonly IAbpApplicationManager _applicationManager;
26    private readonly IOpenIddictScopeManager _scopeManager;
27    private readonly IPermissionDataSeeder _permissionDataSeeder;
28    private readonly IStringLocalizer<OpenIddictResponse> L;
29
30    public OpenIddictDataSeeder(
31        IAbpApplicationManager applicationManager,
32        IOpenIddictScopeManager scopeManager,
33        IPermissionDataSeeder permissionDataSeeder,
34        IStringLocalizer<OpenIddictResponse> l,
35        IGuidGenerator guidGenerator,
36        IConfiguration configuration,
37        ICurrentTenant currentTenant)
38    {
39        _configuration = configuration;
40        _applicationManager = applicationManager;
41        _scopeManager = scopeManager;
42        _permissionDataSeeder = permissionDataSeeder;
43        _guidGenerator = guidGenerator;
44        _currentTenant = currentTenant;
45        L = l;
46    }
47
48    [UnitOfWork]
49    public async virtual Task SeedAsync()
50    {
51        using (_currentTenant.Change(null))
52        {
53            await CreateApiResourcesAsync();
54            await CreateClientsAsync();
55        }
56    }
57
58    private async Task CreateClientsAsync()
59    {
60        var clients = _configuration.GetSection("Clients").Get<List<ServiceClient>>();
61        var commonScopes = new[] {
62            OpenIddictConstants.Permissions.Scopes.Address,
63            OpenIddictConstants.Permissions.Scopes.Email,
64            OpenIddictConstants.Permissions.Scopes.Phone,
65            OpenIddictConstants.Permissions.Scopes.Profile,
66            OpenIddictConstants.Permissions.Scopes.Roles,
67            "offline_access"
68        };
69
70        foreach (var client in clients)
71        {
72            var isClientSecretAvailable = !string.IsNullOrEmpty(client.ClientSecret);
73
74            await CreateClientAsync(
75                    client.ClientId,
76                    displayName: client.ClientId,
77                    secret: isClientSecretAvailable ? client.ClientSecret : null,
78                    type: isClientSecretAvailable ? OpenIddictConstants.ClientTypes.Confidential : OpenIddictConstants.ClientTypes.Public,
79                    scopes: commonScopes.Union(client.Scopes).ToList(),
80                    grantTypes: client.GrantTypes.ToList(),
81                    redirectUris: client.RedirectUris,
82                    postLogoutRedirectUris: client.PostLogoutRedirectUris,
83                    consentType: OpenIddictConstants.ConsentTypes.Implicit
84                );
85        }
86    }
87
88
89    private async Task CreateApiResourcesAsync()
90    {
91        var apiResources = _configuration.GetSection("ApiResource").Get<string[]>();
92
93        foreach (var item in apiResources)
94        {
95            await CreateApiResourceAsync(item);
96        }
97    }
98
99    private async Task CreateApiResourceAsync(string name)
100    {
101        if (await _scopeManager.FindByNameAsync(name) == null)
102        {
103            await _scopeManager.CreateAsync(new OpenIddictScopeDescriptor
104            {
105                Name = name,
106                DisplayName = name + " API",
107                Resources =
108                {
109                    name
110                }
111            });
112        }
113    }
114
115    private async Task CreateClientAsync(
116        [NotNull] string name,
117        [NotNull] string type,
118        [NotNull] string consentType,
119        string displayName,
120        string secret,
121        List<string> grantTypes,
122        List<string> scopes,
123        string[] redirectUris = null,
124        string[] postLogoutRedirectUris = null,
125        List<string> permissions = null)
126    {
127        if (!string.IsNullOrEmpty(secret) && string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
128        {
129            throw new BusinessException(L["NoClientSecretCanBeSetForPublicApplications"]);
130        }
131
132        if (string.IsNullOrEmpty(secret) && string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
133        {
134            throw new BusinessException(L["TheClientSecretIsRequiredForConfidentialApplications"]);
135        }
136
137        if (!string.IsNullOrEmpty(name) && await _applicationManager.FindByClientIdAsync(name) != null)
138        {
139            return;
140            //throw new BusinessException(L["TheClientIdentifierIsAlreadyTakenByAnotherApplication"]);
141        }
142
143        var client = await _applicationManager.FindByClientIdAsync(name);
144        if (client == null)
145        {
146            var application = new OpenIddictApplicationDescriptor
147            {
148                ClientId = name,
149                Type = type,
150                ClientSecret = secret,
151                ConsentType = consentType,
152                DisplayName = displayName
153            };
154
155            Check.NotNullOrEmpty(grantTypes, nameof(grantTypes));
156            Check.NotNullOrEmpty(scopes, nameof(scopes));
157
158            if (new[] { OpenIddictConstants.GrantTypes.AuthorizationCode, OpenIddictConstants.GrantTypes.Implicit }.All(grantTypes.Contains))
159            {
160                application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.CodeIdToken);
161                if (string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
162                {
163                    application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.CodeIdTokenToken);
164                    application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.CodeToken);
165                }
166            }
167            application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Logout);
168
169
170            foreach (var grantType in grantTypes)
171            {
172                if (grantType == OpenIddictConstants.GrantTypes.AuthorizationCode)
173                {
174                    application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode);
175                    application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.Code);
176                }
177
178                if (grantType == OpenIddictConstants.GrantTypes.AuthorizationCode || grantType == OpenIddictConstants.GrantTypes.Implicit)
179                {
180                    application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Authorization);
181                }
182
183                if (grantType == OpenIddictConstants.GrantTypes.AuthorizationCode ||
184                    grantType == OpenIddictConstants.GrantTypes.ClientCredentials ||
185                    grantType == OpenIddictConstants.GrantTypes.Password ||
186                    grantType == OpenIddictConstants.GrantTypes.RefreshToken ||
187                    grantType == OpenIddictConstants.GrantTypes.DeviceCode)
188                {
189                    application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);
190                    application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Revocation);
191                    application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Introspection);
192                }
193
194                if (grantType == OpenIddictConstants.GrantTypes.ClientCredentials)
195                {
196                    application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.ClientCredentials);
197                }
198
199                if (grantType == OpenIddictConstants.GrantTypes.Implicit)
200                {
201                    application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.Implicit);
202                }
203
204                if (grantType == OpenIddictConstants.GrantTypes.Password)
205                {
206                    application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.Password);
207                }
208
209                if (grantType == OpenIddictConstants.GrantTypes.RefreshToken)
210                {
211                    application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.RefreshToken);
212                }
213
214                if (grantType == OpenIddictConstants.GrantTypes.DeviceCode)
215                {
216                    application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.DeviceCode);
217                    application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Device);
218                }
219
220                if (grantType == OpenIddictConstants.GrantTypes.Implicit)
221                {
222                    application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.IdToken);
223                    if (string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
224                    {
225                        application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.IdTokenToken);
226                        application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.Token);
227                    }
228                }
229            }
230
231            var buildInScopes = new[]
232            {
233                OpenIddictConstants.Permissions.Scopes.Address,
234                OpenIddictConstants.Permissions.Scopes.Email,
235                OpenIddictConstants.Permissions.Scopes.Phone,
236                OpenIddictConstants.Permissions.Scopes.Profile,
237                OpenIddictConstants.Permissions.Scopes.Roles,
238                "offline_access"
239            };
240
241            foreach (var scope in scopes)
242            {
243                if (buildInScopes.Contains(scope))
244                {
245                    application.Permissions.Add(scope);
246                }
247                else
248                {
249                    application.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scope);
250                }
251            }
252
253            if (redirectUris != null)
254            {
255                foreach (var redirectUri in redirectUris)
256                {
257                    if (!redirectUri.IsNullOrEmpty())
258                    {
259                        if (!Uri.TryCreate(redirectUri, UriKind.Absolute, out var uri) || !uri.IsWellFormedOriginalString())
260                        {
261                            throw new BusinessException(L["InvalidRedirectUri", redirectUri]);
262                        }
263
264                        if (application.RedirectUris.All(x => x != uri))
265                        {
266                            application.RedirectUris.Add(uri);
267                        }
268                    }
269                }
270            }
271
272            if (postLogoutRedirectUris != null)
273            {
274                foreach (var postLogoutRedirectUri in postLogoutRedirectUris)
275                {
276                    if (!postLogoutRedirectUri.IsNullOrEmpty())
277                    {
278                        if (!Uri.TryCreate(postLogoutRedirectUri, UriKind.Absolute, out var uri) || !uri.IsWellFormedOriginalString())
279                        {
280                            throw new BusinessException(L["InvalidPostLogoutRedirectUri", postLogoutRedirectUri]);
281                        }
282
283                        if (application.PostLogoutRedirectUris.All(x => x != uri))
284                        {
285                            application.PostLogoutRedirectUris.Add(uri);
286                        }
287                    }
288                }
289            }
290
291            if (permissions != null)
292            {
293                await _permissionDataSeeder.SeedAsync(
294                    ClientPermissionValueProvider.ProviderName,
295                    name,
296                    permissions,
297                    null
298                );
299            }
300
301            await _applicationManager.CreateAsync(application);
302        }
303    }
304}

Create a new class OpenIddictDataSeedContributor and add the following code:

1using System.Threading.Tasks;
2using Volo.Abp.Data;
3using Volo.Abp.DependencyInjection;
4
5namespace Tasky.DbMigrator;
6
7public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDependency
8{
9    private readonly OpenIddictDataSeeder _OpenIddictDataSeeder;
10
11    public OpenIddictDataSeedContributor(OpenIddictDataSeeder OpenIddictDataSeeder)
12    {
13        _OpenIddictDataSeeder = OpenIddictDataSeeder;
14    }
15
16
17    public async Task SeedAsync(DataSeedContext context)
18    {
19        await _OpenIddictDataSeeder.SeedAsync();
20    }
21}

With the above code, we have created a new data seed contributor for OpenIddict. Now, we can delete the old IdentityServerDataSeedContributor class and IdentityServerDataSeeder class.

Seed the Application Client

Now, we have our OpenIddict data seed contributor. We can run the DbMigration project to create the seed data in the database.

1dotnet run

This will create the new Scopes and Clients in the database. We can check the database to see the new data.

Test the AuthServer

We can test the AuthServer by running the AuthServer project:

1dotnet run

If everything is configured correctly, we will be able to login to the AuthServer.

Now, run all the projects using tye and test the entire application with the angular application.

Conclusion

In this article, I have shown how to migrate the IdentityServer to OpenIddict. We have also seen how to create a new data seed contributor for OpenIddict and configure the AuthServer to use RabbitMQ and PostgreSQL. We have also tested the AuthServer and the entire application.

you can find the source code for this article on GitHub

Buy Me a Coffee at ko-fi.com