From 221a435c629149e5fadb0514be6a595fe968594a Mon Sep 17 00:00:00 2001 From: Phil Schneider Date: Tue, 2 Apr 2024 14:42:21 +0200 Subject: [PATCH] feat(authorization): add role authorization (#19) --- charts/dim/templates/deployment.yaml | 12 ++- charts/dim/templates/service.yaml | 1 + charts/dim/values.yaml | 16 ++++ consortia/environments/values-dev.yaml | 6 ++ consortia/environments/values-int.yaml | 6 ++ .../Authentication/CustomClaimTypes.cs | 28 +++++++ .../KeycloakClaimsTransformation.cs | 73 +++++++++++++++++++ src/web/Dim.Web/Controllers/DimController.cs | 2 + src/web/Dim.Web/Dim.Web.csproj | 1 + src/web/Dim.Web/Program.cs | 3 + src/web/Dim.Web/appsettings.json | 16 +++- 11 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 src/web/Dim.Web/Authentication/CustomClaimTypes.cs create mode 100644 src/web/Dim.Web/Authentication/KeycloakClaimsTransformation.cs diff --git a/charts/dim/templates/deployment.yaml b/charts/dim/templates/deployment.yaml index 5f51fce..9289dac 100644 --- a/charts/dim/templates/deployment.yaml +++ b/charts/dim/templates/deployment.yaml @@ -80,7 +80,17 @@ spec: - name: "SWAGGERENABLED" value: "{{ .Values.dim.swaggerEnabled }}" - name: "DIM__ROOTDIRECTORYID" - value: "{{ .Values.dim.rootDirectoryId }}" + value: "{{ .Values.dim.rootDirectoryId }}" + - name: "JWTBEAREROPTIONS__METADATAADDRESS" + value: "{{ .Values.idp.address }}{{ .Values.idp.jwtBearerOptions.metadataPath }}" + - name: "JWTBEAREROPTIONS__REQUIREHTTPSMETADATA" + value: "{{ .Values.idp.jwtBearerOptions.requireHttpsMetadata }}" + - name: "JWTBEAREROPTIONS__TOKENVALIDATIONPARAMETERS__VALIDAUDIENCE" + value: "{{ .Values.idp.jwtBearerOptions.tokenValidationParameters.validAudience }}" + - name: "JWTBEAREROPTIONS__TOKENVALIDATIONPARAMETERS__VALIDISSUER" + value: "{{ .Values.idp.address }}{{ .Values.idp.jwtBearerOptions.tokenValidationParameters.validIssuerPath }}" + - name: "JWTBEAREROPTIONS__REFRESHINTERVAL" + value: "{{ .Values.idp.jwtBearerOptions.refreshInterval }}" ports: - name: http containerPort: {{ .Values.portContainer }} diff --git a/charts/dim/templates/service.yaml b/charts/dim/templates/service.yaml index e9d62f9..cc4ffc8 100644 --- a/charts/dim/templates/service.yaml +++ b/charts/dim/templates/service.yaml @@ -31,3 +31,4 @@ spec: targetPort: {{ .Values.portContainer }} selector: {{- include "dim.selectorLabels" . | nindent 4 }} + diff --git a/charts/dim/values.yaml b/charts/dim/values.yaml index 8d7e4c0..a8c4d50 100644 --- a/charts/dim/values.yaml +++ b/charts/dim/values.yaml @@ -172,6 +172,22 @@ externalDatabase: # -- Secret containing the password non-root username, (default 'dim'). existingSecret: "dim-external-db" +# -- Provide details about idp instance. +idp: + # -- Provide idp base address, without trailing '/auth'. + address: "https://centralidp.example.org" + authRealm: "CX-Central" + jwtBearerOptions: + requireHttpsMetadata: "true" + metadataPath: "/auth/realms/CX-Central/.well-known/openid-configuration" + tokenValidationParameters: + validIssuerPath: "/auth/realms/CX-Central" + validAudience: "Cl25-CX-Dim" + refreshInterval: "00:00:30" + tokenPath: "/auth/realms/CX-Central/protocol/openid-connect/token" + # -- Flag if the api should be used with an leading /auth path + useAuthTrail: true + ingress: # -- DIM ingress parameters, # enable ingress record generation for dim. diff --git a/consortia/environments/values-dev.yaml b/consortia/environments/values-dev.yaml index ffc131a..ef41465 100644 --- a/consortia/environments/values-dev.yaml +++ b/consortia/environments/values-dev.yaml @@ -86,6 +86,12 @@ processesworker: # -- Url to the cf service api baseAddress: "https://portal-backend.dev.demo.catena-x.net/api/administration/registration/dim/" +idp: + address: "https://centralidp.dev.demo.catena-x.net" + jwtBearerOptions: + tokenValidationParameters: + validAudience: "Cl24-CX-Dim" + postgresql: auth: postgrespassword: "" diff --git a/consortia/environments/values-int.yaml b/consortia/environments/values-int.yaml index 2473193..d18527e 100644 --- a/consortia/environments/values-int.yaml +++ b/consortia/environments/values-int.yaml @@ -77,6 +77,12 @@ processesworker: # -- Url to the cf service api baseAddress: "https://portal-backend.dev.demo.catena-x.net/api/administration/registration/dim/" +idp: + address: "https://centralidp.int.demo.catena-x.net" + jwtBearerOptions: + tokenValidationParameters: + validAudience: "Cl25-CX-Dim" + postgresql: auth: postgrespassword: "" diff --git a/src/web/Dim.Web/Authentication/CustomClaimTypes.cs b/src/web/Dim.Web/Authentication/CustomClaimTypes.cs new file mode 100644 index 0000000..9d1b7e9 --- /dev/null +++ b/src/web/Dim.Web/Authentication/CustomClaimTypes.cs @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (c) 2024 BMW Group AG + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.Web.Authentication; + +public static class CustomClaimTypes +{ + public const string Sub = "sub"; + public const string ClientId = "clientId"; + public const string PreferredUserName = "preferred_username"; + public const string ResourceAccess = "resource_access"; +} diff --git a/src/web/Dim.Web/Authentication/KeycloakClaimsTransformation.cs b/src/web/Dim.Web/Authentication/KeycloakClaimsTransformation.cs new file mode 100644 index 0000000..cf7e2f0 --- /dev/null +++ b/src/web/Dim.Web/Authentication/KeycloakClaimsTransformation.cs @@ -0,0 +1,73 @@ +/******************************************************************************** + * Copyright (c) 2024 BMW Group AG + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; +using System.Json; +using System.Security.Claims; + +namespace Dim.Web.Authentication +{ + public class KeycloakClaimsTransformation : IClaimsTransformation + { + private readonly JwtBearerOptions _options; + + public KeycloakClaimsTransformation(IOptions options) + { + _options = options.Value; + } + + public Task TransformAsync(ClaimsPrincipal principal) + { + var claimsIdentity = new ClaimsIdentity(); + if (AddRoles(principal, claimsIdentity)) + { + principal.AddIdentity(claimsIdentity); + } + + return Task.FromResult(principal); + } + + private bool AddRoles(ClaimsPrincipal principal, ClaimsIdentity claimsIdentity) => + principal.Claims + .Where(claim => + claim.Type == CustomClaimTypes.ResourceAccess && + claim.ValueType == "JSON") + .SelectMany(claim => + JsonValue.Parse(claim.Value) is JsonObject jsonObject && + jsonObject.TryGetValue( + _options.TokenValidationParameters.ValidAudience, + out var audience) && + audience is JsonObject client && + client.TryGetValue("roles", out var jsonRoles) && + jsonRoles is JsonArray roles + ? roles.Where(x => x.JsonType == JsonType.String) + .Select(role => new Claim(ClaimTypes.Role, role)) + : Enumerable.Empty()) + .IfAny(claims => + { + foreach (var claim in claims) + { + claimsIdentity.AddClaim(claim); + } + }); + } +} diff --git a/src/web/Dim.Web/Controllers/DimController.cs b/src/web/Dim.Web/Controllers/DimController.cs index 4a8fab5..36374d0 100644 --- a/src/web/Dim.Web/Controllers/DimController.cs +++ b/src/web/Dim.Web/Controllers/DimController.cs @@ -38,6 +38,7 @@ public static RouteGroupBuilder MapDimApi(this RouteGroupBuilder group) "the name of the company", "bpn of the wallets company", "The did document location") + .RequireAuthorization(r => r.RequireRole("setup_wallet")) .Produces(StatusCodes.Status201Created); policyHub.MapPost("setup-issuer", ([FromQuery] string companyName, [FromQuery] string bpn, [FromQuery] string didDocumentLocation, IDimBusinessLogic dimBusinessLogic) => dimBusinessLogic.StartSetupDim(companyName, bpn, didDocumentLocation, true)) @@ -46,6 +47,7 @@ public static RouteGroupBuilder MapDimApi(this RouteGroupBuilder group) "the name of the company", "bpn of the wallets company", "The did document location") + .RequireAuthorization(r => r.RequireRole("setup_wallet")) .Produces(StatusCodes.Status201Created); return group; diff --git a/src/web/Dim.Web/Dim.Web.csproj b/src/web/Dim.Web/Dim.Web.csproj index 6c897f4..f147a9d 100644 --- a/src/web/Dim.Web/Dim.Web.csproj +++ b/src/web/Dim.Web/Dim.Web.csproj @@ -16,6 +16,7 @@ + diff --git a/src/web/Dim.Web/Program.cs b/src/web/Dim.Web/Program.cs index 16544ef..846c98e 100644 --- a/src/web/Dim.Web/Program.cs +++ b/src/web/Dim.Web/Program.cs @@ -18,8 +18,10 @@ ********************************************************************************/ using Dim.DbAccess.DependencyInjection; +using Dim.Web.Authentication; using Dim.Web.Controllers; using Dim.Web.Extensions; +using Microsoft.AspNetCore.Authentication; using Org.Eclipse.TractusX.Portal.Backend.Framework.Web; using System.Text.Json.Serialization; @@ -30,6 +32,7 @@ builder => { builder.Services + .AddTransient() .AddDim(builder.Configuration.GetSection("Dim")) .AddEndpointsApiExplorer() .AddDatabase(builder.Configuration) diff --git a/src/web/Dim.Web/appsettings.json b/src/web/Dim.Web/appsettings.json index 6a5536a..0431a71 100644 --- a/src/web/Dim.Web/appsettings.json +++ b/src/web/Dim.Web/appsettings.json @@ -12,5 +12,19 @@ "Dim": { "RootDirectoryId": "" }, - "SwaggerEnabled": false + "SwaggerEnabled": false, + "JwtBearerOptions": { + "RequireHttpsMetadata": true, + "MetadataAddress": "", + "SaveToken": true, + "TokenValidationParameters": { + "ValidateIssuer": true, + "ValidIssuer": "", + "ValidateIssuerSigningKey": true, + "ValidAudience": "", + "ValidateAudience": true, + "ValidateLifetime": true, + "ClockSkew": 600000 + } + } } \ No newline at end of file