diff --git a/EShopOnAbp.sln b/EShopOnAbp.sln index e7a4264d..73d5919b 100644 --- a/EShopOnAbp.sln +++ b/EShopOnAbp.sln @@ -43,6 +43,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EShopOnAbp.BasketService", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EShopOnAbp.CmskitService.HttpApi.Host", "services\cmskit\src\EShopOnAbp.CmskitService.HttpApi.Host\EShopOnAbp.CmskitService.HttpApi.Host.csproj", "{D5B9D5A5-44AA-42F8-867C-95B54780C9DC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EShopOnAbp.Keycloak.DbMigrator", "shared\EShopOnAbp.Keycloak.DbMigrator\EShopOnAbp.Keycloak.DbMigrator.csproj", "{774C6ADF-BDD0-431C-A9F3-8BAFD5A49C8C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -113,6 +115,10 @@ Global {D5B9D5A5-44AA-42F8-867C-95B54780C9DC}.Debug|Any CPU.Build.0 = Debug|Any CPU {D5B9D5A5-44AA-42F8-867C-95B54780C9DC}.Release|Any CPU.ActiveCfg = Release|Any CPU {D5B9D5A5-44AA-42F8-867C-95B54780C9DC}.Release|Any CPU.Build.0 = Release|Any CPU + {774C6ADF-BDD0-431C-A9F3-8BAFD5A49C8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {774C6ADF-BDD0-431C-A9F3-8BAFD5A49C8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {774C6ADF-BDD0-431C-A9F3-8BAFD5A49C8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {774C6ADF-BDD0-431C-A9F3-8BAFD5A49C8C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -134,6 +140,7 @@ Global {7AE4C613-780E-4DA5-9B57-76F0D40D5146} = {F415FFFD-E52D-4EBE-98DC-067C1EFFFFE3} {E373DD66-3247-4D95-B325-064BBBC337B1} = {F415FFFD-E52D-4EBE-98DC-067C1EFFFFE3} {D5B9D5A5-44AA-42F8-867C-95B54780C9DC} = {F415FFFD-E52D-4EBE-98DC-067C1EFFFFE3} + {774C6ADF-BDD0-431C-A9F3-8BAFD5A49C8C} = {B8B59303-2178-459B-91A8-DF353044E090} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {26F82565-C6A4-439D-93A4-3251E3E7D5B0} diff --git a/apps/angular/src/environments/environment.ts b/apps/angular/src/environments/environment.ts index 9c3c768d..243cea14 100644 --- a/apps/angular/src/environments/environment.ts +++ b/apps/angular/src/environments/environment.ts @@ -9,11 +9,12 @@ export const environment = { name: 'EShopOnAbp', }, oAuthConfig: { - issuer: 'https://localhost:44330', + issuer: 'http://localhost:8080/realms/master', redirectUri: baseUrl, clientId: 'Web', responseType: 'code', - scope: 'offline_access openid profile email phone AccountService IdentityService AdministrationService CatalogService OrderingService', + scope: 'offline_access openid profile email phone', + // scope: 'offline_access openid profile email phone AccountService IdentityService AdministrationService CatalogService OrderingService', //TODO: Update when https://github.com/AnderssonPeter/Keycloak.Net/pull/5 is merged //requireHttps: true, }, apis: { diff --git a/apps/public-web/src/EShopOnAbp.PublicWeb/EShopOnAbpPublicWebModule.cs b/apps/public-web/src/EShopOnAbp.PublicWeb/EShopOnAbpPublicWebModule.cs index 42efd925..b250e7b9 100644 --- a/apps/public-web/src/EShopOnAbp.PublicWeb/EShopOnAbpPublicWebModule.cs +++ b/apps/public-web/src/EShopOnAbp.PublicWeb/EShopOnAbpPublicWebModule.cs @@ -23,6 +23,7 @@ using Polly; using StackExchange.Redis; using System; using System.Net.Http.Headers; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Volo.Abp; using Volo.Abp.Account; using Volo.Abp.AspNetCore.Authentication.OpenIdConnect; @@ -42,6 +43,7 @@ using Volo.Abp.Http.Client; using Volo.Abp.Http.Client.IdentityModel.Web; using Volo.Abp.Modularity; using Volo.Abp.MultiTenancy; +using Volo.Abp.Security.Claims; using Volo.Abp.UI.Navigation; using Volo.Abp.UI.Navigation.Urls; using Volo.Abp.VirtualFileSystem; @@ -142,25 +144,45 @@ public class EShopOnAbpPublicWebModule : AbpModule .AddAbpOpenIdConnect("oidc", options => { options.Authority = configuration["AuthServer:Authority"]; - options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); - options.ResponseType = OpenIdConnectResponseType.CodeIdToken; - options.ClientId = configuration["AuthServer:ClientId"]; - options.ClientSecret = configuration["AuthServer:ClientSecret"]; - - options.SaveTokens = true; + options.MetadataAddress = configuration["AuthServer:MetaAddress"]; + options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); options.GetClaimsFromUserInfoEndpoint = true; - - options.Scope.Add("role"); + options.Scope.Add("openid"); + options.Scope.Add("profile"); options.Scope.Add("email"); options.Scope.Add("phone"); - options.Scope.Add("AccountService"); - options.Scope.Add("AdministrationService"); - options.Scope.Add("BasketService"); - options.Scope.Add("CatalogService"); - options.Scope.Add("PaymentService"); - options.Scope.Add("OrderingService"); - options.Scope.Add("CmskitService"); + options.Scope.Add("roles"); + options.Scope.Add("offline_access"); + // Audiences couldn't be seeded -> TODO: Update when library is updated + // options.Scope.Add("AccountService"); + // options.Scope.Add("AdministrationService"); + // options.Scope.Add("BasketService"); + // options.Scope.Add("CatalogService"); + // options.Scope.Add("PaymentService"); + // options.Scope.Add("OrderingService"); + // options.Scope.Add("CmskitService"); + + options.SaveTokens = true; + //Token response type, will sometimes need to be changed to IdToken, depending on config. + options.ResponseType = OpenIdConnectResponseType.Code; + //SameSite is needed for Chrome/Firefox, as they will give http error 500 back, if not set to unspecified. + // options.NonceCookie.SameSite = SameSiteMode.Unspecified; + // options.CorrelationCookie.SameSite = SameSiteMode.Unspecified; + // + // options.TokenValidationParameters = new TokenValidationParameters + // { + // NameClaimType = "name", + // RoleClaimType = ClaimTypes.Role, + // ValidateIssuer = true + // }; + + if (AbpClaimTypes.UserName != "preferred_username") + { + options.ClaimActions.MapJsonKey(AbpClaimTypes.UserName, "preferred_username"); + options.ClaimActions.DeleteClaim("preferred_username"); + options.ClaimActions.RemoveDuplicate(AbpClaimTypes.UserName); + } }); if (Convert.ToBoolean(configuration["AuthServer:IsOnProd"])) { diff --git a/apps/public-web/src/EShopOnAbp.PublicWeb/appsettings.json b/apps/public-web/src/EShopOnAbp.PublicWeb/appsettings.json index 8d0d9e5b..c339e14c 100644 --- a/apps/public-web/src/EShopOnAbp.PublicWeb/appsettings.json +++ b/apps/public-web/src/EShopOnAbp.PublicWeb/appsettings.json @@ -36,12 +36,18 @@ "Url": "http://localhost:9200" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", "ClientId": "PublicWeb", - "ClientSecret": "1q2w3e*", "IsOnProd": "false", - "MetaAddress": "https://localhost:44330" + "MetaAddress": "http://localhost:8080/realms/master/.well-known/openid-configuration" + }, + "Keycloak": { + "ServerRealm": "http://localhost:8080/realms/master", + "Metadata": "http://localhost:8080/realms/master/.well-known/openid-configuration", + "ClientId": "PublicWeb", + "TokenExchange": "http://localhost:8080/realms/master/protocol/openid-connect/token", + "Audience": "some-audience" }, "ReverseProxy": { "Routes": { diff --git a/etc/docker/docker-compose.infrastructure.override.yml b/etc/docker/docker-compose.infrastructure.override.yml index 3ae3deb5..7b9c83a2 100644 --- a/etc/docker/docker-compose.infrastructure.override.yml +++ b/etc/docker/docker-compose.infrastructure.override.yml @@ -1,4 +1,4 @@ -version: '3.7' +version: '3.8' services: rabbitmq: @@ -18,6 +18,7 @@ services: ports: - "5432:5432" environment: + POSTGRES_DB: keycloak POSTGRES_PASSWORD: "myPassw0rd" pgadmin: @@ -26,3 +27,19 @@ services: environment: PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org} PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} + + keycloak: + ports: + - "8080:8080" + environment: + DB_VENDOR: postgres + DB_ADDR: "postgres-db" + DB_DATABASE: "keycloak" + DB_USER: "postgres" + DB_PASSWORD: "myPassw0rd" + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: "1q2w3E*" + KC_HEALTH_ENABLED: "true" + entrypoint: ["/opt/keycloak/bin/kc.sh", "start-dev"] + + \ No newline at end of file diff --git a/etc/docker/docker-compose.infrastructure.yml b/etc/docker/docker-compose.infrastructure.yml index 547b3aae..3f72762f 100644 --- a/etc/docker/docker-compose.infrastructure.yml +++ b/etc/docker/docker-compose.infrastructure.yml @@ -1,4 +1,4 @@ -version: '3.7' +version: '3.8' services: rabbitmq: @@ -46,12 +46,21 @@ services: - eshoponabp-network pgadmin: - container_name: pgadmin_container + container_name: pgadmin image: dpage/pgadmin4:6.2 volumes: - pgadmin_data:/var/lib/pgadmin networks: - - eshoponabp-network + - eshoponabp-network + + keycloak: + container_name: keycloak + image: quay.io/keycloak/keycloak:19.0.2 + depends_on: + - postgres-db + restart: unless-stopped + networks: + - eshoponabp-network volumes: postgres_data: diff --git a/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/AdministrationServiceHttpApiHostModule.cs b/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/AdministrationServiceHttpApiHostModule.cs index 9b349ff8..7ecb1575 100644 --- a/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/AdministrationServiceHttpApiHostModule.cs +++ b/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/AdministrationServiceHttpApiHostModule.cs @@ -91,7 +91,7 @@ public class AdministrationServiceHttpApiHostModule : AbpModule var configuration = context.ServiceProvider.GetRequiredService(); options.SwaggerEndpoint("/swagger/v1/swagger.json", "Administration Service API"); options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]); - options.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]); + // options.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]); }); app.UseAbpSerilogEnrichers(); app.UseAuditing(); diff --git a/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/appsettings.json b/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/appsettings.json index 8610b450..9368fc53 100644 --- a/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/appsettings.json +++ b/services/administration/src/EShopOnAbp.AdministrationService.HttpApi.Host/appsettings.json @@ -4,10 +4,9 @@ "CorsOrigins": "https://localhost:44372,https://localhost:44373" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", - "SwaggerClientId": "WebGateway_Swagger", - "SwaggerClientSecret": "1q2w3e*" + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", + "SwaggerClientId": "SwaggerClient" }, "RemoteServices": { "AbpIdentity": { diff --git a/services/basket/src/EShopOnAbp.BasketService/appsettings.json b/services/basket/src/EShopOnAbp.BasketService/appsettings.json index 93adbecc..983d031c 100644 --- a/services/basket/src/EShopOnAbp.BasketService/appsettings.json +++ b/services/basket/src/EShopOnAbp.BasketService/appsettings.json @@ -4,8 +4,8 @@ "CorsOrigins": "https://localhost:44372,https://localhost:44373,https://localhost:44335" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" }, diff --git a/services/catalog/src/EShopOnAbp.CatalogService.HttpApi.Host/appsettings.json b/services/catalog/src/EShopOnAbp.CatalogService.HttpApi.Host/appsettings.json index 645ce941..2a8273b1 100644 --- a/services/catalog/src/EShopOnAbp.CatalogService.HttpApi.Host/appsettings.json +++ b/services/catalog/src/EShopOnAbp.CatalogService.HttpApi.Host/appsettings.json @@ -4,8 +4,8 @@ "CorsOrigins": "https://localhost:44372,https://localhost:44373,https://localhost:44335,http://localhost:4200" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" }, diff --git a/services/cmskit/src/EShopOnAbp.CmskitService.HttpApi.Host/appsettings.json b/services/cmskit/src/EShopOnAbp.CmskitService.HttpApi.Host/appsettings.json index 151dd3ec..2558921d 100644 --- a/services/cmskit/src/EShopOnAbp.CmskitService.HttpApi.Host/appsettings.json +++ b/services/cmskit/src/EShopOnAbp.CmskitService.HttpApi.Host/appsettings.json @@ -4,8 +4,8 @@ "CorsOrigins": "https://localhost:44372,https://localhost:44373,http://localhost:4200,https://localhost:44335" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" }, diff --git a/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json b/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json index 9678ab64..014033f8 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json +++ b/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json @@ -4,8 +4,8 @@ "CorsOrigins": "https://localhost:44372,https://localhost:44373" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" }, diff --git a/services/ordering/src/EShopOnAbp.OrderingService.HttpApi.Host/appsettings.json b/services/ordering/src/EShopOnAbp.OrderingService.HttpApi.Host/appsettings.json index 5d6b54e1..865a6130 100644 --- a/services/ordering/src/EShopOnAbp.OrderingService.HttpApi.Host/appsettings.json +++ b/services/ordering/src/EShopOnAbp.OrderingService.HttpApi.Host/appsettings.json @@ -4,8 +4,8 @@ "CorsOrigins": "https://localhost:44372,https://localhost:44373,http://localhost:4200" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" }, diff --git a/services/payment/src/EShopOnAbp.PaymentService.HttpApi.Host/appsettings.json b/services/payment/src/EShopOnAbp.PaymentService.HttpApi.Host/appsettings.json index 83d05c81..93582be1 100644 --- a/services/payment/src/EShopOnAbp.PaymentService.HttpApi.Host/appsettings.json +++ b/services/payment/src/EShopOnAbp.PaymentService.HttpApi.Host/appsettings.json @@ -4,8 +4,8 @@ "CorsOrigins": "https://localhost:44372,https://localhost:44373" }, "AuthServer": { - "Authority": "https://localhost:44330", - "RequireHttpsMetadata": "true", + "Authority": "http://localhost:8080/realms/master", + "RequireHttpsMetadata": "false", "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" }, diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/DbMigratorHostedService.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/DbMigratorHostedService.cs new file mode 100644 index 00000000..d28e0ecf --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/DbMigratorHostedService.cs @@ -0,0 +1,47 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using Volo.Abp; + +namespace EShopOnAbp.DbMigrator; + +public class DbMigratorHostedService : IHostedService +{ + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly IConfiguration _configuration; + + public DbMigratorHostedService( + IHostApplicationLifetime hostApplicationLifetime, + IConfiguration configuration) + { + _hostApplicationLifetime = hostApplicationLifetime; + _configuration = configuration; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + using (var application = AbpApplicationFactory.Create(options => + { + options.Services.ReplaceConfiguration(_configuration); + options.UseAutofac(); + options.Services.AddLogging(c => c.AddSerilog()); + })) + { + application.Initialize(); + + await application + .ServiceProvider + .GetRequiredService() + .MigrateAsync(cancellationToken); + + application.Shutdown(); + + _hostApplicationLifetime.StopApplication(); + } + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbp.Keycloak.DbMigrator.csproj b/shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbp.Keycloak.DbMigrator.csproj new file mode 100644 index 00000000..f7f9c1ba --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbp.Keycloak.DbMigrator.csproj @@ -0,0 +1,34 @@ + + + + Exe + net6.0 + EShopOnAbp.DbMigrator + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + Always + + + + diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbpDbMigratorModule.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbpDbMigratorModule.cs new file mode 100644 index 00000000..fcdb8df2 --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/EShopOnAbpDbMigratorModule.cs @@ -0,0 +1,25 @@ +using EShopOnAbp.Shared.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; + +namespace EShopOnAbp.DbMigrator; + +[DependsOn( + typeof(EShopOnAbpSharedHostingModule) +)] +public class EShopOnAbpDbMigratorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + Configure(options => + { + options.Url = configuration["Keycloak:url"]; + options.AdminUserName = configuration["Keycloak:adminUsername"]; + options.AdminPassword = configuration["Keycloak:adminPassword"]; + options.RealmName = configuration["Keycloak:realmName"]; + } + ); + } +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakClientOptions.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakClientOptions.cs new file mode 100644 index 00000000..6c24a9b8 --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakClientOptions.cs @@ -0,0 +1,9 @@ +namespace EShopOnAbp.DbMigrator; + +public class KeycloakClientOptions +{ + public string Url { get; set; } + public string AdminUserName { get; set; } + public string AdminPassword { get; set; } + public string RealmName { get; set; } +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakDataSeeder.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakDataSeeder.cs new file mode 100644 index 00000000..32aff192 --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakDataSeeder.cs @@ -0,0 +1,283 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Keycloak.Net; +using Keycloak.Net.Models.Clients; +using Keycloak.Net.Models.ClientScopes; +using Keycloak.Net.Models.ProtocolMappers; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.DbMigrator; + +public class KeyCloakDataSeeder : IDataSeedContributor, ITransientDependency +{ + private readonly KeycloakClient _keycloakClient; + private readonly KeycloakClientOptions _keycloakOptions; + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + + public KeyCloakDataSeeder(IOptions keycloakClientOptions, ILogger logger, IConfiguration configuration) + { + _logger = logger; + _configuration = configuration; + _keycloakOptions = keycloakClientOptions.Value; + + _keycloakClient = new KeycloakClient( + _keycloakOptions.Url, + _keycloakOptions.AdminUserName, + _keycloakOptions.AdminPassword + ); + } + + public async Task SeedAsync(DataSeedContext context) + { + await UpdateAdminUserAsync(); + await CreateClientScopesAsync(); + await CreateClientsAsync(); + } + + private async Task CreateClientScopesAsync() + { + await CreateScopeAsync("AccountService"); + await CreateScopeAsync("AdministrationService"); + await CreateScopeAsync("IdentityService"); + await CreateScopeAsync("BasketService"); + await CreateScopeAsync("CatalogService"); + await CreateScopeAsync("OrderingService"); + await CreateScopeAsync("PaymentService"); + await CreateScopeAsync("CmskitService"); + } + + private async Task CreateScopeAsync(string scopeName) + { + var scope = (await _keycloakClient.GetClientScopesAsync(_keycloakOptions.RealmName)) + .FirstOrDefault(q => q.Name == scopeName); + + if (scope == null) + { + scope = new ClientScope + { + Name = scopeName, + Description = scopeName + " scope", + Protocol = "openid-connect", + Attributes = new Attributes + { + ConsentScreenText = scopeName, + DisplayOnConsentScreen = "true", + IncludeInTokenScope = "true" + }, + ProtocolMappers = new List() + { + new ProtocolMapper() + { + Name = scopeName, + Protocol = "openid-connect", + _ProtocolMapper = "oidc-audience-mapper", + // Config = new Dictionary() //TODO: Update when //https://github.com/AnderssonPeter/Keycloak.Net/pull/5 is merged + // { + // { "id.token.claim", "false" }, + // { "access.token.claim", "true" }, + // { "included.custom.audience", scopeName } + // } + Config = new Config() // This should be dictionary -> Outdated library + { + AccessTokenClaim = "true", + IdTokenClaim = "false" + } + } + } + }; + + await _keycloakClient.CreateClientScopeAsync(_keycloakOptions.RealmName, scope); + } + } + + private async Task CreateClientsAsync() + { + await CreatePublicWebClientAsync(); + await CreateSwaggerClientAsync(); // TODO: Test when Volo.Abp.Swashbuckle v6.0.1 is released (https://github.com/abpframework/abp/pull/14409) + await CreateWebClientAsync(); + } + + private async Task CreateWebClientAsync() + { + var webClient = (await _keycloakClient.GetClientsAsync(_keycloakOptions.RealmName, clientId: "Web")) + .FirstOrDefault(); + + if (webClient == null) + { + var webRootUrl = _configuration[$"Clients:Web:RootUrl"]; + webClient = new Client + { + ClientId = "Web", + Name = "Angular Back-Office Web Application", + Protocol = "openid-connect", + Enabled = true, + BaseUrl = webRootUrl, + RedirectUris = new List + { + $"{webRootUrl.TrimEnd('/')}" + }, + FrontChannelLogout = true, + PublicClient = true + }; + webClient.Attributes = new Dictionary + { + { "post.logout.redirect.uris", $"{webRootUrl.TrimEnd('/')}" } + }; + + await _keycloakClient.CreateClientAsync(_keycloakOptions.RealmName, webClient); + + //TODO: Update when //https://github.com/AnderssonPeter/Keycloak.Net/pull/5 is merged + // await AddOptionalClientScopesAsync( + // "PublicWeb", + // new List + // { + // "AdministrationService", "IdentityService", "BasketService", "CatalogService", + // "OrderingService", "PaymentService", "CmskitService" + // } + // ); + } + } + + private async Task CreateSwaggerClientAsync() + { + var swaggerClient = (await _keycloakClient.GetClientsAsync(_keycloakOptions.RealmName, clientId: "SwaggerClient")) + .FirstOrDefault(); + + if (swaggerClient == null) + { + var webGatewaySwaggerRootUrl = _configuration[$"Clients:WebGateway:RootUrl"].TrimEnd('/'); + var publicWebGatewayRootUrl = _configuration[$"Clients:PublicWebGateway:RootUrl"].TrimEnd('/'); + var accountServiceRootUrl = _configuration[$"Clients:AccountService:RootUrl"].TrimEnd('/'); + var identityServiceRootUrl = _configuration[$"Clients:IdentityService:RootUrl"].TrimEnd('/'); + var administrationServiceRootUrl = _configuration[$"Clients:AdministrationService:RootUrl"].TrimEnd('/'); + var catalogServiceRootUrl = _configuration[$"Clients:CatalogService:RootUrl"].TrimEnd('/'); + var basketServiceRootUrl = _configuration[$"Clients:BasketService:RootUrl"].TrimEnd('/'); + var orderingServiceRootUrl = _configuration[$"Clients:OrderingService:RootUrl"].TrimEnd('/'); + var paymentServiceRootUrl = _configuration[$"Clients:PaymentService:RootUrl"].TrimEnd('/'); + var cmskitServiceRootUrl = _configuration[$"Clients:CmskitService:RootUrl"].TrimEnd('/'); + + swaggerClient = new Client + { + ClientId = "SwaggerClient", + Name = "Swagger Client Application", + Protocol = "openid-connect", + Enabled = true, + RedirectUris = new List + { + $"{webGatewaySwaggerRootUrl}/swagger/oauth2-redirect.html", // WebGateway redirect uri + $"{publicWebGatewayRootUrl}/swagger/oauth2-redirect.html", // PublicWebGateway redirect uri + $"{accountServiceRootUrl}/swagger/oauth2-redirect.html", // AccountService redirect uri + $"{identityServiceRootUrl}/swagger/oauth2-redirect.html", // IdentityService redirect uri + $"{administrationServiceRootUrl}/swagger/oauth2-redirect.html", // AdministrationService redirect uri + $"{catalogServiceRootUrl}/swagger/oauth2-redirect.html", // CatalogService redirect uri + $"{basketServiceRootUrl}/swagger/oauth2-redirect.html", // BasketService redirect uri + $"{orderingServiceRootUrl}/swagger/oauth2-redirect.html", // OrderingService redirect uri + $"{paymentServiceRootUrl}/swagger/oauth2-redirect.html", // PaymentService redirect uri + $"{cmskitServiceRootUrl}/swagger/oauth2-redirect.html" // CmskitService redirect uri + }, + FrontChannelLogout = true, + PublicClient = true + }; + + await _keycloakClient.CreateClientAsync(_keycloakOptions.RealmName, swaggerClient); + } + } + + private async Task CreatePublicWebClientAsync() + { + var publicWebClient = (await _keycloakClient.GetClientsAsync(_keycloakOptions.RealmName, clientId: "PublicWeb")) + .FirstOrDefault(); + + if (publicWebClient == null) + { + var publicWebRootUrl = _configuration[$"Clients:PublicWeb:RootUrl"]; + publicWebClient = new Client + { + ClientId = "PublicWeb", + Name = "Public Web Application", + Protocol = "openid-connect", + Enabled = true, + BaseUrl = publicWebRootUrl, + RedirectUris = new List + { + $"{publicWebRootUrl.TrimEnd('/')}/signin-oidc" + }, + FrontChannelLogout = true, + PublicClient = true + }; + publicWebClient.Attributes = new Dictionary + { + { "post.logout.redirect.uris", $"{publicWebRootUrl.TrimEnd('/')}/signout-callback-oidc" } + }; + + await _keycloakClient.CreateClientAsync(_keycloakOptions.RealmName, publicWebClient); + + //TODO: Update when //https://github.com/AnderssonPeter/Keycloak.Net/pull/5 is merged + // await AddOptionalClientScopesAsync( + // "PublicWeb", + // new List + // { + // "AdministrationService", "IdentityService", "BasketService", "CatalogService", + // "OrderingService", "PaymentService", "CmskitService" + // } + // ); + } + } + + private async Task AddOptionalClientScopesAsync(string clientName, List scopes) + { + var client = (await _keycloakClient.GetClientsAsync(_keycloakOptions.RealmName, clientId: clientName)) + .FirstOrDefault(); + if (client == null) + { + _logger.LogError($"Couldn't find {clientName}! Could not seed optional scopes!"); + return; + } + + var clientOptionalScopes = + (await _keycloakClient.GetOptionalClientScopesAsync(_keycloakOptions.RealmName, client.Id)).ToList(); + + var clientScopes = (await _keycloakClient.GetClientScopesAsync(_keycloakOptions.RealmName)).ToList(); + + foreach (var scope in scopes) + { + if (!clientOptionalScopes.Any(q => q.Name == scope)) + { + var serviceScope = clientScopes.First(q => q.Name == scope); + _logger.LogInformation($"Seeding {scope} scope to {clientName}."); + await _keycloakClient.UpdateOptionalClientScopeAsync(_keycloakOptions.RealmName, client.Id, + serviceScope.Id); + } + } + } + + private async Task UpdateAdminUserAsync() + { + var users = await _keycloakClient.GetUsersAsync(_keycloakOptions.RealmName, username: "admin"); + var adminUser = users.FirstOrDefault(); + if (adminUser == null) + { + _logger.LogError( + "Keycloak admin user is not provided, check if KEYCLOAK_ADMIN environment variable is passed properly."); + throw new Exception( + "Keycloak admin user is not provided, check if KEYCLOAK_ADMIN environment variable is passed properly."); + } + + if (string.IsNullOrEmpty(adminUser.Email)) + { + adminUser.Email = "admin@abp.io"; + adminUser.FirstName = "admin"; + adminUser.EmailVerified = true; + + _logger.LogInformation("Updating admin user with email and first name..."); + await _keycloakClient.UpdateUserAsync(_keycloakOptions.RealmName, adminUser.Id, adminUser); + } + } +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/MigrationService.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/MigrationService.cs new file mode 100644 index 00000000..aa0cf304 --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/MigrationService.cs @@ -0,0 +1,30 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.DbMigrator; + +public class MigrationService: ITransientDependency +{ + private readonly ILogger _logger; + private readonly IDataSeeder _dataSeeder; + + public MigrationService(ILogger logger, IDataSeeder dataSeeder) + { + _logger = logger; + _dataSeeder = dataSeeder; + } + + public async Task MigrateAsync(CancellationToken cancellationToken) + { + // Check if keycloak api is available + + //Seed data + await _dataSeeder.SeedAsync(); + + _logger.LogInformation("Migration completed!"); + } + +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/Program.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/Program.cs new file mode 100644 index 00000000..bd60e4fa --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/Program.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using EShopOnAbp.DbMigrator; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Events; + +class Program +{ + async static Task Main(string[] args) + { + Log.Logger = new LoggerConfiguration() +#if DEBUG + .MinimumLevel.Debug() +#else + .MinimumLevel.Information() +#endif + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning) + .Enrich.FromLogContext() + .Enrich.WithProperty("Application", $"DbMigrator") + .WriteTo.Async(c => c.File("Logs/logs.txt")) + .WriteTo.Async(c => c.Console()) + .CreateLogger(); + + await CreateHostBuilder(args).RunConsoleAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .AddAppSettingsSecretsJson() + .ConfigureLogging((context, logging) => logging.ClearProviders()) + .ConfigureServices((hostContext, services) => { services.AddHostedService(); }); +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/appsettings.json b/shared/EShopOnAbp.Keycloak.DbMigrator/appsettings.json new file mode 100644 index 00000000..13bcec2d --- /dev/null +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/appsettings.json @@ -0,0 +1,46 @@ +{ + "Keycloak": { + "url": "http://localhost:8080", + "adminUsername": "admin", + "adminPassword": "1q2w3E*", + "realmName": "master" + }, + "Clients": { + "Web": { + "RootUrl": "http://localhost:4200" + }, + "PublicWeb": { + "RootUrl": "https://localhost:44335" + }, + "WebGateway": { + "RootUrl": "https://localhost:44372" + }, + "PublicWebGateway": { + "RootUrl": "https://localhost:44373" + }, + "AccountService": { + "RootUrl": "https://localhost:44330" + }, + "IdentityService": { + "RootUrl": "https://localhost:44351" + }, + "AdministrationService": { + "RootUrl": "https://localhost:44353" + }, + "CatalogService": { + "RootUrl": "https://localhost:44354" + }, + "BasketService": { + "RootUrl": "https://localhost:44355" + }, + "OrderingService": { + "RootUrl": "https://localhost:44356" + }, + "PaymentService": { + "RootUrl": "https://localhost:44357" + }, + "CmskitService": { + "RootUrl": "https://localhost:44358" + } + } +} \ No newline at end of file diff --git a/shared/EShopOnAbp.Shared.Hosting.Microservices/EShopOnAbpSharedHostingMicroservicesModule.cs b/shared/EShopOnAbp.Shared.Hosting.Microservices/EShopOnAbpSharedHostingMicroservicesModule.cs index e497d6d5..fdad69fd 100644 --- a/shared/EShopOnAbp.Shared.Hosting.Microservices/EShopOnAbpSharedHostingMicroservicesModule.cs +++ b/shared/EShopOnAbp.Shared.Hosting.Microservices/EShopOnAbpSharedHostingMicroservicesModule.cs @@ -29,6 +29,7 @@ public class EShopOnAbpSharedHostingMicroservicesModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { + Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true; var configuration = context.Services.GetConfiguration(); Configure(options => diff --git a/shared/EShopOnAbp.Shared.Hosting.Microservices/JwtBearerConfigurationHelper.cs b/shared/EShopOnAbp.Shared.Hosting.Microservices/JwtBearerConfigurationHelper.cs index b00229f1..0cc48603 100644 --- a/shared/EShopOnAbp.Shared.Hosting.Microservices/JwtBearerConfigurationHelper.cs +++ b/shared/EShopOnAbp.Shared.Hosting.Microservices/JwtBearerConfigurationHelper.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.DependencyInjection; using System; +using Microsoft.IdentityModel.Tokens; using Volo.Abp.Modularity; namespace EShopOnAbp.Shared.Hosting.Microservices; @@ -19,6 +20,10 @@ public static class JwtBearerConfigurationHelper options.Authority = configuration["AuthServer:Authority"]; options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); options.Audience = audience; + options.TokenValidationParameters = new TokenValidationParameters() + { + ValidateAudience = false //Disabled since seeding audience is not possible with the current keycloak.net library version + }; }); } } \ No newline at end of file