- Published on
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.
abp 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.
abp 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.
<TargetFramework>net6.0</TargetFramework>
Update the tye.yaml file
Update the tye.yaml
file to add the AuthServer
project.
- name: tasky-auth-server
project: apps/Tasky.AuthServer/Tasky.AuthServer.csproj
bindings:
- protocol: https
port: 7600
Configure the AuthServer
Add Auth Server to the solution
Add the AuthServer
project to the solution.
dotnet sln add .\apps\Tasky.AuthServer\Tasky.AuthServer.csproj
Add the Project references to the AuthServer
project.
dotnet add .\apps\Tasky.AuthServer\Tasky.AuthServer.csproj reference .\services\administration\src\Tasky.AdministrationService.EntityFrameworkCore\Tasky.AdministrationService.EntityFrameworkCore.csproj
dotnet add .\apps\Tasky.AuthServer\Tasky.AuthServer.csproj reference .\services\identity\src\Tasky.IdentityService.EntityFrameworkCore\Tasky.IdentityService.EntityFrameworkCore.csproj
dotnet add .\apps\Tasky.AuthServer\Tasky.AuthServer.csproj reference .\services\saas\src\Tasky.SaasService.EntityFrameworkCore\Tasky.SaasService.EntityFrameworkCore.csproj
dotnet 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:
typeof(AdministrationServiceEntityFrameworkCoreModule),
typeof(SaaSServiceEntityFrameworkCoreModule),
typeof(IdentityServiceEntityFrameworkCoreModule),
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:
"RabbitMQ": {
"Connections": {
"Default": {
"HostName": "localhost"
}
},
"EventBus": {
"ClientName": "Tasky_AuthServer",
"ExchangeName": "Tasky"
}
}
Configure the Database
We will update the connection string in the appsettings.json
file:
"ConnectionStrings": {
"SaaS": "User ID=postgres;Password=postgres;Host=localhost;Port=5432;Database=SaasService;Pooling=false;",
"IdentityService": "User ID=postgres;Password=postgres;Host=localhost;Port=5432;Database=IdentityService;Pooling=false;",
"Administration": "User ID=postgres;Password=postgres;Host=localhost;Port=5432;Database=AdministrationService;Pooling=false;"
},
Update CORS and Redirect URIs
We will update the CORS and Redirect URIs in the appsettings.json
file:
"App": {
"SelfUrl": "https://localhost:44346",
"ClientUrl": "http://localhost:4200",
"CorsOrigins": "http://localhost:4200,http://localhost:3000,https://localhost:7001,https://localhost:7002,https://localhost:7003,https://localhost:7004,https://localhost:7005",
"RedirectAllowedUrls": "http://localhost:4200,http://localhost:3000,https://localhost:7001"
},
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.
"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.
public DbSet<OpenIddictApplication> Applications { get; set; }
public DbSet<OpenIddictAuthorization> Authorizations { get; set; }
public DbSet<OpenIddictScope> Scopes { get; set; }
public DbSet<OpenIddictToken> Tokens { get; set; }
Add the migrations
After updating the IdentityServiceDbContext
class, add the migrations using the following command.
dotnet ef migrations add Init-OpenIddict
This will add the migrations for the OpenIddict. Now, we can run the migrations using the following command.
dotnet 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.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using OpenIddict.Abstractions;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.OpenIddict.Applications;
using Volo.Abp.PermissionManagement;
using Volo.Abp.Uow;
using Microsoft.Extensions.Localization;
using Volo.Abp;
using JetBrains.Annotations;
namespace Tasky.DbMigrator;
public class OpenIddictDataSeeder : ITransientDependency
{
private readonly IConfiguration _configuration;
private readonly ICurrentTenant _currentTenant;
private readonly IGuidGenerator _guidGenerator;
private readonly IAbpApplicationManager _applicationManager;
private readonly IOpenIddictScopeManager _scopeManager;
private readonly IPermissionDataSeeder _permissionDataSeeder;
private readonly IStringLocalizer<OpenIddictResponse> L;
public OpenIddictDataSeeder(
IAbpApplicationManager applicationManager,
IOpenIddictScopeManager scopeManager,
IPermissionDataSeeder permissionDataSeeder,
IStringLocalizer<OpenIddictResponse> l,
IGuidGenerator guidGenerator,
IConfiguration configuration,
ICurrentTenant currentTenant)
{
_configuration = configuration;
_applicationManager = applicationManager;
_scopeManager = scopeManager;
_permissionDataSeeder = permissionDataSeeder;
_guidGenerator = guidGenerator;
_currentTenant = currentTenant;
L = l;
}
[UnitOfWork]
public async virtual Task SeedAsync()
{
using (_currentTenant.Change(null))
{
await CreateApiResourcesAsync();
await CreateClientsAsync();
}
}
private async Task CreateClientsAsync()
{
var clients = _configuration.GetSection("Clients").Get<List<ServiceClient>>();
var commonScopes = new[] {
OpenIddictConstants.Permissions.Scopes.Address,
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Scopes.Phone,
OpenIddictConstants.Permissions.Scopes.Profile,
OpenIddictConstants.Permissions.Scopes.Roles,
"offline_access"
};
foreach (var client in clients)
{
var isClientSecretAvailable = !string.IsNullOrEmpty(client.ClientSecret);
await CreateClientAsync(
client.ClientId,
displayName: client.ClientId,
secret: isClientSecretAvailable ? client.ClientSecret : null,
type: isClientSecretAvailable ? OpenIddictConstants.ClientTypes.Confidential : OpenIddictConstants.ClientTypes.Public,
scopes: commonScopes.Union(client.Scopes).ToList(),
grantTypes: client.GrantTypes.ToList(),
redirectUris: client.RedirectUris,
postLogoutRedirectUris: client.PostLogoutRedirectUris,
consentType: OpenIddictConstants.ConsentTypes.Implicit
);
}
}
private async Task CreateApiResourcesAsync()
{
var apiResources = _configuration.GetSection("ApiResource").Get<string[]>();
foreach (var item in apiResources)
{
await CreateApiResourceAsync(item);
}
}
private async Task CreateApiResourceAsync(string name)
{
if (await _scopeManager.FindByNameAsync(name) == null)
{
await _scopeManager.CreateAsync(new OpenIddictScopeDescriptor
{
Name = name,
DisplayName = name + " API",
Resources =
{
name
}
});
}
}
private async Task CreateClientAsync(
[NotNull] string name,
[NotNull] string type,
[NotNull] string consentType,
string displayName,
string secret,
List<string> grantTypes,
List<string> scopes,
string[] redirectUris = null,
string[] postLogoutRedirectUris = null,
List<string> permissions = null)
{
if (!string.IsNullOrEmpty(secret) && string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
{
throw new BusinessException(L["NoClientSecretCanBeSetForPublicApplications"]);
}
if (string.IsNullOrEmpty(secret) && string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
{
throw new BusinessException(L["TheClientSecretIsRequiredForConfidentialApplications"]);
}
if (!string.IsNullOrEmpty(name) && await _applicationManager.FindByClientIdAsync(name) != null)
{
return;
//throw new BusinessException(L["TheClientIdentifierIsAlreadyTakenByAnotherApplication"]);
}
var client = await _applicationManager.FindByClientIdAsync(name);
if (client == null)
{
var application = new OpenIddictApplicationDescriptor
{
ClientId = name,
Type = type,
ClientSecret = secret,
ConsentType = consentType,
DisplayName = displayName
};
Check.NotNullOrEmpty(grantTypes, nameof(grantTypes));
Check.NotNullOrEmpty(scopes, nameof(scopes));
if (new[] { OpenIddictConstants.GrantTypes.AuthorizationCode, OpenIddictConstants.GrantTypes.Implicit }.All(grantTypes.Contains))
{
application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.CodeIdToken);
if (string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
{
application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.CodeIdTokenToken);
application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.CodeToken);
}
}
application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Logout);
foreach (var grantType in grantTypes)
{
if (grantType == OpenIddictConstants.GrantTypes.AuthorizationCode)
{
application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode);
application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.Code);
}
if (grantType == OpenIddictConstants.GrantTypes.AuthorizationCode || grantType == OpenIddictConstants.GrantTypes.Implicit)
{
application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Authorization);
}
if (grantType == OpenIddictConstants.GrantTypes.AuthorizationCode ||
grantType == OpenIddictConstants.GrantTypes.ClientCredentials ||
grantType == OpenIddictConstants.GrantTypes.Password ||
grantType == OpenIddictConstants.GrantTypes.RefreshToken ||
grantType == OpenIddictConstants.GrantTypes.DeviceCode)
{
application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);
application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Revocation);
application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Introspection);
}
if (grantType == OpenIddictConstants.GrantTypes.ClientCredentials)
{
application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.ClientCredentials);
}
if (grantType == OpenIddictConstants.GrantTypes.Implicit)
{
application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.Implicit);
}
if (grantType == OpenIddictConstants.GrantTypes.Password)
{
application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.Password);
}
if (grantType == OpenIddictConstants.GrantTypes.RefreshToken)
{
application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.RefreshToken);
}
if (grantType == OpenIddictConstants.GrantTypes.DeviceCode)
{
application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.DeviceCode);
application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Device);
}
if (grantType == OpenIddictConstants.GrantTypes.Implicit)
{
application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.IdToken);
if (string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
{
application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.IdTokenToken);
application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.Token);
}
}
}
var buildInScopes = new[]
{
OpenIddictConstants.Permissions.Scopes.Address,
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Scopes.Phone,
OpenIddictConstants.Permissions.Scopes.Profile,
OpenIddictConstants.Permissions.Scopes.Roles,
"offline_access"
};
foreach (var scope in scopes)
{
if (buildInScopes.Contains(scope))
{
application.Permissions.Add(scope);
}
else
{
application.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scope);
}
}
if (redirectUris != null)
{
foreach (var redirectUri in redirectUris)
{
if (!redirectUri.IsNullOrEmpty())
{
if (!Uri.TryCreate(redirectUri, UriKind.Absolute, out var uri) || !uri.IsWellFormedOriginalString())
{
throw new BusinessException(L["InvalidRedirectUri", redirectUri]);
}
if (application.RedirectUris.All(x => x != uri))
{
application.RedirectUris.Add(uri);
}
}
}
}
if (postLogoutRedirectUris != null)
{
foreach (var postLogoutRedirectUri in postLogoutRedirectUris)
{
if (!postLogoutRedirectUri.IsNullOrEmpty())
{
if (!Uri.TryCreate(postLogoutRedirectUri, UriKind.Absolute, out var uri) || !uri.IsWellFormedOriginalString())
{
throw new BusinessException(L["InvalidPostLogoutRedirectUri", postLogoutRedirectUri]);
}
if (application.PostLogoutRedirectUris.All(x => x != uri))
{
application.PostLogoutRedirectUris.Add(uri);
}
}
}
}
if (permissions != null)
{
await _permissionDataSeeder.SeedAsync(
ClientPermissionValueProvider.ProviderName,
name,
permissions,
null
);
}
await _applicationManager.CreateAsync(application);
}
}
}
Create a new class OpenIddictDataSeedContributor
and add the following code:
using System.Threading.Tasks;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
namespace Tasky.DbMigrator;
public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDependency
{
private readonly OpenIddictDataSeeder _OpenIddictDataSeeder;
public OpenIddictDataSeedContributor(OpenIddictDataSeeder OpenIddictDataSeeder)
{
_OpenIddictDataSeeder = OpenIddictDataSeeder;
}
public async Task SeedAsync(DataSeedContext context)
{
await _OpenIddictDataSeeder.SeedAsync();
}
}
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.
dotnet 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:
dotnet 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
Related Posts
Migrating Tye to Aspire
In this post we will see how to migrate the Tye to Aspire
.Net Microservice template with ABP
In this post I will show you how to create ABP microservice using a dotnet new template.
.NET Microservice with ABP - Full Series
This post contains all the parts of the microservice development with ABP