diff --git a/FoxIDs.sln b/FoxIDs.sln index 4c97cdf82..4c88c1758 100644 --- a/FoxIDs.sln +++ b/FoxIDs.sln @@ -73,7 +73,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{CB5D86A0-D docs\get-started.md = docs\get-started.md docs\gs-context-handler.md = docs\gs-context-handler.md docs\gs-nemlogin.md = docs\gs-nemlogin.md - docs\health.md = docs\health.md docs\howto-connect.md = docs\howto-connect.md docs\howto-environmentlink-foxids.md = docs\howto-environmentlink-foxids.md docs\howto-oidc-foxids.md = docs\howto-oidc-foxids.md @@ -82,6 +81,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{CB5D86A0-D docs\language.md = docs\language.md docs\logging.md = docs\logging.md docs\login.md = docs\login.md + docs\monitoring.md = docs\monitoring.md docs\name-title-icon-css.md = docs\name-title-icon-css.md docs\oauth-2.0.md = docs\oauth-2.0.md docs\oidc.md = docs\oidc.md @@ -174,8 +174,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{CB8812 docs\images\how-to-context-handler.svg = docs\images\how-to-context-handler.svg docs\images\how-to-environment-link.svg = docs\images\how-to-environment-link.svg docs\images\how-to.vsdx = docs\images\how-to.vsdx - docs\images\howto-environmentlink-foxids-app-reg.png = docs\images\howto-environmentlink-foxids-app-reg.png - docs\images\howto-environmentlink-foxids-auth-method.png = docs\images\howto-environmentlink-foxids-auth-method.png + docs\images\howto-environmentlink-foxids-auth-method-select.png = docs\images\howto-environmentlink-foxids-auth-method-select.png + docs\images\howto-environmentlink-foxids-auth-method-y-select.png = docs\images\howto-environmentlink-foxids-auth-method-y-select.png docs\images\howto-oidc-azuread-readredirect.png = docs\images\howto-oidc-azuread-readredirect.png docs\images\howto-oidc-facebook-app-details.png = docs\images\howto-oidc-facebook-app-details.png docs\images\howto-oidc-facebook-config.png = docs\images\howto-oidc-facebook-config.png diff --git a/docs/app-reg-oauth-2.0.md b/docs/app-reg-oauth-2.0.md index a2bdd3291..093ae0e2f 100644 --- a/docs/app-reg-oauth-2.0.md +++ b/docs/app-reg-oauth-2.0.md @@ -7,7 +7,7 @@ FoxIDs OAuth 2.0 application registration enable you to connect an APIs as [OAut ## OAuth 2.0 Resource An API is configured as a OAuth 2.0 application registration resource. -- Click New registration and then OAuth 2.0 - Resource (API) +- Click New application and then OAuth 2.0 - Resource (API) - Specify resource (API) name in application registration name. - Specify one or more scopes. @@ -18,7 +18,7 @@ A client can subsequently be given access by configuring [resource and scopes](a ## Client Credentials Grant An application using Client Credentials Grant could be a backend service secured by a client id and secret or key. -- Click New registration and then OAuth 2.0 - Client Credentials Grant +- Click New application and then OAuth 2.0 - Client Credentials Grant - Specify client name in application registration name. - Specify client authentication method, default `client secret post` - A secret is default generated diff --git a/docs/auth-method-howto-oidc-facebook.md b/docs/auth-method-howto-oidc-facebook.md index 134f07649..a8775262a 100644 --- a/docs/auth-method-howto-oidc-facebook.md +++ b/docs/auth-method-howto-oidc-facebook.md @@ -13,7 +13,7 @@ This chapter describes how to configure a connection with OpenID Connect Authori **1 - Start by creating an OpenID Connect authentication method in [FoxIDs Control Client](control.md#foxids-control-client)** 1. Navigate to the **Authentication Methods** tab - 2. Click **New method** + 2. Click **New authentication** 3. Select **OpenID Provider** 4. Add the **Name** e.g. Facebook 5. Add the Facebook **Authority**, you can either select to use Facebook login with the `https://www.facebook.com/` authority or Facebook Limited login with the `https://limited.facebook.com/` authority diff --git a/docs/auth-method-howto-oidc-google.md b/docs/auth-method-howto-oidc-google.md index 1a0cca1be..be202f85b 100644 --- a/docs/auth-method-howto-oidc-google.md +++ b/docs/auth-method-howto-oidc-google.md @@ -13,7 +13,7 @@ This chapter describes how to configure a connection with OpenID Connect Authori **1 - Start by creating an OpenID Connect authentication method in [FoxIDs Control Client](control.md#foxids-control-client)** 1. Navigate to the **Authentication Methods** tab - 2. Click **New method** + 2. Click **New authentication** 3. Select **OpenID Provider** 4. Add the **Name** e.g. Google 5. Add the Google authority `https://accounts.google.com/` in **Authority** diff --git a/docs/auth-method-howto-saml-2.0-nemlogin.md b/docs/auth-method-howto-saml-2.0-nemlogin.md index 627419de7..181f44f36 100644 --- a/docs/auth-method-howto-saml-2.0-nemlogin.md +++ b/docs/auth-method-howto-saml-2.0-nemlogin.md @@ -63,7 +63,7 @@ It is subsequently possible to add a secondary certificate and to swap between t **1) - Start by creating an SAML 2.0 authentication method in [FoxIDs Control Client](control.md#foxids-control-client)** 1. Select the Authentication methods tab -2. Click Create authentication method and then SAML 2.0 +2. Click New authentication and then SAML 2.0 3. Add the name 4. Select show advanced settings 5. Select the dot URL binding pattern diff --git a/docs/get-started.md b/docs/get-started.md index cec595a61..73a75fc44 100644 --- a/docs/get-started.md +++ b/docs/get-started.md @@ -23,7 +23,7 @@ You can select another environment, create a new environment or start building i Let's configure the first OpenID Connect application and log in with a test user. You can optionally start by taking a look at the [sample applications](samples.md) which also can be [configured](samples.md#configure-samples-in-foxids-environment) in the you test environment. -Click `New registration` to configure your OpenID Connect application and select the type of application. +Click `New application` to configure your OpenID Connect application and select the type of application. ![New app registration](images/get-started-new-app-reg.png) diff --git a/docs/howto-environmentlink-foxids.md b/docs/howto-environmentlink-foxids.md index 982d72a13..2bc7c4f84 100644 --- a/docs/howto-environmentlink-foxids.md +++ b/docs/howto-environmentlink-foxids.md @@ -1,6 +1,6 @@ # Connect two environments with Environment Link -FoxIDs environments in the same tenant can be connected with environment links. A Environment Link acts mostly like OpenID Connect but it is simpler to configure and the steps it goes through is faster. +FoxIDs environments in the same tenant can be connected with environment links. An Environment Link acts mostly like OpenID Connect but it is simpler to configure and the steps it goes through is faster. ![Environment Link](images/how-to-environment-link.svg) @@ -9,36 +9,27 @@ Environment links is fast and secure but can only be used in the same tenant. A > Take a look at the sample environment links configuration in FoxIDs Control: [https://control.foxids.com/test-corp](https://control.foxids.com/test-corp) > Get read access with the user `reader@foxids.com` and password `TestAccess!` then e.g., take a look at the `nemlogin` and `Production` environments. -Environment links support login, RP-initiated logout and front-channel logout. Furthermore, it is possible to configure [claim and claim transforms](claim.md), logout session and home realm discovery (HRD) like all other connecting authentication methods and application registrations. +Environment links support login, logout and single logout and it is possible to configure [claim and claim transforms](claim.md), logout session and home realm discovery (HRD) like all other connecting authentication methods and application registrations. ## Configure integration -The following describes how to connect two environments called `track_x` and `track_y` where `track_y` become an authentication method on `track_x`. +The following describes how to connect two environments called `Environment X` and `Environment Y`. The environment `Environment X` will be enabled to login with `Environment Y` as an authentication method. -**1 - Start in the `track_x` environment by creating a Environment Link in [FoxIDs Control Client](control.md#foxids-control-client)** +**Select in the `Environment X` environment in [FoxIDs Control Client](control.md#foxids-control-client)** -1. Select the Authentication methods tab -2. Click Create authentication method and then Environment Link -3. Add the name e.g., `track_y-connection` -4. Add the `track_y` environment name -5. Add the application registration name in the `track_y` environment e.g., `track_x-connection` -6. Click Create - -![Create Environment Link authentication method](images/howto-environmentlink-foxids-auth-method.png) - -**2 - Then go to the `track_y` environment and create a Environment Link in [FoxIDs Control Client](control.md#foxids-control-client)** +1. Select the **Authentication Methods** tab +2. Click **New authentication** +3. Select **Show advanced** +4. Select **Environment Link** + ![Select Environment Link authentication method](images/howto-environmentlink-foxids-auth-method-select.png) -1. Select the Applications tab -2. Click Create application registration and then Environment Link -3. Add the name e.g., `track_x-connection` -4. Add the `track_x` environment name -5. Add the authentication method name in the `track_x` environment e.g., `track_y-connection` -6. Select which authentication methods in the `track_y` environment the user is allowed to use for authentication +5. Add the name e.g., `Environment X to Y` +4. Select the `Environment Y` environment + ![Select Environment Link authentication method](images/howto-environmentlink-foxids-auth-method-y-select.png) 6. Click Create -![Create Environment Link application registration](images/howto-environmentlink-foxids-app-reg.png) - That's it, you are done. -> Your new authentication method `track_y-connection` can now be selected as an allowed authentication method in the application registrations in you `track_x` environment. -> The application registrations in you `track_x` environment can read the claims from your `track_y-connection` authentication method. \ No newline at end of file +Your new authentication method `Environment X to Y` can now be selected as an allowed authentication method in the application registrations in you `Environment X` environment. + +You can find the application registration `Environment X to Y` in the `Environment Y` environment where authentication method(s) can be selected. \ No newline at end of file diff --git a/docs/howto-saml-2.0-context-handler.md b/docs/howto-saml-2.0-context-handler.md index ee4ca5b5b..d9791fcb6 100644 --- a/docs/howto-saml-2.0-context-handler.md +++ b/docs/howto-saml-2.0-context-handler.md @@ -65,7 +65,7 @@ This guide describe how to setup Context Handler as a SAML 2.0 Identity Provider **1 - Start by creating an SAML 2.0 authentication method in [FoxIDs Control Client](control.md#foxids-control-client)** 1. Select the Authentication methods tab -2. Click Create authentication method and then SAML 2.0 +2. Click New authentication and then SAML 2.0 3. Add the name 4. Add the Context Handler IdP metadata in the Metadata URL field Test metadata: `https://n2adgangsstyring.eksterntest-stoettesystemerne.dk/runtime/saml2/metadata.idp` diff --git a/docs/images/howto-environmentlink-foxids-app-reg.png b/docs/images/howto-environmentlink-foxids-app-reg.png deleted file mode 100644 index 926b97504..000000000 Binary files a/docs/images/howto-environmentlink-foxids-app-reg.png and /dev/null differ diff --git a/docs/images/howto-environmentlink-foxids-auth-method-select.png b/docs/images/howto-environmentlink-foxids-auth-method-select.png new file mode 100644 index 000000000..a8d2e9391 Binary files /dev/null and b/docs/images/howto-environmentlink-foxids-auth-method-select.png differ diff --git a/docs/images/howto-environmentlink-foxids-auth-method-y-select.png b/docs/images/howto-environmentlink-foxids-auth-method-y-select.png new file mode 100644 index 000000000..5f65e3a9a Binary files /dev/null and b/docs/images/howto-environmentlink-foxids-auth-method-y-select.png differ diff --git a/docs/images/howto-environmentlink-foxids-auth-method.png b/docs/images/howto-environmentlink-foxids-auth-method.png deleted file mode 100644 index b933c17a5..000000000 Binary files a/docs/images/howto-environmentlink-foxids-auth-method.png and /dev/null differ diff --git a/src/FoxIDs.Control/Controllers/Client/WController.cs b/src/FoxIDs.Control/Controllers/Client/WController.cs index 31c2440e0..e05166df1 100644 --- a/src/FoxIDs.Control/Controllers/Client/WController.cs +++ b/src/FoxIDs.Control/Controllers/Client/WController.cs @@ -1,13 +1,14 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Hosting; -using System.Globalization; using System.Reflection; -using System; using ITfoxtec.Identity; +using Microsoft.AspNetCore.Diagnostics; +using System; +using FoxIDs.Repository; +using FoxIDs.Models; namespace FoxIDs.Controllers.Client -{ +{ public class WController : Controller { private static string indexFile; @@ -24,7 +25,43 @@ public IActionResult Index() return GetProcessedIndexFile(); } - private IActionResult GetProcessedIndexFile() + [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() + { + var exceptionHandlerPathFeature = HttpContext.Features.Get(); + return GetProcessedIndexFile(GetTechnicalError(exceptionHandlerPathFeature?.Error)); + } + + private string GetTechnicalError(Exception exception) + { + if (exception != null) + { + var dataException = FindException(exception); + if (dataException != null && dataException.StatusCode == DataStatusCode.NotFound) + { + return $"Unknown tenant{GetTenantName(dataException)}."; + } + else + { + return exception.Message; + } + } + + return "Unknown error"; + } + + private string GetTenantName(FoxIDsDataException dataException) + { + var eSplit = dataException.Message.Split(':'); + if (eSplit.Length > 1) + { + eSplit = eSplit[1].Split('\''); + return $" '{eSplit[0]}'"; + } + return string.Empty; + } + + private IActionResult GetProcessedIndexFile(string technicalError = null) { if (indexFile == null) { @@ -32,7 +69,25 @@ private IActionResult GetProcessedIndexFile() indexFile = System.IO.File.ReadAllText(file.PhysicalPath); indexFile = indexFile.Replace("{version}", GetBuildDate()); } - return Content(indexFile, "text/html"); + return Content(AddErrorInfo(indexFile, technicalError), "text/HTML"); + } + + private string AddErrorInfo(string indexFile, string technicalError) + { + if (technicalError.IsNullOrEmpty()) + { + return indexFile.Replace("{error}", string.Empty); + } + else + { + var errorInfo = new ErrorInfo + { + CreateTime = DateTimeOffset.Now.ToUnixTimeSeconds(), + RequestId = HttpContext.TraceIdentifier, + TechnicalError = technicalError + }; + return indexFile.Replace("{error}", errorInfo.ToJson()); + } } private static string GetBuildDate() @@ -52,5 +107,21 @@ private static string GetBuildDate() } return default; } + + private T FindException(Exception exception) where T : Exception + { + if (exception is T) + { + return exception as T; + } + else if (exception.InnerException != null) + { + return FindException(exception.InnerException); + } + else + { + return null; + } + } } } diff --git a/src/FoxIDs.Control/Controllers/Helpers/TDownPartyTestController.cs b/src/FoxIDs.Control/Controllers/Helpers/TDownPartyTestController.cs index 1ceba4541..e37b81f51 100644 --- a/src/FoxIDs.Control/Controllers/Helpers/TDownPartyTestController.cs +++ b/src/FoxIDs.Control/Controllers/Helpers/TDownPartyTestController.cs @@ -57,13 +57,13 @@ public TDownPartyTestController(FoxIDsControlSettings settings, TelemetryScopedL /// /// Start new down-party test. /// - /// Down-party test start request. + /// Down-party test start request. /// Down-party test. [ProducesResponseType(typeof(Api.DownPartyTestStartResponse), StatusCodes.Status200OK)] [TenantScopeAuthorize(Constants.ControlApi.Segment.Base, Constants.ControlApi.Segment.Party)] - public async Task> PostDownPartyTest([FromBody] Api.DownPartyTestStartRequest testUpPartyRequest) + public async Task> PostDownPartyTest([FromBody] Api.DownPartyTestStartRequest testDownPartyRequest) { - if (!await ModelState.TryValidateObjectAsync(testUpPartyRequest)) return BadRequest(ModelState); + if (!await ModelState.TryValidateObjectAsync(testDownPartyRequest)) return BadRequest(ModelState); await partyLogic.DeleteExporedDownParties(); @@ -77,9 +77,9 @@ public TDownPartyTestController(FoxIDsControlSettings settings, TelemetryScopedL var authenticationRequest = new AuthenticationRequest { ClientId = partyName, - ResponseMode = testUpPartyRequest.ResponseMode, + ResponseMode = testDownPartyRequest.ResponseMode, ResponseType = ResponseTypes.Code, - RedirectUri = testUpPartyRequest.RedirectUri, + RedirectUri = testDownPartyRequest.RedirectUri, Scope = new[] { DefaultOidcScopes.OpenId, DefaultOidcScopes.Profile, DefaultOidcScopes.Email, DefaultOidcScopes.Address, DefaultOidcScopes.Phone }.ToSpaceList(), Nonce = RandomGenerator.GenerateNonce(), State = $"{RouteBinding.TrackName}{Constants.Models.OidcDownPartyTest.StateSplitKey}{partyName}{Constants.Models.OidcDownPartyTest.StateSplitKey}{CreateProtector(partyName).Protect(secret)}" @@ -103,14 +103,14 @@ public TDownPartyTestController(FoxIDsControlSettings settings, TelemetryScopedL TestExpireAt = DateTimeOffset.UtcNow.AddSeconds(settings.DownPartyTestLifetime).ToUnixTimeSeconds(), Nonce = authenticationRequest.Nonce, CodeVerifier = codeVerifier, - AllowUpParties = testUpPartyRequest.UpPartyNames.Select(pName => new UpPartyLink { Name = pName }).ToList(), + AllowUpParties = testDownPartyRequest.UpPartyNames.Select(pName => new UpPartyLink { Name = pName }).ToList(), Client = new OidcDownClient { RedirectUris = new List { authenticationRequest.RedirectUri }, ResponseTypes = new List { authenticationRequest.ResponseType }, ClientAuthenticationMethod = Models.ClientAuthenticationMethods.ClientSecretPost, RequirePkce = true, - Claims = testUpPartyRequest.Claims.Select(c => new OidcDownClaim { Claim = c }).ToList(), + Claims = testDownPartyRequest.Claims.Select(c => new OidcDownClaim { Claim = c }).ToList(), ResourceScopes = new List { new OAuthDownResourceScope { Resource = partyName } }, Scopes = new List { @@ -139,7 +139,7 @@ public TDownPartyTestController(FoxIDsControlSettings settings, TelemetryScopedL await secretHashLogic.AddSecretHashAsync(oauthClientSecret, secret); mParty.Client.Secrets = [oauthClientSecret]; - if (!await validateModelGenericPartyLogic.ValidateModelAllowUpPartiesAsync(ModelState, nameof(testUpPartyRequest.UpPartyNames), mParty)) return BadRequest(ModelState); + if (!await validateModelGenericPartyLogic.ValidateModelAllowUpPartiesAsync(ModelState, nameof(testDownPartyRequest.UpPartyNames), mParty)) return BadRequest(ModelState); mParty.DisplayName = $"Test application {(mParty.AllowUpParties.Count() == 1 ? $"[{GetUpPartyDisplayName(mParty.AllowUpParties.First())}]" : $"- {mParty.Name}")}"; @@ -196,14 +196,14 @@ private string GetUpPartyDisplayName(UpPartyLink upParty) /// /// Get the down-party test result. /// - /// Down-party test result request. + /// Down-party test result request. /// Down-party test. [ProducesResponseType(typeof(Api.DownPartyTestResultResponse), StatusCodes.Status200OK)] - public async Task> PutDownPartyTest([FromBody] Api.DownPartyTestResultRequest testUpPartyRequest) + public async Task> PutDownPartyTest([FromBody] Api.DownPartyTestResultRequest testDownPartyRequest) { - if (!await ModelState.TryValidateObjectAsync(testUpPartyRequest)) return BadRequest(ModelState); + if (!await ModelState.TryValidateObjectAsync(testDownPartyRequest)) return BadRequest(ModelState); - var stateSplit = testUpPartyRequest.State.Split(Constants.Models.OidcDownPartyTest.StateSplitKey); + var stateSplit = testDownPartyRequest.State.Split(Constants.Models.OidcDownPartyTest.StateSplitKey); if (stateSplit.Length != 3) { throw new Exception("Invalid state format."); @@ -223,7 +223,7 @@ private string GetUpPartyDisplayName(UpPartyLink upParty) { var mParty = await tenantDataRepository.GetAsync(await DownParty.IdFormatAsync(RouteBinding, partyName)); - (var tokenResponse, var idTokenPrincipal, var accessTokenPrincipal) = await AcquireTokensAsync(mParty, clientSecret, mParty.Nonce, testUpPartyRequest.Code); + (var tokenResponse, var idTokenPrincipal, var accessTokenPrincipal) = await AcquireTokensAsync(mParty, clientSecret, mParty.Nonce, testDownPartyRequest.Code); var rpInitiatedLogoutRequest = new RpInitiatedLogoutRequest { @@ -267,18 +267,20 @@ private string GetUpPartyDisplayName(UpPartyLink upParty) } private string GetAuthority(string partyName, bool backendCall = false) { + var routeBinding = RouteBinding; var useBackendCall = backendCall && !settings.FoxIDsBackendEndpoint.IsNullOrWhiteSpace(); + var useValidCustomDomain = !routeBinding.TrackName.Equals(Constants.Routes.MasterTrackName, StringComparison.OrdinalIgnoreCase) && + !routeBinding.CustomDomain.IsNullOrEmpty() && routeBinding.CustomDomainVerified; - var routeBinding = RouteBinding; var urlItems = new List(); - if (useBackendCall || !(!routeBinding.CustomDomain.IsNullOrEmpty() && routeBinding.CustomDomainVerified)) + if (useBackendCall || !useValidCustomDomain) { urlItems.Add(routeBinding.TenantName); } urlItems.Add(routeBinding.TrackName); urlItems.Add($"{partyName}(*)"); - return UrlCombine.Combine(!useBackendCall ? settings.FoxIDsEndpoint : settings.FoxIDsBackendEndpoint, urlItems.ToArray()); + return UrlCombine.Combine(useBackendCall ? settings.FoxIDsBackendEndpoint : (useValidCustomDomain ? $"{HttpContext.Request.Scheme}://{routeBinding.CustomDomain}" : settings.FoxIDsEndpoint), urlItems.ToArray()); } private async Task<(TokenResponse tokenResponse, ClaimsPrincipal idTokenPrincipal, ClaimsPrincipal accessTokenPrincipal)> AcquireTokensAsync(OidcDownPartyTest mParty, string clientSecret, string nonce, string code) diff --git a/src/FoxIDs.Control/FoxIDs.Control.csproj b/src/FoxIDs.Control/FoxIDs.Control.csproj index b325bb899..f226894e3 100644 --- a/src/FoxIDs.Control/FoxIDs.Control.csproj +++ b/src/FoxIDs.Control/FoxIDs.Control.csproj @@ -2,7 +2,7 @@ net8.0 - 1.6.9 + 1.6.12 FoxIDs Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs.Control/Infrastructure/Hosting/FoxIDsClientRouteBindingMiddleware.cs b/src/FoxIDs.Control/Infrastructure/Hosting/FoxIDsClientRouteBindingMiddleware.cs index 6d0750f7c..212fd3c31 100644 --- a/src/FoxIDs.Control/Infrastructure/Hosting/FoxIDsClientRouteBindingMiddleware.cs +++ b/src/FoxIDs.Control/Infrastructure/Hosting/FoxIDsClientRouteBindingMiddleware.cs @@ -32,7 +32,13 @@ protected override ValueTask PreAsync(HttpContext httpContext, string[] ro protected override Track.IdKey GetTrackIdKey(string[] route, bool useCustomDomain) { - if (route.Length >= 1) + if (route.Length == 2 && + route[route.Length - 2].Equals(Constants.Routes.DefaultSiteController, StringComparison.InvariantCultureIgnoreCase) && + route[route.Length - 1].Equals(Constants.Routes.ErrorAction, StringComparison.InvariantCultureIgnoreCase)) + { + return null; + } + else if (route.Length >= 1) { return new Track.IdKey { @@ -44,6 +50,6 @@ protected override Track.IdKey GetTrackIdKey(string[] route, bool useCustomDomai { throw new NotSupportedException($"FoxIDs client route '{string.Join('/', route)}' not supported."); } - } + } } } diff --git a/src/FoxIDs.Control/Infrastructure/Hosting/FoxIDsClientRouteTransformer.cs b/src/FoxIDs.Control/Infrastructure/Hosting/FoxIDsClientRouteTransformer.cs new file mode 100644 index 000000000..898ab0a85 --- /dev/null +++ b/src/FoxIDs.Control/Infrastructure/Hosting/FoxIDsClientRouteTransformer.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Routing; +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace FoxIDs.Infrastructure.Hosting +{ + public class FoxIDsClientRouteTransformer : SiteRouteTransformer + { + protected override bool CheckCustomDomainSupport(string[] route) + { + throw new NotSupportedException("Host in header not supported in Control Client."); + } + + protected override string MapPath(string path) + { + return path; + } + + protected override Task HandleRouteAsync(HttpContext httpContext, bool useCustomDomain, RouteValueDictionary values, string[] route) + { + values[Constants.Routes.RouteControllerKey] = Constants.Routes.DefaultSiteController; + + if (route.Length == 2 && + route[route.Length - 2].Equals(Constants.Routes.DefaultSiteController, StringComparison.InvariantCultureIgnoreCase) && + route[route.Length - 1].Equals(Constants.Routes.ErrorAction, StringComparison.InvariantCultureIgnoreCase)) + { + values[Constants.Routes.RouteActionKey] = Constants.Routes.ErrorAction; + } + else + { + values[Constants.Routes.RouteActionKey] = Constants.Routes.DefaultAction; + } + + return Task.FromResult(values); + } + } +} diff --git a/src/FoxIDs.Control/Infrastructure/Hosting/ServiceCollectionExtensions.cs b/src/FoxIDs.Control/Infrastructure/Hosting/ServiceCollectionExtensions.cs index a8b2472d1..074a5a352 100644 --- a/src/FoxIDs.Control/Infrastructure/Hosting/ServiceCollectionExtensions.cs +++ b/src/FoxIDs.Control/Infrastructure/Hosting/ServiceCollectionExtensions.cs @@ -16,7 +16,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; -using StackExchange.Redis; using System; using System.IdentityModel.Tokens.Jwt; using System.IO; @@ -94,6 +93,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi services.AddSharedInfrastructure(settings, environment); services.AddScoped(); + services.AddScoped(); services.AddSingleton(); services.AddHostedService(); diff --git a/src/FoxIDs.Control/Startup.cs b/src/FoxIDs.Control/Startup.cs index 512fbe825..aee63e05c 100644 --- a/src/FoxIDs.Control/Startup.cs +++ b/src/FoxIDs.Control/Startup.cs @@ -48,11 +48,7 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app, Settings settings) { - if (CurrentEnvironment.IsDevelopment()) - { - app.UseWebAssemblyDebugging(); - } - else + if (!CurrentEnvironment.IsDevelopment()) { app.UseHsts(); } @@ -89,6 +85,7 @@ public void Configure(IApplicationBuilder app, Settings settings) }); }); + app.UseExceptionHandler($"/{Constants.Routes.DefaultSiteController}/{Constants.Routes.ErrorAction}"); if (CurrentEnvironment.IsDevelopment()) { app.UseWebAssemblyDebugging(); @@ -101,7 +98,7 @@ public void Configure(IApplicationBuilder app, Settings settings) app.UseRouting(); app.UseEndpoints(endpoints => { - endpoints.MapFallbackToController(Constants.Routes.DefaultAction, Constants.Routes.DefaultSiteController); + endpoints.MapDynamicControllerRoute($"{{**{Constants.Routes.RouteTransformerPathKey}}}"); }); } } diff --git a/src/FoxIDs.ControlClient/App.razor b/src/FoxIDs.ControlClient/App.razor index 4cfd47021..fc756a81c 100644 --- a/src/FoxIDs.ControlClient/App.razor +++ b/src/FoxIDs.ControlClient/App.razor @@ -1,23 +1,47 @@ +@inject ServerErrorLogic serverErrorLogic +@inject NavigationManager NavigationManager + - - - @if (!context.User.Identity.IsAuthenticated) - { - - } - else - { -

You are not authorized to access this resource.

- } -
-
+ @if (!hasError) + { + + + @if (!context.User.Identity.IsAuthenticated) + { + + } + else + { +

You are not authorized to access this resource.

+ } +
+
+ } + else + { + + + }
- +

Sorry, there's nothing at this address.

+ +@code { + private bool hasError; + + protected override async Task OnInitializedAsync() + { + hasError = await serverErrorLogic.HasErrorAsync(); + @if (hasError) + { + NavigationManager.NavigateTo("-/error"); + } + } +} \ No newline at end of file diff --git a/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj b/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj index 2d8ea59b7..3ec7b6e19 100644 --- a/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj +++ b/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj @@ -2,7 +2,7 @@ net8.0 - 1.6.9 + 1.6.12 FoxIDs.Client Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs.ControlClient/Infrastructure/Hosting/ServiceCollectionExtensions.cs b/src/FoxIDs.ControlClient/Infrastructure/Hosting/ServiceCollectionExtensions.cs index 8c28fe1c9..6cde1e959 100644 --- a/src/FoxIDs.ControlClient/Infrastructure/Hosting/ServiceCollectionExtensions.cs +++ b/src/FoxIDs.ControlClient/Infrastructure/Hosting/ServiceCollectionExtensions.cs @@ -30,6 +30,7 @@ public static IServiceCollection AddLogic(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/src/FoxIDs.ControlClient/Logic/ServerErrorLogic.cs b/src/FoxIDs.ControlClient/Logic/ServerErrorLogic.cs new file mode 100644 index 000000000..0af9693df --- /dev/null +++ b/src/FoxIDs.ControlClient/Logic/ServerErrorLogic.cs @@ -0,0 +1,41 @@ +using FoxIDs.Models; +using ITfoxtec.Identity; +using Microsoft.JSInterop; +using System.Threading.Tasks; + +namespace FoxIDs.Client.Logic +{ + public class ServerErrorLogic + { + private readonly IJSRuntime jsRuntime; + private bool isLoaded = false; + private string errorCache; + + public ServerErrorLogic(IJSRuntime jsRuntime) + { + this.jsRuntime = jsRuntime; + } + + public async ValueTask HasErrorAsync() + { + var error = await LoadErrorInternalAsync(); + return !error.IsNullOrEmpty(); + } + + public async ValueTask ReadErrorAsync() + { + var error = await LoadErrorInternalAsync(); + return error?.ToObject(); + } + + private async ValueTask LoadErrorInternalAsync() + { + if(!isLoaded) + { + errorCache = await jsRuntime.InvokeAsync("readError"); + isLoaded = true; + } + return errorCache; + } + } +} diff --git a/src/FoxIDs.ControlClient/Pages/Error.razor b/src/FoxIDs.ControlClient/Pages/Error.razor new file mode 100644 index 000000000..2a1a3d7df --- /dev/null +++ b/src/FoxIDs.ControlClient/Pages/Error.razor @@ -0,0 +1,30 @@ +@page "/-/error" +@using FoxIDs.Models +@layout HeaderLayout +@inject ServerErrorLogic serverErrorLogic + +
+

+

Error

+

An error has occurred. Please try again.

+

+ + @if (errorInfo != null) + { +

+ Time: @DateTimeOffset.FromUnixTimeSeconds(errorInfo.CreateTime).ToLocalTime()
+ Request ID: @errorInfo.RequestId
+ Technical error:
+ @errorInfo.TechnicalError +

+ } +
+ +@code { + private ErrorInfo errorInfo; + + protected override async Task OnInitializedAsync() + { + errorInfo = await serverErrorLogic.ReadErrorAsync(); + } +} diff --git a/src/FoxIDs.ControlClient/Shared/MainLayout.cs b/src/FoxIDs.ControlClient/Shared/MainLayout.cs index e56f7198f..5c8ce0589 100644 --- a/src/FoxIDs.ControlClient/Shared/MainLayout.cs +++ b/src/FoxIDs.ControlClient/Shared/MainLayout.cs @@ -70,6 +70,9 @@ public partial class MainLayout [Inject] public TrackSelectedLogic TrackSelectedLogic { get; set; } + [Inject] + public ServerErrorLogic ServerErrorLogic { get; set; } + [Inject] public TenantService TenantService { get; set; } @@ -89,21 +92,24 @@ protected override async Task OnInitializedAsync() protected override async Task OnParametersSetAsync() { - var user = (await authenticationStateTask).User; - if (user.Identity.IsAuthenticated) + if (!await ServerErrorLogic.HasErrorAsync()) { - await LoadAndSelectTracAsync(); - myProfileClaims = user.Claims; - if (user.Claims.Where(c => c.Type == Constants.JwtClaimTypes.AuthMethodType && c.Value == Constants.DefaultLogin.Name).Any() && - user.Claims.Where(c => c.Type == Constants.JwtClaimTypes.AuthMethod && c.Value == Constants.DefaultLogin.Name).Any() && - user.Claims.Where(c => c.Type == JwtClaimTypes.Email).Any()) + var user = (await authenticationStateTask).User; + if (user.Identity.IsAuthenticated) { - myProfileMasterMasterLogin = true; + await LoadAndSelectTracAsync(); + myProfileClaims = user.Claims; + if (user.Claims.Where(c => c.Type == Constants.JwtClaimTypes.AuthMethodType && c.Value == Constants.DefaultLogin.Name).Any() && + user.Claims.Where(c => c.Type == Constants.JwtClaimTypes.AuthMethod && c.Value == Constants.DefaultLogin.Name).Any() && + user.Claims.Where(c => c.Type == JwtClaimTypes.Email).Any()) + { + myProfileMasterMasterLogin = true; + } + } + else if (notAccessModal != null) + { + notAccessModal.Show(); } - } - else if(notAccessModal != null) - { - notAccessModal.Show(); } await base.OnParametersSetAsync(); } diff --git a/src/FoxIDs.ControlClient/wwwroot/css/app.css b/src/FoxIDs.ControlClient/wwwroot/css/app.css index 6175c80c8..a5f0655aa 100644 --- a/src/FoxIDs.ControlClient/wwwroot/css/app.css +++ b/src/FoxIDs.ControlClient/wwwroot/css/app.css @@ -11,6 +11,10 @@ a { color: #d87513; } +code { + color: #212529 !important; +} + .btn-link { color: #df8935; } @@ -723,4 +727,8 @@ img.logo { height: 100%; opacity: 0; cursor: pointer; - } \ No newline at end of file + } + +.error-page h4 { + font-weight: 500; +} \ No newline at end of file diff --git a/src/FoxIDs.ControlClient/wwwroot/index.html b/src/FoxIDs.ControlClient/wwwroot/index.html index 497edfe64..fc9d3b30c 100644 --- a/src/FoxIDs.ControlClient/wwwroot/index.html +++ b/src/FoxIDs.ControlClient/wwwroot/index.html @@ -31,9 +31,10 @@ - + + diff --git a/src/FoxIDs.ControlClient/wwwroot/js/save-cert-file.js b/src/FoxIDs.ControlClient/wwwroot/js/functions.js similarity index 80% rename from src/FoxIDs.ControlClient/wwwroot/js/save-cert-file.js rename to src/FoxIDs.ControlClient/wwwroot/js/functions.js index 2e24b88cc..291ab1f5e 100644 --- a/src/FoxIDs.ControlClient/wwwroot/js/save-cert-file.js +++ b/src/FoxIDs.ControlClient/wwwroot/js/functions.js @@ -5,4 +5,8 @@ document.body.appendChild(link); link.click(); document.body.removeChild(link); +} + +function readError() { + return document.getElementById('error').textContent; } \ No newline at end of file diff --git a/src/FoxIDs.ControlShared/FoxIDs.ControlShared.csproj b/src/FoxIDs.ControlShared/FoxIDs.ControlShared.csproj index 6aa20e481..d78d28842 100644 --- a/src/FoxIDs.ControlShared/FoxIDs.ControlShared.csproj +++ b/src/FoxIDs.ControlShared/FoxIDs.ControlShared.csproj @@ -2,7 +2,7 @@ net8.0 - 1.6.9 + 1.6.12 FoxIDs Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs.ControlShared/Models/ErrorInfo.cs b/src/FoxIDs.ControlShared/Models/ErrorInfo.cs new file mode 100644 index 000000000..d97f853db --- /dev/null +++ b/src/FoxIDs.ControlShared/Models/ErrorInfo.cs @@ -0,0 +1,9 @@ +namespace FoxIDs.Models +{ + public class ErrorInfo + { + public long CreateTime { get; set; } + public string RequestId { get; set; } + public string TechnicalError { get; set; } + } +} diff --git a/src/FoxIDs.Shared/FoxIDs.Shared.csproj b/src/FoxIDs.Shared/FoxIDs.Shared.csproj index 4acd3bd29..cf791e6a4 100644 --- a/src/FoxIDs.Shared/FoxIDs.Shared.csproj +++ b/src/FoxIDs.Shared/FoxIDs.Shared.csproj @@ -2,7 +2,7 @@ net8.0 - 1.6.9 + 1.6.12 FoxIDs Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs.SharedBase/Constants.cs b/src/FoxIDs.SharedBase/Constants.cs index adfb45eaa..2b44ae421 100644 --- a/src/FoxIDs.SharedBase/Constants.cs +++ b/src/FoxIDs.SharedBase/Constants.cs @@ -26,7 +26,7 @@ public static class Routes public const string DefaultAction = "index"; public const string DefaultSiteController = "w"; public const string ErrorController = "error"; - public const string DefaultClientController = "client"; + public const string ErrorAction = "error"; public const string OidcDiscoveryAction = "OpenidConfiguration"; public const string OidcDiscoveryKeyAction = "Keys"; diff --git a/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj b/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj index 1bb1d1eae..379e5a078 100644 --- a/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj +++ b/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj @@ -2,7 +2,7 @@ net8.0 - 1.6.9 + 1.6.12 FoxIDs Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs/FoxIDs.csproj b/src/FoxIDs/FoxIDs.csproj index b4e68683a..bc3c40b70 100644 --- a/src/FoxIDs/FoxIDs.csproj +++ b/src/FoxIDs/FoxIDs.csproj @@ -1,7 +1,7 @@  net8.0 - 1.6.9 + 1.6.12 FoxIDs Anders Revsgaard ITfoxtec