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 },
AuthServer
project
Update the port number in the 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