From 0f789a1d8bec072c4976e79b948c028a3d10e95e Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Wed, 12 Nov 2025 16:48:44 +0100 Subject: [PATCH 01/15] Bug fix: No authentification popup when API For Registry and Repository.../Connect (extended) --- src/AasxPackageLogic/DispEditHelperEntities.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/AasxPackageLogic/DispEditHelperEntities.cs b/src/AasxPackageLogic/DispEditHelperEntities.cs index 28e0c3ee9..0440eebe5 100644 --- a/src/AasxPackageLogic/DispEditHelperEntities.cs +++ b/src/AasxPackageLogic/DispEditHelperEntities.cs @@ -1557,6 +1557,7 @@ public static async Task ExecuteUiForFetchOfElements( "Error building location from fetch selection. Aborting."); return false; } + Options.Curr.AutoAuthenticateAsk = fetchContext.Record.AutoAuthenticate; // more details into container options var containerOptions = new PackageContainerHttpRepoSubset. From 9fba7a8a6101d3fd15c3804b4b33180a0c978b92 Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Fri, 23 Jan 2026 12:15:55 +0100 Subject: [PATCH 02/15] Add first token exchange --- src/AasxOpenidClient/AasxOpenidClient.csproj | 2 +- src/AasxPackageLogic/AasxPackageLogic.csproj | 2 +- .../SecurityAccessHandlerLogicBase.cs | 376 +++++++++++++++++- 3 files changed, 365 insertions(+), 15 deletions(-) diff --git a/src/AasxOpenidClient/AasxOpenidClient.csproj b/src/AasxOpenidClient/AasxOpenidClient.csproj index b402be530..ad0a38eaa 100644 --- a/src/AasxOpenidClient/AasxOpenidClient.csproj +++ b/src/AasxOpenidClient/AasxOpenidClient.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/AasxPackageLogic/AasxPackageLogic.csproj b/src/AasxPackageLogic/AasxPackageLogic.csproj index f33343909..851868824 100644 --- a/src/AasxPackageLogic/AasxPackageLogic.csproj +++ b/src/AasxPackageLogic/AasxPackageLogic.csproj @@ -43,7 +43,7 @@ - + diff --git a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs index da5ad1887..160571657 100644 --- a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs +++ b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs @@ -10,15 +10,6 @@ This source code is licensed under the Apache License 2.0 (see LICENSE.txt). This source code may use other Open Source software components (see LICENSE.txt). */ -using AasxIntegrationBase; -using AasxPackageLogic; -using AasxPackageLogic.PackageCentral; -using AdminShellNS; -using AnyUi; -using Extensions; -using Microsoft.Identity.Client; -using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.ComponentModel; @@ -30,13 +21,27 @@ This source code may use other Open Source software components (see LICENSE.txt) using System.Net.Http.Headers; using System.Reflection; using System.Security.Claims; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Security.RightsManagement; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows; +using System.Windows.Controls; +using AasxIntegrationBase; +using AasxPackageLogic; +using AasxPackageLogic.PackageCentral; +using AdminShellNS; +using AnyUi; +using Extensions; +using Microsoft.Identity.Client; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json.Linq; +using static AasxPackageLogic.PackageCentral.PackageContainerHttpRepoSubset; using Aas = AasCore.Aas3_1; namespace AasxPackageExplorer @@ -430,6 +435,97 @@ protected virtual async Task AskForAuthServerUri(string baseAddress, str return null; } + protected virtual async Task AskForTokenExchange() + { + string[] tokenExchanges = ["none", "none", "none"]; + + var ucJob = new AnyUiDialogueDataModalPanel("With token exchange?"); + ucJob.ActivateRenderPanel(null, + disableScrollArea: false, + dialogButtons: AnyUiMessageBoxButton.OK, + renderPanel: (uci) => + { + // create panel + var panel = new AnyUiStackPanel(); + var helper = new AnyUiSmallWidgetToolkit(); + + var g = helper.AddSmallGrid(25, 2, new[] { "200:", "*" }, + padding: new AnyUiThickness(0, 5, 0, 5), + margin: new AnyUiThickness(10, 0, 30, 0)); + + panel.Add(g); + + // dynamic rows + int row = 0; + + // Info + helper.Set( + helper.AddSmallLabelTo(g, row, 0, content: + "For some authentification methods, you will need to do a token exchange.", + wrapping: AnyUiTextWrapping.Wrap), + colSpan: 2); + row++; + + helper.AddSmallLabelTo(g, row, 0, content: "Token Exchange 1:", + verticalAlignment: AnyUiVerticalAlignment.Center, + verticalContentAlignment: AnyUiVerticalAlignment.Center); + + AnyUiUIElement.SetStringFromControl( + helper.Set( + helper.AddSmallComboBoxTo(g, row, 1, + isEditable: false, + text: "none", + items: new[] { "none", "factory-x" }, + margin: new AnyUiThickness(10, 0, 0, 0), + padding: new AnyUiThickness(5, 0, 5, 0), + minWidth: 100, + horizontalAlignment: AnyUiHorizontalAlignment.Stretch)), + (s) => { tokenExchanges[0] = s; }); + + row++; + + helper.AddSmallLabelTo(g, row, 0, content: "Token Exchange 2:", + verticalAlignment: AnyUiVerticalAlignment.Center, + verticalContentAlignment: AnyUiVerticalAlignment.Center); + + AnyUiUIElement.SetStringFromControl( + helper.Set( + helper.AddSmallComboBoxTo(g, row, 1, + isEditable: false, + text: "none", + items: new[] { "none", "assetfox", "factory-x" }, + margin: new AnyUiThickness(10, 0, 0, 0), + padding: new AnyUiThickness(5, 0, 5, 0), + minWidth: 100, + horizontalAlignment: AnyUiHorizontalAlignment.Stretch)), + (s) => { tokenExchanges[1] = s; }); + + row++; + + helper.AddSmallLabelTo(g, row, 0, content: "Token Exchange 3:", + verticalAlignment: AnyUiVerticalAlignment.Center, + verticalContentAlignment: AnyUiVerticalAlignment.Center); + + AnyUiUIElement.SetStringFromControl( + helper.Set( + helper.AddSmallComboBoxTo(g, row, 1, + isEditable: false, + text: "none", + items: new[] { "none", "assetfox"}, + margin: new AnyUiThickness(10, 0, 0, 0), + padding: new AnyUiThickness(5, 0, 5, 0), + minWidth: 100, + horizontalAlignment: AnyUiHorizontalAlignment.Stretch)), + (s) => { tokenExchanges[2] = s; }); + return g; + }); + + if (_displayContext != null && await _displayContext.StartFlyoverModalAsync(ucJob)) + return tokenExchanges; + return tokenExchanges; + } + + protected virtual async Task AskForSelectFromCertCollection( string baseAddress, X509Certificate2Collection fcollection) @@ -692,6 +788,132 @@ protected virtual async Task DetermineAuthenticateHeaderForE var secretAccessToken = secretContentJson["access_token"].ToString(); Log.Singleton.Info($"Security access handler: Found 'access_token': {secretAccessToken}"); + + var tokenExchanges2 = await AskForTokenExchange(); + + if (tokenExchanges2 != null && !String.IsNullOrEmpty(tokenExchanges2[0])) + { + var target = tokenExchanges2[0]; + var configUrl = "https://iam-security-training.com/consumer/sts"; + var d = new Dictionary + { + { "grant_type", "urn:ietf:params:oauth:grant-type:token-exchange" }, + { "subject_token_type", "urn:ietf:params:oauth:token-type:jwt" }, + { "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" }, + { "subject_token", secretAccessToken }, + }; + if (target != "") + { + d.Add("audience", target); + } + var request2 = new HttpRequestMessage(HttpMethod.Post, $"{configUrl}/token") + { + Content = new FormUrlEncodedContent(d) + }; + request2.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + + var response2 = await client.SendAsync(request2); + var content = await response2.Content.ReadAsStringAsync(); + + var accessToken2 = ""; + var doc2 = JsonDocument.Parse(content); + if (doc2.RootElement.TryGetProperty("access_token", out var tokenElement)) + { + accessToken2 = tokenElement.GetString(); + Console.WriteLine("Access Token: " + accessToken2); + + + //var handler2 = new JwtSecurityTokenHandler(); + //var jwt2 = handler2.ReadJwtToken(accessToken2); + //var kid = jwt2.Header["kid"].ToString(); + + //// 3. Find matching key + //var key = jwks.First(k => k["kid"].ToString() == kid); + + //// 4. Build RSA key + //var e = Base64UrlEncoder.DecodeBytes(key["e"].ToString()); + //var n = Base64UrlEncoder.DecodeBytes(key["n"].ToString()); + //var rsa = new RSAParameters { Exponent = e, Modulus = n }; + //var rsaKey = new RsaSecurityKey(rsa); + + //// 5. Validate token + //var validationParams = new TokenValidationParameters + //{ + // ValidateIssuer = false, + // ValidateAudience = false, + // ValidateLifetime = false, + // ValidateIssuerSigningKey = true, + // IssuerSigningKey = rsaKey, + // ClockSkew = TimeSpan.FromMinutes(5) + //}; + + //try + //{ + // handler2.ValidateToken(accessToken2, validationParams, out _); + // Console.WriteLine("Token is valid"); + //} + //catch (Exception ex) + //{ + // Console.WriteLine($"Validation failed: {ex.Message}"); + //} + + using var httpClient = new HttpClient(handler); + var jwksJson = await httpClient.GetStringAsync($"{configUrl}/jwks"); + var jwks = JObject.Parse(jwksJson)["keys"]; + + // 1) Handler + var handler2 = new JsonWebTokenHandler(); + + // 2) JWK aus JWKS direkt verwenden (hier exemplarisch LINQ auf Dein jwks-Array) + var jsonWebToken = new JsonWebToken(accessToken2); + var jwtHeaderKid = jsonWebToken.Kid; // liest 'kid' robust + var jwkJson = jwks.First(k => k["kid"].ToString() == jwtHeaderKid).ToString(); // k ist i. d. R. ein JObject + var jwk = new JsonWebKey(jwkJson); + + // 3) Validierungsparameter + var validationParams = new TokenValidationParameters + { + // Signaturprüfung + IssuerSigningKey = jwk, // kein manuelles RSAParameters nötig + ValidateIssuerSigningKey = true, + + // Lebenszeit + ValidateLifetime = true, + RequireExpirationTime = true, + ClockSkew = TimeSpan.FromMinutes(5), // bei UTC+0 gut, ggf. 2–5 Minuten + + // Issuer/Audience je nach Bedarf (bei Tests oft aus) + ValidateIssuer = false, // später auf true + ValidIssuer setzen + ValidateAudience = false, // später auf true + ValidAudience setzen + + // Keine Legacy-Claim-Mappings + // MapInboundClaims = false, + + // Optional: Name-/Rollen-Claims aus dem JWT + NameClaimType = "name", + RoleClaimType = "role" + }; + + try + { + var result = handler2.ValidateToken(accessToken2, validationParams); + if (!result.IsValid) + { + Console.WriteLine($"Validation failed: {result.Exception?.Message}"); + } + else + { + Console.WriteLine("Token is valid"); + secretAccessToken = accessToken2; + } + } + catch (Exception ex) + { + Console.WriteLine($"Validation failed: {ex.Message}"); + } + } + } + // build correct header key, remember, return endpoint.LastRenewed = DateTime.UtcNow; endpoint.LastHeaderItem = new HttpHeaderDataItem("Authorization", $"Bearer {secretAccessToken.ToString()}"); @@ -878,7 +1100,8 @@ protected virtual async Task DetermineAuthenticateHeaderForE chain.Import(certFn, PW); x5c = chain.Cast().Reverse().Select(c => Convert.ToBase64String(c.RawData)).ToArray(); - } catch (Exception ex) + } + catch (Exception ex) { Log.Singleton.Error(ex, $"when building certificate chain for certificate file {certFn}. " + $"Aborting. Using no security!"); @@ -887,8 +1110,8 @@ protected virtual async Task DetermineAuthenticateHeaderForE } else if (preferredMethod == SecurityAccessMethod.InteractiveEntry) - { - + { + // test tenant for ENTRA id // TODO: configure clientId var tenant = "common"; // Damit auch externe Konten wie @live.de funktionieren @@ -1008,9 +1231,136 @@ protected virtual async Task DetermineAuthenticateHeaderForE var accessToken = contentJson["access_token"]; Log.Singleton.Info($"Security access handler: Found 'access_token': {accessToken}"); + var accessTokenString = accessToken.ToString(); + + var tokenExchanges = await AskForTokenExchange(); + + if (tokenExchanges != null && !String.IsNullOrEmpty(tokenExchanges[0])) + { + var target = tokenExchanges[0]; + var configUrl = "https://iam-security-training.com/consumer/sts"; + var d = new Dictionary + { + { "grant_type", "urn:ietf:params:oauth:grant-type:token-exchange" }, + { "subject_token_type", "urn:ietf:params:oauth:token-type:jwt" }, + { "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" }, + { "subject_token", accessToken.ToString() }, + }; + if (target != "") + { + d.Add("audience", target); + } + var request2 = new HttpRequestMessage(HttpMethod.Post, $"{configUrl}/token") + { + Content = new FormUrlEncodedContent(d) + }; + request2.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + + var response2 = await client.SendAsync(request2); + var content = await response2.Content.ReadAsStringAsync(); + + var accessToken2 = ""; + var doc2 = JsonDocument.Parse(content); + if (doc2.RootElement.TryGetProperty("access_token", out var tokenElement)) + { + accessToken2 = tokenElement.GetString(); + Console.WriteLine("Access Token: " + accessToken2); + + using var httpClient = new HttpClient(handler); + var jwksJson = await httpClient.GetStringAsync($"{configUrl}/jwks"); + var jwks = JObject.Parse(jwksJson)["keys"]; + + /* + var handler2 = new JwtSecurityTokenHandler(); + var jwt2 = handler2.ReadJwtToken(accessToken); + var kid = jwt2.Header["kid"].ToString(); + + // 3. Find matching key + var key = jwks.First(k => k["kid"].ToString() == kid); + + // 4. Build RSA key + var e = Base64UrlEncoder.DecodeBytes(key["e"].ToString()); + var n = Base64UrlEncoder.DecodeBytes(key["n"].ToString()); + var rsa = new RSAParameters { Exponent = e, Modulus = n }; + var rsaKey = new RsaSecurityKey(rsa); + + // 5. Validate token + var validationParams = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = rsaKey, + ClockSkew = TimeSpan.FromMinutes(5) + }; + + try + { + handler2.ValidateToken(accessToken, validationParams, out _); + ioConsole.WriteLine("Token is valid"); + } + catch (Exception ex) + { + ioConsole.WriteLine($"Validation failed: {ex.Message}"); + } + */ + + // 1) Handler + var handler2 = new JsonWebTokenHandler(); + + // 2) JWK aus JWKS direkt verwenden (hier exemplarisch LINQ auf Dein jwks-Array) + var jwtHeaderKid = new JsonWebToken(accessToken2).Kid; // liest 'kid' robust + var jwkJson = jwks.First(k => k["kid"].ToString() == jwtHeaderKid).ToString(); // k ist i. d. R. ein JObject + var jwk = new JsonWebKey(jwkJson); + + // 3) Validierungsparameter + var validationParams = new TokenValidationParameters + { + // Signaturprüfung + IssuerSigningKey = jwk, // kein manuelles RSAParameters nötig + ValidateIssuerSigningKey = true, + + // Lebenszeit + ValidateLifetime = true, + RequireExpirationTime = true, + ClockSkew = TimeSpan.FromMinutes(5), // bei UTC+0 gut, ggf. 2–5 Minuten + + // Issuer/Audience je nach Bedarf (bei Tests oft aus) + ValidateIssuer = false, // später auf true + ValidIssuer setzen + ValidateAudience = false, // später auf true + ValidAudience setzen + + // Keine Legacy-Claim-Mappings + // MapInboundClaims = false, + + // Optional: Name-/Rollen-Claims aus dem JWT + NameClaimType = "name", + RoleClaimType = "role" + }; + + try + { + var result = handler2.ValidateToken(accessToken2, validationParams); + if (!result.IsValid) + { + Console.WriteLine($"Validation failed: {result.Exception?.Message}"); + } + else + { + Console.WriteLine("Token is valid"); + accessTokenString = accessToken2; + } + } + catch (Exception ex) + { + Console.WriteLine($"Validation failed: {ex.Message}"); + } + } + } + // build correct header key, remember, return endpoint.LastRenewed = DateTime.UtcNow; - endpoint.LastHeaderItem = new HttpHeaderDataItem("Authorization", $"Bearer {accessToken.ToString()}"); + endpoint.LastHeaderItem = new HttpHeaderDataItem("Authorization", $"Bearer {accessTokenString}"); return endpoint.LastHeaderItem; } } From 1c51d913b34d9879be02a185c72981d1b9d1060f Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Fri, 23 Jan 2026 13:22:41 +0100 Subject: [PATCH 03/15] Add second token exchange --- .../SecurityAccessHandlerLogicBase.cs | 149 +++++++++++++++++- 1 file changed, 142 insertions(+), 7 deletions(-) diff --git a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs index 160571657..418f6a74d 100644 --- a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs +++ b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs @@ -791,17 +791,24 @@ protected virtual async Task DetermineAuthenticateHeaderForE var tokenExchanges2 = await AskForTokenExchange(); - if (tokenExchanges2 != null && !String.IsNullOrEmpty(tokenExchanges2[0])) + if (tokenExchanges2 != null && !String.IsNullOrEmpty(tokenExchanges2[0]) + && tokenExchanges2[0] != "none") { var target = tokenExchanges2[0]; var configUrl = "https://iam-security-training.com/consumer/sts"; var d = new Dictionary - { - { "grant_type", "urn:ietf:params:oauth:grant-type:token-exchange" }, - { "subject_token_type", "urn:ietf:params:oauth:token-type:jwt" }, - { "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" }, - { "subject_token", secretAccessToken }, - }; + { + + { "grant_type", "urn:ietf:params:oauth:grant-type:token-exchange" }, + + { "subject_token_type", "urn:ietf:params:oauth:token-type:jwt" }, + + { "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" }, + + { "subject_token", secretAccessToken }, + + }; + if (target != "") { d.Add("audience", target); @@ -912,6 +919,134 @@ protected virtual async Task DetermineAuthenticateHeaderForE Console.WriteLine($"Validation failed: {ex.Message}"); } } + + if (tokenExchanges2[1] != "none") + { + handler = new HttpClientHandler { DefaultProxyCredentials = CredentialCache.DefaultCredentials }; + client = new HttpClient(handler); + + var configUrl2 = "https://iam-security-training.com/provider/sts"; + target = tokenExchanges2[1]; + + var d2 = new Dictionary + { + { "grant_type", "urn:ietf:params:oauth:grant-type:token-exchange" }, + { "subject_token_type", "urn:ietf:params:oauth:token-type:jwt" }, + { "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" }, + { "subject_token", accessToken2 }, + }; + if (target != "") + { + d2.Add("audience", target); + } + var request3 = new HttpRequestMessage(HttpMethod.Post, $"{configUrl}/token") + { + Content = new FormUrlEncodedContent(d) + }; + request3.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + + var response3 = await client.SendAsync(request3); + var content3 = await response3.Content.ReadAsStringAsync(); + + var accessToken3 = ""; + var doc3 = JsonDocument.Parse(content); + if (doc3.RootElement.TryGetProperty("access_token", out var tokenElement2)) + { + accessToken3 = tokenElement2.GetString(); + Console.WriteLine("Access Token: " + accessToken3); + + using var httpClient = new HttpClient(handler); + var jwksJson = await httpClient.GetStringAsync($"{configUrl}/jwks"); + var jwks = JObject.Parse(jwksJson)["keys"]; + + /* + var handler2 = new JwtSecurityTokenHandler(); + var jwt2 = handler2.ReadJwtToken(accessToken); + var kid = jwt2.Header["kid"].ToString(); + + // 3. Find matching key + var key = jwks.First(k => k["kid"].ToString() == kid); + + // 4. Build RSA key + var e = Base64UrlEncoder.DecodeBytes(key["e"].ToString()); + var n = Base64UrlEncoder.DecodeBytes(key["n"].ToString()); + var rsa = new RSAParameters { Exponent = e, Modulus = n }; + var rsaKey = new RsaSecurityKey(rsa); + + // 5. Validate token + var validationParams = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = rsaKey, + ClockSkew = TimeSpan.FromMinutes(5) + }; + + try + { + handler2.ValidateToken(accessToken, validationParams, out _); + ioConsole.WriteLine("Token is valid"); + } + catch (Exception ex) + { + ioConsole.WriteLine($"Validation failed: {ex.Message}"); + } + */ + + // 1) Handler + var handler2 = new JsonWebTokenHandler(); + + // 2) JWK aus JWKS direkt verwenden (hier exemplarisch LINQ auf Dein jwks-Array) + var jwtHeaderKid = new JsonWebToken(accessToken3).Kid; // liest 'kid' robust + var jwkJson = jwks.First(k => k["kid"].ToString() == jwtHeaderKid).ToString(); // k ist i. d. R. ein JObject + var jwk = new JsonWebKey(jwkJson); + + // 3) Validierungsparameter + var validationParams = new TokenValidationParameters + { + // Signaturprüfung + IssuerSigningKey = jwk, // kein manuelles RSAParameters nötig + ValidateIssuerSigningKey = true, + + // Lebenszeit + ValidateLifetime = true, + RequireExpirationTime = true, + ClockSkew = TimeSpan.FromMinutes(5), // bei UTC+0 gut, ggf. 2–5 Minuten + + // Issuer/Audience je nach Bedarf (bei Tests oft aus) + ValidateIssuer = false, // später auf true + ValidIssuer setzen + ValidateAudience = false, // später auf true + ValidAudience setzen + + // Keine Legacy-Claim-Mappings + // MapInboundClaims = false, + + // Optional: Name-/Rollen-Claims aus dem JWT + NameClaimType = "name", + RoleClaimType = "role" + }; + + try + { + var result = handler2.ValidateToken(accessToken3, validationParams); + if (!result.IsValid) + { + Console.WriteLine($"Validation failed: {result.Exception?.Message}"); + } + else + { + Console.WriteLine("Token is valid"); + secretAccessToken = accessToken3; + } + } + catch (Exception ex) + { + Console.WriteLine($"Validation failed: {ex.Message}"); + } + + } + } } // build correct header key, remember, return From c43f4afa11c78c4e203e9e91679429073a1b530b Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Fri, 23 Jan 2026 13:33:47 +0100 Subject: [PATCH 04/15] Return null for non-valid keys --- .../PackageCentral/SecurityAccessHandlerLogicBase.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs index 418f6a74d..82db78bc4 100644 --- a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs +++ b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs @@ -907,6 +907,7 @@ protected virtual async Task DetermineAuthenticateHeaderForE if (!result.IsValid) { Console.WriteLine($"Validation failed: {result.Exception?.Message}"); + return null; } else { @@ -917,6 +918,7 @@ protected virtual async Task DetermineAuthenticateHeaderForE catch (Exception ex) { Console.WriteLine($"Validation failed: {ex.Message}"); + return null; } } @@ -1033,6 +1035,7 @@ protected virtual async Task DetermineAuthenticateHeaderForE if (!result.IsValid) { Console.WriteLine($"Validation failed: {result.Exception?.Message}"); + return null; } else { @@ -1043,6 +1046,7 @@ protected virtual async Task DetermineAuthenticateHeaderForE catch (Exception ex) { Console.WriteLine($"Validation failed: {ex.Message}"); + return null; } } From acb169acec055f66fa7cc963f945d75350e04603 Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Mon, 26 Jan 2026 09:47:40 +0100 Subject: [PATCH 05/15] Add third token exchange for asset fox, bug fixes --- .../SecurityAccessHandlerLogicBase.cs | 140 ++++++++++++++++-- 1 file changed, 131 insertions(+), 9 deletions(-) diff --git a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs index 82db78bc4..40296ce01 100644 --- a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs +++ b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs @@ -798,15 +798,10 @@ protected virtual async Task DetermineAuthenticateHeaderForE var configUrl = "https://iam-security-training.com/consumer/sts"; var d = new Dictionary { - { "grant_type", "urn:ietf:params:oauth:grant-type:token-exchange" }, - { "subject_token_type", "urn:ietf:params:oauth:token-type:jwt" }, - { "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" }, - { "subject_token", secretAccessToken }, - }; if (target != "") @@ -931,7 +926,7 @@ protected virtual async Task DetermineAuthenticateHeaderForE target = tokenExchanges2[1]; var d2 = new Dictionary - { + { { "grant_type", "urn:ietf:params:oauth:grant-type:token-exchange" }, { "subject_token_type", "urn:ietf:params:oauth:token-type:jwt" }, { "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" }, @@ -941,9 +936,9 @@ protected virtual async Task DetermineAuthenticateHeaderForE { d2.Add("audience", target); } - var request3 = new HttpRequestMessage(HttpMethod.Post, $"{configUrl}/token") + var request3 = new HttpRequestMessage(HttpMethod.Post, $"{configUrl2}/token") { - Content = new FormUrlEncodedContent(d) + Content = new FormUrlEncodedContent(d2) }; request3.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); @@ -1048,10 +1043,137 @@ protected virtual async Task DetermineAuthenticateHeaderForE Console.WriteLine($"Validation failed: {ex.Message}"); return null; } + } + + if (tokenExchanges2[2] != "none") + { + handler = new HttpClientHandler { DefaultProxyCredentials = CredentialCache.DefaultCredentials }; + client = new HttpClient(handler); + + var configUrl3 = "https://integration.assetfox.apps.siemens.cloud/auth/realms/assetfox/protocol/openid-connect"; + target = tokenExchanges2[2]; + + var d3 = new Dictionary + { + { "grant_type", "urn:ietf:params:oauth:grant-type:token-exchange" }, + { "subject_token_type", "urn:ietf:params:oauth:token-type:jwt" }, + { "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" }, + { "subject_token", accessToken3 }, + }; + if (target != "") + { + d3.Add("audience", target); + } + var request4 = new HttpRequestMessage(HttpMethod.Post, $"{configUrl}/token") + { + Content = new FormUrlEncodedContent(d3) + }; + request4.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + + var response4 = await client.SendAsync(request3); + var content4 = await response4.Content.ReadAsStringAsync(); + + var accessToken4 = ""; + var doc4 = JsonDocument.Parse(content); + if (doc4.RootElement.TryGetProperty("access_token", out var tokenElement3)) + { + accessToken4 = tokenElement2.GetString(); + Console.WriteLine("Access Token: " + accessToken3); + + using var httpClient = new HttpClient(handler); + var jwksJson = await httpClient.GetStringAsync($"{configUrl3}/jwks"); + var jwks = JObject.Parse(jwksJson)["keys"]; + + /* + var handler2 = new JwtSecurityTokenHandler(); + var jwt2 = handler2.ReadJwtToken(accessToken); + var kid = jwt2.Header["kid"].ToString(); + + // 3. Find matching key + var key = jwks.First(k => k["kid"].ToString() == kid); + // 4. Build RSA key + var e = Base64UrlEncoder.DecodeBytes(key["e"].ToString()); + var n = Base64UrlEncoder.DecodeBytes(key["n"].ToString()); + var rsa = new RSAParameters { Exponent = e, Modulus = n }; + var rsaKey = new RsaSecurityKey(rsa); + + // 5. Validate token + var validationParams = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = rsaKey, + ClockSkew = TimeSpan.FromMinutes(5) + }; + + try + { + handler2.ValidateToken(accessToken, validationParams, out _); + ioConsole.WriteLine("Token is valid"); + } + catch (Exception ex) + { + ioConsole.WriteLine($"Validation failed: {ex.Message}"); + } + */ + + // 1) Handler + var handler2 = new JsonWebTokenHandler(); + + // 2) JWK aus JWKS direkt verwenden (hier exemplarisch LINQ auf Dein jwks-Array) + var jwtHeaderKid = new JsonWebToken(accessToken3).Kid; // liest 'kid' robust + var jwkJson = jwks.First(k => k["kid"].ToString() == jwtHeaderKid).ToString(); // k ist i. d. R. ein JObject + var jwk = new JsonWebKey(jwkJson); + + // 3) Validierungsparameter + var validationParams = new TokenValidationParameters + { + // Signaturprüfung + IssuerSigningKey = jwk, // kein manuelles RSAParameters nötig + ValidateIssuerSigningKey = true, + + // Lebenszeit + ValidateLifetime = true, + RequireExpirationTime = true, + ClockSkew = TimeSpan.FromMinutes(5), // bei UTC+0 gut, ggf. 2–5 Minuten + + // Issuer/Audience je nach Bedarf (bei Tests oft aus) + ValidateIssuer = false, // später auf true + ValidIssuer setzen + ValidateAudience = false, // später auf true + ValidAudience setzen + + // Keine Legacy-Claim-Mappings + // MapInboundClaims = false, + + // Optional: Name-/Rollen-Claims aus dem JWT + NameClaimType = "name", + RoleClaimType = "role" + }; + + try + { + var result = handler2.ValidateToken(accessToken3, validationParams); + if (!result.IsValid) + { + Console.WriteLine($"Validation failed: {result.Exception?.Message}"); + return null; + } + else + { + Console.WriteLine("Token is valid"); + secretAccessToken = accessToken3; + } + } + catch (Exception ex) + { + Console.WriteLine($"Validation failed: {ex.Message}"); + return null; + } + } } } - } // build correct header key, remember, return endpoint.LastRenewed = DateTime.UtcNow; From f8e92f6ae1ab508fc3f866fbdf9d4e0ab4de6758 Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Mon, 26 Jan 2026 15:00:41 +0100 Subject: [PATCH 06/15] Fix compilation error --- .../PackageCentral/SecurityAccessHandlerLogicBase.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs index 40296ce01..71c976ccc 100644 --- a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs +++ b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs @@ -926,7 +926,7 @@ protected virtual async Task DetermineAuthenticateHeaderForE target = tokenExchanges2[1]; var d2 = new Dictionary - { + { { "grant_type", "urn:ietf:params:oauth:grant-type:token-exchange" }, { "subject_token_type", "urn:ietf:params:oauth:token-type:jwt" }, { "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" }, @@ -1174,6 +1174,7 @@ protected virtual async Task DetermineAuthenticateHeaderForE } } } + } // build correct header key, remember, return endpoint.LastRenewed = DateTime.UtcNow; From 108b995d94384bdef192096ee7699ab5a420ac2f Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Wed, 25 Feb 2026 09:38:49 +0100 Subject: [PATCH 07/15] Fix typo --- .../PackageCentral/SecurityAccessHandlerLogicBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs index 71c976ccc..88771ea8e 100644 --- a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs +++ b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs @@ -953,7 +953,7 @@ protected virtual async Task DetermineAuthenticateHeaderForE Console.WriteLine("Access Token: " + accessToken3); using var httpClient = new HttpClient(handler); - var jwksJson = await httpClient.GetStringAsync($"{configUrl}/jwks"); + var jwksJson = await httpClient.GetStringAsync($"{configUrl2}/jwks"); var jwks = JObject.Parse(jwksJson)["keys"]; /* @@ -1064,7 +1064,7 @@ protected virtual async Task DetermineAuthenticateHeaderForE { d3.Add("audience", target); } - var request4 = new HttpRequestMessage(HttpMethod.Post, $"{configUrl}/token") + var request4 = new HttpRequestMessage(HttpMethod.Post, $"{configUrl3}/token") { Content = new FormUrlEncodedContent(d3) }; From 22d28cdcf8a6cfdc15aec748cc9c0bd41624ef52 Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Fri, 27 Feb 2026 10:55:32 +0100 Subject: [PATCH 08/15] Token exchange: Clean up code, fix token exchange 2 --- .../SecurityAccessHandlerLogicBase.cs | 390 ++---------------- 1 file changed, 38 insertions(+), 352 deletions(-) diff --git a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs index 88771ea8e..661a40024 100644 --- a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs +++ b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs @@ -449,7 +449,7 @@ protected virtual async Task AskForTokenExchange() var panel = new AnyUiStackPanel(); var helper = new AnyUiSmallWidgetToolkit(); - var g = helper.AddSmallGrid(25, 2, new[] { "200:", "*" }, + var g = helper.AddSmallGrid(25, 2, new[] { "350:", "*" }, padding: new AnyUiThickness(0, 5, 0, 5), margin: new AnyUiThickness(10, 0, 30, 0)); @@ -466,7 +466,7 @@ protected virtual async Task AskForTokenExchange() colSpan: 2); row++; - helper.AddSmallLabelTo(g, row, 0, content: "Token Exchange 1:", + helper.AddSmallLabelTo(g, row, 0, content: "Token Exchange 1 (with consumer STS)", verticalAlignment: AnyUiVerticalAlignment.Center, verticalContentAlignment: AnyUiVerticalAlignment.Center); @@ -478,13 +478,13 @@ protected virtual async Task AskForTokenExchange() items: new[] { "none", "factory-x" }, margin: new AnyUiThickness(10, 0, 0, 0), padding: new AnyUiThickness(5, 0, 5, 0), - minWidth: 100, + minWidth: 50, horizontalAlignment: AnyUiHorizontalAlignment.Stretch)), (s) => { tokenExchanges[0] = s; }); row++; - helper.AddSmallLabelTo(g, row, 0, content: "Token Exchange 2:", + helper.AddSmallLabelTo(g, row, 0, content: "Token Exchange 2 (with producer STS):", verticalAlignment: AnyUiVerticalAlignment.Center, verticalContentAlignment: AnyUiVerticalAlignment.Center); @@ -493,30 +493,30 @@ protected virtual async Task AskForTokenExchange() helper.AddSmallComboBoxTo(g, row, 1, isEditable: false, text: "none", - items: new[] { "none", "assetfox", "factory-x" }, + items: new[] { "none", /*"assetfox", */"factory-x" }, margin: new AnyUiThickness(10, 0, 0, 0), padding: new AnyUiThickness(5, 0, 5, 0), - minWidth: 100, + minWidth: 50, horizontalAlignment: AnyUiHorizontalAlignment.Stretch)), (s) => { tokenExchanges[1] = s; }); row++; - helper.AddSmallLabelTo(g, row, 0, content: "Token Exchange 3:", - verticalAlignment: AnyUiVerticalAlignment.Center, - verticalContentAlignment: AnyUiVerticalAlignment.Center); - - AnyUiUIElement.SetStringFromControl( - helper.Set( - helper.AddSmallComboBoxTo(g, row, 1, - isEditable: false, - text: "none", - items: new[] { "none", "assetfox"}, - margin: new AnyUiThickness(10, 0, 0, 0), - padding: new AnyUiThickness(5, 0, 5, 0), - minWidth: 100, - horizontalAlignment: AnyUiHorizontalAlignment.Stretch)), - (s) => { tokenExchanges[2] = s; }); + //helper.AddSmallLabelTo(g, row, 0, content: "Token Exchange 3:", + // verticalAlignment: AnyUiVerticalAlignment.Center, + // verticalContentAlignment: AnyUiVerticalAlignment.Center); + + //AnyUiUIElement.SetStringFromControl( + // helper.Set( + // helper.AddSmallComboBoxTo(g, row, 1, + // isEditable: false, + // text: "none", + // items: new[] { "none", "assetfox"}, + // margin: new AnyUiThickness(10, 0, 0, 0), + // padding: new AnyUiThickness(5, 0, 5, 0), + // minWidth: 100, + // horizontalAlignment: AnyUiHorizontalAlignment.Stretch)), + // (s) => { tokenExchanges[2] = s; }); return g; }); @@ -789,12 +789,12 @@ protected virtual async Task DetermineAuthenticateHeaderForE Log.Singleton.Info($"Security access handler: Found 'access_token': {secretAccessToken}"); - var tokenExchanges2 = await AskForTokenExchange(); + var tokenExchanges = await AskForTokenExchange(); - if (tokenExchanges2 != null && !String.IsNullOrEmpty(tokenExchanges2[0]) - && tokenExchanges2[0] != "none") + if (tokenExchanges != null && !String.IsNullOrEmpty(tokenExchanges[0]) + && tokenExchanges[0] != "none") { - var target = tokenExchanges2[0]; + var target = tokenExchanges[0]; var configUrl = "https://iam-security-training.com/consumer/sts"; var d = new Dictionary { @@ -824,41 +824,6 @@ protected virtual async Task DetermineAuthenticateHeaderForE accessToken2 = tokenElement.GetString(); Console.WriteLine("Access Token: " + accessToken2); - - //var handler2 = new JwtSecurityTokenHandler(); - //var jwt2 = handler2.ReadJwtToken(accessToken2); - //var kid = jwt2.Header["kid"].ToString(); - - //// 3. Find matching key - //var key = jwks.First(k => k["kid"].ToString() == kid); - - //// 4. Build RSA key - //var e = Base64UrlEncoder.DecodeBytes(key["e"].ToString()); - //var n = Base64UrlEncoder.DecodeBytes(key["n"].ToString()); - //var rsa = new RSAParameters { Exponent = e, Modulus = n }; - //var rsaKey = new RsaSecurityKey(rsa); - - //// 5. Validate token - //var validationParams = new TokenValidationParameters - //{ - // ValidateIssuer = false, - // ValidateAudience = false, - // ValidateLifetime = false, - // ValidateIssuerSigningKey = true, - // IssuerSigningKey = rsaKey, - // ClockSkew = TimeSpan.FromMinutes(5) - //}; - - //try - //{ - // handler2.ValidateToken(accessToken2, validationParams, out _); - // Console.WriteLine("Token is valid"); - //} - //catch (Exception ex) - //{ - // Console.WriteLine($"Validation failed: {ex.Message}"); - //} - using var httpClient = new HttpClient(handler); var jwksJson = await httpClient.GetStringAsync($"{configUrl}/jwks"); var jwks = JObject.Parse(jwksJson)["keys"]; @@ -917,13 +882,13 @@ protected virtual async Task DetermineAuthenticateHeaderForE } } - if (tokenExchanges2[1] != "none") + if (tokenExchanges[1] != "none") { handler = new HttpClientHandler { DefaultProxyCredentials = CredentialCache.DefaultCredentials }; client = new HttpClient(handler); var configUrl2 = "https://iam-security-training.com/provider/sts"; - target = tokenExchanges2[1]; + target = tokenExchanges[1]; var d2 = new Dictionary { @@ -946,7 +911,7 @@ protected virtual async Task DetermineAuthenticateHeaderForE var content3 = await response3.Content.ReadAsStringAsync(); var accessToken3 = ""; - var doc3 = JsonDocument.Parse(content); + var doc3 = JsonDocument.Parse(content3); if (doc3.RootElement.TryGetProperty("access_token", out var tokenElement2)) { accessToken3 = tokenElement2.GetString(); @@ -956,48 +921,15 @@ protected virtual async Task DetermineAuthenticateHeaderForE var jwksJson = await httpClient.GetStringAsync($"{configUrl2}/jwks"); var jwks = JObject.Parse(jwksJson)["keys"]; - /* - var handler2 = new JwtSecurityTokenHandler(); - var jwt2 = handler2.ReadJwtToken(accessToken); - var kid = jwt2.Header["kid"].ToString(); - - // 3. Find matching key - var key = jwks.First(k => k["kid"].ToString() == kid); - - // 4. Build RSA key - var e = Base64UrlEncoder.DecodeBytes(key["e"].ToString()); - var n = Base64UrlEncoder.DecodeBytes(key["n"].ToString()); - var rsa = new RSAParameters { Exponent = e, Modulus = n }; - var rsaKey = new RsaSecurityKey(rsa); - - // 5. Validate token - var validationParams = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - IssuerSigningKey = rsaKey, - ClockSkew = TimeSpan.FromMinutes(5) - }; - - try - { - handler2.ValidateToken(accessToken, validationParams, out _); - ioConsole.WriteLine("Token is valid"); - } - catch (Exception ex) - { - ioConsole.WriteLine($"Validation failed: {ex.Message}"); - } - */ - // 1) Handler var handler2 = new JsonWebTokenHandler(); + var jsonWebToken2 = new JsonWebToken(accessToken3); + var jwtHeaderKid2 = jsonWebToken2.Kid; // liest 'kid' robust + + // 2) JWK aus JWKS direkt verwenden (hier exemplarisch LINQ auf Dein jwks-Array) - var jwtHeaderKid = new JsonWebToken(accessToken3).Kid; // liest 'kid' robust - var jwkJson = jwks.First(k => k["kid"].ToString() == jwtHeaderKid).ToString(); // k ist i. d. R. ein JObject + var jwkJson = jwks.First(k => k["kid"].ToString() == jwtHeaderKid2).ToString(); // k ist i. d. R. ein JObject var jwk = new JsonWebKey(jwkJson); // 3) Validierungsparameter @@ -1045,134 +977,6 @@ protected virtual async Task DetermineAuthenticateHeaderForE } } - if (tokenExchanges2[2] != "none") - { - handler = new HttpClientHandler { DefaultProxyCredentials = CredentialCache.DefaultCredentials }; - client = new HttpClient(handler); - - var configUrl3 = "https://integration.assetfox.apps.siemens.cloud/auth/realms/assetfox/protocol/openid-connect"; - target = tokenExchanges2[2]; - - var d3 = new Dictionary - { - { "grant_type", "urn:ietf:params:oauth:grant-type:token-exchange" }, - { "subject_token_type", "urn:ietf:params:oauth:token-type:jwt" }, - { "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" }, - { "subject_token", accessToken3 }, - }; - if (target != "") - { - d3.Add("audience", target); - } - var request4 = new HttpRequestMessage(HttpMethod.Post, $"{configUrl3}/token") - { - Content = new FormUrlEncodedContent(d3) - }; - request4.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); - - var response4 = await client.SendAsync(request3); - var content4 = await response4.Content.ReadAsStringAsync(); - - var accessToken4 = ""; - var doc4 = JsonDocument.Parse(content); - if (doc4.RootElement.TryGetProperty("access_token", out var tokenElement3)) - { - accessToken4 = tokenElement2.GetString(); - Console.WriteLine("Access Token: " + accessToken3); - - using var httpClient = new HttpClient(handler); - var jwksJson = await httpClient.GetStringAsync($"{configUrl3}/jwks"); - var jwks = JObject.Parse(jwksJson)["keys"]; - - /* - var handler2 = new JwtSecurityTokenHandler(); - var jwt2 = handler2.ReadJwtToken(accessToken); - var kid = jwt2.Header["kid"].ToString(); - - // 3. Find matching key - var key = jwks.First(k => k["kid"].ToString() == kid); - - // 4. Build RSA key - var e = Base64UrlEncoder.DecodeBytes(key["e"].ToString()); - var n = Base64UrlEncoder.DecodeBytes(key["n"].ToString()); - var rsa = new RSAParameters { Exponent = e, Modulus = n }; - var rsaKey = new RsaSecurityKey(rsa); - - // 5. Validate token - var validationParams = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - IssuerSigningKey = rsaKey, - ClockSkew = TimeSpan.FromMinutes(5) - }; - - try - { - handler2.ValidateToken(accessToken, validationParams, out _); - ioConsole.WriteLine("Token is valid"); - } - catch (Exception ex) - { - ioConsole.WriteLine($"Validation failed: {ex.Message}"); - } - */ - - // 1) Handler - var handler2 = new JsonWebTokenHandler(); - - // 2) JWK aus JWKS direkt verwenden (hier exemplarisch LINQ auf Dein jwks-Array) - var jwtHeaderKid = new JsonWebToken(accessToken3).Kid; // liest 'kid' robust - var jwkJson = jwks.First(k => k["kid"].ToString() == jwtHeaderKid).ToString(); // k ist i. d. R. ein JObject - var jwk = new JsonWebKey(jwkJson); - - // 3) Validierungsparameter - var validationParams = new TokenValidationParameters - { - // Signaturprüfung - IssuerSigningKey = jwk, // kein manuelles RSAParameters nötig - ValidateIssuerSigningKey = true, - - // Lebenszeit - ValidateLifetime = true, - RequireExpirationTime = true, - ClockSkew = TimeSpan.FromMinutes(5), // bei UTC+0 gut, ggf. 2–5 Minuten - - // Issuer/Audience je nach Bedarf (bei Tests oft aus) - ValidateIssuer = false, // später auf true + ValidIssuer setzen - ValidateAudience = false, // später auf true + ValidAudience setzen - - // Keine Legacy-Claim-Mappings - // MapInboundClaims = false, - - // Optional: Name-/Rollen-Claims aus dem JWT - NameClaimType = "name", - RoleClaimType = "role" - }; - - try - { - var result = handler2.ValidateToken(accessToken3, validationParams); - if (!result.IsValid) - { - Console.WriteLine($"Validation failed: {result.Exception?.Message}"); - return null; - } - else - { - Console.WriteLine("Token is valid"); - secretAccessToken = accessToken3; - } - } - catch (Exception ex) - { - Console.WriteLine($"Validation failed: {ex.Message}"); - return null; - } - } - } } } @@ -1495,130 +1299,12 @@ protected virtual async Task DetermineAuthenticateHeaderForE var accessTokenString = accessToken.ToString(); - var tokenExchanges = await AskForTokenExchange(); - - if (tokenExchanges != null && !String.IsNullOrEmpty(tokenExchanges[0])) - { - var target = tokenExchanges[0]; - var configUrl = "https://iam-security-training.com/consumer/sts"; - var d = new Dictionary - { - { "grant_type", "urn:ietf:params:oauth:grant-type:token-exchange" }, - { "subject_token_type", "urn:ietf:params:oauth:token-type:jwt" }, - { "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" }, - { "subject_token", accessToken.ToString() }, - }; - if (target != "") - { - d.Add("audience", target); - } - var request2 = new HttpRequestMessage(HttpMethod.Post, $"{configUrl}/token") - { - Content = new FormUrlEncodedContent(d) - }; - request2.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); - - var response2 = await client.SendAsync(request2); - var content = await response2.Content.ReadAsStringAsync(); - - var accessToken2 = ""; - var doc2 = JsonDocument.Parse(content); - if (doc2.RootElement.TryGetProperty("access_token", out var tokenElement)) - { - accessToken2 = tokenElement.GetString(); - Console.WriteLine("Access Token: " + accessToken2); - - using var httpClient = new HttpClient(handler); - var jwksJson = await httpClient.GetStringAsync($"{configUrl}/jwks"); - var jwks = JObject.Parse(jwksJson)["keys"]; - - /* - var handler2 = new JwtSecurityTokenHandler(); - var jwt2 = handler2.ReadJwtToken(accessToken); - var kid = jwt2.Header["kid"].ToString(); - - // 3. Find matching key - var key = jwks.First(k => k["kid"].ToString() == kid); + //ToDo: Need token exchange here? + //var tokenExchanges = await AskForTokenExchange(); - // 4. Build RSA key - var e = Base64UrlEncoder.DecodeBytes(key["e"].ToString()); - var n = Base64UrlEncoder.DecodeBytes(key["n"].ToString()); - var rsa = new RSAParameters { Exponent = e, Modulus = n }; - var rsaKey = new RsaSecurityKey(rsa); - - // 5. Validate token - var validationParams = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - IssuerSigningKey = rsaKey, - ClockSkew = TimeSpan.FromMinutes(5) - }; - - try - { - handler2.ValidateToken(accessToken, validationParams, out _); - ioConsole.WriteLine("Token is valid"); - } - catch (Exception ex) - { - ioConsole.WriteLine($"Validation failed: {ex.Message}"); - } - */ - - // 1) Handler - var handler2 = new JsonWebTokenHandler(); - - // 2) JWK aus JWKS direkt verwenden (hier exemplarisch LINQ auf Dein jwks-Array) - var jwtHeaderKid = new JsonWebToken(accessToken2).Kid; // liest 'kid' robust - var jwkJson = jwks.First(k => k["kid"].ToString() == jwtHeaderKid).ToString(); // k ist i. d. R. ein JObject - var jwk = new JsonWebKey(jwkJson); - - // 3) Validierungsparameter - var validationParams = new TokenValidationParameters - { - // Signaturprüfung - IssuerSigningKey = jwk, // kein manuelles RSAParameters nötig - ValidateIssuerSigningKey = true, - - // Lebenszeit - ValidateLifetime = true, - RequireExpirationTime = true, - ClockSkew = TimeSpan.FromMinutes(5), // bei UTC+0 gut, ggf. 2–5 Minuten - - // Issuer/Audience je nach Bedarf (bei Tests oft aus) - ValidateIssuer = false, // später auf true + ValidIssuer setzen - ValidateAudience = false, // später auf true + ValidAudience setzen - - // Keine Legacy-Claim-Mappings - // MapInboundClaims = false, - - // Optional: Name-/Rollen-Claims aus dem JWT - NameClaimType = "name", - RoleClaimType = "role" - }; - - try - { - var result = handler2.ValidateToken(accessToken2, validationParams); - if (!result.IsValid) - { - Console.WriteLine($"Validation failed: {result.Exception?.Message}"); - } - else - { - Console.WriteLine("Token is valid"); - accessTokenString = accessToken2; - } - } - catch (Exception ex) - { - Console.WriteLine($"Validation failed: {ex.Message}"); - } - } - } + //if (tokenExchanges != null && !String.IsNullOrEmpty(tokenExchanges[0])) + //{ + //} // build correct header key, remember, return endpoint.LastRenewed = DateTime.UtcNow; From 678886a03456d622b7ead11967c07d66dc6e5b3b Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Mon, 23 Mar 2026 10:51:04 +0100 Subject: [PATCH 09/15] Add security token to header for POST, PUT and DELETE operations --- .../PackageCentral/PackageHttpDownloadUtil.cs | 69 +++++++++++++++---- .../SecurityAccessHandlerLogicBase.cs | 14 +--- 2 files changed, 55 insertions(+), 28 deletions(-) diff --git a/src/AasxPackageLogic/PackageCentral/PackageHttpDownloadUtil.cs b/src/AasxPackageLogic/PackageCentral/PackageHttpDownloadUtil.cs index 928ab7f9a..f5e6d4f64 100644 --- a/src/AasxPackageLogic/PackageCentral/PackageHttpDownloadUtil.cs +++ b/src/AasxPackageLogic/PackageCentral/PackageHttpDownloadUtil.cs @@ -7,10 +7,6 @@ This source code is licensed under the Apache License 2.0 (see LICENSE.txt). This source code may use other Open Source software components (see LICENSE.txt). */ -using AasxOpenIdClient; -using AdminShellNS; -using Namotion.Reflection; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; @@ -22,6 +18,10 @@ This source code may use other Open Source software components (see LICENSE.txt) using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; +using AasxOpenIdClient; +using AdminShellNS; +using Namotion.Reflection; +using Newtonsoft.Json; using Aas = AasCore.Aas3_1; namespace AasxPackageLogic.PackageCentral @@ -290,10 +290,25 @@ await memStream.WriteAsync(buffer, 0, bytesRead, // prepare request var requestContent = new StringContent(requestBody, Encoding.UTF8, requestContentType); - // retrieve response - var response = await client.PostAsync(requestPath, requestContent); + // make a request + HttpResponseMessage response = null; + using (var requestMessage = + new HttpRequestMessage(HttpMethod.Post, requestPath)) + { + // assume headers to be for authorization + if (runtimeOptions?.HttpHeaderData?.Headers != null) + foreach (var header in runtimeOptions.HttpHeaderData?.Headers) + { + requestMessage.Headers.Add(header.Key, header.Value); + } + + requestMessage.Content = requestContent; + response = await client.SendAsync(requestMessage, + HttpCompletionOption.ResponseHeadersRead); + + } - if (response.IsSuccessStatusCode) + if (response != null && response.IsSuccessStatusCode) { // // this portion of the code is prepared to receive large sets of content data @@ -304,7 +319,7 @@ await memStream.WriteAsync(buffer, 0, bytesRead, // log if (runtimeOptions?.ExtendedConnectionDebug == true) - runtimeOptions .Log?.Info($".. response with header-content-len {contentLength} " + + runtimeOptions.Log?.Info($".. response with header-content-len {contentLength} " + $"and file-name {contentFn} .."); var contentStream = await response?.Content?.ReadAsStreamAsync(); @@ -318,7 +333,7 @@ await memStream.WriteAsync(buffer, 0, bytesRead, if (contentFn != null) givenFn = contentFn; if (runtimeOptions?.ExtendedConnectionDebug == true) - runtimeOptions .Log?.Info($".. downloading and scanning by proxy/firewall {client.BaseAddress} " + + runtimeOptions.Log?.Info($".. downloading and scanning by proxy/firewall {client.BaseAddress} " + $"and request {requestPath} .. "); using (var memStream = new MemoryStream()) @@ -372,7 +387,7 @@ await memStream.WriteAsync(buffer, 0, bytesRead, else { // - // assume smaller conent data + // assume smaller content data // var responseContents = await response.Content.ReadAsStringAsync(); @@ -474,6 +489,13 @@ public static async Task> HttpPutPostFromMemoryStr // var data = new StringContent("1.2345", Encoding.UTF8, "application/json"); } + // assume headers to be for authorization + if (runtimeOptions?.HttpHeaderData?.Headers != null) + foreach (var header in runtimeOptions.HttpHeaderData?.Headers) + { + overallContent.Headers.Add(header.Key, header.Value); + } + // get response? using (var response = (!usePost) ? await client.PutAsync(requestPath, overallContent) @@ -510,18 +532,35 @@ public static async Task> HttpDeleteUri( runtimeOptions.Log?.Info($"HttpClient DELETE() with base-address {client.BaseAddress} " + $"and request {requestPath} .. "); + + // make a request + HttpResponseMessage response = null; + using (var requestMessage = + new HttpRequestMessage(HttpMethod.Delete, requestPath)) + { + // assume headers to be for authorization + if (runtimeOptions?.HttpHeaderData?.Headers != null) + foreach (var header in runtimeOptions.HttpHeaderData?.Headers) + { + requestMessage.Headers.Add(header.Key, header.Value); + } + + response = await client.SendAsync(requestMessage); + } + + var content = ""; + // get response? - using (var response = await client.DeleteAsync(requestPath)) + if (response != null) { // try read in any case, despite: // https://stackoverflow.com/questions/17640666/httpclient-deleteasync-and-content-readadstringasync-always-return-null - var content = ""; if (response.StatusCode != HttpStatusCode.NoContent) await response.Content.ReadAsStringAsync(); - - // ok! - return new Tuple(response.StatusCode, content); } + // ok! + return new Tuple(response.StatusCode, content); + } public static async Task> HttpPutPostIdentifiable( diff --git a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs index 661a40024..d7d085875 100644 --- a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs +++ b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs @@ -12,37 +12,25 @@ This source code may use other Open Source software components (see LICENSE.txt) using System; using System.Collections.Generic; -using System.ComponentModel; using System.IdentityModel.Tokens.Jwt; -using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; -using System.Reflection; using System.Security.Claims; -using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using System.Security.RightsManagement; using System.Text; using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading; using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; using AasxIntegrationBase; using AasxPackageLogic; using AasxPackageLogic.PackageCentral; using AdminShellNS; using AnyUi; -using Extensions; using Microsoft.Identity.Client; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json.Linq; -using static AasxPackageLogic.PackageCentral.PackageContainerHttpRepoSubset; -using Aas = AasCore.Aas3_1; namespace AasxPackageExplorer { @@ -437,7 +425,7 @@ protected virtual async Task AskForAuthServerUri(string baseAddress, str protected virtual async Task AskForTokenExchange() { - string[] tokenExchanges = ["none", "none", "none"]; + string[] tokenExchanges = ["none", "none"]; var ucJob = new AnyUiDialogueDataModalPanel("With token exchange?"); ucJob.ActivateRenderPanel(null, From efaf4e7674de1587c612f901cecdeaf6bf58bbe7 Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Thu, 26 Mar 2026 11:58:37 +0100 Subject: [PATCH 10/15] Refactor Token exchange, add possibility to enter audience and config url in UI --- .../SecurityAccessHandlerLogicBase.cs | 425 ++++++++---------- 1 file changed, 195 insertions(+), 230 deletions(-) diff --git a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs index d7d085875..cc7f4536a 100644 --- a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs +++ b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs @@ -423,11 +423,114 @@ protected virtual async Task AskForAuthServerUri(string baseAddress, str return null; } - protected virtual async Task AskForTokenExchange() + private async Task DoTokenExchange(string[] tokenExchangeParams, string secretAccessToken) { - string[] tokenExchanges = ["none", "none"]; + var handler = new HttpClientHandler { DefaultProxyCredentials = CredentialCache.DefaultCredentials }; + var client = new HttpClient(handler); + + var exchangendToken = String.Empty; + + var configUrl = tokenExchangeParams[0]; + var target = tokenExchangeParams[1]; + + var d = new Dictionary + { + { "grant_type", "urn:ietf:params:oauth:grant-type:token-exchange" }, + { "subject_token_type", "urn:ietf:params:oauth:token-type:jwt" }, + { "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" }, + { "subject_token", secretAccessToken }, + }; + + if (target != "") + { + d.Add("audience", target); + } + var request = new HttpRequestMessage(HttpMethod.Post, $"{configUrl}/token") + { + Content = new FormUrlEncodedContent(d) + }; + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + var accessToken = ""; + var doc = JsonDocument.Parse(content); + if (doc.RootElement.TryGetProperty("access_token", out var tokenElement)) + { + accessToken = tokenElement.GetString(); + Console.WriteLine("Access Token: " + accessToken); + + using var httpClient = new HttpClient(handler); + var jwksJson = await httpClient.GetStringAsync($"{configUrl}/jwks"); + var jwks = JObject.Parse(jwksJson)["keys"]; + + // 1) Handler + var jsonWebTokenHandler = new JsonWebTokenHandler(); + + // 2) JWK aus JWKS direkt verwenden (hier exemplarisch LINQ auf Dein jwks-Array) + var jsonWebToken = new JsonWebToken(accessToken); + var jwtHeaderKid = jsonWebToken.Kid; // liest 'kid' robust + var jwkJson = jwks.First(k => k["kid"].ToString() == jwtHeaderKid).ToString(); // k ist i. d. R. ein JObject + var jwk = new JsonWebKey(jwkJson); + + // 3) Validierungsparameter + var validationParams = new TokenValidationParameters + { + // Signaturprüfung + IssuerSigningKey = jwk, // kein manuelles RSAParameters nötig + ValidateIssuerSigningKey = true, + + // Lebenszeit + ValidateLifetime = true, + RequireExpirationTime = true, + ClockSkew = TimeSpan.FromMinutes(5), // bei UTC+0 gut, ggf. 2–5 Minuten + + // Issuer/Audience je nach Bedarf (bei Tests oft aus) + ValidateIssuer = false, // später auf true + ValidIssuer setzen + ValidateAudience = false, // später auf true + ValidAudience setzen + + // Keine Legacy-Claim-Mappings + // MapInboundClaims = false, + + // Optional: Name-/Rollen-Claims aus dem JWT + NameClaimType = "name", + RoleClaimType = "role" + }; + + try + { + var result = jsonWebTokenHandler.ValidateToken(accessToken, validationParams); + if (!result.IsValid) + { + Console.WriteLine($"Validation failed: {result.Exception?.Message}"); + return null; + } + else + { + Console.WriteLine("Token is valid"); + exchangendToken = accessToken; + } + } + catch (Exception ex) + { + Console.WriteLine($"Validation failed: {ex.Message}"); + return null; + } + } + + return exchangendToken; + } + + protected virtual async Task> AskForTokenExchange() + { + bool needsTokenExchange1 = false; + bool needsTokenExchange2 = false; + string[] tokenExchange1Params = ["https://iam-security-training.com/consumer/sts", "factory-x",]; + string[] tokenExchange2Params = ["https://iam-security-training.com/provider/sts", "factory-x",]; var ucJob = new AnyUiDialogueDataModalPanel("With token exchange?"); + ucJob.ActivateRenderPanel(null, disableScrollArea: false, dialogButtons: AnyUiMessageBoxButton.OK, @@ -437,7 +540,7 @@ protected virtual async Task AskForTokenExchange() var panel = new AnyUiStackPanel(); var helper = new AnyUiSmallWidgetToolkit(); - var g = helper.AddSmallGrid(25, 2, new[] { "350:", "*" }, + var g = helper.AddSmallGrid(25, 2, new[] { "200:", "*" }, padding: new AnyUiThickness(0, 5, 0, 5), margin: new AnyUiThickness(10, 0, 30, 0)); @@ -448,72 +551,112 @@ protected virtual async Task AskForTokenExchange() // Info helper.Set( - helper.AddSmallLabelTo(g, row, 0, content: - "For some authentification methods, you will need to do a token exchange.", - wrapping: AnyUiTextWrapping.Wrap), + helper.AddSmallLabelTo(g, row, 0, content: "For some authentification methods, you will need to do a token exchange", wrapping: AnyUiTextWrapping.Wrap), + colSpan: 2); row++; - helper.AddSmallLabelTo(g, row, 0, content: "Token Exchange 1 (with consumer STS)", + // separation + helper.AddSmallBorderTo(g, row, 0, + borderThickness: new AnyUiThickness(0.5), borderBrush: AnyUiBrushes.White, + colSpan: 2, + margin: new AnyUiThickness(0, 0, 0, 20)); + row++; + + AnyUiUIElement.SetBoolFromControl( + helper.AddSmallCheckBoxTo(g, row, 0, + content: "Do token exchange 1", + isChecked: needsTokenExchange1, + verticalContentAlignment: AnyUiVerticalAlignment.Center), + (b) => { needsTokenExchange1 = b; }); + + row++; + + // Content URL + + helper.AddSmallLabelTo(g, row, 0, content: "Content URL:", verticalAlignment: AnyUiVerticalAlignment.Center, verticalContentAlignment: AnyUiVerticalAlignment.Center); AnyUiUIElement.SetStringFromControl( - helper.Set( - helper.AddSmallComboBoxTo(g, row, 1, - isEditable: false, - text: "none", - items: new[] { "none", "factory-x" }, - margin: new AnyUiThickness(10, 0, 0, 0), - padding: new AnyUiThickness(5, 0, 5, 0), - minWidth: 50, - horizontalAlignment: AnyUiHorizontalAlignment.Stretch)), - (s) => { tokenExchanges[0] = s; }); + helper.Set( + helper.AddSmallTextBoxTo(g, row, 1, + text: $"{tokenExchange1Params[0]}", + verticalAlignment: AnyUiVerticalAlignment.Center, + verticalContentAlignment: AnyUiVerticalAlignment.Center), + horizontalAlignment: AnyUiHorizontalAlignment.Stretch), + (s) => { tokenExchange1Params[0] = s; }); + + row++; + + // Target (Audience) + + helper.AddSmallLabelTo(g, row, 0, content: "Target (Audience):", + verticalAlignment: AnyUiVerticalAlignment.Center, + verticalContentAlignment: AnyUiVerticalAlignment.Center); + + AnyUiUIElement.SetStringFromControl( + helper.Set( + helper.AddSmallTextBoxTo(g, row, 1, + text: $"{tokenExchange1Params[1]}", + verticalAlignment: AnyUiVerticalAlignment.Center, + verticalContentAlignment: AnyUiVerticalAlignment.Center), + horizontalAlignment: AnyUiHorizontalAlignment.Stretch), + (s) => { tokenExchange1Params[1] = s; }); + + row++; + + + AnyUiUIElement.SetBoolFromControl( + helper.AddSmallCheckBoxTo(g, row, 0, + content: "Do token exchange 2", + isChecked: needsTokenExchange2, + verticalContentAlignment: AnyUiVerticalAlignment.Center), + (b) => { needsTokenExchange2 = b; }); row++; - helper.AddSmallLabelTo(g, row, 0, content: "Token Exchange 2 (with producer STS):", + // Content URL + + helper.AddSmallLabelTo(g, row, 0, content: "Content URL:", verticalAlignment: AnyUiVerticalAlignment.Center, verticalContentAlignment: AnyUiVerticalAlignment.Center); AnyUiUIElement.SetStringFromControl( - helper.Set( - helper.AddSmallComboBoxTo(g, row, 1, - isEditable: false, - text: "none", - items: new[] { "none", /*"assetfox", */"factory-x" }, - margin: new AnyUiThickness(10, 0, 0, 0), - padding: new AnyUiThickness(5, 0, 5, 0), - minWidth: 50, - horizontalAlignment: AnyUiHorizontalAlignment.Stretch)), - (s) => { tokenExchanges[1] = s; }); + helper.Set( + helper.AddSmallTextBoxTo(g, row, 1, + text: $"{tokenExchange2Params[0]}", + verticalAlignment: AnyUiVerticalAlignment.Center, + verticalContentAlignment: AnyUiVerticalAlignment.Center), + horizontalAlignment: AnyUiHorizontalAlignment.Stretch), + (s) => { tokenExchange2Params[0] = s; }); row++; - //helper.AddSmallLabelTo(g, row, 0, content: "Token Exchange 3:", - // verticalAlignment: AnyUiVerticalAlignment.Center, - // verticalContentAlignment: AnyUiVerticalAlignment.Center); - - //AnyUiUIElement.SetStringFromControl( - // helper.Set( - // helper.AddSmallComboBoxTo(g, row, 1, - // isEditable: false, - // text: "none", - // items: new[] { "none", "assetfox"}, - // margin: new AnyUiThickness(10, 0, 0, 0), - // padding: new AnyUiThickness(5, 0, 5, 0), - // minWidth: 100, - // horizontalAlignment: AnyUiHorizontalAlignment.Stretch)), - // (s) => { tokenExchanges[2] = s; }); + // Target (Audience) + + helper.AddSmallLabelTo(g, row, 0, content: "Target (Audience):", + verticalAlignment: AnyUiVerticalAlignment.Center, + verticalContentAlignment: AnyUiVerticalAlignment.Center); + + AnyUiUIElement.SetStringFromControl( + helper.Set( + helper.AddSmallTextBoxTo(g, row, 1, + text: $"{tokenExchange2Params[1]}", + verticalAlignment: AnyUiVerticalAlignment.Center, + verticalContentAlignment: AnyUiVerticalAlignment.Center), + horizontalAlignment: AnyUiHorizontalAlignment.Stretch), + (s) => { tokenExchange2Params[1] = s; }); + + // give back return g; }); if (_displayContext != null && await _displayContext.StartFlyoverModalAsync(ucJob)) - return tokenExchanges; - return tokenExchanges; + return new Tuple(needsTokenExchange1, tokenExchange1Params, needsTokenExchange2, tokenExchange2Params); + return new Tuple(needsTokenExchange1, tokenExchange1Params, needsTokenExchange2, tokenExchange2Params); ; } - protected virtual async Task AskForSelectFromCertCollection( string baseAddress, X509Certificate2Collection fcollection) @@ -776,198 +919,20 @@ protected virtual async Task DetermineAuthenticateHeaderForE var secretAccessToken = secretContentJson["access_token"].ToString(); Log.Singleton.Info($"Security access handler: Found 'access_token': {secretAccessToken}"); - var tokenExchanges = await AskForTokenExchange(); - if (tokenExchanges != null && !String.IsNullOrEmpty(tokenExchanges[0]) - && tokenExchanges[0] != "none") + if (tokenExchanges != null && tokenExchanges.Item1) { - var target = tokenExchanges[0]; - var configUrl = "https://iam-security-training.com/consumer/sts"; - var d = new Dictionary - { - { "grant_type", "urn:ietf:params:oauth:grant-type:token-exchange" }, - { "subject_token_type", "urn:ietf:params:oauth:token-type:jwt" }, - { "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" }, - { "subject_token", secretAccessToken }, - }; + secretAccessToken = await DoTokenExchange(tokenExchanges.Item2, secretAccessToken); - if (target != "") + if (tokenExchanges.Item3) { - d.Add("audience", target); - } - var request2 = new HttpRequestMessage(HttpMethod.Post, $"{configUrl}/token") - { - Content = new FormUrlEncodedContent(d) - }; - request2.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); - - var response2 = await client.SendAsync(request2); - var content = await response2.Content.ReadAsStringAsync(); - - var accessToken2 = ""; - var doc2 = JsonDocument.Parse(content); - if (doc2.RootElement.TryGetProperty("access_token", out var tokenElement)) - { - accessToken2 = tokenElement.GetString(); - Console.WriteLine("Access Token: " + accessToken2); - - using var httpClient = new HttpClient(handler); - var jwksJson = await httpClient.GetStringAsync($"{configUrl}/jwks"); - var jwks = JObject.Parse(jwksJson)["keys"]; - - // 1) Handler - var handler2 = new JsonWebTokenHandler(); - - // 2) JWK aus JWKS direkt verwenden (hier exemplarisch LINQ auf Dein jwks-Array) - var jsonWebToken = new JsonWebToken(accessToken2); - var jwtHeaderKid = jsonWebToken.Kid; // liest 'kid' robust - var jwkJson = jwks.First(k => k["kid"].ToString() == jwtHeaderKid).ToString(); // k ist i. d. R. ein JObject - var jwk = new JsonWebKey(jwkJson); - - // 3) Validierungsparameter - var validationParams = new TokenValidationParameters - { - // Signaturprüfung - IssuerSigningKey = jwk, // kein manuelles RSAParameters nötig - ValidateIssuerSigningKey = true, - - // Lebenszeit - ValidateLifetime = true, - RequireExpirationTime = true, - ClockSkew = TimeSpan.FromMinutes(5), // bei UTC+0 gut, ggf. 2–5 Minuten - - // Issuer/Audience je nach Bedarf (bei Tests oft aus) - ValidateIssuer = false, // später auf true + ValidIssuer setzen - ValidateAudience = false, // später auf true + ValidAudience setzen - - // Keine Legacy-Claim-Mappings - // MapInboundClaims = false, - - // Optional: Name-/Rollen-Claims aus dem JWT - NameClaimType = "name", - RoleClaimType = "role" - }; - - try - { - var result = handler2.ValidateToken(accessToken2, validationParams); - if (!result.IsValid) - { - Console.WriteLine($"Validation failed: {result.Exception?.Message}"); - return null; - } - else - { - Console.WriteLine("Token is valid"); - secretAccessToken = accessToken2; - } - } - catch (Exception ex) - { - Console.WriteLine($"Validation failed: {ex.Message}"); - return null; - } - } - - if (tokenExchanges[1] != "none") - { - handler = new HttpClientHandler { DefaultProxyCredentials = CredentialCache.DefaultCredentials }; - client = new HttpClient(handler); - - var configUrl2 = "https://iam-security-training.com/provider/sts"; - target = tokenExchanges[1]; - - var d2 = new Dictionary - { - { "grant_type", "urn:ietf:params:oauth:grant-type:token-exchange" }, - { "subject_token_type", "urn:ietf:params:oauth:token-type:jwt" }, - { "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" }, - { "subject_token", accessToken2 }, - }; - if (target != "") - { - d2.Add("audience", target); - } - var request3 = new HttpRequestMessage(HttpMethod.Post, $"{configUrl2}/token") - { - Content = new FormUrlEncodedContent(d2) - }; - request3.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); - - var response3 = await client.SendAsync(request3); - var content3 = await response3.Content.ReadAsStringAsync(); - - var accessToken3 = ""; - var doc3 = JsonDocument.Parse(content3); - if (doc3.RootElement.TryGetProperty("access_token", out var tokenElement2)) - { - accessToken3 = tokenElement2.GetString(); - Console.WriteLine("Access Token: " + accessToken3); - - using var httpClient = new HttpClient(handler); - var jwksJson = await httpClient.GetStringAsync($"{configUrl2}/jwks"); - var jwks = JObject.Parse(jwksJson)["keys"]; - - // 1) Handler - var handler2 = new JsonWebTokenHandler(); - - var jsonWebToken2 = new JsonWebToken(accessToken3); - var jwtHeaderKid2 = jsonWebToken2.Kid; // liest 'kid' robust - - - // 2) JWK aus JWKS direkt verwenden (hier exemplarisch LINQ auf Dein jwks-Array) - var jwkJson = jwks.First(k => k["kid"].ToString() == jwtHeaderKid2).ToString(); // k ist i. d. R. ein JObject - var jwk = new JsonWebKey(jwkJson); - - // 3) Validierungsparameter - var validationParams = new TokenValidationParameters - { - // Signaturprüfung - IssuerSigningKey = jwk, // kein manuelles RSAParameters nötig - ValidateIssuerSigningKey = true, - - // Lebenszeit - ValidateLifetime = true, - RequireExpirationTime = true, - ClockSkew = TimeSpan.FromMinutes(5), // bei UTC+0 gut, ggf. 2–5 Minuten - - // Issuer/Audience je nach Bedarf (bei Tests oft aus) - ValidateIssuer = false, // später auf true + ValidIssuer setzen - ValidateAudience = false, // später auf true + ValidAudience setzen - - // Keine Legacy-Claim-Mappings - // MapInboundClaims = false, - - // Optional: Name-/Rollen-Claims aus dem JWT - NameClaimType = "name", - RoleClaimType = "role" - }; - - try - { - var result = handler2.ValidateToken(accessToken3, validationParams); - if (!result.IsValid) - { - Console.WriteLine($"Validation failed: {result.Exception?.Message}"); - return null; - } - else - { - Console.WriteLine("Token is valid"); - secretAccessToken = accessToken3; - } - } - catch (Exception ex) - { - Console.WriteLine($"Validation failed: {ex.Message}"); - return null; - } - } - + secretAccessToken = await DoTokenExchange(tokenExchanges.Item4, secretAccessToken); } } + Log.Singleton.Info($"Security access handler: Exchanged token to 'access_token': {secretAccessToken}"); + // build correct header key, remember, return endpoint.LastRenewed = DateTime.UtcNow; endpoint.LastHeaderItem = new HttpHeaderDataItem("Authorization", $"Bearer {secretAccessToken.ToString()}"); From 4a8616049d42829aec27bce259e2fa7ed646b8a0 Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Wed, 1 Apr 2026 16:59:14 +0200 Subject: [PATCH 11/15] Add possibility to use leo discovery servoce as Reg-of-Reg --- .../options-debug.MIHO.json | 6 ++ .../PackageContainerHttpRepoSubset.cs | 75 +++++++++++++++---- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/AasxPackageExplorer/options-debug.MIHO.json b/src/AasxPackageExplorer/options-debug.MIHO.json index cc0a0aad3..3a2fbd744 100644 --- a/src/AasxPackageExplorer/options-debug.MIHO.json +++ b/src/AasxPackageExplorer/options-debug.MIHO.json @@ -53,6 +53,12 @@ "AccessInfo": { "Method": "None" // None, Ask, Basic, CertificateStore, File, InteractiveEntry, Secret } + }, + { + "BaseAddress": "https://leo-discovery.admin-shell-io.com/api/v2/", + "AccessInfo": { + "Method": "None" // None, Ask, Basic, CertificateStore, File, InteractiveEntry, Secret + } }, { "BaseAddress": "https://aasx-server-pcf.dev.pxcio.net/", diff --git a/src/AasxPackageLogic/PackageCentral/PackageContainerHttpRepoSubset.cs b/src/AasxPackageLogic/PackageCentral/PackageContainerHttpRepoSubset.cs index 0cc18260e..8aa74dafe 100644 --- a/src/AasxPackageLogic/PackageCentral/PackageContainerHttpRepoSubset.cs +++ b/src/AasxPackageLogic/PackageCentral/PackageContainerHttpRepoSubset.cs @@ -303,14 +303,14 @@ public static bool IsValidUriForRegistryAasByAssetIds(string location) public static bool IsValidUriForRegOfRegAasByAssetId(string location) { - var m = Regex.Match(location, @"^(http(|s))://(.*?)/registry-descriptors/([-A-Za-z0-9_]{1,999})$", + var m = Regex.Match(location, @"^(http(|s))://(.*?)/(?:registry-descriptors/|companies\?asset_id=)([-A-Za-z0-9_]{1,999})$", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); return m.Success; } public static string IsValidAndDecodeUriForRegOfRegAasByAssetId(string location) { - var m = Regex.Match(location, @"^(http(|s))://(.*?)/registry-descriptors/([-A-Za-z0-9_]{1,999})$", + var m = Regex.Match(location, @"^(http(|s))://(.*?)/(?:registry-descriptors/|companies\?asset_id=)([-A-Za-z0-9_]{1,999})$", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); if (!m.Success) return null; @@ -353,7 +353,7 @@ public static Uri GetBaseUri(string location) // try an explicit search for known parts of ressources // (preserves scheme, host and leading pathes) var m = Regex.Match(location, @"^(.*?)(/shells|/submodel|/concept-description|/lookup|/description" - + "|/shell-descriptor|/submodel-descriptor|/bulk|/serialization|/package)"); + + "|/shell-descriptor|/submodel-descriptor|/bulk|/serialization|/package|/registry-descriptors|/companies)"); if (m.Success) return new Uri(m.Groups[1].ToString() + "/"); @@ -366,6 +366,10 @@ public static Uri GetBaseUri(string location) { return new Uri(location.Substring(0, p) + "/"); } + else + { + return new Uri(location); + } } // go to error @@ -732,10 +736,17 @@ public static Uri BuildUriForRegOfRegAasByAssetId(Uri baseUri, string id, bool e // access if (id?.HasContent() != true) return null; + var route = "registry-descriptors/"; - // try combine var assenc = encryptIds ? AdminShellUtil.Base64UrlEncode(id) : id; - return CombineUri(baseUri, $"registry-descriptors/{assenc}"); + + if (baseUri.Host.Contains("leo-discovery")) + { + route = "companies"; + assenc = $"?asset_id={assenc}"; + } + + return CombineUri(baseUri, $"{route}{assenc}"); } // @@ -819,7 +830,10 @@ private static async Task FromRegistryGetAasAndSubmodels( { // strictly check IFC var aasIfc = "" + ep["interface"]; - if (aasIfc != "AAS-1.0") + if (!(aasIfc == "AAS-1.0" + || aasIfc == "AAS-2.0" + || aasIfc == "AAS-3.0" + || aasIfc == "AAS-3.1")) continue; // direct access HREF @@ -843,8 +857,11 @@ private static async Task FromRegistryGetAasAndSubmodels( { // strictly check IFC var smIfc = "" + smep["interface"]; - if (smIfc != "SUBMODEL-1.0") - continue; + if (!(aasIfc == "SUBMODEL-1.0" + || aasIfc == "SUBMODEL-2.0" + || aasIfc == "SUBMODEL-3.0" + || aasIfc == "SUBMODEL-3.1")) + continue; // ok string href = smep.protocolInformation.href; @@ -980,7 +997,8 @@ private static async Task FromRegOfRegGetAasAndSubmodels( List trackNewIdentifiables = null, List trackLoadedIdentifiables = null, Action lambdaReportProgress = null, - bool compatOldAasxServer = false) + bool compatOldAasxServer = false, + bool isFXLeoDiscoveryServer = false) { // access if (record == null || regDescriptor == null || assetId?.HasContent() != true) @@ -998,10 +1016,30 @@ private static async Task FromRegOfRegGetAasAndSubmodels( // } // However, only Url and Id are currently useful - string regUrl = "" + regDescriptor["url"]; - string regInfo = "" + regDescriptor["info"]; - if (regInfo == "") - regInfo = ""; + string regUrl = String.Empty; + + if (isFXLeoDiscoveryServer) + { + var endpoints = regDescriptor?.endpoints; + + if (endpoints != null) + { + foreach (var endpoint in endpoints) + { + if (endpoint["interface"] == "AAS-REGISTRY-3.1" + || endpoint["interface"] == "AAS-REGISTRY-3.0") + { + regUrl = endpoint.protocolInformation.href; + break; + } + } + } + } + else + { + regUrl = "" + regDescriptor["url"]; + } + // valid url? if (regUrl == "") @@ -1013,7 +1051,7 @@ private static async Task FromRegOfRegGetAasAndSubmodels( // build again a set of baseUris, but only one pattern set var baseUris = new BaseUriDict(key: "AAS-REG", value: basicUri.ToString()); - + // translate to a list of AAS-Ids .. var uriGetListOfAids = BuildUriForRegistryAasByAssetId(baseUris.GetBaseUriForAasReg(), assetId); if (compatOldAasxServer) @@ -1221,6 +1259,12 @@ protected static async Task LoadFromSourceInternalAsyn } else { + bool isFXLeoDiscoveryServer = fullItemLocation.Contains("leo-discovery"); + if (isFXLeoDiscoveryServer) + { + resObj = resObj["data"]; + } + foreach (var res in resObj) { // refer to dedicated function @@ -1230,7 +1274,8 @@ await FromRegOfRegGetAasAndSubmodels( trackNewIdentifiables, trackLoadedIdentifiables, lambdaReportProgress: lambdaReportAasSm, // TODO: check!! - compatOldAasxServer: true); + compatOldAasxServer: isFXLeoDiscoveryServer ? false : true, + isFXLeoDiscoveryServer : isFXLeoDiscoveryServer); } } } From 52559388c0b8c534b8ba6cde27aced559ce38159 Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Thu, 2 Apr 2026 16:42:45 +0200 Subject: [PATCH 12/15] Use well-known to get jwks and token endpoint --- .../SecurityAccessHandlerLogicBase.cs | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs index cc7f4536a..4fbb9ebe4 100644 --- a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs +++ b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs @@ -426,9 +426,9 @@ protected virtual async Task AskForAuthServerUri(string baseAddress, str private async Task DoTokenExchange(string[] tokenExchangeParams, string secretAccessToken) { var handler = new HttpClientHandler { DefaultProxyCredentials = CredentialCache.DefaultCredentials }; - var client = new HttpClient(handler); + using var client = new HttpClient(handler); - var exchangendToken = String.Empty; + var exchangedToken = String.Empty; var configUrl = tokenExchangeParams[0]; var target = tokenExchangeParams[1]; @@ -445,7 +445,25 @@ private async Task DoTokenExchange(string[] tokenExchangeParams, string { d.Add("audience", target); } - var request = new HttpRequestMessage(HttpMethod.Post, $"{configUrl}/token") + + //Get Well-Known + var openIdConfig = await client.GetStringAsync($"{configUrl}/.well-known/openid-configuration"); + var openIdConfigJson = JsonDocument.Parse(openIdConfig); + + string jwksUri = $"{configUrl}/jwks"; + if (openIdConfigJson.RootElement.TryGetProperty("jwks_uri", out var propJwksUri)) + { + jwksUri = propJwksUri.GetString(); + } + + string tokenEndpoint = $"{configUrl}/token"; + if (openIdConfigJson.RootElement.TryGetProperty("token_endpoint", out var propTokenUrl)) + { + tokenEndpoint = propTokenUrl.GetString(); + } + + + var request = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint) { Content = new FormUrlEncodedContent(d) }; @@ -461,8 +479,7 @@ private async Task DoTokenExchange(string[] tokenExchangeParams, string accessToken = tokenElement.GetString(); Console.WriteLine("Access Token: " + accessToken); - using var httpClient = new HttpClient(handler); - var jwksJson = await httpClient.GetStringAsync($"{configUrl}/jwks"); + var jwksJson = await client.GetStringAsync(jwksUri); var jwks = JObject.Parse(jwksJson)["keys"]; // 1) Handler @@ -509,7 +526,7 @@ private async Task DoTokenExchange(string[] tokenExchangeParams, string else { Console.WriteLine("Token is valid"); - exchangendToken = accessToken; + exchangedToken = accessToken; } } catch (Exception ex) @@ -519,7 +536,7 @@ private async Task DoTokenExchange(string[] tokenExchangeParams, string } } - return exchangendToken; + return exchangedToken; } protected virtual async Task> AskForTokenExchange() From 51a9602443eef1cda028a5e5f5956819366ad185 Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Thu, 2 Apr 2026 19:35:06 +0200 Subject: [PATCH 13/15] Try catch Well-Known Request for token server --- .../SecurityAccessHandlerLogicBase.cs | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs index 4fbb9ebe4..0b1c0a27e 100644 --- a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs +++ b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs @@ -446,23 +446,30 @@ private async Task DoTokenExchange(string[] tokenExchangeParams, string d.Add("audience", target); } - //Get Well-Known - var openIdConfig = await client.GetStringAsync($"{configUrl}/.well-known/openid-configuration"); - var openIdConfigJson = JsonDocument.Parse(openIdConfig); - string jwksUri = $"{configUrl}/jwks"; - if (openIdConfigJson.RootElement.TryGetProperty("jwks_uri", out var propJwksUri)) + string tokenEndpoint = $"{configUrl}/token"; + + //Get Well-Known + try { - jwksUri = propJwksUri.GetString(); - } + var openIdConfig = await client.GetStringAsync($"{configUrl}/.well-known/openid-configuration"); + var openIdConfigJson = JsonDocument.Parse(openIdConfig); - string tokenEndpoint = $"{configUrl}/token"; - if (openIdConfigJson.RootElement.TryGetProperty("token_endpoint", out var propTokenUrl)) + if (openIdConfigJson.RootElement.TryGetProperty("jwks_uri", out var propJwksUri)) + { + jwksUri = propJwksUri.GetString(); + } + + if (openIdConfigJson.RootElement.TryGetProperty("token_endpoint", out var propTokenUrl)) + { + tokenEndpoint = propTokenUrl.GetString(); + } + } + catch (Exception ex) { - tokenEndpoint = propTokenUrl.GetString(); + Log.Singleton.Info($"Could not access well-known from {configUrl}"); } - var request = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint) { Content = new FormUrlEncodedContent(d) From 2d00f5208ec1f2f05835ec98823b9d5cc4d8e864 Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Thu, 23 Apr 2026 16:22:02 +0200 Subject: [PATCH 14/15] Reg-of-Reg: Allow to load from multiple URLs for one asset id --- .../PackageContainerHttpRepoSubset.cs | 95 +++++++++++-------- 1 file changed, 56 insertions(+), 39 deletions(-) diff --git a/src/AasxPackageLogic/PackageCentral/PackageContainerHttpRepoSubset.cs b/src/AasxPackageLogic/PackageCentral/PackageContainerHttpRepoSubset.cs index 8aa74dafe..bf6cab974 100644 --- a/src/AasxPackageLogic/PackageCentral/PackageContainerHttpRepoSubset.cs +++ b/src/AasxPackageLogic/PackageCentral/PackageContainerHttpRepoSubset.cs @@ -9,6 +9,7 @@ This source code may use other Open Source software components (see LICENSE.txt) using AasxIntegrationBase; using AdminShellNS; +using AdminShellNS.Extensions; using AngleSharp.Dom; using AnyUi; using Extensions; @@ -1016,7 +1017,7 @@ private static async Task FromRegOfRegGetAasAndSubmodels( // } // However, only Url and Id are currently useful - string regUrl = String.Empty; + List regUrls = new List(); if (isFXLeoDiscoveryServer) { @@ -1029,67 +1030,83 @@ private static async Task FromRegOfRegGetAasAndSubmodels( if (endpoint["interface"] == "AAS-REGISTRY-3.1" || endpoint["interface"] == "AAS-REGISTRY-3.0") { - regUrl = endpoint.protocolInformation.href; - break; + var regUrl = endpoint.protocolInformation["href"]; + regUrls.Add("" + regUrl); } } } } else { - regUrl = "" + regDescriptor["url"]; + regUrls.Add("" + regDescriptor["url"]); } // valid url? - if (regUrl == "") + if (regUrls.IsNullOrEmpty()) return false; - var basicUri = GetBaseUri(regUrl); - if (basicUri == null) - return false; + List basicUris = new List(); + + foreach (var regUrl in regUrls) + { + var basicUri = GetBaseUri(regUrl); + if (basicUri != null) + { + basicUris.Add(basicUri); + } + } - // build again a set of baseUris, but only one pattern set - var baseUris = new BaseUriDict(key: "AAS-REG", value: basicUri.ToString()); - - // translate to a list of AAS-Ids .. - var uriGetListOfAids = BuildUriForRegistryAasByAssetId(baseUris.GetBaseUriForAasReg(), assetId); - if (compatOldAasxServer) - uriGetListOfAids = BuildUriForRegistryAasByAssetLinkDeprecated(baseUris.GetBaseUriForAasReg(), assetId); - var listOfAids = await PackageHttpDownloadUtil.DownloadEntityToDynamicObject( - uriGetListOfAids, runtimeOptions, allowFakeResponses); - - if (listOfAids == null || !(listOfAids is JArray) || (listOfAids as JArray).Count < 1) + if(basicUris.IsNullOrEmpty()) { - runtimeOptions?.Log?.Info("Registry {0} did not translate glopbalAssetId={1} to any AAS Ids. " + - "Aborting! URi was: {2}", - basicUri, assetId, uriGetListOfAids); return false; } - // take the individual AAS ids - foreach (var aid in listOfAids) + foreach (var basicUri in basicUris) { - // prepare receiving the descriptor - var uriGetAasDescr = BuildUriForRegistrySingleAAS(baseUris.GetBaseUriForAasReg(), aid.ToString()); - var resAasDescr = await PackageHttpDownloadUtil.DownloadEntityToDynamicObject( - uriGetAasDescr, runtimeOptions, allowFakeResponses); + // build again a set of baseUris, but only one pattern set + var baseUris = new BaseUriDict(key: "AAS-REG", value: basicUri.ToString()); + + // translate to a list of AAS-Ids .. + var uriGetListOfAids = BuildUriForRegistryAasByAssetId(baseUris.GetBaseUriForAasReg(), assetId); + if (compatOldAasxServer) + uriGetListOfAids = BuildUriForRegistryAasByAssetLinkDeprecated(baseUris.GetBaseUriForAasReg(), assetId); + var listOfAids = await PackageHttpDownloadUtil.DownloadEntityToDynamicObject( + uriGetListOfAids, runtimeOptions, allowFakeResponses); - // have directly a single descriptor?! - if (!(resAasDescr is JObject)) + if (listOfAids == null || !(listOfAids is JArray) || (listOfAids as JArray).Count < 1) { - runtimeOptions?.Log?.Info("Registry did not return a single AAS descriptor! Aborting. URI was: {0}", - uriGetAasDescr); - return false; + runtimeOptions?.Log?.Info("Registry {0} did not translate glopbalAssetId={1} to any AAS Ids. " + + "Aborting! URi was: {2}", + basicUri, assetId, uriGetListOfAids); } + else + { + // take the individual AAS ids + foreach (var aid in listOfAids) + { + // prepare receiving the descriptor + var uriGetAasDescr = BuildUriForRegistrySingleAAS(baseUris.GetBaseUriForAasReg(), aid.ToString()); + var resAasDescr = await PackageHttpDownloadUtil.DownloadEntityToDynamicObject( + uriGetAasDescr, runtimeOptions, allowFakeResponses); - lambdaReportProgress?.Invoke(0, 0, 0, 1); + // have directly a single descriptor?! + if (!(resAasDescr is JObject)) + { + runtimeOptions?.Log?.Info("Registry did not return a single AAS descriptor! Aborting. URI was: {0}", + uriGetAasDescr); + return false; + } - // refer to dedicated function - await FromRegistryGetAasAndSubmodels( - prepAas, prepSM, record, runtimeOptions, allowFakeResponses, resAasDescr, - trackNewIdentifiables, trackLoadedIdentifiables, - lambdaReportProgress: lambdaReportProgress); + lambdaReportProgress?.Invoke(0, 0, 0, 1); + + // refer to dedicated function + await FromRegistryGetAasAndSubmodels( + prepAas, prepSM, record, runtimeOptions, allowFakeResponses, resAasDescr, + trackNewIdentifiables, trackLoadedIdentifiables, + lambdaReportProgress: lambdaReportProgress); + } + } } // OK? From 2b70bddb4bacf60254379843b721f3ec31330745 Mon Sep 17 00:00:00 2001 From: Wadim Hamm Date: Mon, 4 May 2026 11:06:47 +0200 Subject: [PATCH 15/15] Download of aas und submodels from registries in reg of reg: Ignore not working --- .../PackageContainerHttpRepoSubset.cs | 120 +++++++++++------- 1 file changed, 71 insertions(+), 49 deletions(-) diff --git a/src/AasxPackageLogic/PackageCentral/PackageContainerHttpRepoSubset.cs b/src/AasxPackageLogic/PackageCentral/PackageContainerHttpRepoSubset.cs index bf6cab974..2ca52d82a 100644 --- a/src/AasxPackageLogic/PackageCentral/PackageContainerHttpRepoSubset.cs +++ b/src/AasxPackageLogic/PackageCentral/PackageContainerHttpRepoSubset.cs @@ -987,7 +987,7 @@ private static async Task FromRegistryGetAasAndSubmodels( return true; } - private static async Task FromRegOfRegGetAasAndSubmodels( + private static async Task FromRegOfRegGetAasAndSubmodels( OnDemandListIdentifiable prepAas, OnDemandListIdentifiable prepSM, ConnectExtendedRecord record, @@ -999,28 +999,20 @@ private static async Task FromRegOfRegGetAasAndSubmodels( List trackLoadedIdentifiables = null, Action lambdaReportProgress = null, bool compatOldAasxServer = false, - bool isFXLeoDiscoveryServer = false) + bool isLeoDiscoveryServer = false) { // access if (record == null || regDescriptor == null || assetId?.HasContent() != true) - return false; + return; + - // The format is: - // { - // "Url": "http://example.com/6789", - // "Security": "", - // "Match": "LIKE", - // "Pattern": "%6789%", - // "Domain": "example.com", - // "Id": "xxx", - // "Info": "xxx" - // } - // However, only Url and Id are currently useful List regUrls = new List(); - if (isFXLeoDiscoveryServer) + if (isLeoDiscoveryServer) { + //See: https://leo-discovery.admin-shell-io.com/ + var endpoints = regDescriptor?.endpoints; if (endpoints != null) @@ -1037,14 +1029,26 @@ private static async Task FromRegOfRegGetAasAndSubmodels( } } else - { + { + // The format is: + // { + // "Url": "http://example.com/6789", + // "Security": "", + // "Match": "LIKE", + // "Pattern": "%6789%", + // "Domain": "example.com", + // "Id": "xxx", + // "Info": "xxx" + // } + // However, only Url and Id are currently useful + regUrls.Add("" + regDescriptor["url"]); } // valid url? if (regUrls.IsNullOrEmpty()) - return false; + return; List basicUris = new List(); @@ -1059,7 +1063,7 @@ private static async Task FromRegOfRegGetAasAndSubmodels( if(basicUris.IsNullOrEmpty()) { - return false; + return; } foreach (var basicUri in basicUris) @@ -1071,46 +1075,64 @@ private static async Task FromRegOfRegGetAasAndSubmodels( var uriGetListOfAids = BuildUriForRegistryAasByAssetId(baseUris.GetBaseUriForAasReg(), assetId); if (compatOldAasxServer) uriGetListOfAids = BuildUriForRegistryAasByAssetLinkDeprecated(baseUris.GetBaseUriForAasReg(), assetId); - var listOfAids = await PackageHttpDownloadUtil.DownloadEntityToDynamicObject( - uriGetListOfAids, runtimeOptions, allowFakeResponses); - if (listOfAids == null || !(listOfAids is JArray) || (listOfAids as JArray).Count < 1) - { - runtimeOptions?.Log?.Info("Registry {0} did not translate glopbalAssetId={1} to any AAS Ids. " + - "Aborting! URi was: {2}", - basicUri, assetId, uriGetListOfAids); - } - else + try { - // take the individual AAS ids - foreach (var aid in listOfAids) - { - // prepare receiving the descriptor - var uriGetAasDescr = BuildUriForRegistrySingleAAS(baseUris.GetBaseUriForAasReg(), aid.ToString()); - var resAasDescr = await PackageHttpDownloadUtil.DownloadEntityToDynamicObject( - uriGetAasDescr, runtimeOptions, allowFakeResponses); + var listOfAids = await PackageHttpDownloadUtil.DownloadEntityToDynamicObject( + uriGetListOfAids, runtimeOptions, allowFakeResponses); - // have directly a single descriptor?! - if (!(resAasDescr is JObject)) + if (listOfAids == null || !(listOfAids is JArray) || (listOfAids as JArray).Count < 1) + { + runtimeOptions?.Log?.Info("Registry {0} did not translate globalAssetId={1} to any AAS Ids. " + + "Aborting! URi was: {2}", + basicUri, assetId, uriGetListOfAids); + } + else + { + // take the individual AAS ids + foreach (var aid in listOfAids) { - runtimeOptions?.Log?.Info("Registry did not return a single AAS descriptor! Aborting. URI was: {0}", - uriGetAasDescr); - return false; - } + // prepare receiving the descriptor + var uriGetAasDescr = BuildUriForRegistrySingleAAS(baseUris.GetBaseUriForAasReg(), aid.ToString()); + var resAasDescr = await PackageHttpDownloadUtil.DownloadEntityToDynamicObject( + uriGetAasDescr, runtimeOptions, allowFakeResponses); - lambdaReportProgress?.Invoke(0, 0, 0, 1); + // have directly a single descriptor?! + if (!(resAasDescr is JObject)) + { + runtimeOptions?.Log?.Info("Registry did not return a single AAS descriptor! Aborting. URI was: {0}", + uriGetAasDescr); + } - // refer to dedicated function - await FromRegistryGetAasAndSubmodels( - prepAas, prepSM, record, runtimeOptions, allowFakeResponses, resAasDescr, - trackNewIdentifiables, trackLoadedIdentifiables, - lambdaReportProgress: lambdaReportProgress); + lambdaReportProgress?.Invoke(0, 0, 0, 1); + + try + { + // refer to dedicated function + await FromRegistryGetAasAndSubmodels( + prepAas, prepSM, record, runtimeOptions, allowFakeResponses, resAasDescr, + trackNewIdentifiables, trackLoadedIdentifiables, + lambdaReportProgress: lambdaReportProgress); + } + catch (Exception e) + { + runtimeOptions?.Log?.Info("Download aas and submodels from registry {0} failed globalAssetId={1} to any AAS Ids. " + + "Aborting! AAS id was: {2}, message was {3}", + basicUri, assetId, aid, e.Message); + } + + } } } + catch (Exception e) + { + runtimeOptions?.Log?.Info("Download descriptors from registry {0} failed globalAssetId={1} to any AAS Ids. " + + "Aborting! URi was: {2}, message was {3}", + basicUri, assetId, uriGetListOfAids, e.Message); + } } - // OK? - return true; + return; } public override async Task LoadFromSourceAsync( @@ -1292,7 +1314,7 @@ await FromRegOfRegGetAasAndSubmodels( lambdaReportProgress: lambdaReportAasSm, // TODO: check!! compatOldAasxServer: isFXLeoDiscoveryServer ? false : true, - isFXLeoDiscoveryServer : isFXLeoDiscoveryServer); + isLeoDiscoveryServer : isFXLeoDiscoveryServer); } } }