diff --git a/FoxIDs.sln b/FoxIDs.sln index c2b2a33d6..c9045df53 100644 --- a/FoxIDs.sln +++ b/FoxIDs.sln @@ -165,7 +165,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{CB8812 docs\images\configure-tenant-custom-domain-my-environment.png = docs\images\configure-tenant-custom-domain-my-environment.png docs\images\configure-tenant-text.png = docs\images\configure-tenant-text.png docs\images\configure-tenant.png = docs\images\configure-tenant.png - docs\images\configure-user-external.png = docs\images\configure-user-external.png docs\images\configure-user-mfa.png = docs\images\configure-user-mfa.png docs\images\configure-user.png = docs\images\configure-user.png docs\images\connections-app-reg-oauth.svg = docs\images\connections-app-reg-oauth.svg @@ -271,14 +270,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{CB8812 docs\images\upload-risk-passwords-seed-client.png = docs\images\upload-risk-passwords-seed-client.png docs\images\user-create-new-account-config.png = docs\images\user-create-new-account-config.png docs\images\user-create-new-account.png = docs\images\user-create-new-account.png + docs\images\user-external-auth-method-redemption.png = docs\images\user-external-auth-method-redemption.png docs\images\user-external-create-new-account-config.png = docs\images\user-external-create-new-account-config.png docs\images\user-external-create-new-account.png = docs\images\user-external-create-new-account.png + docs\images\user-external-redemption.png = docs\images\user-external-redemption.png docs\images\user-login.png = docs\images\user-login.png EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FoxIDs.ConvertCertificateTool", "tools\FoxIDs.ConvertCertificateTool\FoxIDs.ConvertCertificateTool.csproj", "{AF16CC91-2EEA-4790-8672-9ACCA430991D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FoxIDs.ResourceTranslateTool", "src\FoxIDs.ResourceTranslateTool\FoxIDs.ResourceTranslateTool.csproj", "{B63EC694-505A-4730-92B7-6827B8E61A6E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FoxIDs.ResourceTranslateTool", "tools\FoxIDs.ResourceTranslateTool\FoxIDs.ResourceTranslateTool.csproj", "{B63EC694-505A-4730-92B7-6827B8E61A6E}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{93A3AFF1-8A33-4CA6-8F25-EECD477D73D1}" ProjectSection(SolutionItems) = preProject diff --git a/docs/auth-method-howto-saml-2.0-google-workspace.md b/docs/auth-method-howto-saml-2.0-google-workspace.md index 9b154b111..72e418ca0 100644 --- a/docs/auth-method-howto-saml-2.0-google-workspace.md +++ b/docs/auth-method-howto-saml-2.0-google-workspace.md @@ -4,6 +4,8 @@ Connect Google Workspace to FoxIDs with an [SAML 2.0 authentication method](auth By configuring an [SAML 2.0 authentication method](auth-method-saml-2.0.md) and a [OpenID Connect application](app-reg-oidc.md) FoxIDs become a [bridge](bridge.md) between SAML 2.0 and OpenID Connect and automatically convert SAML 2.0 claims to JWT (OAuth 2.0) claims. +> The Google Workspace OpenID Connect implementation is lacking, mostly because it does not support any custom claims or group claims. It is therefor recommended to use Google Workspace with SAML 2.0. + ## Configuring Google Workspace This guide describe how to setup Google Workspace as a SAML 2.0 Identity Provider. diff --git a/docs/auth-method-howto-saml-2.0-nemlogin.md b/docs/auth-method-howto-saml-2.0-nemlogin.md index 5597cd6df..3686a6bac 100644 --- a/docs/auth-method-howto-saml-2.0-nemlogin.md +++ b/docs/auth-method-howto-saml-2.0-nemlogin.md @@ -16,7 +16,7 @@ FoxIDs support NemLog-in and the SAML 2.0 based OIOSAML3 including single logout NemLog-in documentation: - The [NemLog-in development portal](https://tu.nemlog-in.dk/oprettelse-og-administration-af-tjenester/) with documentation - - [test](https://tu.nemlog-in.dk/oprettelse-og-administration-af-tjenester/log-in/dokumentation-og-guides/integrationstestmiljo/), where you can find the NemLog-in IdP-metadata for test and get OCES3 test certificates + - [test](https://tu.nemlog-in.dk/oprettelse-og-administration-af-tjenester/log-in/dokumentation-og-guides/integrationstestmiljo/), where you can find the NemLog-in IdP-metadata for test and download the OCES3 test certificate - [production](https://tu.nemlog-in.dk/oprettelse-og-administration-af-tjenester/log-in/dokumentation-og-guides/produktionsmiljo/), where you can find the NemLog-in IdP-metadata for production - Create OCES3 production certificate in the [certificate administration](https://erhvervsadministration.nemlog-in.dk/certificates) - The [NemLog-in administration portal](https://administration.nemlog-in.dk/) where you configure IT-systems diff --git a/docs/control.md b/docs/control.md index c500b0ea8..54f3898ee 100644 --- a/docs/control.md +++ b/docs/control.md @@ -97,7 +97,7 @@ The administrator role `foxids:tenant.admin` grants access to all data in a tena #### Tenant access rights The tenant access rights is at the same time both scopes and roles. -> If the scope you need is not defined on the Control API `foxids_control_api` you can add the scope. The same goes for roles which has to be defined on the user or the calling client. +> If the scope you need is not defined on the Control API `foxids_control_api` you can add the scope. The `:track[xxxx]` specifies a tenant e.g., the `dev` tenant is `:track[dev]`. diff --git a/docs/images/configure-user-external.png b/docs/images/configure-user-external.png deleted file mode 100644 index 1524e0127..000000000 Binary files a/docs/images/configure-user-external.png and /dev/null differ diff --git a/docs/images/user-external-auth-method-redemption.png b/docs/images/user-external-auth-method-redemption.png new file mode 100644 index 000000000..d47cdfd23 Binary files /dev/null and b/docs/images/user-external-auth-method-redemption.png differ diff --git a/docs/images/user-external-create-new-account-config.png b/docs/images/user-external-create-new-account-config.png index caf583c9f..7796f100e 100644 Binary files a/docs/images/user-external-create-new-account-config.png and b/docs/images/user-external-create-new-account-config.png differ diff --git a/docs/images/user-external-redemption.png b/docs/images/user-external-redemption.png new file mode 100644 index 000000000..64c68ea85 Binary files /dev/null and b/docs/images/user-external-redemption.png differ diff --git a/docs/users.md b/docs/users.md index 57d87e59f..a742f0ae3 100644 --- a/docs/users.md +++ b/docs/users.md @@ -3,7 +3,7 @@ Users are saved in the environment's user repository. To achieve multiple user s There are two different types of users: - [Internal users](#internal-users) which are authenticated using the [login](login.md) authentication method. -- [External users](#external-users) which are linked by an authenticated method to an external user/identity with a claim. The user is authenticated in an external Identity Provider using an authenticated method: OpenID Connect, SAML 2.0, External Login or Environment Link. +- [External users](#external-users) which are linked by an authenticated method to an external user/identity with a claim. The users are authenticated in an external Identity Provider and the users can be [redeemed](#provision-and-redeem) based on e.g. an `email` claim. ## Internal users Internal users can be authenticated in all [login](login.md) authentication methods in an environment, making is possible to [customize](customization.md) the login experience e.g., depending on different [application](connections.md#application-registration) requirements. @@ -53,17 +53,17 @@ Current supported hash algorithm `P2HS512:10` which is defined as: Standard .NET liberals are used to calculate the hash. ## External users -An external user is linked to one authentication method and can only be authenticated with that particular authentication method. External users can be linked to the authentication methods: OpenID Connect, SAML 2.0, External Login or Environment Link. +An external user is linked to one authentication method and can only be authenticated with that particular authentication method. External users can be linked to the authentication methods: OpenID Connect, SAML 2.0, External Login and Environment Link. It is optional to use external users, they are not created by default. -All external user grouped under a authentication method is linked with the same claim type (e.g. the `sub` or `email` claim type) and the users are separated by unique claim values. +All external user grouped under an authentication method is linked with the same claim type (e.g. the `sub` claim type) and the users are separated by unique claim values. > With external users you can store claims on each user. E.g. store the your user ID claim representing the user in your system and thereby mapping the external user ID to your user ID. -A unique ID is by default added to each external user. +An automatically generated unique ID is added to each external user by default. ### Create external user -Depending on the selected authentication method's configuration, new users is asked to fill out a form to create a user. +Depending on the selected authentication method's configuration, new users is optionally asked to fill out a form to create a user. ![New external users create an account](images/user-external-create-new-account.png) @@ -78,9 +78,24 @@ This is the configuration in a [OpenID Connect](auth-method-oidc.md) authenticat > If the login sequence is started base on a [login](login.md) authentication method, it provides the basis for the UI look and feel ([customize](customization.md)). Otherwise, the default [login](login.md) authentication method is selected as the base. -### Provisioning +### Provision and redeem External users can be created, changed and deleted with the [Control Client](control.md#foxids-control-client) or be provisioned through the [Control API](control.md#foxids-control-api). -![Configure Login](images/configure-user-external.png) +You probably do not know the link claim value in advanced because it is an external user ID. But if you do, it is possible to create users and associate them with the link claim value. Most often, you will know a redemption claim in advanced instead. + +The external users can be redeemed by a redemption claim type (e.g. `email`) and they are then automatically linked with the link claim type. +It is bad practice to link users based on there email over a long period of time, as emails can change. But the email is unlikely to change within the short redemption period. + +Once the user has been redeemed, the external user is subsequently logged in based on the link claim value. + +This authentication method is configured with `email` claim redemption and `sub` link claim type. + +![Authentication method, external user redemption](images/user-external-auth-method-redemption.png) + +And user's is added with their known email as the redemption claim value. + +![External user redemption](images/user-external-redemption.png) + +In this example the user is connected to Google Workspace with an OpenID Connect authentication method and a `app_user_id` claim is added with an internal user ID. -In this example the user is connected to Azure AD with an OpenID Connect authentication method and a `app_user_id` claim is added with the internal user ID. \ No newline at end of file +> You can reset a redeemed user by deleting the link claim value and, if necessary, also changing the redemption claim value. The external user is then redeemed again next time the user logs in. \ No newline at end of file diff --git a/src/FoxIDs.Control/Controllers/Tracks/TExternalUserController.cs b/src/FoxIDs.Control/Controllers/Tracks/TExternalUserController.cs index 086d6f91a..d28e45189 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TExternalUserController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TExternalUserController.cs @@ -8,7 +8,9 @@ using System.Threading.Tasks; using FoxIDs.Infrastructure.Security; using System; +using System.Linq; using FoxIDs.Logic; +using ITfoxtec.Identity; namespace FoxIDs.Controllers { @@ -40,16 +42,25 @@ public TExternalUserController(TelemetryScopedLogger logger, IMapper mapper, ITe { if (!await ModelState.TryValidateObjectAsync(userRequest)) return BadRequest(ModelState); - var linkClaimHash = await userRequest.LinkClaimValue.HashIdStringAsync(); - var mExternalUser = await tenantDataRepository.GetAsync(await ExternalUser.IdFormatAsync(RouteBinding, userRequest.UpPartyName, linkClaimHash)); + var mExternalUser = await tenantDataRepository.GetAsync(await ExternalUser.IdFormatAsync(RouteBinding, userRequest.UpPartyName, await GetLinkClaimHashAsync(userRequest.LinkClaimValue, userRequest.RedemptionClaimValue)), required: !userRequest.LinkClaimValue.IsNullOrWhiteSpace()); + if (mExternalUser == null) + { + var idKey = new Track.IdKey { TenantName = RouteBinding.TenantName, TrackName = RouteBinding.TrackName }; + (var mExternalUsers, _) = await tenantDataRepository.GetListAsync(idKey, whereQuery: u => u.RedemptionClaimValue.Equals(userRequest.RedemptionClaimValue)); + mExternalUser = mExternalUsers?.FirstOrDefault(); + if (mExternalUser == null) + { + throw new FoxIDsDataException() { StatusCode = DataStatusCode.NotFound }; + } + } return Ok(mapper.Map(mExternalUser)); } catch (FoxIDsDataException ex) { if (ex.StatusCode == DataStatusCode.NotFound) { - logger.Warning(ex, $"NotFound, Get '{typeof(Api.ExternalUser).Name}' by up-party name '{userRequest.UpPartyName}' and link claim '{userRequest.LinkClaimValue}'."); - return NotFound(typeof(Api.ExternalUser).Name, $"{userRequest.UpPartyName}:{userRequest.LinkClaimValue}"); + logger.Warning(ex, $"NotFound, Get '{typeof(Api.ExternalUser).Name}' by up-party name '{userRequest.UpPartyName}' and link claim '{userRequest.LinkClaimValue}' or redemption claim '{userRequest.RedemptionClaimValue}'."); + return NotFound(typeof(Api.ExternalUser).Name, $"{userRequest.UpPartyName}:{(!userRequest.LinkClaimValue.IsNullOrWhiteSpace() ? userRequest.LinkClaimValue : userRequest.RedemptionClaimValue)}"); } throw; } @@ -58,20 +69,19 @@ public TExternalUserController(TelemetryScopedLogger logger, IMapper mapper, ITe /// /// Create external user. /// - /// ExternalUser. + /// ExternalUser. /// ExternalUser. [ProducesResponseType(typeof(Api.ExternalUser), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task> PostExternalUser([FromBody] Api.ExternalUserRequest externalUserRequest) + public async Task> PostExternalUser([FromBody] Api.ExternalUserRequest userRequest) { try { - if (!await ModelState.TryValidateObjectAsync(externalUserRequest) || !await validateApiModelExternalUserLogic.ValidateApiModelAsync(ModelState, externalUserRequest)) return BadRequest(ModelState); + if (!await ModelState.TryValidateObjectAsync(userRequest) || !await validateApiModelExternalUserLogic.ValidateApiModelAsync(ModelState, userRequest)) return BadRequest(ModelState); - var mExternalUser = mapper.Map(externalUserRequest); - var linkClaimHash = await externalUserRequest.LinkClaimValue.HashIdStringAsync(); - mExternalUser.Id = await ExternalUser.IdFormatAsync(RouteBinding, externalUserRequest.UpPartyName, linkClaimHash); + var mExternalUser = mapper.Map(userRequest); + mExternalUser.Id = await ExternalUser.IdFormatAsync(RouteBinding, userRequest.UpPartyName, await GetLinkClaimHashAsync(userRequest.LinkClaimValue, userRequest.RedemptionClaimValue)); mExternalUser.UserId = Guid.NewGuid().ToString(); await tenantDataRepository.CreateAsync(mExternalUser); @@ -81,8 +91,8 @@ public TExternalUserController(TelemetryScopedLogger logger, IMapper mapper, ITe { if (ex.StatusCode == DataStatusCode.Conflict) { - logger.Warning(ex, $"Conflict, Create '{typeof(Api.ExternalUserId).Name}' by up-party name '{externalUserRequest.UpPartyName}' and link claim '{externalUserRequest.LinkClaimValue}'."); - return Conflict(typeof(Api.ExternalUserId).Name, $"{externalUserRequest.UpPartyName}:{externalUserRequest.LinkClaimValue}"); + logger.Warning(ex, $"Conflict, Create '{typeof(Api.ExternalUserId).Name}' by up-party name '{userRequest.UpPartyName}' and link claim '{userRequest.LinkClaimValue}' or redemption claim '{userRequest.RedemptionClaimValue}'."); + return Conflict(typeof(Api.ExternalUserId).Name, $"{userRequest.UpPartyName}:{(!userRequest.LinkClaimValue.IsNullOrWhiteSpace() ? userRequest.LinkClaimValue : userRequest.RedemptionClaimValue)}"); } throw; } @@ -91,23 +101,44 @@ public TExternalUserController(TelemetryScopedLogger logger, IMapper mapper, ITe /// /// Update external user. /// - /// External user. + /// External user. /// External user. [ProducesResponseType(typeof(Api.ExternalUser), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> PutExternalUser([FromBody] Api.ExternalUserRequest externalUserRequest) + public async Task> PutExternalUser([FromBody] Api.ExternalUserUpdateRequest userRequest) { try { - if (!await ModelState.TryValidateObjectAsync(externalUserRequest)) return BadRequest(ModelState); + if (!await ModelState.TryValidateObjectAsync(userRequest)) return BadRequest(ModelState); - var linkClaimHash = await externalUserRequest.LinkClaimValue.HashIdStringAsync(); - var mExternalUser = await tenantDataRepository.GetAsync(await ExternalUser.IdFormatAsync(RouteBinding, externalUserRequest.UpPartyName, linkClaimHash)); + var mExternalUser = await tenantDataRepository.GetAsync(await ExternalUser.IdFormatAsync(RouteBinding, userRequest.UpPartyName, await GetLinkClaimHashAsync(userRequest.LinkClaimValue, userRequest.RedemptionClaimValue))); - var tempMExternalUser = mapper.Map(externalUserRequest); - mExternalUser.DisableAccount = tempMExternalUser.DisableAccount; - mExternalUser.Claims = tempMExternalUser.Claims; - await tenantDataRepository.SaveAsync(mExternalUser); + mExternalUser.LinkClaimValue = userRequest.UpdateLinkClaimValue; + if(mExternalUser.LinkClaimValue.IsNullOrWhiteSpace()) + { + mExternalUser.LinkClaimValue = null; + } + mExternalUser.RedemptionClaimValue = userRequest.UpdateRedemptionClaimValue; + if (mExternalUser.RedemptionClaimValue.IsNullOrWhiteSpace()) + { + mExternalUser.RedemptionClaimValue = null; + } + mExternalUser.DisableAccount = userRequest.DisableAccount; + var tempMExternalUser = mapper.Map(userRequest); + mExternalUser.Claims = tempMExternalUser.Claims; + + if (!userRequest.LinkClaimValue.IsNullOrWhiteSpace() && userRequest.LinkClaimValue != userRequest.UpdateLinkClaimValue || // if link claim change + userRequest.LinkClaimValue.IsNullOrWhiteSpace() && !userRequest.UpdateLinkClaimValue.IsNullOrWhiteSpace() || // if link claim is added + userRequest.LinkClaimValue.IsNullOrWhiteSpace() && userRequest.RedemptionClaimValue != userRequest.UpdateRedemptionClaimValue) // if link claim not set and redemption claim change + { + await tenantDataRepository.DeleteAsync(mExternalUser.Id); + mExternalUser.Id = await ExternalUser.IdFormatAsync(RouteBinding, userRequest.UpPartyName, await GetLinkClaimHashAsync(mExternalUser.LinkClaimValue, mExternalUser.RedemptionClaimValue)); + await tenantDataRepository.CreateAsync(mExternalUser); + } + else + { + await tenantDataRepository.SaveAsync(mExternalUser); + } return Ok(mapper.Map(mExternalUser)); } @@ -115,8 +146,8 @@ public TExternalUserController(TelemetryScopedLogger logger, IMapper mapper, ITe { if (ex.StatusCode == DataStatusCode.NotFound) { - logger.Warning(ex, $"NotFound, Update '{typeof(Api.ExternalUserId).Name}' by up-party name '{externalUserRequest.UpPartyName}' and link claim '{externalUserRequest.LinkClaimValue}'."); - return NotFound(typeof(Api.ExternalUserId).Name, $"{externalUserRequest.UpPartyName}:{externalUserRequest.LinkClaimValue}"); + logger.Warning(ex, $"NotFound, Update '{typeof(Api.ExternalUserId).Name}' by up-party name '{userRequest.UpPartyName}' and link claim '{userRequest.LinkClaimValue}' or redemption claim '{userRequest.RedemptionClaimValue}'."); + return NotFound(typeof(Api.ExternalUserId).Name, $"{userRequest.UpPartyName}:{(!userRequest.LinkClaimValue.IsNullOrWhiteSpace() ? userRequest.LinkClaimValue : userRequest.RedemptionClaimValue)}"); } throw; } @@ -133,19 +164,30 @@ public async Task DeleteExternalUser(Api.ExternalUserId userReque { if (!await ModelState.TryValidateObjectAsync(userRequest)) return BadRequest(ModelState); - var linkClaimHash = await userRequest.LinkClaimValue.HashIdStringAsync(); - await tenantDataRepository.DeleteAsync(await ExternalUser.IdFormatAsync(RouteBinding, userRequest.UpPartyName, linkClaimHash)); + await tenantDataRepository.DeleteAsync(await ExternalUser.IdFormatAsync(RouteBinding, userRequest.UpPartyName, await GetLinkClaimHashAsync(userRequest.LinkClaimValue, userRequest.RedemptionClaimValue))); return NoContent(); } catch (FoxIDsDataException ex) { if (ex.StatusCode == DataStatusCode.NotFound) { - logger.Warning(ex, $"NotFound, Delete '{typeof(Api.ExternalUserId).Name}' by up-party name '{userRequest.UpPartyName}' and link claim '{userRequest.LinkClaimValue}'."); - return NotFound(typeof(Api.ExternalUserId).Name, $"{userRequest.UpPartyName}:{userRequest.LinkClaimValue}"); + logger.Warning(ex, $"NotFound, Delete '{typeof(Api.ExternalUserId).Name}' by up-party name '{userRequest.UpPartyName}' and link claim '{userRequest.LinkClaimValue}' or redemption claim '{userRequest.RedemptionClaimValue}'."); + return NotFound(typeof(Api.ExternalUserId).Name, $"{userRequest.UpPartyName}:{(!userRequest.LinkClaimValue.IsNullOrWhiteSpace() ? userRequest.LinkClaimValue : userRequest.RedemptionClaimValue)}"); } throw; } } + + private Task GetLinkClaimHashAsync(string linkClaimValue, string redemptionClaimValue) + { + if (linkClaimValue.IsNullOrWhiteSpace()) + { + return redemptionClaimValue.HashIdStringAsync(); + } + else + { + return linkClaimValue.HashIdStringAsync(); + } + } } } diff --git a/src/FoxIDs.Control/Controllers/Tracks/TExternalUsersController.cs b/src/FoxIDs.Control/Controllers/Tracks/TExternalUsersController.cs index 1a71d8fea..c7f8779f4 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TExternalUsersController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TExternalUsersController.cs @@ -45,14 +45,16 @@ public TExternalUsersController(TelemetryScopedLogger logger, IMapper mapper, IT (var mExternalUsers, var nextPaginationToken) = filterValue.IsNullOrWhiteSpace() ? await tenantDataRepository.GetListAsync(idKey, whereQuery: u => u.DataType.Equals(dataType), paginationToken: paginationToken) : await tenantDataRepository.GetListAsync(idKey, whereQuery: u => u.DataType.Equals(dataType) && - (u.LinkClaimValue.Contains(filterValue, StringComparison.CurrentCultureIgnoreCase) || u.UserId.Contains(filterValue, StringComparison.CurrentCultureIgnoreCase)), paginationToken: paginationToken); + ((u.LinkClaimValue != null && u.LinkClaimValue.Contains(filterValue, StringComparison.CurrentCultureIgnoreCase)) || + (u.RedemptionClaimValue != null && u.RedemptionClaimValue.Contains(filterValue, StringComparison.CurrentCultureIgnoreCase)) || + u.UserId.Contains(filterValue, StringComparison.CurrentCultureIgnoreCase)), paginationToken: paginationToken); var response = new Api.PaginationResponse { Data = new HashSet(mExternalUsers.Count()), PaginationToken = nextPaginationToken, }; - foreach(var mUser in mExternalUsers.OrderBy(t => t.LinkClaimValue)) + foreach(var mUser in mExternalUsers.OrderBy(t => t.LinkClaimValue ?? t.RedemptionClaimValue)) { response.Data.Add(mapper.Map(mUser)); } diff --git a/src/FoxIDs.Control/Controllers/Tracks/TFilterExternalUserController.cs b/src/FoxIDs.Control/Controllers/Tracks/TFilterExternalUserController.cs index cd809ecfd..49e24015d 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TFilterExternalUserController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TFilterExternalUserController.cs @@ -46,11 +46,13 @@ public TFilterExternalUserController(TelemetryScopedLogger logger, IMapper mappe var idKey = new Track.IdKey { TenantName = RouteBinding.TenantName, TrackName = RouteBinding.TrackName }; (var mExternalUsers, _) = filterValue.IsNullOrWhiteSpace() ? await tenantDataRepository.GetListAsync(idKey, whereQuery: u => u.DataType.Equals(dataType)) : - await tenantDataRepository.GetListAsync(idKey, whereQuery: u => u.DataType.Equals(dataType) && - (u.LinkClaimValue.Contains(filterValue, StringComparison.CurrentCultureIgnoreCase) || u.UserId.Contains(filterValue, StringComparison.CurrentCultureIgnoreCase))); + await tenantDataRepository.GetListAsync(idKey, whereQuery: u => u.DataType.Equals(dataType) && + ((u.LinkClaimValue != null && u.LinkClaimValue.Contains(filterValue, StringComparison.CurrentCultureIgnoreCase)) || + (u.RedemptionClaimValue != null && u.RedemptionClaimValue.Contains(filterValue, StringComparison.CurrentCultureIgnoreCase)) || + u.UserId.Contains(filterValue, StringComparison.CurrentCultureIgnoreCase))); var aExternalUsers = new HashSet(mExternalUsers.Count()); - foreach(var mUser in mExternalUsers.OrderBy(t => t.LinkClaimValue)) + foreach(var mUser in mExternalUsers.OrderBy(t => t.LinkClaimValue ?? t.RedemptionClaimValue)) { aExternalUsers.Add(mapper.Map(mUser)); } diff --git a/src/FoxIDs.Control/Controllers/Tracks/TUserChangePasswordController.cs b/src/FoxIDs.Control/Controllers/Tracks/TUserChangePasswordController.cs new file mode 100644 index 000000000..e69bdb50d --- /dev/null +++ b/src/FoxIDs.Control/Controllers/Tracks/TUserChangePasswordController.cs @@ -0,0 +1,97 @@ +using AutoMapper; +using FoxIDs.Infrastructure; +using FoxIDs.Models; +using Api = FoxIDs.Models.Api; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using FoxIDs.Logic; +using FoxIDs.Infrastructure.Security; + +namespace FoxIDs.Controllers +{ + [TenantScopeAuthorize(Constants.ControlApi.Segment.User)] + public class TUserChangePasswordController : ApiController + { + private readonly TelemetryScopedLogger logger; + private readonly IMapper mapper; + private readonly BaseAccountLogic accountLogic; + + public TUserChangePasswordController(TelemetryScopedLogger logger, IMapper mapper, BaseAccountLogic accountLogic) : base(logger) + { + this.logger = logger; + this.mapper = mapper; + this.accountLogic = accountLogic; + } + + /// + /// Change the users password. + /// + /// User with current and new password. + /// User. + [ProducesResponseType(typeof(Api.User), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> PutUserChangePassword([FromBody] Api.UserChangePasswordRequest userRequest) + { + try + { + if (!await ModelState.TryValidateObjectAsync(userRequest)) return BadRequest(ModelState); + userRequest.Email = userRequest.Email?.ToLower(); + + var mUser = await accountLogic.ChangePasswordUser(userRequest.Email, userRequest.CurrentPassword, userRequest.NewPassword); + + return Ok(mapper.Map(mUser)); + } + catch (UserNotExistsException ueex) + { + logger.Warning(ueex, $"NotFound, Change password on '{typeof(Api.User).Name}' by email '{userRequest.Email}'."); + return NotFound(ueex.Message); + } + catch (InvalidPasswordException ipex) + { + logger.ScopeTrace(() => ipex.Message, triggerEvent: true); + ModelState.TryAddModelError(userRequest.CurrentPassword, "Wrong password"); + return BadRequest(ModelState, ipex); + } + catch (NewPasswordEqualsCurrentException npeex) + { + logger.ScopeTrace(() => npeex.Message); + ModelState.AddModelError(nameof(userRequest.NewPassword), "Please use a new password."); + return BadRequest(ModelState, npeex); + } + catch (PasswordLengthException plex) + { + logger.ScopeTrace(() => plex.Message); + ModelState.AddModelError(nameof(userRequest.NewPassword), RouteBinding.CheckPasswordComplexity ? + $"Please use {RouteBinding.PasswordLength} characters or more with a mix of letters, numbers and symbols." : + $"Please use {RouteBinding.PasswordLength} characters or more."); + return BadRequest(ModelState, plex); + } + catch (PasswordComplexityException pcex) + { + logger.ScopeTrace(() => pcex.Message); + ModelState.AddModelError(nameof(userRequest.NewPassword), "Please use a mix of letters, numbers and symbols"); + return BadRequest(ModelState, pcex); + } + catch (PasswordEmailTextComplexityException pecex) + { + logger.ScopeTrace(() => pecex.Message); + ModelState.AddModelError(nameof(userRequest.NewPassword), "Please do not use the email or parts of it."); + return BadRequest(ModelState, pecex); + } + catch (PasswordUrlTextComplexityException pucex) + { + logger.ScopeTrace(() => pucex.Message); + ModelState.AddModelError(nameof(userRequest.NewPassword), "Please do not use parts of the URL."); + return BadRequest(ModelState, pucex); + } + catch (PasswordRiskException prex) + { + logger.ScopeTrace(() => prex.Message); + ModelState.AddModelError(nameof(userRequest.NewPassword), "The password has previously appeared in a data breach. Please choose a more secure alternative."); + return BadRequest(ModelState, prex); + + } + } + } +} diff --git a/src/FoxIDs.Control/Controllers/Usage/TFilterUsageController.cs b/src/FoxIDs.Control/Controllers/Usage/TFilterUsageController.cs index 6296a03ee..737776906 100644 --- a/src/FoxIDs.Control/Controllers/Usage/TFilterUsageController.cs +++ b/src/FoxIDs.Control/Controllers/Usage/TFilterUsageController.cs @@ -24,14 +24,14 @@ public class TFilterUsageController : ApiController private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; private readonly ITenantDataRepository tenantDataRepository; - private readonly UsageMolliePaymentLogic usageMolliePaymentLogic; + private readonly UsageInvoicingLogic usageInvoicingLogic; - public TFilterUsageController(TelemetryScopedLogger logger, IMapper mapper, ITenantDataRepository tenantDataRepository, UsageMolliePaymentLogic usageMolliePaymentLogic) : base(logger) + public TFilterUsageController(TelemetryScopedLogger logger, IMapper mapper, ITenantDataRepository tenantDataRepository, UsageInvoicingLogic usageInvoicingLogic) : base(logger) { this.logger = logger; this.mapper = mapper; this.tenantDataRepository = tenantDataRepository; - this.usageMolliePaymentLogic = usageMolliePaymentLogic; + this.usageInvoicingLogic = usageInvoicingLogic; } /// @@ -63,10 +63,8 @@ await tenantDataRepository.GetListAsync(whereQuery: u => u.PeriodEndDate.M var aUsedList = new HashSet(mUsedList.Count()); foreach (var mUsed in mUsedList.OrderBy(t => t.TenantName)) { - if(mUsed.PaymentStatus == UsagePaymentStatus.Open || mUsed.PaymentStatus == UsagePaymentStatus.Pending || mUsed.PaymentStatus == UsagePaymentStatus.Authorized) - { - await usageMolliePaymentLogic.UpdatePaymentAsync(mUsed); - } + await usageInvoicingLogic.UpdatePaymentAndSendInvoiceAsync(mUsed); + var aUsed = mapper.Map(mUsed); var mLastInvoice = mUsed.Invoices?.LastOrDefault(); if(mLastInvoice != null) diff --git a/src/FoxIDs.Control/Controllers/Usage/TUsageController.cs b/src/FoxIDs.Control/Controllers/Usage/TUsageController.cs index 93219e301..59db3e161 100644 --- a/src/FoxIDs.Control/Controllers/Usage/TUsageController.cs +++ b/src/FoxIDs.Control/Controllers/Usage/TUsageController.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.Linq; using ITfoxtec.Identity; +using FoxIDs.Logic.Usage; namespace FoxIDs.Controllers { @@ -22,14 +23,16 @@ public class TUsageController : ApiController private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; private readonly ITenantDataRepository tenantDataRepository; + private readonly UsageInvoicingLogic usageInvoicingLogic; public object MTenant { get; private set; } - public TUsageController(TelemetryScopedLogger logger, IMapper mapper, ITenantDataRepository tenantDataRepository) : base(logger) + public TUsageController(TelemetryScopedLogger logger, IMapper mapper, ITenantDataRepository tenantDataRepository, UsageInvoicingLogic usageInvoicingLogic) : base(logger) { this.logger = logger; this.mapper = mapper; this.tenantDataRepository = tenantDataRepository; + this.usageInvoicingLogic = usageInvoicingLogic; } /// @@ -50,8 +53,10 @@ public TUsageController(TelemetryScopedLogger logger, IMapper mapper, ITenantDat var mUsed = await tenantDataRepository.GetAsync(await Used.IdFormatAsync(usageRequest.TenantName, usageRequest.PeriodBeginDate.Year, usageRequest.PeriodBeginDate.Month)); + await usageInvoicingLogic.UpdatePaymentAndSendInvoiceAsync(mUsed); + var aUsed = mapper.Map(mUsed); - aUsed.Currency = GetCulture(mTenant); + aUsed.Currency = GetCurrency(mTenant); return Ok(aUsed); } catch (FoxIDsDataException ex) @@ -90,7 +95,7 @@ public TUsageController(TelemetryScopedLogger logger, IMapper mapper, ITenantDat await tenantDataRepository.CreateAsync(mUsed); var aUsed = mapper.Map(mUsed); - aUsed.Currency = GetCulture(mTenant); + aUsed.Currency = GetCurrency(mTenant); return Created(aUsed); } catch (FoxIDsDataException ex) @@ -132,7 +137,7 @@ public TUsageController(TelemetryScopedLogger logger, IMapper mapper, ITenantDat await tenantDataRepository.UpdateAsync(mUsed); var aUsed = mapper.Map(mUsed); - aUsed.Currency = GetCulture(mTenant); + aUsed.Currency = GetCurrency(mTenant); return Ok(aUsed); } catch (FoxIDsDataException ex) @@ -146,7 +151,7 @@ public TUsageController(TelemetryScopedLogger logger, IMapper mapper, ITenantDat } } - private string GetCulture(Tenant mTenant) + private string GetCurrency(Tenant mTenant) { return mTenant.Currency.IsNullOrEmpty() ? Constants.Models.Currency.Eur : mTenant.Currency; } diff --git a/src/FoxIDs.Control/Controllers/Usage/TUsageInvoicingActionController.cs b/src/FoxIDs.Control/Controllers/Usage/TUsageInvoicingActionController.cs index ad3f8f976..a575282a3 100644 --- a/src/FoxIDs.Control/Controllers/Usage/TUsageInvoicingActionController.cs +++ b/src/FoxIDs.Control/Controllers/Usage/TUsageInvoicingActionController.cs @@ -13,6 +13,7 @@ using FoxIDs.Logic.Usage; using System.Linq; using System.Threading; +using ITfoxtec.Identity; namespace FoxIDs.Controllers { @@ -223,13 +224,13 @@ private async Task MarkAsPaid(Used mUsed) private async Task MarkAsNotPaid(Used mUsed) { - if (mUsed.IsInvoiceReady && mUsed.PaymentStatus == UsagePaymentStatus.Paid) + if (mUsed.PaymentId.IsNullOrEmpty() && mUsed.IsInvoiceReady && mUsed.PaymentStatus == UsagePaymentStatus.Paid) { await usageMolliePaymentLogic.MarkAsNotPaidAsync(mUsed); } else { - throw new Exception("The invoice is not ready and it is not possible to make as paid."); + throw new Exception("The invoice is not ready and/or it is not possible to make as paid."); } } } diff --git a/src/FoxIDs.Control/Controllers/Usage/TUsagesController.cs b/src/FoxIDs.Control/Controllers/Usage/TUsagesController.cs index 8abdf229d..c338f853d 100644 --- a/src/FoxIDs.Control/Controllers/Usage/TUsagesController.cs +++ b/src/FoxIDs.Control/Controllers/Usage/TUsagesController.cs @@ -23,14 +23,14 @@ public class TUsagesController : ApiController private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; private readonly ITenantDataRepository tenantDataRepository; - private readonly UsageMolliePaymentLogic usageMolliePaymentLogic; + private readonly UsageInvoicingLogic usageInvoicingLogic; - public TUsagesController(TelemetryScopedLogger logger, IMapper mapper, ITenantDataRepository tenantDataRepository, UsageMolliePaymentLogic usageMolliePaymentLogic) : base(logger) + public TUsagesController(TelemetryScopedLogger logger, IMapper mapper, ITenantDataRepository tenantDataRepository, UsageInvoicingLogic usageInvoicingLogic) : base(logger) { this.logger = logger; this.mapper = mapper; this.tenantDataRepository = tenantDataRepository; - this.usageMolliePaymentLogic = usageMolliePaymentLogic; + this.usageInvoicingLogic = usageInvoicingLogic; } /// @@ -65,10 +65,8 @@ await tenantDataRepository.GetListAsync(whereQuery: u => u.PeriodEndDate.M }; foreach (var mUsed in mUsedList.OrderBy(t => t.TenantName)) { - if(mUsed.PaymentStatus == UsagePaymentStatus.Open || mUsed.PaymentStatus == UsagePaymentStatus.Pending || mUsed.PaymentStatus == UsagePaymentStatus.Authorized) - { - await usageMolliePaymentLogic.UpdatePaymentAsync(mUsed); - } + await usageInvoicingLogic.UpdatePaymentAndSendInvoiceAsync(mUsed); + var aUsed = mapper.Map(mUsed); var mLastInvoice = mUsed.Invoices?.LastOrDefault(); if(mLastInvoice != null) diff --git a/src/FoxIDs.Control/FoxIDs.Control.csproj b/src/FoxIDs.Control/FoxIDs.Control.csproj index 75f070168..978a65a7d 100644 --- a/src/FoxIDs.Control/FoxIDs.Control.csproj +++ b/src/FoxIDs.Control/FoxIDs.Control.csproj @@ -2,7 +2,7 @@ net9.0 - 1.13.4 + 1.14.2 FoxIDs Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs.Control/Infrastructure/Security/BaseAuthorizationRequirement.cs b/src/FoxIDs.Control/Infrastructure/Security/BaseAuthorizationRequirement.cs index 31928c7f3..0c42d7cb1 100644 --- a/src/FoxIDs.Control/Infrastructure/Security/BaseAuthorizationRequirement.cs +++ b/src/FoxIDs.Control/Infrastructure/Security/BaseAuthorizationRequirement.cs @@ -1,6 +1,7 @@ using ITfoxtec.Identity; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Linq; @@ -15,22 +16,45 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte { if (context.User != null && context.Resource is HttpContext httpContext) { - var executingEnpoint = httpContext.GetEndpoint(); - var scopeAuthorizeAttribute = executingEnpoint.Metadata.OfType().FirstOrDefault(); - if (scopeAuthorizeAttribute != null) + var scopedLogger = httpContext.RequestServices.GetService(); + try { - var userScopes = context.User.Claims.Where(c => string.Equals(c.Type, JwtClaimTypes.Scope, StringComparison.OrdinalIgnoreCase)).Select(c => c.Value).FirstOrDefault().ToSpaceList(); - var userRoles = context.User.Claims.Where(c => string.Equals(c.Type, JwtClaimTypes.Role, StringComparison.OrdinalIgnoreCase)).Select(c => c.Value).ToList(); - if (userScopes?.Count() > 0 && userRoles?.Count > 0) + var executingEnpoint = httpContext.GetEndpoint(); + var scopeAuthorizeAttribute = executingEnpoint.Metadata.OfType().FirstOrDefault(); + if (scopeAuthorizeAttribute == null) { - (var acceptedScopes, var acceptedRoles) = GetAcceptedScopesAndRoles(scopeAuthorizeAttribute.Segments, httpContext.GetRouteBinding()?.TrackName, httpContext.Request?.Method); + throw new Exception($"Scope authorize attribute '{typeof(Tsc)}' is null"); + } + else + { + var userScopes = context.User.Claims.Where(c => string.Equals(c.Type, JwtClaimTypes.Scope, StringComparison.OrdinalIgnoreCase)).Select(c => c.Value).FirstOrDefault().ToSpaceList(); + var userRoles = context.User.Claims.Where(c => string.Equals(c.Type, JwtClaimTypes.Role, StringComparison.OrdinalIgnoreCase)).Select(c => c.Value).ToList(); + if (userScopes?.Count() > 0 && userRoles?.Count > 0) + { + (var acceptedScopes, var acceptedRoles) = GetAcceptedScopesAndRoles(scopeAuthorizeAttribute.Segments, httpContext.GetRouteBinding().TrackName, httpContext.Request?.Method); - if (userScopes.Where(us => acceptedScopes.Any(s => s.Equals(us, StringComparison.Ordinal))).Any() && userRoles.Where(ur => acceptedRoles.Any(r => r.Equals(ur, StringComparison.Ordinal))).Any()) + if (userScopes.Where(us => acceptedScopes.Any(s => s.Equals(us, StringComparison.Ordinal))).Any() && userRoles.Where(ur => acceptedRoles.Any(r => r.Equals(ur, StringComparison.Ordinal))).Any()) + { + context.Succeed(requirement); + } + else + { + scopedLogger.ScopeTrace(() => $"Control API, Users scope '{(userScopes != null ? string.Join(", ", userScopes) : string.Empty)}' and role '{(userRoles != null ? string.Join(", ", userRoles) : string.Empty)}'."); + scopedLogger.ScopeTrace(() => $"Control API, Accepted scope '{(acceptedScopes != null ? string.Join(", ", acceptedScopes) : string.Empty)}'."); + scopedLogger.ScopeTrace(() => $"Control API, Accepted role '{(acceptedRoles != null ? string.Join(", ", acceptedRoles) : string.Empty)}'."); + throw new Exception("Users scope and role not accepted."); + } + } + else { - context.Succeed(requirement); + throw new Exception("Users scope or role is empty."); } } } + catch (Exception ex) + { + scopedLogger.Error(ex, "Control API access denied."); + } } return Task.CompletedTask; diff --git a/src/FoxIDs.Control/Infrastructure/Security/JwtBearerMultipleTenantsHandler.cs b/src/FoxIDs.Control/Infrastructure/Security/JwtBearerMultipleTenantsHandler.cs index be43e3670..5eabb17f4 100644 --- a/src/FoxIDs.Control/Infrastructure/Security/JwtBearerMultipleTenantsHandler.cs +++ b/src/FoxIDs.Control/Infrastructure/Security/JwtBearerMultipleTenantsHandler.cs @@ -20,12 +20,12 @@ public class JwtBearerMultipleTenantsHandler : AuthenticationHandler options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) - { - } + public JwtBearerMultipleTenantsHandler(IOptionsMonitor options, ILoggerFactory loggerFactory, UrlEncoder encoder) : base(options, loggerFactory, encoder) + { } protected override async Task HandleAuthenticateAsync() { + var scopedLogger = Context.RequestServices.GetService(); try { var accessToken = GetAccessTokenFromHeader(); @@ -63,15 +63,13 @@ protected override async Task HandleAuthenticateAsync() (principal, _) = JwtHandler.ValidateToken(accessToken, oidcDiscovery.Issuer, oidcDiscoveryKeySet.Keys, Options.DownParty); } - var logger = Context.RequestServices.GetService(); - logger.SetUserScopeProperty(principal.Claims); - + scopedLogger.SetUserScopeProperty(principal.Claims); var ticket = new AuthenticationTicket(principal, Scheme.Name); return AuthenticateResult.Success(ticket); } catch (Exception ex) { - Logger.LogError(ex, ex.Message); + scopedLogger.Error(ex); return AuthenticateResult.Fail(ex.Message); } } diff --git a/src/FoxIDs.Control/Infrastructure/Security/TenantAuthorizationRequirement.cs b/src/FoxIDs.Control/Infrastructure/Security/TenantAuthorizationRequirement.cs index a7e1c4b5f..4011efd52 100644 --- a/src/FoxIDs.Control/Infrastructure/Security/TenantAuthorizationRequirement.cs +++ b/src/FoxIDs.Control/Infrastructure/Security/TenantAuthorizationRequirement.cs @@ -29,8 +29,8 @@ protected override (List acceptedScopes, List acceptedRoles) Get else { AddScopeAndRoleByTrack(acceptedScopes, acceptedRoles, trackName, httpMethod, - $"{Constants.ControlApi.ResourceAndScope.Tenant}{Constants.ControlApi.AccessElement.Track}", - Constants.ControlApi.Access.Tenant, + $"{Constants.ControlApi.ResourceAndScope.Tenant}{Constants.ControlApi.AccessElement.Track}", + $"{Constants.ControlApi.Access.Tenant}{Constants.ControlApi.AccessElement.Track}", segment); } } @@ -38,8 +38,8 @@ protected override (List acceptedScopes, List acceptedRoles) Get else { AddScopeAndRoleByTrack(acceptedScopes, acceptedRoles, trackName, httpMethod, - $"{Constants.ControlApi.ResourceAndScope.Tenant}{Constants.ControlApi.AccessElement.Track}", - Constants.ControlApi.Access.Tenant); + $"{Constants.ControlApi.ResourceAndScope.Tenant}{Constants.ControlApi.AccessElement.Track}", + $"{Constants.ControlApi.Access.Tenant}{Constants.ControlApi.AccessElement.Track}"); } } diff --git a/src/FoxIDs.Control/Logic/Usage/UsageInvoicingLogic.cs b/src/FoxIDs.Control/Logic/Usage/UsageInvoicingLogic.cs index 2cdab53a4..99648608a 100644 --- a/src/FoxIDs.Control/Logic/Usage/UsageInvoicingLogic.cs +++ b/src/FoxIDs.Control/Logic/Usage/UsageInvoicingLogic.cs @@ -134,6 +134,29 @@ public async Task DoInvoicingAsync(Tenant tenant, Used used, CancellationT return taskDone; } + public async Task UpdatePaymentAndSendInvoiceAsync(Used used, Tenant tenant = null) + { + if (!used.PaymentId.IsNullOrEmpty() && (used.PaymentStatus == UsagePaymentStatus.Open || used.PaymentStatus == UsagePaymentStatus.Pending || used.PaymentStatus == UsagePaymentStatus.Authorized)) + { + var oldPaymentStatus = used.PaymentStatus; + + if (await usageMolliePaymentLogic.UpdatePaymentAsync(used)) + { + if (oldPaymentStatus != UsagePaymentStatus.Paid && used.PaymentStatus == UsagePaymentStatus.Paid) + { + logger.Event($"Usage, update payment and send invoice 'card' for tenant '{used.TenantName}'."); + + tenant = tenant ?? await tenantDataRepository.GetAsync(await Tenant.IdFormatAsync(used.TenantName)); + (var invoiceTaskDone, var invoice) = await GetInvoiceAsync(tenant, used, true); + if (invoiceTaskDone && invoice.SendStatus == UsageInvoiceSendStatus.None) + { + await SendInvoiceAsync(used, invoice); + } + } + } + } + } + public async Task CreateAndSendCreditNoteAsync(Used used) { if (used.PaymentStatus != UsagePaymentStatus.None && !used.PaymentStatus.PaymentStatusIsGenerallyFailed()) @@ -195,7 +218,7 @@ public async Task SendInvoiceAsync(Used used, Invoice invoice) invoice.SendStatus = UsageInvoiceSendStatus.Failed; await tenantDataRepository.UpdateAsync(used); } - catch (OperationCanceledException) + catch (OperationCanceledException) { throw; } @@ -212,7 +235,7 @@ public async Task SendInvoiceAsync(Used used, Invoice invoice) } } - private async Task<(bool taskDone, Invoice invoice)> GetInvoiceAsync(Tenant tenant, Used used, bool isCardPayment, CancellationToken stoppingToken) + private async Task<(bool taskDone, Invoice invoice)> GetInvoiceAsync(Tenant tenant, Used used, bool isCardPayment, CancellationToken? stoppingToken = null) { try { @@ -244,7 +267,7 @@ public async Task SendInvoiceAsync(Used used, Invoice invoice) } } - private async Task<(bool taskDone, Invoice invoice)> GetInvoiceInternalAsync(Tenant tenant, Used used, CancellationToken stoppingToken) + private async Task<(bool taskDone, Invoice invoice)> GetInvoiceInternalAsync(Tenant tenant, Used used, CancellationToken? stoppingToken) { if (used.IsInvoiceReady) { @@ -258,7 +281,7 @@ public async Task SendInvoiceAsync(Used used, Invoice invoice) } } - private async Task CreateInvoiceAsync(Tenant tenant, Used used, CancellationToken stoppingToken) + private async Task CreateInvoiceAsync(Tenant tenant, Used used, CancellationToken? stoppingToken) { var invoice = new Invoice { @@ -272,7 +295,7 @@ private async Task CreateInvoiceAsync(Tenant tenant, Used used, Cancell var usageSettings = await GetUsageSettingsAsync(); var plan = tenant.EnableUsage == true && !tenant.PlanName.IsNullOrEmpty() ? await masterDataRepository.GetAsync(await Plan.IdFormatAsync(tenant.PlanName)) : null; - stoppingToken.ThrowIfCancellationRequested(); + if(stoppingToken.HasValue) stoppingToken.Value.ThrowIfCancellationRequested(); CalculateInvoice(used, plan, invoice, tenant.IncludeVat == true, GetExchangesRate(invoice.Currency, usageSettings.CurrencyExchanges)); invoice.InvoiceNumber = await GetInvoiceNumberAsync(usageSettings); @@ -287,7 +310,7 @@ private async Task CreateInvoiceAsync(Tenant tenant, Used used, Cancell { used.Invoices = [invoice]; } - stoppingToken.ThrowIfCancellationRequested(); + if (stoppingToken.HasValue) stoppingToken.Value.ThrowIfCancellationRequested(); await tenantDataRepository.UpdateAsync(used); return invoice; } diff --git a/src/FoxIDs.Control/Logic/Usage/UsageMolliePaymentLogic.cs b/src/FoxIDs.Control/Logic/Usage/UsageMolliePaymentLogic.cs index 0269eb3de..ff84bc1a5 100644 --- a/src/FoxIDs.Control/Logic/Usage/UsageMolliePaymentLogic.cs +++ b/src/FoxIDs.Control/Logic/Usage/UsageMolliePaymentLogic.cs @@ -137,13 +137,13 @@ public async Task UpdatePaymentAsync(Used used) try { - logger.Event($"Usage, read payment 'card' for tenant '{used.TenantName}' started."); + logger.Event($"Usage, update payment 'card' for tenant '{used.TenantName}' started."); var paymentResponse = await paymentClient.GetPaymentAsync(used.PaymentId); used.PaymentStatus = paymentResponse.Status.FromMollieStatusToPaymentStatus(); await tenantDataRepository.UpdateAsync(used); - logger.Event($"Usage, read payment 'card' for tenant '{used.TenantName}' status '{used.PaymentStatus}'."); + logger.Event($"Usage, update payment 'card' for tenant '{used.TenantName}' status '{used.PaymentStatus}'."); return true; } @@ -171,6 +171,11 @@ public async Task MarkAsPaidAsync(Used used) public async Task MarkAsNotPaidAsync(Used used) { + if (!used.PaymentId.IsNullOrEmpty()) + { + throw new InvalidOperationException("Has a payment id and can not be marked as not paid."); + } + used.PaymentStatus = UsagePaymentStatus.None; await tenantDataRepository.UpdateAsync(used); } diff --git a/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj b/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj index c59e38a2d..d8f8e51e5 100644 --- a/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj +++ b/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj @@ -2,7 +2,7 @@ net9.0 - 1.13.4 + 1.14.2 FoxIDs.Client Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/ILinkExternalUser.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/ILinkExternalUser.cs index bb9e3f825..cf81acb4d 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/ILinkExternalUser.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/ILinkExternalUser.cs @@ -16,6 +16,11 @@ public interface ILinkExternalUser : IClaimTransformViewModel, IDynamicElementsV [Display(Name = "Link claim type")] public string LinkClaimType { get; set; } + [MaxLength(Constants.Models.Claim.JwtTypeLength)] + [RegularExpression(Constants.Models.Claim.JwtTypeRegExPattern)] + [Display(Name = "Redemption claim type (inactive if empty)")] + public string RedemptionClaimType { get; set; } + [Display(Name = "Overwrite received claims")] public bool OverwriteClaims { get; set; } } diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/LinkExternalUserViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/LinkExternalUserViewModel.cs index c0e823eee..d3268cf4f 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/LinkExternalUserViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/LinkExternalUserViewModel.cs @@ -18,8 +18,13 @@ public class LinkExternalUserViewModel : IValidatableObject, ILinkExternalUser [MaxLength(Constants.Models.Claim.JwtTypeLength)] [RegularExpression(Constants.Models.Claim.JwtTypeRegExPattern)] - [Display(Name = "Link claim")] - public string LinkClaimType { get; set; } = JwtClaimTypes.Subject; + [Display(Name = "Link claim type")] + public string LinkClaimType { get; set; } + + [MaxLength(Constants.Models.Claim.JwtTypeLength)] + [RegularExpression(Constants.Models.Claim.JwtTypeRegExPattern)] + [Display(Name = "Redemption claim (inactive if empty)")] + public string RedemptionClaimType { get; set; } [Display(Name = "Overwrite received claims")] public bool OverwriteClaims { get; set; } @@ -37,7 +42,7 @@ public class LinkExternalUserViewModel : IValidatableObject, ILinkExternalUser public IEnumerable Validate(ValidationContext validationContext) { var results = new List(); - if ((AutoCreateUser || RequireUser) && LinkClaimType.IsNullOrWhiteSpace()) + if ((AutoCreateUser || RequireUser || !RedemptionClaimType.IsNullOrWhiteSpace()) && LinkClaimType.IsNullOrWhiteSpace()) { results.Add(new ValidationResult($"The link claim type is required.", [nameof(LinkClaimType)])); } diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/ExternalUserViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/ExternalUserViewModel.cs index 74be6035d..a119c7fb4 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/ExternalUserViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/ExternalUserViewModel.cs @@ -1,11 +1,12 @@ using FoxIDs.Infrastructure.DataAnnotations; using FoxIDs.Models.Api; +using ITfoxtec.Identity; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace FoxIDs.Client.Models.ViewModels { - public class ExternalUserViewModel + public class ExternalUserViewModel : IValidatableObject { public ExternalUserViewModel() { @@ -21,11 +22,14 @@ public ExternalUserViewModel() [Display(Name = "Connected authentication method")] public string UpPartyDisplayName { get; set; } - [Required(ErrorMessage = "A unique claim value is required.")] [MaxLength(Constants.Models.Claim.ValueLength)] [Display(Name = "Link claim value")] public string LinkClaimValue { get; set; } + [MaxLength(Constants.Models.Claim.ValueLength)] + [Display(Name = "Redemption claim value")] + public string RedemptionClaimValue { get; set; } + [MaxLength(Constants.Models.User.UserIdLength)] [Display(Name = "User id (unique and persistent)")] public string UserId { get; set; } @@ -37,5 +41,15 @@ public ExternalUserViewModel() [ListLength(Constants.Models.User.ClaimsMin, Constants.Models.User.ClaimsMax)] [Display(Name = "Claims")] public List Claims { get; set; } + + public virtual IEnumerable Validate(ValidationContext validationContext) + { + var results = new List(); + if (LinkClaimValue.IsNullOrWhiteSpace() && RedemptionClaimValue.IsNullOrWhiteSpace()) + { + results.Add(new ValidationResult($"A unique link claim value or redemption claim value is required.", [nameof(LinkClaimValue), nameof(RedemptionClaimValue)])); + } + return results; + } } } diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/GeneralExternalUserViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/GeneralExternalUserViewModel.cs index 7cba76bd1..4d6714df2 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/GeneralExternalUserViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/GeneralExternalUserViewModel.cs @@ -12,6 +12,7 @@ public GeneralExternalUserViewModel(ExternalUser externalUser) { UpPartyName = externalUser.UpPartyName; LinkClaimValue = externalUser.LinkClaimValue; + RedemptionClaimValue = externalUser.RedemptionClaimValue; } public string UpPartyDisplayName { get; set; } diff --git a/src/FoxIDs.ControlClient/Pages/Certificates.cs b/src/FoxIDs.ControlClient/Pages/Certificates.cs index a76044caa..f220b8732 100644 --- a/src/FoxIDs.ControlClient/Pages/Certificates.cs +++ b/src/FoxIDs.ControlClient/Pages/Certificates.cs @@ -280,7 +280,7 @@ private async Task OnEditCertificateValidSubmitAsync(GeneralTrackCertificateView { if(generalCertificate.Form.Model.Key == null) { - throw new ArgumentNullException("Model.Key"); + throw new Exception("Please add the certificate."); } _ = await TrackService.UpdateTrackKeyContainedAsync(generalCertificate.Form.Model.Map()); @@ -300,9 +300,13 @@ private async Task OnEditCertificateValidSubmitAsync(GeneralTrackCertificateView } else { - throw; + generalCertificate.Form.SetError(ex.Message); } } + catch (Exception ex) + { + generalCertificate.Form.SetError(ex.Message); + } } private async Task DeleteSecondaryCertificateAsync(GeneralTrackCertificateViewModel generalCertificate) diff --git a/src/FoxIDs.ControlClient/Pages/Certificates.razor b/src/FoxIDs.ControlClient/Pages/Certificates.razor index e65f41124..802f83e8f 100644 --- a/src/FoxIDs.ControlClient/Pages/Certificates.razor +++ b/src/FoxIDs.ControlClient/Pages/Certificates.razor @@ -155,7 +155,7 @@ } else { -
Valid from @certificate.ValidFrom.ToShortDateString() to @certificate.ValidTo.ToShortDateString()
diff --git a/src/FoxIDs.ControlClient/Pages/DownParties.razor b/src/FoxIDs.ControlClient/Pages/DownParties.razor index 3a00e4c03..cb0a52c2c 100644 --- a/src/FoxIDs.ControlClient/Pages/DownParties.razor +++ b/src/FoxIDs.ControlClient/Pages/DownParties.razor @@ -74,7 +74,7 @@ } else { - } @@ -92,7 +92,7 @@ @if (paginationToken != null) {
-
diff --git a/src/FoxIDs.ControlClient/Pages/DownPartyTest.cs b/src/FoxIDs.ControlClient/Pages/DownPartyTest.cs index 0a84b90f4..a751226b8 100644 --- a/src/FoxIDs.ControlClient/Pages/DownPartyTest.cs +++ b/src/FoxIDs.ControlClient/Pages/DownPartyTest.cs @@ -47,7 +47,7 @@ protected override async Task OnInitializedAsync() try { var responseQuery = GetResponseQuery(navigationManager.Uri); - if (responseQuery.Count() > 0) + if (responseQuery?.Count() > 0) { var authenticationResponse = responseQuery.ToObject(); authenticationResponse.Validate(); @@ -86,7 +86,7 @@ private Dictionary GetResponseQuery(string responseUrl) var rUri = new Uri(responseUrl); if (rUri.Query.IsNullOrWhiteSpace() && rUri.Fragment.IsNullOrWhiteSpace()) { - throw new SecurityException("Invalid response URL."); + return null; } return QueryHelpers.ParseQuery(!rUri.Query.IsNullOrWhiteSpace() ? rUri.Query.TrimStart('?') : rUri.Fragment.TrimStart('#')).ToDictionary(); } diff --git a/src/FoxIDs.ControlClient/Pages/ExternalUsers.cs b/src/FoxIDs.ControlClient/Pages/ExternalUsers.cs index 7be2e2b41..fc61a863a 100644 --- a/src/FoxIDs.ControlClient/Pages/ExternalUsers.cs +++ b/src/FoxIDs.ControlClient/Pages/ExternalUsers.cs @@ -148,11 +148,7 @@ private async Task SetGeneralExternalUsersAsync(PaginationResponse } else { - var subUps = (await UpPartyService.GetUpPartiesAsync(externalUser.UpPartyName)).Data; - if(subUps.Count() > 0) - { - externalUser.UpPartyDisplayName = subUps.Where(u => u.Name == externalUser.UpPartyName).Select(u => u.DisplayName).FirstOrDefault(); - } + externalUser.UpPartyDisplayName = await GetUpPartyDisplayName(externalUser.UpPartyName); } } } @@ -160,6 +156,16 @@ private async Task SetGeneralExternalUsersAsync(PaginationResponse paginationToken = dataExternalUsers.PaginationToken; } + private async Task GetUpPartyDisplayName(string upPartyName) + { + var subUps = (await UpPartyService.GetUpPartiesAsync(upPartyName)).Data; + if (subUps.Count() > 0) + { + return subUps.Where(u => u.Name == upPartyName).Select(u => u.DisplayName)?.FirstOrDefault(); + } + return null; + } + private void ShowCreateExternalUser() { var externalUser = new GeneralExternalUserViewModel(); @@ -178,7 +184,9 @@ private async Task ShowUpdateExternalUserAsync(GeneralExternalUserViewModel gene try { - var externalUser = await ExternalUserService.GetExternalUserAsync(generalExternalUser.UpPartyName, generalExternalUser.LinkClaimValue); + var externalUser = await ExternalUserService.GetExternalUserAsync(generalExternalUser.UpPartyName, generalExternalUser.LinkClaimValue, generalExternalUser.RedemptionClaimValue); + generalExternalUser.LinkClaimValue = externalUser.LinkClaimValue; + generalExternalUser.RedemptionClaimValue = externalUser.RedemptionClaimValue; await generalExternalUser.Form.InitAsync(externalUser.Map(afterMap: afterMap => { afterMap.UpPartyDisplayName = generalExternalUser.UpPartyDisplayName; @@ -271,25 +279,35 @@ private async Task OnEditExternalUserValidSubmitAsync(GeneralExternalUserViewMod if (generalExternalUser.CreateMode) { var externalUserResult = await ExternalUserService.CreateExternalUserAsync(generalExternalUser.Form.Model.Map()); - generalExternalUser.Form.UpdateModel(externalUserResult.Map(afterMap: afterMap => + generalExternalUser.CreateMode = false; + toastService.ShowSuccess("External user created."); + generalExternalUser.LinkClaimValue = externalUserResult.LinkClaimValue; + generalExternalUser.RedemptionClaimValue = externalUserResult.RedemptionClaimValue; + generalExternalUser.UserId = externalUserResult.UserId; + generalExternalUser.UpPartyName = externalUserResult.UpPartyName; + generalExternalUser.UpPartyDisplayName = await GetUpPartyDisplayName(externalUserResult.UpPartyName); + generalExternalUser.Form.UpdateModel(externalUserResult.Map(afterMap: afterMap => { afterMap.UpPartyDisplayName = generalExternalUser.UpPartyDisplayName; })); - generalExternalUser.CreateMode = false; - toastService.ShowSuccess("External user created."); - generalExternalUser.LinkClaimValue = generalExternalUser.Form.Model.LinkClaimValue; - generalExternalUser.UpPartyName = generalExternalUser.Form.Model.UpPartyName; - generalExternalUser.UpPartyDisplayName = generalExternalUser.Form.Model.UpPartyDisplayName; - generalExternalUser.UserId = generalExternalUser.Form.Model.UserId; } else { - var externalUserResult = await ExternalUserService.UpdateExternalUserAsync(generalExternalUser.Form.Model.Map()); + var externalUserResult = await ExternalUserService.UpdateExternalUserAsync(generalExternalUser.Form.Model.Map(afterMap: afterMap => + { + afterMap.UpdateLinkClaimValue = afterMap.LinkClaimValue; + afterMap.UpdateRedemptionClaimValue = afterMap.RedemptionClaimValue; + afterMap.LinkClaimValue = generalExternalUser.LinkClaimValue; + afterMap.RedemptionClaimValue= generalExternalUser.RedemptionClaimValue; + })); + toastService.ShowSuccess("External user updated."); + generalExternalUser.LinkClaimValue = externalUserResult.LinkClaimValue; + generalExternalUser.RedemptionClaimValue = externalUserResult.RedemptionClaimValue; generalExternalUser.Form.UpdateModel(externalUserResult.Map(afterMap: afterMap => { afterMap.UpPartyDisplayName = generalExternalUser.UpPartyDisplayName; + })); - toastService.ShowSuccess("External user updated."); } } catch (FoxIDsApiException ex) @@ -309,7 +327,7 @@ private async Task DeleteExternalUserAsync(GeneralExternalUserViewModel generalE { try { - await ExternalUserService.DeleteExternalUserAsync(generalExternalUser.UpPartyName, generalExternalUser.LinkClaimValue); + await ExternalUserService.DeleteExternalUserAsync(generalExternalUser.UpPartyName, generalExternalUser.LinkClaimValue, generalExternalUser.RedemptionClaimValue); externalUsers.Remove(generalExternalUser); } catch (TokenUnavailableException) diff --git a/src/FoxIDs.ControlClient/Pages/ExternalUsers.razor b/src/FoxIDs.ControlClient/Pages/ExternalUsers.razor index f62d9430b..54da2e83e 100644 --- a/src/FoxIDs.ControlClient/Pages/ExternalUsers.razor +++ b/src/FoxIDs.ControlClient/Pages/ExternalUsers.razor @@ -44,8 +44,11 @@ @@ -147,7 +147,7 @@ } else { - } diff --git a/src/FoxIDs.ControlClient/Pages/MasterTenant.razor b/src/FoxIDs.ControlClient/Pages/MasterTenant.razor index c4a521ad3..2c0d1a475 100644 --- a/src/FoxIDs.ControlClient/Pages/MasterTenant.razor +++ b/src/FoxIDs.ControlClient/Pages/MasterTenant.razor @@ -166,7 +166,7 @@
-
@@ -148,7 +148,7 @@ } else { -
ID: @resource.Id diff --git a/src/FoxIDs.ControlClient/Pages/Tenants.razor b/src/FoxIDs.ControlClient/Pages/Tenants.razor index 0b8cdbfe2..ac99140fc 100644 --- a/src/FoxIDs.ControlClient/Pages/Tenants.razor +++ b/src/FoxIDs.ControlClient/Pages/Tenants.razor @@ -211,7 +211,7 @@ } else { - @if (tenant.Name != Constants.Routes.MasterTenantName) @@ -233,7 +233,7 @@ @if (paginationToken != null) {
-
diff --git a/src/FoxIDs.ControlClient/Pages/UpParties.razor b/src/FoxIDs.ControlClient/Pages/UpParties.razor index b2a768f3a..ac4ac6c93 100644 --- a/src/FoxIDs.ControlClient/Pages/UpParties.razor +++ b/src/FoxIDs.ControlClient/Pages/UpParties.razor @@ -62,7 +62,7 @@ } else { - @if (upParty.Type != PartyTypes.OAuth2) @@ -86,7 +86,7 @@ @if (paginationToken != null) {
-
@@ -211,7 +211,7 @@ Your tenant access is configured in the master environment. } - + } } diff --git a/src/FoxIDs.ControlClient/Pages/Usage/Usage.cs b/src/FoxIDs.ControlClient/Pages/Usage/Usage.cs index a469d26ab..57e1505ab 100644 --- a/src/FoxIDs.ControlClient/Pages/Usage/Usage.cs +++ b/src/FoxIDs.ControlClient/Pages/Usage/Usage.cs @@ -187,6 +187,11 @@ private string UsagePriceText(GeneralUsedViewModel generalUsed) } } + if(statusTest.Count() <= 0) + { + statusTest.Add("registered"); + } + var sendItemsInvoice = generalUsed.HasItems && invoice?.SendStatus != UsageInvoiceSendStatus.Send; var failed = invoice?.SendStatus == UsageInvoiceSendStatus.Failed || generalUsed?.PaymentStatus.PaymentApiStatusIsGenerallyFailed() == true; return (sendItemsInvoice, failed, generalUsed.PaymentStatus == UsagePaymentStatus.Paid, $"Status: {string.Join(", ", statusTest)}"); @@ -400,6 +405,26 @@ private async Task DeleteUsedAsync(GeneralUsedViewModel generalUsed) } } + private async Task SaveAndDoInvoicingAsync(GeneralUsedViewModel generalUsed) + { + try + { + (var isValid, var error) = await generalUsed.Form.Submit(); + if (isValid) + { + await DoInvoicingAsync(generalUsed); + } + else if (!error.IsNullOrWhiteSpace()) + { + toastService.ShowError(error); + } + } + catch (TokenUnavailableException) + { + await (OpenidConnectPkce as TenantOpenidConnectPkce).TenantLoginAsync(); + } + } + private async Task DoInvoicingAsync(GeneralUsedViewModel generalUsed) { generalUsed.InvoicingActionButtonDisabled = true; diff --git a/src/FoxIDs.ControlClient/Pages/Usage/Usage.razor b/src/FoxIDs.ControlClient/Pages/Usage/Usage.razor index 176f441c5..a8f96ce30 100644 --- a/src/FoxIDs.ControlClient/Pages/Usage/Usage.razor +++ b/src/FoxIDs.ControlClient/Pages/Usage/Usage.razor @@ -209,6 +209,21 @@ { } + @if (ShowDoInvoicingButton(used)) + { + @if (!used.InvoicingActionButtonDisabled) + { + + } + else + { + + } + } @@ -216,7 +231,7 @@ } else { - diff --git a/src/FoxIDs.ControlClient/Pages/Usage/UsageTenants.razor b/src/FoxIDs.ControlClient/Pages/Usage/UsageTenants.razor index ee83dfdb8..e2eb23107 100644 --- a/src/FoxIDs.ControlClient/Pages/Usage/UsageTenants.razor +++ b/src/FoxIDs.ControlClient/Pages/Usage/UsageTenants.razor @@ -184,7 +184,7 @@ } else { - @if (tenant.Name != Constants.Routes.MasterTenantName) @@ -206,7 +206,7 @@ @if (paginationToken != null) {
-
diff --git a/src/FoxIDs.ControlClient/Pages/Users.razor b/src/FoxIDs.ControlClient/Pages/Users.razor index 1a5f15b29..ecf66bdd8 100644 --- a/src/FoxIDs.ControlClient/Pages/Users.razor +++ b/src/FoxIDs.ControlClient/Pages/Users.razor @@ -114,7 +114,7 @@ } else { - } @@ -124,7 +124,7 @@ @if (paginationToken != null) {
-
diff --git a/src/FoxIDs.ControlClient/Services/ExternalUserService.cs b/src/FoxIDs.ControlClient/Services/ExternalUserService.cs index 8c1a3cf4b..053f0fc70 100644 --- a/src/FoxIDs.ControlClient/Services/ExternalUserService.cs +++ b/src/FoxIDs.ControlClient/Services/ExternalUserService.cs @@ -15,9 +15,9 @@ public ExternalUserService(IHttpClientFactory httpClientFactory, RouteBindingLog public async Task> GetExternalUsersAsync(string filterValue, string paginationToken = null) => await GetListAsync(listApiUri, filterValue, parmName1: nameof(filterValue), paginationToken: paginationToken); - public async Task GetExternalUserAsync(string upPartyName, string linkClaim) => await GetAsync(apiUri, new ExternalUserId { UpPartyName = upPartyName, LinkClaimValue = linkClaim }); + public async Task GetExternalUserAsync(string upPartyName, string linkClaimValue, string redemptionClaimValue) => await GetAsync(apiUri, new ExternalUserId { UpPartyName = upPartyName, LinkClaimValue = linkClaimValue, RedemptionClaimValue = redemptionClaimValue }); public async Task CreateExternalUserAsync(ExternalUserRequest externalUser) => await PostResponseAsync(apiUri, externalUser); - public async Task UpdateExternalUserAsync(ExternalUserRequest user) => await PutResponseAsync(apiUri, user); - public async Task DeleteExternalUserAsync(string upPartyName, string linkClaim) => await DeleteByRequestObjAsync(apiUri, new ExternalUserId { UpPartyName = upPartyName, LinkClaimValue = linkClaim }); + public async Task UpdateExternalUserAsync(ExternalUserUpdateRequest user) => await PutResponseAsync(apiUri, user); + public async Task DeleteExternalUserAsync(string upPartyName, string linkClaim, string redemptionClaimValue) => await DeleteByRequestObjAsync(apiUri, new ExternalUserId { UpPartyName = upPartyName, LinkClaimValue = linkClaim, RedemptionClaimValue = redemptionClaimValue }); } } diff --git a/src/FoxIDs.ControlClient/Shared/Components/DynamicElements.razor b/src/FoxIDs.ControlClient/Shared/Components/DynamicElements.razor index dc3c9475e..88da75801 100644 --- a/src/FoxIDs.ControlClient/Shared/Components/DynamicElements.razor +++ b/src/FoxIDs.ControlClient/Shared/Components/DynamicElements.razor @@ -82,19 +82,19 @@ diff --git a/src/FoxIDs.ControlClient/Shared/Components/FFieldTextClipboard.razor b/src/FoxIDs.ControlClient/Shared/Components/FFieldTextClipboard.razor index bd94f6b1c..557e2b6ad 100644 --- a/src/FoxIDs.ControlClient/Shared/Components/FFieldTextClipboard.razor +++ b/src/FoxIDs.ControlClient/Shared/Components/FFieldTextClipboard.razor @@ -6,7 +6,7 @@ @CurrentValue
- +
public static class HtmActionExtensions { - private const string defaultTitle = "FoxIDs"; - /// - /// Converts a Dictionary<string, string> to a HTML Post ContentResult. + /// Converts URL and Dictionary<string, string> to a HTML Post ContentResult. /// - public static Task ToHtmlPostContentResultAsync(this Dictionary items, string url, string title) + public static ContentResult ToHtmlPostContentResult(this string url, Dictionary items) { - return items.ToHtmlPostPage(url, title: title ?? defaultTitle).ToContentResultAsync(); + return url.ToHtmlPostPage(items).ToContentResult(); } /// - /// Converts a URL to a redirect ContentResult. + /// Converts a URL to a RedirectResult. /// - public static ContentResult ToRedirectResult(this string url, string title) + public static IActionResult ToRedirectResult(this string url) { - return url.HtmRedirectActionPage(title: title ?? defaultTitle).ToContentResult(); + return new RedirectResult(url); } /// - /// Converts a Dictionary<string, string> to a redirect ContentResult. + /// Converts URL and Dictionary<string, string> to a RedirectResult. /// - public static Task ToRedirectResultAsync(this Dictionary items, string url, string title) + public static IActionResult ToRedirectResult(this string url, Dictionary items) { - return items.ToHtmlGetPage(url, title: title ?? defaultTitle).ToContentResultAsync(); + return new RedirectResult(QueryHelpers.AddQueryString(url, items)); } /// - /// Converts a Dictionary<string, string> to a fragment ContentResult. + /// Converts URL and Dictionary<string, string> to a RedirectResult. /// - public static Task ToFragmentResultAsync(this Dictionary items, string url, string title) + public static IActionResult ToFragmentResult(this string url, Dictionary items) { - return items.ToHtmlFragmentPage(url, title: title ?? defaultTitle).ToContentResultAsync(); + return new RedirectResult(url.AddFragment(items)); } /// @@ -55,13 +53,5 @@ public static ContentResult ToContentResult(this string html) Content = html, }; } - - /// - /// HTML to ContentResult. - /// - public static Task ToContentResultAsync(this string html) - { - return Task.FromResult(html.ToContentResult()); - } } } diff --git a/src/FoxIDs/Extensions/Saml2BindingExtensions.cs b/src/FoxIDs/Extensions/Saml2BindingExtensions.cs index 8ecc4c7cf..325ce029f 100644 --- a/src/FoxIDs/Extensions/Saml2BindingExtensions.cs +++ b/src/FoxIDs/Extensions/Saml2BindingExtensions.cs @@ -1,37 +1,26 @@ -using ITfoxtec.Identity; -using ITfoxtec.Identity.Saml2; +using ITfoxtec.Identity.Saml2; +using ITfoxtec.Identity.Saml2.MvcCore; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.WebUtilities; -using System.Linq; -using System.Threading.Tasks; +using System; namespace FoxIDs { public static class Saml2BindingExtensions { - public static Task ToActionFormResultAsync(this Saml2RedirectBinding binding) + public static IActionResult ToSamlActionResult(this Saml2Binding binding) { - var urlSplit = binding.RedirectLocation.OriginalString.Split('?'); - if(urlSplit?.Count() != 2) + if (binding is Saml2RedirectBinding saml2RedirectBinding) { - throw new InvalidSaml2BindingException($"Invalid Saml2RedirectBinding URL '{binding.RedirectLocation.OriginalString}'."); + return saml2RedirectBinding.ToActionResult(); } - var nameValueCollection = QueryHelpers.ParseQuery(urlSplit[1]).ToDictionary(); - - return Task.FromResult(new ContentResult + else if (binding is Saml2PostBinding saml2PostBinding) { - ContentType = "text/html", - Content = nameValueCollection.ToHtmlGetPage(urlSplit[0]), - }); - } - - public static Task ToActionFormResultAsync(this Saml2PostBinding binding) - { - return Task.FromResult(new ContentResult + return saml2PostBinding.ToActionResult(); + } + else { - ContentType = "text/html", - Content = binding.PostContent, - }); + throw new NotSupportedException(); + } } } } diff --git a/src/FoxIDs/FoxIDs.csproj b/src/FoxIDs/FoxIDs.csproj index d47304f4d..a3a8eef15 100644 --- a/src/FoxIDs/FoxIDs.csproj +++ b/src/FoxIDs/FoxIDs.csproj @@ -1,7 +1,7 @@  net9.0 - 1.13.4 + 1.14.2 FoxIDs Anders Revsgaard ITfoxtec @@ -27,7 +27,7 @@ - + diff --git a/src/FoxIDs/Logic/ExternalLogin/ExternalLoginUpLogic.cs b/src/FoxIDs/Logic/ExternalLogin/ExternalLoginUpLogic.cs index ceb866a7a..f2f7dc76a 100644 --- a/src/FoxIDs/Logic/ExternalLogin/ExternalLoginUpLogic.cs +++ b/src/FoxIDs/Logic/ExternalLogin/ExternalLoginUpLogic.cs @@ -62,7 +62,7 @@ await sequenceLogic.SaveSequenceDataAsync(new ExternalLoginUpSequenceData Acr = loginRequest.Acr }); - return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.ExtLoginController, includeSequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.ExtLoginController, includeSequence: true).ToRedirectResult(); } public async Task LoginResponseAsync(ExternalLoginUpParty extLoginUpParty, List claims) { diff --git a/src/FoxIDs/Logic/ExternalLogin/ExternalLogoutUpLogic.cs b/src/FoxIDs/Logic/ExternalLogin/ExternalLogoutUpLogic.cs index a0e7fcce7..017ceee25 100644 --- a/src/FoxIDs/Logic/ExternalLogin/ExternalLogoutUpLogic.cs +++ b/src/FoxIDs/Logic/ExternalLogin/ExternalLogoutUpLogic.cs @@ -41,7 +41,7 @@ await sequenceLogic.SaveSequenceDataAsync(new ExternalLoginUpSequenceData PostLogoutRedirect = logoutRequest.PostLogoutRedirect }); - return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.ExtLoginController, Constants.Endpoints.Logout, includeSequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.ExtLoginController, Constants.Endpoints.Logout, includeSequence: true).ToRedirectResult(); } public async Task LogoutResponseAsync(ExternalLoginUpSequenceData sequenceData) diff --git a/src/FoxIDs/Logic/Link/TrackLinkAuthDownLogic.cs b/src/FoxIDs/Logic/Link/TrackLinkAuthDownLogic.cs index 686362a09..d1fec07ac 100644 --- a/src/FoxIDs/Logic/Link/TrackLinkAuthDownLogic.cs +++ b/src/FoxIDs/Logic/Link/TrackLinkAuthDownLogic.cs @@ -123,7 +123,7 @@ public async Task AuthResponseAsync(string partyId, List c sequenceData.ErrorDescription = errorDescription; await sequenceLogic.SaveSequenceDataAsync(sequenceData, setKeyValidUntil: true); - return HttpContext.GetTrackUpPartyUrl(party.ToUpTrackName, party.ToUpPartyName, Constants.Routes.TrackLinkController, Constants.Endpoints.TrackLinkAuthResponse, includeKeySequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetTrackUpPartyUrl(party.ToUpTrackName, party.ToUpPartyName, Constants.Routes.TrackLinkController, Constants.Endpoints.TrackLinkAuthResponse, includeKeySequence: true).ToRedirectResult(); } } } diff --git a/src/FoxIDs/Logic/Link/TrackLinkAuthUpLogic.cs b/src/FoxIDs/Logic/Link/TrackLinkAuthUpLogic.cs index a86731497..07cddb041 100644 --- a/src/FoxIDs/Logic/Link/TrackLinkAuthUpLogic.cs +++ b/src/FoxIDs/Logic/Link/TrackLinkAuthUpLogic.cs @@ -77,7 +77,7 @@ await sequenceLogic.SaveSequenceDataAsync(new TrackLinkUpSequenceData selectedUpParties = profile.SelectedUpParties; } - return HttpContext.GetTrackDownPartyUrl(party.ToDownTrackName, party.ToDownPartyName, party.SelectedUpParties, Constants.Routes.TrackLinkController, Constants.Endpoints.TrackLinkAuthRequest, includeKeySequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetTrackDownPartyUrl(party.ToDownTrackName, party.ToDownPartyName, party.SelectedUpParties, Constants.Routes.TrackLinkController, Constants.Endpoints.TrackLinkAuthRequest, includeKeySequence: true).ToRedirectResult(); } private TrackLinkUpPartyProfile GetProfile(TrackLinkUpParty party, string profileName) diff --git a/src/FoxIDs/Logic/Link/TrackLinkRpInitiatedLogoutDownLogic.cs b/src/FoxIDs/Logic/Link/TrackLinkRpInitiatedLogoutDownLogic.cs index 3b3083618..6c363bfbc 100644 --- a/src/FoxIDs/Logic/Link/TrackLinkRpInitiatedLogoutDownLogic.cs +++ b/src/FoxIDs/Logic/Link/TrackLinkRpInitiatedLogoutDownLogic.cs @@ -91,7 +91,7 @@ public async Task LogoutResponseAsync(string partyId) var sequenceData = await sequenceLogic.GetSequenceDataAsync(remove: false); await sequenceLogic.SaveSequenceDataAsync(sequenceData, setKeyValidUntil: true); - return HttpContext.GetTrackUpPartyUrl(party.ToUpTrackName, party.ToUpPartyName, Constants.Routes.TrackLinkController, Constants.Endpoints.TrackLinkRpLogoutResponse, includeKeySequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetTrackUpPartyUrl(party.ToUpTrackName, party.ToUpPartyName, Constants.Routes.TrackLinkController, Constants.Endpoints.TrackLinkRpLogoutResponse, includeKeySequence: true).ToRedirectResult(); } } } diff --git a/src/FoxIDs/Logic/Link/TrackLinkRpInitiatedLogoutUpLogic.cs b/src/FoxIDs/Logic/Link/TrackLinkRpInitiatedLogoutUpLogic.cs index 9c21dc2e2..cf6f8d5c9 100644 --- a/src/FoxIDs/Logic/Link/TrackLinkRpInitiatedLogoutUpLogic.cs +++ b/src/FoxIDs/Logic/Link/TrackLinkRpInitiatedLogoutUpLogic.cs @@ -52,7 +52,7 @@ await sequenceLogic.SaveSequenceDataAsync(new TrackLinkUpSequenceData RequireLogoutConsent = logoutRequest.RequireLogoutConsent }); - return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.TrackLinkController, Constants.Endpoints.UpJump.TrackLinkRpLogoutRequestJump, includeSequence: true, partyBindingPattern: party.PartyBindingPattern).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.TrackLinkController, Constants.Endpoints.UpJump.TrackLinkRpLogoutRequestJump, includeSequence: true, partyBindingPattern: party.PartyBindingPattern).ToRedirectResult(); } public async Task LogoutRequestAsync(string partyId) @@ -88,7 +88,7 @@ public async Task LogoutRequestAsync(string partyId) selectedUpParties = profile.SelectedUpParties; } - return HttpContext.GetTrackDownPartyUrl(party.ToDownTrackName, party.ToDownPartyName, selectedUpParties, Constants.Routes.TrackLinkController, Constants.Endpoints.TrackLinkRpLogoutRequest, includeKeySequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetTrackDownPartyUrl(party.ToDownTrackName, party.ToDownPartyName, selectedUpParties, Constants.Routes.TrackLinkController, Constants.Endpoints.TrackLinkRpLogoutRequest, includeKeySequence: true).ToRedirectResult(); } private TrackLinkUpPartyProfile GetProfile(TrackLinkUpParty party, TrackLinkUpSequenceData trackLinkUpSequenceData) diff --git a/src/FoxIDs/Logic/Login/LoginPageLogic.cs b/src/FoxIDs/Logic/Login/LoginPageLogic.cs index ebc5e1d89..c7306d2f5 100644 --- a/src/FoxIDs/Logic/Login/LoginPageLogic.cs +++ b/src/FoxIDs/Logic/Login/LoginPageLogic.cs @@ -81,21 +81,21 @@ public async Task LoginResponseSequenceAsync(LoginUpSequenceData if (fromStep <= LoginResponseSequenceSteps.FromEmailVerificationStep && user.ConfirmAccount && !user.EmailVerified) { await sequenceLogic.SaveSequenceDataAsync(sequenceData); - return HttpContext.GetUpPartyUrl(loginUpParty.Name, Constants.Routes.ActionController, Constants.Endpoints.EmailConfirmation, includeSequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(loginUpParty.Name, Constants.Routes.ActionController, Constants.Endpoints.EmailConfirmation, includeSequence: true).ToRedirectResult(); } else if (fromStep <= LoginResponseSequenceSteps.FromMfaStep && GetRequereMfa(user, loginUpParty, sequenceData)) { if (!user.EmailVerified) { await sequenceLogic.SaveSequenceDataAsync(sequenceData); - return HttpContext.GetUpPartyUrl(loginUpParty.Name, Constants.Routes.ActionController, Constants.Endpoints.EmailConfirmation, includeSequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(loginUpParty.Name, Constants.Routes.ActionController, Constants.Endpoints.EmailConfirmation, includeSequence: true).ToRedirectResult(); } if (RegisterTwoFactor(user)) { sequenceData.TwoFactorAppState = TwoFactorAppSequenceStates.DoRegistration; await sequenceLogic.SaveSequenceDataAsync(sequenceData); - return HttpContext.GetUpPartyUrl(loginUpParty.Name, Constants.Routes.MfaController, Constants.Endpoints.RegisterTwoFactor, includeSequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(loginUpParty.Name, Constants.Routes.MfaController, Constants.Endpoints.RegisterTwoFactor, includeSequence: true).ToRedirectResult(); } else { @@ -118,7 +118,7 @@ public async Task LoginResponseSequenceAsync(LoginUpSequenceData } sequenceData.TwoFactorAppState = TwoFactorAppSequenceStates.Validate; await sequenceLogic.SaveSequenceDataAsync(sequenceData); - return HttpContext.GetUpPartyUrl(loginUpParty.Name, Constants.Routes.MfaController, Constants.Endpoints.TwoFactor, includeSequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(loginUpParty.Name, Constants.Routes.MfaController, Constants.Endpoints.TwoFactor, includeSequence: true).ToRedirectResult(); } } else diff --git a/src/FoxIDs/Logic/Login/LoginUpLogic.cs b/src/FoxIDs/Logic/Login/LoginUpLogic.cs index 48da2d281..3630c5673 100644 --- a/src/FoxIDs/Logic/Login/LoginUpLogic.cs +++ b/src/FoxIDs/Logic/Login/LoginUpLogic.cs @@ -62,7 +62,7 @@ await sequenceLogic.SaveSequenceDataAsync(new LoginUpSequenceData DoLoginIdentifierStep = loginRequest.EmailHint.IsNullOrWhiteSpace() }); - return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.LoginController, includeSequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.LoginController, includeSequence: true).ToRedirectResult(); } public async Task LoginRedirectAsync(LoginRequest loginRequest) @@ -116,7 +116,7 @@ await sequenceLogic.SaveSequenceDataAsync(new LoginUpSequenceData DoLoginIdentifierStep = !(autoSelectedUpParty != null && autoSelectedUpParty.Name == loginName && !loginRequest.EmailHint.IsNullOrWhiteSpace()) }); - return HttpContext.GetUpPartyUrl(loginName, Constants.Routes.LoginController, includeSequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(loginName, Constants.Routes.LoginController, includeSequence: true).ToRedirectResult(); } } diff --git a/src/FoxIDs/Logic/Login/LogoutUpLogic.cs b/src/FoxIDs/Logic/Login/LogoutUpLogic.cs index 0d3a06e04..10f0a7478 100644 --- a/src/FoxIDs/Logic/Login/LogoutUpLogic.cs +++ b/src/FoxIDs/Logic/Login/LogoutUpLogic.cs @@ -41,7 +41,7 @@ await sequenceLogic.SaveSequenceDataAsync(new LoginUpSequenceData PostLogoutRedirect = logoutRequest.PostLogoutRedirect }); - return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.LoginController, Constants.Endpoints.Logout, includeSequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.LoginController, Constants.Endpoints.Logout, includeSequence: true).ToRedirectResult(); } public async Task LogoutResponseAsync(LoginUpSequenceData sequenceData) diff --git a/src/FoxIDs/Logic/Login/SingleLogoutDownLogic.cs b/src/FoxIDs/Logic/Login/SingleLogoutDownLogic.cs index ad7bf0854..36c532e9d 100644 --- a/src/FoxIDs/Logic/Login/SingleLogoutDownLogic.cs +++ b/src/FoxIDs/Logic/Login/SingleLogoutDownLogic.cs @@ -125,15 +125,15 @@ private async Task ResponseUpPartyAsync(string upPartyName, Party switch (upPartyType) { case PartyTypes.Login: - return HttpContext.GetUpPartyUrl(upPartyName, Constants.Routes.LoginController, Constants.Endpoints.SingleLogoutDone, includeSequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(upPartyName, Constants.Routes.LoginController, Constants.Endpoints.SingleLogoutDone, includeSequence: true).ToRedirectResult(); case PartyTypes.Oidc: var oidcUpParty = await tenantDataRepository.GetAsync(partyId); - return HttpContext.GetUpPartyUrl(upPartyName, Constants.Routes.OAuthController, Constants.Endpoints.SingleLogoutDone, includeSequence: true, partyBindingPattern: oidcUpParty.PartyBindingPattern).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(upPartyName, Constants.Routes.OAuthController, Constants.Endpoints.SingleLogoutDone, includeSequence: true, partyBindingPattern: oidcUpParty.PartyBindingPattern).ToRedirectResult(); case PartyTypes.Saml2: var samlUpParty = await tenantDataRepository.GetAsync(partyId); - return HttpContext.GetUpPartyUrl(upPartyName, Constants.Routes.SamlController, Constants.Endpoints.SingleLogoutDone, includeSequence: true, partyBindingPattern: samlUpParty.PartyBindingPattern).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(upPartyName, Constants.Routes.SamlController, Constants.Endpoints.SingleLogoutDone, includeSequence: true, partyBindingPattern: samlUpParty.PartyBindingPattern).ToRedirectResult(); case PartyTypes.TrackLink: - return HttpContext.GetUpPartyUrl(upPartyName, Constants.Routes.TrackLinkController, Constants.Endpoints.SingleLogoutDone, includeSequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(upPartyName, Constants.Routes.TrackLinkController, Constants.Endpoints.SingleLogoutDone, includeSequence: true).ToRedirectResult(); default: throw new NotSupportedException(); diff --git a/src/FoxIDs/Logic/Oidc/OidcAuthDownLogic.cs b/src/FoxIDs/Logic/Oidc/OidcAuthDownLogic.cs index 2796ea38e..2d8bea450 100644 --- a/src/FoxIDs/Logic/Oidc/OidcAuthDownLogic.cs +++ b/src/FoxIDs/Logic/Oidc/OidcAuthDownLogic.cs @@ -301,11 +301,11 @@ public async Task AuthenticationResponseAsync(string partyId, Lis switch (responseMode) { case IdentityConstants.ResponseModes.FormPost: - return await nameValueCollection.ToHtmlPostContentResultAsync(sequenceData.RedirectUri, RouteBinding.DisplayName); + return sequenceData.RedirectUri.ToHtmlPostContentResult(nameValueCollection); case IdentityConstants.ResponseModes.Query: - return await nameValueCollection.ToRedirectResultAsync(sequenceData.RedirectUri, RouteBinding.DisplayName); + return sequenceData.RedirectUri.ToRedirectResult(nameValueCollection); case IdentityConstants.ResponseModes.Fragment: - return await nameValueCollection.ToFragmentResultAsync(sequenceData.RedirectUri, RouteBinding.DisplayName); + return sequenceData.RedirectUri.ToFragmentResult(nameValueCollection); default: throw new NotSupportedException(); @@ -425,7 +425,7 @@ private async Task AuthenticationResponseErrorAsync(bool restrict { securityHeaderLogic.AddFormActionAllowAll(); } - return await nameValueCollection.ToRedirectResultAsync(redirectUri, RouteBinding.DisplayName); + return redirectUri.ToRedirectResult(nameValueCollection); } } } diff --git a/src/FoxIDs/Logic/Oidc/OidcAuthUpLogic.cs b/src/FoxIDs/Logic/Oidc/OidcAuthUpLogic.cs index 0bc27687a..861e14b25 100644 --- a/src/FoxIDs/Logic/Oidc/OidcAuthUpLogic.cs +++ b/src/FoxIDs/Logic/Oidc/OidcAuthUpLogic.cs @@ -85,7 +85,7 @@ public async Task AuthenticationRequestRedirectAsync(UpPartyLink }; await sequenceLogic.SaveSequenceDataAsync(oidcUpSequenceData); - return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.OAuthUpJumpController, Constants.Endpoints.UpJump.AuthenticationRequest, includeSequence: true, partyBindingPattern: party.PartyBindingPattern).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.OAuthUpJumpController, Constants.Endpoints.UpJump.AuthenticationRequest, includeSequence: true, partyBindingPattern: party.PartyBindingPattern).ToRedirectResult(); } public async Task AuthenticationRequestAsync(string partyId) @@ -220,7 +220,7 @@ public async Task AuthenticationRequestAsync(string partyId) logger.ScopeTrace(() => $"AuthMethod, Authentication request URL '{party.Client.AuthorizeUrl}'."); logger.ScopeTrace(() => "AuthMethod, Sending OIDC Authentication request.", triggerEvent: true); - return await nameValueCollection.ToRedirectResultAsync(party.Client.AuthorizeUrl, RouteBinding.DisplayName); + return party.Client.AuthorizeUrl.ToRedirectResult(nameValueCollection); } private OAuthUpPartyProfile GetProfile(TParty party, OidcUpSequenceData oidcUpSequenceData) diff --git a/src/FoxIDs/Logic/Oidc/OidcRpInitiatedLogoutDownLogic.cs b/src/FoxIDs/Logic/Oidc/OidcRpInitiatedLogoutDownLogic.cs index 603254c12..5aff27c3b 100644 --- a/src/FoxIDs/Logic/Oidc/OidcRpInitiatedLogoutDownLogic.cs +++ b/src/FoxIDs/Logic/Oidc/OidcRpInitiatedLogoutDownLogic.cs @@ -238,7 +238,7 @@ public async Task EndSessionResponseAsync(string partyId) { securityHeaderLogic.AddFormActionAllowAll(); } - return await nameValueCollection.ToRedirectResultAsync(sequenceData.RedirectUri, RouteBinding.DisplayName); + return sequenceData.RedirectUri.ToRedirectResult(nameValueCollection); } } } diff --git a/src/FoxIDs/Logic/Oidc/OidcRpInitiatedLogoutUpLogic.cs b/src/FoxIDs/Logic/Oidc/OidcRpInitiatedLogoutUpLogic.cs index 7f7bf9952..092da67b1 100644 --- a/src/FoxIDs/Logic/Oidc/OidcRpInitiatedLogoutUpLogic.cs +++ b/src/FoxIDs/Logic/Oidc/OidcRpInitiatedLogoutUpLogic.cs @@ -58,7 +58,7 @@ await sequenceLogic.SaveSequenceDataAsync(new OidcUpSequenceData ClientId = ResolveClientId(party) }); - return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.OAuthUpJumpController, Constants.Endpoints.UpJump.EndSessionRequest, includeSequence: true, partyBindingPattern: party.PartyBindingPattern).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.OAuthUpJumpController, Constants.Endpoints.UpJump.EndSessionRequest, includeSequence: true, partyBindingPattern: party.PartyBindingPattern).ToRedirectResult(); } public async Task EndSessionRequestAsync(string partyId) @@ -132,7 +132,7 @@ public async Task EndSessionRequestAsync(string partyId) logger.ScopeTrace(() => $"AuthMethod, End session request URL '{party.Client.EndSessionUrl}'."); logger.ScopeTrace(() => "AuthMethod, Sending OIDC End session request.", triggerEvent: true); - return await nameValueCollection.ToRedirectResultAsync(party.Client.EndSessionUrl, RouteBinding.DisplayName); + return party.Client.EndSessionUrl.ToRedirectResult(nameValueCollection); } private void ValidatePartyLogoutSupport(OidcUpParty party) diff --git a/src/FoxIDs/Logic/Saml/SamlAuthnDownLogic.cs b/src/FoxIDs/Logic/Saml/SamlAuthnDownLogic.cs index 6d4ea4bcf..2310e60c6 100644 --- a/src/FoxIDs/Logic/Saml/SamlAuthnDownLogic.cs +++ b/src/FoxIDs/Logic/Saml/SamlAuthnDownLogic.cs @@ -262,7 +262,7 @@ private async Task AuthnResponseAsync(SamlDownParty party, Saml2C } binding.Bind(saml2AuthnResponse); - var actionResult = await GetAuthnResponseActionResult(binding); + var actionResult = binding.ToSamlActionResult(); if (samlConfig.EncryptionCertificate != null) { // Re-bind to log unencrypted XML. @@ -283,21 +283,5 @@ private async Task AuthnResponseAsync(SamlDownParty party, Saml2C } return actionResult; } - - private static async Task GetAuthnResponseActionResult(Saml2Binding binding) - { - if (binding is Saml2RedirectBinding saml2RedirectBinding) - { - return await saml2RedirectBinding.ToActionFormResultAsync(); - } - else if (binding is Saml2PostBinding saml2PostBinding) - { - return await saml2PostBinding.ToActionFormResultAsync(); - } - else - { - throw new NotSupportedException(); - } - } } } diff --git a/src/FoxIDs/Logic/Saml/SamlAuthnUpLogic.cs b/src/FoxIDs/Logic/Saml/SamlAuthnUpLogic.cs index dd3a8d5c7..52fe50386 100644 --- a/src/FoxIDs/Logic/Saml/SamlAuthnUpLogic.cs +++ b/src/FoxIDs/Logic/Saml/SamlAuthnUpLogic.cs @@ -82,7 +82,7 @@ await sequenceLogic.SaveSequenceDataAsync(new SamlUpSequenceData LoginEmailHint = loginRequest.EmailHint }); - return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.SamlUpJumpController, Constants.Endpoints.UpJump.AuthnRequest, includeSequence: true, partyBindingPattern: party.PartyBindingPattern).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.SamlUpJumpController, Constants.Endpoints.UpJump.AuthnRequest, includeSequence: true, partyBindingPattern: party.PartyBindingPattern).ToRedirectResult(); } public async Task AuthnRequestAsync(string partyId) @@ -194,18 +194,7 @@ private async Task AuthnRequestAsync(SamlUpParty party, Saml2Bind securityHeaderLogic.AddFormActionAllowAll(); - if (binding is Saml2RedirectBinding saml2RedirectBinding) - { - return await saml2RedirectBinding.ToActionFormResultAsync(); - } - else if (binding is Saml2PostBinding saml2PostBinding) - { - return await saml2PostBinding.ToActionFormResultAsync(); - } - else - { - throw new NotSupportedException(); - } + return binding.ToSamlActionResult(); } private SamlUpPartyProfile GetProfile(SamlUpParty party, SamlUpSequenceData samlUpSequenceData) diff --git a/src/FoxIDs/Logic/Saml/SamlLogoutDownLogic.cs b/src/FoxIDs/Logic/Saml/SamlLogoutDownLogic.cs index fa47a6e2b..ed80ff397 100644 --- a/src/FoxIDs/Logic/Saml/SamlLogoutDownLogic.cs +++ b/src/FoxIDs/Logic/Saml/SamlLogoutDownLogic.cs @@ -228,18 +228,7 @@ private async Task LogoutResponseAsync(SamlDownParty party, Saml2 { securityHeaderLogic.AddFormActionAllowAll(); } - if (binding is Saml2RedirectBinding saml2RedirectBinding) - { - return await saml2RedirectBinding.ToActionFormResultAsync(); - } - if (binding is Saml2PostBinding saml2PostBinding) - { - return await saml2PostBinding.ToActionFormResultAsync(); - } - else - { - throw new NotSupportedException(); - } + return binding.ToSamlActionResult(); } public async Task SingleLogoutRequestAsync(string partyId, SingleLogoutSequenceData sequenceData) @@ -301,18 +290,7 @@ private async Task SingleLogoutRequestAsync(SamlDownParty party, { securityHeaderLogic.AddFormActionAllowAll(); } - if (binding is Saml2RedirectBinding saml2RedirectBinding) - { - return await saml2RedirectBinding.ToActionFormResultAsync(); - } - if (binding is Saml2PostBinding saml2PostBinding) - { - return await saml2PostBinding.ToActionFormResultAsync(); - } - else - { - throw new NotSupportedException(); - } + return binding.ToSamlActionResult(); } public async Task SingleLogoutResponseAsync(string partyId, Saml2Http.HttpRequest samlHttpRequest) diff --git a/src/FoxIDs/Logic/Saml/SamlLogoutUpLogic.cs b/src/FoxIDs/Logic/Saml/SamlLogoutUpLogic.cs index 311640e6e..c7c8d0eea 100644 --- a/src/FoxIDs/Logic/Saml/SamlLogoutUpLogic.cs +++ b/src/FoxIDs/Logic/Saml/SamlLogoutUpLogic.cs @@ -64,7 +64,7 @@ await sequenceLogic.SaveSequenceDataAsync(new SamlUpSequenceData PostLogoutRedirect = logoutRequest.PostLogoutRedirect }); - return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.SamlUpJumpController, Constants.Endpoints.UpJump.LogoutRequest, includeSequence: true, partyBindingPattern: party.PartyBindingPattern).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(partyLink.Name, Constants.Routes.SamlUpJumpController, Constants.Endpoints.UpJump.LogoutRequest, includeSequence: true, partyBindingPattern: party.PartyBindingPattern).ToRedirectResult(); } public async Task LogoutRequestAsync(string partyId) @@ -179,18 +179,7 @@ private async Task LogoutRequestAsync(SamlUpParty party, Saml2Bin securityHeaderLogic.AddFormActionAllowAll(); - if (binding is Saml2RedirectBinding saml2RedirectBinding) - { - return await saml2RedirectBinding.ToActionFormResultAsync(); - } - else if (binding is Saml2PostBinding saml2PostBinding) - { - return await saml2PostBinding.ToActionFormResultAsync(); - } - else - { - throw new NotSupportedException(); - } + return binding.ToSamlActionResult(); } public async Task LogoutResponseAsync(string partyId, Saml2Http.HttpRequest samlHttpRequest) @@ -413,7 +402,7 @@ private async Task SingleLogoutRequestAsync(SamlUpParty party, Sa if (samlHttpRequest.Binding is Saml2PostBinding) { - return HttpContext.GetUpPartyUrl(party.Name, Constants.Routes.SamlController, Constants.Endpoints.UpJump.SingleLogoutRequestJump, includeSequence: true, partyBindingPattern: party.PartyBindingPattern).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(party.Name, Constants.Routes.SamlController, Constants.Endpoints.UpJump.SingleLogoutRequestJump, includeSequence: true, partyBindingPattern: party.PartyBindingPattern).ToRedirectResult(); } else { @@ -519,18 +508,7 @@ private async Task LogoutResponseAsync(Saml2Configuration samlCon await sequenceLogic.RemoveSequenceDataAsync(); securityHeaderLogic.AddFormActionAllowAll(); - if (binding is Saml2RedirectBinding saml2RedirectBinding) - { - return await saml2RedirectBinding.ToActionFormResultAsync(); - } - if (binding is Saml2PostBinding saml2PostBinding) - { - return await saml2PostBinding.ToActionFormResultAsync(); - } - else - { - throw new NotSupportedException(); - } + return binding.ToSamlActionResult(); } } } diff --git a/src/FoxIDs/Logic/Tracks/AccountLogic.cs b/src/FoxIDs/Logic/Tracks/AccountLogic.cs index 24eb1c84b..9dfc78057 100644 --- a/src/FoxIDs/Logic/Tracks/AccountLogic.cs +++ b/src/FoxIDs/Logic/Tracks/AccountLogic.cs @@ -65,7 +65,7 @@ public async Task ValidateUser(string email, string password) } } - public async Task ChangePasswordUser(string email, string currentPassword, string newPassword) + public override async Task ChangePasswordUser(string email, string currentPassword, string newPassword) { email = email?.ToLowerInvariant(); logger.ScopeTrace(() => $"Change password user '{email}', Route '{RouteBinding?.Route}'."); @@ -109,23 +109,5 @@ public async Task ChangePasswordUser(string email, string currentPassword, throw new InvalidPasswordException($"Current password invalid, user '{email}'."); } } - - public async Task SetPasswordUser(User user, string newPassword) - { - logger.ScopeTrace(() => $"Set password user '{user.Email}', Route '{RouteBinding?.Route}'."); - - if (user.DisableAccount) - { - throw new UserNotExistsException($"User '{user.Email}' is disabled, trying to set password."); - } - - await ValidatePasswordPolicy(user.Email, newPassword); - - await secretHashLogic.AddSecretHashAsync(user, newPassword); - user.ChangePassword = false; - await tenantDataRepository.SaveAsync(user); - - logger.ScopeTrace(() => $"User '{user.Email}', password set.", triggerEvent: true); - } } } diff --git a/src/FoxIDs/Logic/Tracks/ExternalClaimsConnectLogic.cs b/src/FoxIDs/Logic/Tracks/ExternalClaimsConnectLogic.cs index 45b76fafc..3ecb51d6c 100644 --- a/src/FoxIDs/Logic/Tracks/ExternalClaimsConnectLogic.cs +++ b/src/FoxIDs/Logic/Tracks/ExternalClaimsConnectLogic.cs @@ -3,7 +3,6 @@ using ITfoxtec.Identity; using ITfoxtec.Identity.Util; using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; @@ -11,11 +10,8 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; -using System.Text; using System.Threading.Tasks; using Ext = FoxIDs.Models.External; -using System.Net.Mime; -using FoxIDs.Util; using System.Net.Http.Json; namespace FoxIDs.Logic diff --git a/src/FoxIDs/Logic/Tracks/ExternalUserLogic.cs b/src/FoxIDs/Logic/Tracks/ExternalUserLogic.cs index bd2c0f9ae..442174a1f 100644 --- a/src/FoxIDs/Logic/Tracks/ExternalUserLogic.cs +++ b/src/FoxIDs/Logic/Tracks/ExternalUserLogic.cs @@ -33,26 +33,46 @@ public ExternalUserLogic(TelemetryScopedLogger logger, ITenantDataRepository ten public async Task<(IActionResult externalUserActionResult, IEnumerable externalUserClaims)> HandleUserAsync(UpPartyWithExternalUser party, IEnumerable claims, Action populateSequenceDataAction, Action requireUserExceptionAction) where TProfile : UpPartyProfile { - if (string.IsNullOrWhiteSpace(party.LinkExternalUser?.LinkClaimType)) + if (party.LinkExternalUser == null || (party.LinkExternalUser.LinkClaimType.IsNullOrWhiteSpace() && party.LinkExternalUser.RedemptionClaimType.IsNullOrWhiteSpace())) { return (null, null); } - var linkClaimType = party.LinkExternalUser.LinkClaimType; - if (party.Type == PartyTypes.Saml2) - { - var jwtLinkClaimTypes = claimsDownLogic.FromSamlToJwtInfoClaimType(linkClaimType); - if (jwtLinkClaimTypes.Count() > 0) - { - linkClaimType = jwtLinkClaimTypes.First(); - } - } - - var linkClaimValue = GetLinkClaim(linkClaimType, claims); + var linkClaimValue = GetLinkClaim(GetJwtClaimType(party, party.LinkExternalUser.LinkClaimType), claims); logger.ScopeTrace(() => $"Validating external user, link claim type '{party.LinkExternalUser.LinkClaimType}' and value '{linkClaimValue}', Route '{RouteBinding?.Route}'."); if (!linkClaimValue.IsNullOrWhiteSpace()) { var externalUser = await tenantDataRepository.GetAsync(await ExternalUser.IdFormatAsync(RouteBinding, party.Name, await linkClaimValue.HashIdStringAsync()), required: false); + + if (externalUser == null && !party.LinkExternalUser.RedemptionClaimType.IsNullOrWhiteSpace()) + { + var redemptionClaimValue = GetLinkClaim(GetJwtClaimType(party, party.LinkExternalUser.RedemptionClaimType), claims); + logger.ScopeTrace(() => $"Validating external user, redemption claim type '{party.LinkExternalUser.RedemptionClaimType}' and value '{redemptionClaimValue}', Route '{RouteBinding?.Route}'."); + if (!redemptionClaimValue.IsNullOrWhiteSpace()) + { + externalUser = await tenantDataRepository.GetAsync(await ExternalUser.IdFormatAsync(RouteBinding, party.Name, await redemptionClaimValue.HashIdStringAsync()), required: false); + if (externalUser != null) + { + // Change to use a link claim type instead of redemption claim type. + await tenantDataRepository.DeleteAsync(externalUser.Id); + externalUser.Id = await ExternalUser.IdFormatAsync(RouteBinding, party.Name, await linkClaimValue.HashIdStringAsync()); + externalUser.LinkClaimValue = linkClaimValue; + await tenantDataRepository.CreateAsync(externalUser); + } + } + else + { + try + { + throw new EndpointException($"External user, redemption claim value is empty for link claim type '{party.LinkExternalUser.RedemptionClaimType}'.") { RouteBinding = RouteBinding }; + } + catch (Exception ex) + { + logger.Warning(ex); + } + } + } + if (externalUser != null) { if (!externalUser.DisableAccount) @@ -80,7 +100,7 @@ public ExternalUserLogic(TelemetryScopedLogger logger, ITenantDataRepository ten if (party.LinkExternalUser.RequireUser) { - requireUserExceptionAction($"Require external user for link claim type '{party.LinkExternalUser.LinkClaimType}' and value '{linkClaimValue}'."); + requireUserExceptionAction($"Require external user for link claim type '{party.LinkExternalUser.LinkClaimType}' and value '{linkClaimValue}'{(party.LinkExternalUser.RedemptionClaimType.IsNullOrWhiteSpace() ? string.Empty : $" or redemption claim type '{party.LinkExternalUser.RedemptionClaimType}'")}."); } } else @@ -98,6 +118,19 @@ public ExternalUserLogic(TelemetryScopedLogger logger, ITenantDataRepository ten return (null, null); } + private string GetJwtClaimType(UpPartyWithExternalUser party, string claimType) where TProfile : UpPartyProfile + { + if (party.Type == PartyTypes.Saml2) + { + var jwtLinkClaimTypes = claimsDownLogic.FromSamlToJwtInfoClaimType(claimType); + if (jwtLinkClaimTypes.Count() > 0) + { + return jwtLinkClaimTypes.First(); + } + } + return claimType; + } + public async Task> CreateUserAsync(UpPartyWithExternalUser upParty, string linkClaimValue, IEnumerable dynamicElementClaims = null) where TProfile : UpPartyProfile { logger.ScopeTrace(() => $"Creating external user, link claim value '{linkClaimValue}', Route '{RouteBinding?.Route}'."); @@ -133,7 +166,7 @@ private async Task StartUICreateUserAsync(UpPartyWithEx }; populateSequenceDataAction(sequenceData); await sequenceLogic.SaveSequenceDataAsync(sequenceData); - return HttpContext.GetUpPartyUrl(party.Name, Constants.Routes.ExtController, Constants.Endpoints.CreateUser, includeSequence: true).ToRedirectResult(RouteBinding.DisplayName); + return HttpContext.GetUpPartyUrl(party.Name, Constants.Routes.ExtController, Constants.Endpoints.CreateUser, includeSequence: true).ToRedirectResult(); } private List GetExternalUserClaim(UpPartyWithExternalUser party, ExternalUser externalUser) where TProfile : UpPartyProfile diff --git a/src/FoxIDs.ResourceTranslateTool/FoxIDs.ResourceTranslateTool.csproj b/tools/FoxIDs.ResourceTranslateTool/FoxIDs.ResourceTranslateTool.csproj similarity index 100% rename from src/FoxIDs.ResourceTranslateTool/FoxIDs.ResourceTranslateTool.csproj rename to tools/FoxIDs.ResourceTranslateTool/FoxIDs.ResourceTranslateTool.csproj diff --git a/src/FoxIDs.ResourceTranslateTool/Infrastructure/StartupConfigure.cs b/tools/FoxIDs.ResourceTranslateTool/Infrastructure/StartupConfigure.cs similarity index 100% rename from src/FoxIDs.ResourceTranslateTool/Infrastructure/StartupConfigure.cs rename to tools/FoxIDs.ResourceTranslateTool/Infrastructure/StartupConfigure.cs diff --git a/src/FoxIDs.ResourceTranslateTool/Logic/DeepLTranslateLogic.cs b/tools/FoxIDs.ResourceTranslateTool/Logic/DeepLTranslateLogic.cs similarity index 100% rename from src/FoxIDs.ResourceTranslateTool/Logic/DeepLTranslateLogic.cs rename to tools/FoxIDs.ResourceTranslateTool/Logic/DeepLTranslateLogic.cs diff --git a/src/FoxIDs.ResourceTranslateTool/Logic/GoogleTranslateLogic.cs b/tools/FoxIDs.ResourceTranslateTool/Logic/GoogleTranslateLogic.cs similarity index 100% rename from src/FoxIDs.ResourceTranslateTool/Logic/GoogleTranslateLogic.cs rename to tools/FoxIDs.ResourceTranslateTool/Logic/GoogleTranslateLogic.cs diff --git a/src/FoxIDs.ResourceTranslateTool/Logic/ResourceLogic.cs b/tools/FoxIDs.ResourceTranslateTool/Logic/ResourceLogic.cs similarity index 100% rename from src/FoxIDs.ResourceTranslateTool/Logic/ResourceLogic.cs rename to tools/FoxIDs.ResourceTranslateTool/Logic/ResourceLogic.cs diff --git a/src/FoxIDs.ResourceTranslateTool/Models/TranslateSettings.cs b/tools/FoxIDs.ResourceTranslateTool/Models/TranslateSettings.cs similarity index 100% rename from src/FoxIDs.ResourceTranslateTool/Models/TranslateSettings.cs rename to tools/FoxIDs.ResourceTranslateTool/Models/TranslateSettings.cs diff --git a/src/FoxIDs.ResourceTranslateTool/Program.cs b/tools/FoxIDs.ResourceTranslateTool/Program.cs similarity index 100% rename from src/FoxIDs.ResourceTranslateTool/Program.cs rename to tools/FoxIDs.ResourceTranslateTool/Program.cs diff --git a/src/FoxIDs.ResourceTranslateTool/README.md b/tools/FoxIDs.ResourceTranslateTool/README.md similarity index 100% rename from src/FoxIDs.ResourceTranslateTool/README.md rename to tools/FoxIDs.ResourceTranslateTool/README.md diff --git a/src/FoxIDs.ResourceTranslateTool/appsettings.json b/tools/FoxIDs.ResourceTranslateTool/appsettings.json similarity index 100% rename from src/FoxIDs.ResourceTranslateTool/appsettings.json rename to tools/FoxIDs.ResourceTranslateTool/appsettings.json