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/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/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/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. diff --git a/src/AasxPackageLogic/PackageCentral/PackageContainerHttpRepoSubset.cs b/src/AasxPackageLogic/PackageCentral/PackageContainerHttpRepoSubset.cs index 0cc18260e..2ca52d82a 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; @@ -303,14 +304,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 +354,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 +367,10 @@ public static Uri GetBaseUri(string location) { return new Uri(location.Substring(0, p) + "/"); } + else + { + return new Uri(location); + } } // go to error @@ -732,10 +737,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 +831,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 +858,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; @@ -969,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, @@ -980,82 +998,141 @@ private static async Task FromRegOfRegGetAasAndSubmodels( List trackNewIdentifiables = null, List trackLoadedIdentifiables = null, Action lambdaReportProgress = null, - bool compatOldAasxServer = false) + bool compatOldAasxServer = 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 - - string regUrl = "" + regDescriptor["url"]; - string regInfo = "" + regDescriptor["info"]; - if (regInfo == "") - regInfo = ""; - // valid url? - if (regUrl == "") - return false; - var basicUri = GetBaseUri(regUrl); - if (basicUri == null) - return false; + List regUrls = new List(); - // build again a set of baseUris, but only one pattern set - var baseUris = new BaseUriDict(key: "AAS-REG", value: basicUri.ToString()); + if (isLeoDiscoveryServer) + { + //See: https://leo-discovery.admin-shell-io.com/ - // 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); + var endpoints = regDescriptor?.endpoints; - if (listOfAids == null || !(listOfAids is JArray) || (listOfAids as JArray).Count < 1) + if (endpoints != null) + { + foreach (var endpoint in endpoints) + { + if (endpoint["interface"] == "AAS-REGISTRY-3.1" + || endpoint["interface"] == "AAS-REGISTRY-3.0") + { + var regUrl = endpoint.protocolInformation["href"]; + regUrls.Add("" + regUrl); + } + } + } + } + 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; + + List basicUris = new List(); + + foreach (var regUrl in regUrls) { - runtimeOptions?.Log?.Info("Registry {0} did not translate glopbalAssetId={1} to any AAS Ids. " + - "Aborting! URi was: {2}", - basicUri, assetId, uriGetListOfAids); - return false; + var basicUri = GetBaseUri(regUrl); + if (basicUri != null) + { + basicUris.Add(basicUri); + } + } + + if(basicUris.IsNullOrEmpty()) + { + return; } - // 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()); - // have directly a single descriptor?! - if (!(resAasDescr is JObject)) + // translate to a list of AAS-Ids .. + var uriGetListOfAids = BuildUriForRegistryAasByAssetId(baseUris.GetBaseUriForAasReg(), assetId); + if (compatOldAasxServer) + uriGetListOfAids = BuildUriForRegistryAasByAssetLinkDeprecated(baseUris.GetBaseUriForAasReg(), assetId); + + try { - runtimeOptions?.Log?.Info("Registry did not return a single AAS descriptor! Aborting. URI was: {0}", - uriGetAasDescr); - return false; - } + var listOfAids = await PackageHttpDownloadUtil.DownloadEntityToDynamicObject( + uriGetListOfAids, runtimeOptions, allowFakeResponses); - lambdaReportProgress?.Invoke(0, 0, 0, 1); + 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) + { + // prepare receiving the descriptor + var uriGetAasDescr = BuildUriForRegistrySingleAAS(baseUris.GetBaseUriForAasReg(), aid.ToString()); + var resAasDescr = await PackageHttpDownloadUtil.DownloadEntityToDynamicObject( + uriGetAasDescr, runtimeOptions, allowFakeResponses); + + // 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( @@ -1221,6 +1298,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 +1313,8 @@ await FromRegOfRegGetAasAndSubmodels( trackNewIdentifiables, trackLoadedIdentifiables, lambdaReportProgress: lambdaReportAasSm, // TODO: check!! - compatOldAasxServer: true); + compatOldAasxServer: isFXLeoDiscoveryServer ? false : true, + isLeoDiscoveryServer : isFXLeoDiscoveryServer); } } } 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 da5ad1887..0b1c0a27e 100644 --- a/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs +++ b/src/AasxPackageLogic/PackageCentral/SecurityAccessHandlerLogicBase.cs @@ -10,34 +10,27 @@ 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; 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.X509Certificates; -using System.Security.RightsManagement; using System.Text; -using System.Text.RegularExpressions; -using System.Threading; +using System.Text.Json; using System.Threading.Tasks; -using System.Windows; -using Aas = AasCore.Aas3_1; +using AasxIntegrationBase; +using AasxPackageLogic; +using AasxPackageLogic.PackageCentral; +using AdminShellNS; +using AnyUi; +using Microsoft.Identity.Client; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json.Linq; namespace AasxPackageExplorer { @@ -430,6 +423,264 @@ protected virtual async Task AskForAuthServerUri(string baseAddress, str return null; } + private async Task DoTokenExchange(string[] tokenExchangeParams, string secretAccessToken) + { + var handler = new HttpClientHandler { DefaultProxyCredentials = CredentialCache.DefaultCredentials }; + using var client = new HttpClient(handler); + + var exchangedToken = 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); + } + + string jwksUri = $"{configUrl}/jwks"; + string tokenEndpoint = $"{configUrl}/token"; + + //Get Well-Known + try + { + var openIdConfig = await client.GetStringAsync($"{configUrl}/.well-known/openid-configuration"); + var openIdConfigJson = JsonDocument.Parse(openIdConfig); + + 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) + { + Log.Singleton.Info($"Could not access well-known from {configUrl}"); + } + + var request = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint) + { + 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); + + var jwksJson = await client.GetStringAsync(jwksUri); + 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"); + exchangedToken = accessToken; + } + } + catch (Exception ex) + { + Console.WriteLine($"Validation failed: {ex.Message}"); + return null; + } + } + + return exchangedToken; + } + + 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, + 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++; + + // 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.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++; + + // Content URL + + helper.AddSmallLabelTo(g, row, 0, content: "Content URL:", + verticalAlignment: AnyUiVerticalAlignment.Center, + verticalContentAlignment: AnyUiVerticalAlignment.Center); + + AnyUiUIElement.SetStringFromControl( + helper.Set( + helper.AddSmallTextBoxTo(g, row, 1, + text: $"{tokenExchange2Params[0]}", + verticalAlignment: AnyUiVerticalAlignment.Center, + verticalContentAlignment: AnyUiVerticalAlignment.Center), + horizontalAlignment: AnyUiHorizontalAlignment.Stretch), + (s) => { tokenExchange2Params[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: $"{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 new Tuple(needsTokenExchange1, tokenExchange1Params, needsTokenExchange2, tokenExchange2Params); + return new Tuple(needsTokenExchange1, tokenExchange1Params, needsTokenExchange2, tokenExchange2Params); ; + } + protected virtual async Task AskForSelectFromCertCollection( string baseAddress, X509Certificate2Collection fcollection) @@ -692,6 +943,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 && tokenExchanges.Item1) + { + secretAccessToken = await DoTokenExchange(tokenExchanges.Item2, secretAccessToken); + + if (tokenExchanges.Item3) + { + 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()}"); @@ -878,7 +1143,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 +1153,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 +1274,18 @@ protected virtual async Task DetermineAuthenticateHeaderForE var accessToken = contentJson["access_token"]; Log.Singleton.Info($"Security access handler: Found 'access_token': {accessToken}"); + var accessTokenString = accessToken.ToString(); + + //ToDo: Need token exchange here? + //var tokenExchanges = await AskForTokenExchange(); + + //if (tokenExchanges != null && !String.IsNullOrEmpty(tokenExchanges[0])) + //{ + //} + // 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; } }