Browse Source

Updated public-web keycloak integration

pull/225/head
Galip Tolga Erdem 3 years ago
parent
commit
befedd6322
  1. 4
      apps/public-web/src/EShopOnAbp.PublicWeb/EShopOnAbpPublicWebModule.cs
  2. 58
      etc/k8s/eshoponabp/charts/identity/templates/identity-deployment.yaml
  3. 7
      etc/k8s/eshoponabp/charts/identity/values.yaml
  4. 11
      etc/k8s/eshoponabp/values.st.yaml
  5. 432
      services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/DbMigrations/KeycloakDataSeeder.cs
  6. 1
      services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/EShopOnAbp.IdentityService.HttpApi.Host.csproj
  7. 2
      services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json

4
apps/public-web/src/EShopOnAbp.PublicWeb/EShopOnAbpPublicWebModule.cs

@ -215,7 +215,7 @@ public class EShopOnAbpPublicWebModule : AbpModule
{
// Intercept the redirection so the browser navigates to the right URL in your host
ctx.ProtocolMessage.IssuerAddress = configuration["AuthServer:Authority"].EnsureEndsWith('/') +
"connect/authorize";
"protocol/openid-connect/auth";
if (previousOnRedirectToIdentityProvider != null)
{
@ -228,7 +228,7 @@ public class EShopOnAbpPublicWebModule : AbpModule
{
// Intercept the redirection for signout so the browser navigates to the right URL in your host
ctx.ProtocolMessage.IssuerAddress = configuration["AuthServer:Authority"].EnsureEndsWith('/') +
"connect/endsession";
"protocol/openid-connect/logout";
if (previousOnRedirectToIdentityProviderForSignOut != null)
{

58
etc/k8s/eshoponabp/charts/identity/templates/identity-deployment.yaml

@ -36,7 +36,15 @@ spec:
- name: "RabbitMQ__Connections__Default__HostName"
value: "{{ .Values.config.rabbitmqHost }}"
- name: "ElasticSearch__Url"
value: "{{ .Values.config.elasticsearchHost }}"
value: "{{ .Values.config.elasticsearchHost }}"
- name: "Keycloak__url"
value: "{{ .Values.config.keycloak.url }}"
- name: "Keycloak__adminUsername"
value: "{{ .Values.config.keycloak.adminUsername }}"
- name: "Keycloak__adminPassword"
value: "{{ .Values.config.keycloak.adminPassword }}"
- name: "Keycloak__realmName"
value: "{{ .Values.config.keycloak.realmName }}"
- name: "AuthServer__Authority"
value: "{{ .Values.config.authServer.authority }}"
- name: "AuthServer__RequireHttpsMetadata"
@ -47,30 +55,30 @@ spec:
value: "{{ .Values.config.authServer.swaggerClientSecret }}"
- name: "StringEncryption__DefaultPassPhrase"
value: "{{ .Values.config.stringEncryptionDefaultPassPhrase }}"
- name: "IdentityServerClients__Web__RootUrl"
value: {{ .Values.identityServerClients.webRootUrl }}
- name: "IdentityServerClients__PublicWeb__RootUrl"
value: {{ .Values.identityServerClients.publicWebRootUrl }}
- name: "IdentityServerClients__PublicWebGateway__RootUrl"
value: {{ .Values.identityServerClients.publicWebGatewayRootUrl }}
- name: "IdentityServerClients__WebGateway__RootUrl"
value: {{ .Values.identityServerClients.webGatewayRootUrl }}
- name: "IdentityServerClients__IdentityService__RootUrl"
value: {{ .Values.identityServerClients.identityServiceRootUrl }}
- name: "IdentityServerClients__AdministrationService__RootUrl"
value: {{ .Values.identityServerClients.administrationServiceRootUrl }}
- name: "IdentityServerClients__AccountService__RootUrl"
value: {{ .Values.identityServerClients.accountServiceRootUrl }}
- name: "IdentityServerClients__BasketService__RootUrl"
value: {{ .Values.identityServerClients.basketServiceRootUrl }}
- name: "IdentityServerClients__CatalogService__RootUrl"
value: {{ .Values.identityServerClients.catalogServiceRootUrl }}
- name: "IdentityServerClients__OrderingService__RootUrl"
value: {{ .Values.identityServerClients.orderingServiceRootUrl }}
- name: "IdentityServerClients__CmskitService__RootUrl"
value: {{ .Values.identityServerClients.cmskitServiceRootUrl }}
- name: "IdentityServerClients__PaymentService__RootUrl"
value: {{ .Values.identityServerClients.paymentServiceRootUrl }}
- name: "Clients__Web__RootUrl"
value: {{ .Values.keycloakClients.webRootUrl }}
- name: "Clients__PublicWeb__RootUrl"
value: {{ .Values.keycloakClients.publicWebRootUrl }}
- name: "Clients__PublicWebGateway__RootUrl"
value: {{ .Values.keycloakClients.publicWebGatewayRootUrl }}
- name: "Clients__WebGateway__RootUrl"
value: {{ .Values.keycloakClients.webGatewayRootUrl }}
- name: "Clients__IdentityService__RootUrl"
value: {{ .Values.keycloakClients.identityServiceRootUrl }}
- name: "Clients__AdministrationService__RootUrl"
value: {{ .Values.keycloakClients.administrationServiceRootUrl }}
- name: "Clients__AccountService__RootUrl"
value: {{ .Values.keycloakClients.accountServiceRootUrl }}
- name: "Clients__BasketService__RootUrl"
value: {{ .Values.keycloakClients.basketServiceRootUrl }}
- name: "Clients__CatalogService__RootUrl"
value: {{ .Values.keycloakClients.catalogServiceRootUrl }}
- name: "Clients__OrderingService__RootUrl"
value: {{ .Values.keycloakClients.orderingServiceRootUrl }}
- name: "Clients__CmskitService__RootUrl"
value: {{ .Values.keycloakClients.cmskitServiceRootUrl }}
- name: "Clients__PaymentService__RootUrl"
value: {{ .Values.keycloakClients.paymentServiceRootUrl }}
{{- if .Values.env }}
{{ toYaml .Values.env | indent 8 }}
{{- end }}

7
etc/k8s/eshoponabp/charts/identity/values.yaml

@ -9,6 +9,11 @@ config:
requireHttpsMetadata: "false"
swaggerClientId: WebGateway_Swagger
swaggerClientSecret: "1q2w3e*"
keycloak:
url: http://eshop-st-keycloak:8080
adminUsername: admin
adminPassword: 1q2w3E*
realmName: master
dotnetEnv: Staging
redisHost: eshop-st-redis
rabbitmqHost: eshop-st-rabbitmq
@ -16,7 +21,7 @@ config:
stringEncryptionDefaultPassPhrase: gsKnGZ041HLL4IM8
# Seeded clients
identityServerClients:
keycloakClients:
webRootUrl: #
publicWebRootUrl: #
webGatewayRootUrl: #

11
etc/k8s/eshoponabp/values.st.yaml

@ -51,10 +51,10 @@ public-web:
selfUrl: https://eshoponabp.dev
gatewayUrl: http://eshop-st-gateway-web-public:8080/
authServer:
authority: https://account.eshoponabp.dev
authority: https://account.eshoponabp.dev/realms/master
requireHttpsMetadata: "false"
isOnProd: "true"
metaAddress: http://eshop-st-keycloak:8080/
metaAddress: http://eshop-st-keycloak:8080/realms/master
dotnetEnv: Staging
redisHost: eshop-st-redis
rabbitmqHost: eshop-st-rabbitmq
@ -74,6 +74,11 @@ identity:
connectionStrings:
identityService: "Host=eshop-st-postgresdb;Port=5432;Database=EShopOnAbp_Identity;User ID=postgres;password=myPassw0rd;Pooling=false"
administrationService: "Host=eshop-st-postgresdb;Port=5432;Database=EShopOnAbp_Administration;User ID=postgres;password=myPassw0rd;Pooling=false"
keycloak:
url: http://eshop-st-keycloak:8080
adminUsername: admin
adminPassword: 1q2w3E*
realmName: master
authServer:
authority: http://account.eshoponabp.dev
requireHttpsMetadata: "false"
@ -83,7 +88,7 @@ identity:
redisHost: eshop-st-redis
rabbitmqHost: eshop-st-rabbitmq
elasticsearchHost: eshop-st-elasticsearch
identityServerClients: # Seeded Clients
keycloakClients: # Seeded Clients
webRootUrl: https://admin.eshoponabp.dev/
publicWebRootUrl: https://eshoponabp.dev/
webGatewayRootUrl: https://gateway-web.eshoponabp.dev/

432
services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/DbMigrations/KeycloakDataSeeder.cs

@ -0,0 +1,432 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EShopOnAbp.IdentityService.Keycloak.Service;
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.IdentityService.DbMigrations;
public class KeyCloakDataSeeder : IDataSeedContributor, ITransientDependency
{
private readonly KeycloakClient _keycloakClient;
private readonly KeycloakClientOptions _keycloakOptions;
private readonly ILogger<KeyCloakDataSeeder> _logger;
private readonly IConfiguration _configuration;
public KeyCloakDataSeeder(IOptions<KeycloakClientOptions> keycloakClientOptions, ILogger<KeyCloakDataSeeder> 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 UpdateRealmSettingsAsync();
await UpdateAdminUserAsync();
await CreateRoleMapperAsync(); // roles scope
await CreateClientScopesAsync();
await CreateClientsAsync();
}
private async Task UpdateRealmSettingsAsync()
{
var masterRealm = await _keycloakClient.GetRealmAsync(_keycloakOptions.RealmName);
if (masterRealm.AccessTokenLifespan != 30 * 60)
{
masterRealm.AccessTokenLifespan = 30 * 60;
await _keycloakClient.UpdateRealmAsync(_keycloakOptions.RealmName, masterRealm);
}
}
private async Task CreateRoleMapperAsync()
{
var roleScope = (await _keycloakClient.GetClientScopesAsync(_keycloakOptions.RealmName))
.FirstOrDefault(q => q.Name == "roles");
if (roleScope == null)
return;
if (!roleScope.ProtocolMappers.Any(q => q.Name == "roles"))
{
await _keycloakClient.CreateProtocolMapperAsync(_keycloakOptions.RealmName, roleScope.Id,
new ProtocolMapper()
{
Name = "roles",
Protocol = "openid-connect",
_ProtocolMapper = "oidc-usermodel-realm-role-mapper",
Config = new Dictionary<string, string>()
{
{ "access.token.claim", "true" },
{ "id.token.claim", "true" },
{ "claim.name", "role" },
{ "multivalued", "true" },
{ "userinfo.token.claim", "true" },
}
});
}
}
private async Task CreateClientScopesAsync()
{
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<ProtocolMapper>()
{
new ProtocolMapper()
{
Name = scopeName,
Protocol = "openid-connect",
_ProtocolMapper = "oidc-audience-mapper",
Config =
new
Dictionary<string, string>
{
{ "id.token.claim", "false" },
{ "access.token.claim", "true" },
{ "included.custom.audience", scopeName }
}
}
}
};
await _keycloakClient.CreateClientScopeAsync(_keycloakOptions.RealmName, scope);
}
}
private async Task CreateClientsAsync()
{
await CreatePublicWebClientAsync();
await CreateSwaggerClientAsync();
await CreateWebClientAsync();
// await CreateCmskitClientAsync();
// await CreateAdministrationClientAsync();
}
private async Task CreateAdministrationClientAsync()
{
var administrationClient =
(await _keycloakClient.GetClientsAsync(_keycloakOptions.RealmName,
clientId: "EShopOnAbp_AdministrationService"))
.FirstOrDefault();
if (administrationClient == null)
{
administrationClient = new Client()
{
ClientId = "EShopOnAbp_AdministrationService",
Name = "Administration service client",
Protocol = "openid-connect",
PublicClient = false,
ImplicitFlowEnabled = false,
AuthorizationServicesEnabled = false,
StandardFlowEnabled = false,
DirectAccessGrantsEnabled = false,
ServiceAccountsEnabled = true,
Secret = "1q2w3e*"
};
administrationClient.Attributes = new Dictionary<string, object>()
{
{ "oauth2.device.authorization.grant.enabled", false },
{ "oidc.ciba.grant.enabled", false }
};
await _keycloakClient.CreateClientAsync(_keycloakOptions.RealmName, administrationClient);
await AddOptionalClientScopesAsync(
"EShopOnAbp_AdministrationService",
new List<string>
{
"IdentityService"
}
);
var insertedClient =
(await _keycloakClient.GetClientsAsync(_keycloakOptions.RealmName, clientId: "EShopOnAbp_AdministrationService"))
.First();
var clientIdProtocolMapper = insertedClient.ProtocolMappers.First(q => q.Name == "Client ID");
clientIdProtocolMapper.Config["claim.name"] = "client_id";
var result = await _keycloakClient.UpdateClientAsync(_keycloakOptions.RealmName, insertedClient.Id,
insertedClient);
}
}
private async Task CreateCmskitClientAsync()
{
var cmsKitClient =
(await _keycloakClient.GetClientsAsync(_keycloakOptions.RealmName, clientId: "EShopOnAbp_CmskitService"))
.FirstOrDefault();
if (cmsKitClient == null)
{
cmsKitClient = new Client()
{
ClientId = "EShopOnAbp_CmskitService",
Name = "Cmskit microservice client",
Protocol = "openid-connect",
PublicClient = false,
ImplicitFlowEnabled = false,
AuthorizationServicesEnabled = false,
StandardFlowEnabled = false,
DirectAccessGrantsEnabled = false,
ServiceAccountsEnabled = true,
Secret = "1q2w3e*"
};
cmsKitClient.Attributes = new Dictionary<string, object>()
{
{ "oauth2.device.authorization.grant.enabled", false },
{ "oidc.ciba.grant.enabled", false },
{ "client_credentials.use_refresh_token", false }
};
await _keycloakClient.CreateClientAsync(_keycloakOptions.RealmName, cmsKitClient);
await AddOptionalClientScopesAsync(
"EShopOnAbp_CmskitService",
new List<string>
{
"IdentityService"
}
);
var insertedClient =
(await _keycloakClient.GetClientsAsync(_keycloakOptions.RealmName, clientId: "EShopOnAbp_CmskitService"))
.First();
var clientIdProtocolMapper = insertedClient.ProtocolMappers.First(q => q.Name == "Client ID");
clientIdProtocolMapper.Config["claim.name"] = "client_id";
var result = await _keycloakClient.UpdateClientAsync(_keycloakOptions.RealmName, insertedClient.Id,
insertedClient);
}
}
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<string>
{
$"{webRootUrl.TrimEnd('/')}"
},
FrontChannelLogout = true,
PublicClient = true
};
webClient.Attributes = new Dictionary<string, object>
{
{ "post.logout.redirect.uris", $"{webRootUrl.TrimEnd('/')}" }
};
await _keycloakClient.CreateClientAsync(_keycloakOptions.RealmName, webClient);
await AddOptionalClientScopesAsync(
"Web",
new List<string>
{
"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<string>
{
$"{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);
await AddOptionalClientScopesAsync(
"SwaggerClient",
new List<string>
{
"AdministrationService", "IdentityService", "BasketService", "CatalogService",
"OrderingService", "PaymentService", "CmskitService"
}
);
}
}
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<string>
{
$"{publicWebRootUrl.TrimEnd('/')}/signin-oidc"
},
FrontChannelLogout = true,
PublicClient = true,
ImplicitFlowEnabled = true // for hybrid flow
};
publicWebClient.Attributes = new Dictionary<string, object>
{
{ "post.logout.redirect.uris", $"{publicWebRootUrl.TrimEnd('/')}/signout-callback-oidc" }
};
await _keycloakClient.CreateClientAsync(_keycloakOptions.RealmName, publicWebClient);
await AddOptionalClientScopesAsync(
"PublicWeb",
new List<string>
{
"AdministrationService", "IdentityService", "BasketService", "CatalogService",
"OrderingService", "PaymentService", "CmskitService"
}
);
}
}
private async Task AddOptionalClientScopesAsync(string clientName, List<string> 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)
{
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);
}
}
}

1
services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/EShopOnAbp.IdentityService.HttpApi.Host.csproj

@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Keycloak.Net.Core" Version="1.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json

@ -47,7 +47,7 @@
"ElasticSearch": {
"Url": "http://localhost:9200"
},
"IdentityServerClients": {
"Clients": {
"Web": {
"RootUrl": "http://localhost:4200"
},

Loading…
Cancel
Save