diff --git a/FoxIDs.sln b/FoxIDs.sln index debce1aa5..6eb32cf55 100644 --- a/FoxIDs.sln +++ b/FoxIDs.sln @@ -54,6 +54,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{CB5D86A0-D docs\faq.md = docs\faq.md docs\getting-started.md = docs\getting-started.md docs\howto-saml-2.0-context-handler.md = docs\howto-saml-2.0-context-handler.md + docs\howto-oidc-foxids.md = docs\howto-oidc-foxids.md + docs\howto-connect.md = docs\howto-connect.md + docs\howto-tracklink-foxids.md = docs\howto-tracklink-foxids.md docs\index.md = docs\index.md docs\language.md = docs\language.md docs\logging.md = docs\logging.md @@ -71,14 +74,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{CB5D86A0-D docs\standard-support.md = docs\standard-support.md docs\up-party-howto-oidc-azure-ad-b2c.md = docs\up-party-howto-oidc-azure-ad-b2c.md docs\up-party-howto-oidc-azure-ad.md = docs\up-party-howto-oidc-azure-ad.md - docs\up-party-howto-oidc-foxids.md = docs\up-party-howto-oidc-foxids.md docs\up-party-howto-oidc-identityserver.md = docs\up-party-howto-oidc-identityserver.md docs\up-party-howto-oidc-nets-eid-broker.md = docs\up-party-howto-oidc-nets-eid-broker.md docs\up-party-howto-oidc-signicat.md = docs\up-party-howto-oidc-signicat.md docs\up-party-howto-saml-2.0-adfs.md = docs\up-party-howto-saml-2.0-adfs.md docs\up-party-howto-saml-2.0-nemlogin.md = docs\up-party-howto-saml-2.0-nemlogin.md docs\up-party-howto-saml-2.0-pingone.md = docs\up-party-howto-saml-2.0-pingone.md - docs\up-party-howto.md = docs\up-party-howto.md docs\up-party-oidc.md = docs\up-party-oidc.md docs\up-party-saml-2.0.md = docs\up-party-saml-2.0.md docs\update.md = docs\update.md @@ -166,6 +167,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{CB8812 docs\images\howto-saml-nemlogin3-up-privilege-claim-tf.png = docs\images\howto-saml-nemlogin3-up-privilege-claim-tf.png docs\images\howto-saml-nemlogin3-up-read-metadata.png = docs\images\howto-saml-nemlogin3-up-read-metadata.png docs\images\howto-saml-nemlogin3-up-top.png = docs\images\howto-saml-nemlogin3-up-top.png + docs\images\howto-tracklink-foxids-down-party.png = docs\images\howto-tracklink-foxids-down-party.png + docs\images\howto-tracklink-foxids-up-party.png = docs\images\howto-tracklink-foxids-up-party.png docs\images\master-tenant2.png = docs\images\master-tenant2.png docs\images\parties-down-party-oauth.svg = docs\images\parties-down-party-oauth.svg docs\images\parties-down-party-oidc.svg = docs\images\parties-down-party-oidc.svg diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 94fc9b6d9..3c0de6e2d 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -2,7 +2,7 @@ - [Getting Started](getting-started.md) - [Parties](parties.md) - [Login & HRD & 2FA/MFA](login.md) - - [How to connect IdP](up-party-howto.md) + - [How to connect](howto-connect.md) - [OpenID Connect](oidc.md) - [OAuth 2.0](oauth-2.0.md) - [SAML 2.0](saml-2.0.md) diff --git a/docs/deployment.md b/docs/deployment.md index 037860898..e6adacfd6 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -51,11 +51,11 @@ You can increment the password security level by uploading risk passwords. You can upload risk passwords with the FoxIDs seed tool console application. The seed tool code is [downloaded](https://github.com/ITfoxtec/FoxIDs/tree/master/tools/FoxIDs.SeedTool) and need to be compiled and [configured](#configure-the-seed-tool) to run. -Download the `SHA-1` pwned passwords `ordered by prevalence` from [haveibeenpwned.com/passwords](https://haveibeenpwned.com/Passwords). +Download the `SHA-1` pwned passwords in a single file from [haveibeenpwned.com/passwords](https://haveibeenpwned.com/Passwords) using the [PwnedPasswordsDownloader tool](https://github.com/HaveIBeenPwned/PwnedPasswordsDownloader). > Be aware that it takes some time to upload all risk passwords. This step can be omitted and postponed to later. -The risk passwords are uploaded as bulk which has a higher consumption. Please make sure to adjust the Cosmos DB provisioned throughput (e.g. to 20000 RU/s or higher) temporarily. +The risk passwords are uploaded as bulk which has a higher consumption. Please make sure to adjust the Cosmos DB provisioned throughput (e.g. to 4000 RU/s or higher) temporarily. The throughput can be adjusted in Azure Cosmos DB --> Data Explorer --> Scale & Settings. You can read the number of risk passwords uploaded to FoxIDs in [FoxIDs Control Client](control.md#foxids-control-client) master tenant on the Risk Passwords tap. And you can test if a password is okay or has appeared in breaches. diff --git a/docs/down-party-oidc.md b/docs/down-party-oidc.md index 8ea799fa6..e07e4bf7d 100644 --- a/docs/down-party-oidc.md +++ b/docs/down-party-oidc.md @@ -22,6 +22,10 @@ There can be configured a maximum of 10 secrets per client. FoxIDs support the OpenID Connect [UserInfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo). +How to guides: + +- Connect two FoxIDs tracks in the same or different tenants with [OpenID connect](howto-oidc-foxids.md) + > It is recommended to use OpenID Connect Authorization Code flow with PKCE, because it is considered a secure flow. ## Require multi-factor authentication (MFA) diff --git a/docs/up-party-howto.md b/docs/howto-connect.md similarity index 65% rename from docs/up-party-howto.md rename to docs/howto-connect.md index afee3c501..0b3e1ca89 100644 --- a/docs/up-party-howto.md +++ b/docs/howto-connect.md @@ -1,23 +1,31 @@ -# Up-party - How to connect Identity Provider (IdP) +# How to connect + +An [IdP is connected](#up-party---how-to-connect-identity-provider-idp) with a [up-party](parties.md#up-party) and an [application or API is connected]() with a [down-party](parties.md#down-party). + +It is possible to interconnect FoxIDs tracks either with a track link or OpenID Connect: + +- Connect two FoxIDs tracks in a tenant with a [track link](howto-tracklink-foxids.md) +- Connect two FoxIDs tracks in the same or different tenants with [OpenID connect](howto-oidc-foxids.md) + +## Up-party - How to connect Identity Provider (IdP) An Identity Provider (IdP) can be connected with an [OpenID Connect up-party](#openid-connect-up-party) or an [SAML 2.0 up-party](#saml-20-up-party). An Identity Provider (IdP) is more precisely called an OpenID Provider (OP) if configured with OpenID Connect. All IdPs supporting either OpenID Connect or SAML 2.0 can be connected to FoxIDs. The following is how to guides for some IdPs, more guides will be added over time. -## OpenID Connect up-party +### OpenID Connect up-party Configure [OpenID Connect up-party](up-party-oidc.md) which trust an external OpenID Provider (OP) - *an Identity Provider (IdP) is called an OpenID Provider (OP) if configured with OpenID Connect*. How to guides: -- Connect [FoxIDs](up-party-howto-oidc-foxids.md) between tracks, optionally in different tenants - Connect [Azure AD](up-party-howto-oidc-azure-ad.md) - Connect [Azure AD B2C](up-party-howto-oidc-azure-ad-b2c.md) - Connect [IdentityServer](up-party-howto-oidc-identityserver.md) - Connect [Signicat](up-party-howto-oidc-signicat.md) - Connect [Nets eID Broker](up-party-howto-oidc-nets-eid-broker.md) -## SAML 2.0 up-party +### SAML 2.0 up-party Configure [SAML 2.0 up-party](up-party-saml-2.0.md) which trust an external SAML 2.0 Identity Provider (IdP). @@ -26,4 +34,7 @@ How to guides: - Connect [AD FS](up-party-howto-saml-2.0-adfs.md) - Connect [PingIdentity / PingOne](up-party-howto-saml-2.0-pingone.md) - Connect [NemLog-in (Danish IdP)](up-party-howto-saml-2.0-nemlogin.md) -- Connect [Context Handler (Danish IdP)](howto-saml-2.0-context-handler.md#up-party---connect-to-context-handler) \ No newline at end of file +- Connect [Context Handler (Danish IdP)](howto-saml-2.0-context-handler.md#up-party---connect-to-context-handler) + +## Up-party - How to connect relying party (RP) +// TODO diff --git a/docs/up-party-howto-oidc-foxids.md b/docs/howto-oidc-foxids.md similarity index 94% rename from docs/up-party-howto-oidc-foxids.md rename to docs/howto-oidc-foxids.md index e099c9e9a..0cc0a8405 100644 --- a/docs/up-party-howto-oidc-foxids.md +++ b/docs/howto-oidc-foxids.md @@ -1,7 +1,9 @@ # Interconnect FoxIDs with OpenID Connect FoxIDs can be connected to another FoxIDs with OpenID Connect and thereby authenticating end users in another FoxIDs track or an external Identity Provider (IdP) configured as an up-party. -FoxIDs tracks can be interconnect in the same FoxIDs tenant or in different FoxIDs tenants. Interconnect can also be configured between FoxIDs tracks in different FoxIDs deployments. +FoxIDs tracks can be interconnect in the same FoxIDs tenant or in different FoxIDs tenants. Interconnections can also be configured between FoxIDs tracks in different FoxIDs deployments. + +> You can easy connect two tracks in the same tenant with a [track link](howto-tracklink-foxids.md). The integration between two FoxIDs tracks support [OpenID Connect authentication](https://openid.net/specs/openid-connect-core-1_0.html#Authentication) (login), [RP-initiated logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) and [front-channel logout](https://openid.net/specs/openid-connect-frontchannel-1_0.html). A session is established when the user authenticates and the session is invalidated on logout. diff --git a/docs/howto-saml-2.0-context-handler.md b/docs/howto-saml-2.0-context-handler.md index 49a878b31..5b8005ab5 100644 --- a/docs/howto-saml-2.0-context-handler.md +++ b/docs/howto-saml-2.0-context-handler.md @@ -18,7 +18,7 @@ Context Handler documentation and configuration: Context Handler requires the Relying Party (RP) and Identity Provider (IdP) to use different OSES certificates. Therefore, consider connecting Context Handler in separate tracks where the OCES certificates can be configured without affecting any other configurations. -Two FoxIDs tracks can be connected with OpenID Connect. Please see the [connect FoxIDs with OpenID Connect](up-party-howto-oidc-foxids.md) guide. The track with a up-party connected to Context Handler is called the parallel FoxIDs track in the guide. +Two FoxIDs tracks can be connected with OpenID Connect. Please see the [connect FoxIDs with OpenID Connect](howto-oidc-foxids.md) guide. The track with a up-party connected to Context Handler is called the parallel FoxIDs track in the guide. ## Certificate diff --git a/docs/howto-tracklink-foxids.md b/docs/howto-tracklink-foxids.md new file mode 100644 index 000000000..135626cf9 --- /dev/null +++ b/docs/howto-tracklink-foxids.md @@ -0,0 +1,38 @@ +# Interconnect two FoxIDs tracks with a track link + +FoxIDs tracks in the same tenant can be connected with track links. A track link acts mostly like OpenID Connect but it is simpler to configure and the steps it goes through is faster. +Therefor a login sequence that jumps between tracks will execute faster using a track link competed with using OpenID Connect. But an [OpenID connect connection](howto-oidc-foxids.md) is required if you need to jump between tracks located in different tenants. + +Track links support login, RP-initiated logout and front-channel logout. Furthermore, it is possible to configure [claim and claim transforms](claim.md), logout session and home realm discovery (HRD) like all other connecting up-parties and down-parties. + +## Configure integration + +The following describes how to connect two tracks called `track_x` and `track_y` where `track_y` become an up-party on `track_x`. + +**1 - Start in the `track_x` track by creating a track link in [FoxIDs Control Client](control.md#foxids-control-client)** + +1. Select the Parties tab and then the Up-parties +2. Click Create up-party and then Track link +3. Add the name e.g., `track_y-connection` +4. Add the `track_y` track name +5. Add the down-party name in the `track_y` track e.g., `track_x-connection` +6. Click Create + +![Create track link up-party](images/howto-tracklink-foxids-up-party.png) + +**2 - Then go to the `track_y` track and create a track link in [FoxIDs Control Client](control.md#foxids-control-client)** + +1. Select the Parties tab and then the Down-parties +2. Click Create down-party and then Track link +3. Add the name e.g., `track_x-connection` +4. Add the `track_x` track name +5. Add the up-party name in the `track_x` track e.g., `track_y-connection` +6. Select which up-parties in the `track_y` track the user is allowed to use for authentication +6. Click Create + +![Create track link down-party](images/howto-tracklink-foxids-down-party.png) + +That's it, you are done. + +> Your new up-party `track_y-connection` can now be selected as an allowed up-party in the down-parties in you `track_x` track. +> The down-parties in you `track_x` track can read the claims from your `track_y-connection` up-party. \ No newline at end of file diff --git a/docs/images/configure-login-advanced.png b/docs/images/configure-login-advanced.png index 0f033460e..e029ec4c4 100644 Binary files a/docs/images/configure-login-advanced.png and b/docs/images/configure-login-advanced.png differ diff --git a/docs/images/howto-tracklink-foxids-down-party.png b/docs/images/howto-tracklink-foxids-down-party.png new file mode 100644 index 000000000..926b97504 Binary files /dev/null and b/docs/images/howto-tracklink-foxids-down-party.png differ diff --git a/docs/images/howto-tracklink-foxids-up-party.png b/docs/images/howto-tracklink-foxids-up-party.png new file mode 100644 index 000000000..b933c17a5 Binary files /dev/null and b/docs/images/howto-tracklink-foxids-up-party.png differ diff --git a/docs/login.md b/docs/login.md index 99524409f..4cb40d610 100644 --- a/docs/login.md +++ b/docs/login.md @@ -59,12 +59,12 @@ You can select to require two-factor authentication for all users authenticating ### Configure user session The user sessions lifetime can be changed. The default lifetime is 10 hours. The user session is a sliding session, where the lifetime is extended every time, an application makes a login request until the absolute session lifetime is reached. -It is possible to configure an absolute session lifetime in the advanced settings. +It is possible to configure an absolute session lifetime as well. The user session can be changed to a persistent session which is preserved when the browser is closed and reopened. The user session become a persistent session if either the persistent session lifetime is configured to be grater, then 0. Or the persistent session lifetime unlimited setting is set to on. -> Click `show advanced settings` to see all session settings. +> Click the `User session` tag to see all session settings. ![Configure Login](images/configure-login-session.png) diff --git a/docs/name-title-icon-css.md b/docs/name-title-icon-css.md index f7cee372f..1e79b7bf8 100644 --- a/docs/name-title-icon-css.md +++ b/docs/name-title-icon-css.md @@ -30,7 +30,7 @@ Find the up-party login in [FoxIDs Control Client](control.md#foxids-control-cli ## CSS examples - Change background and add logo text. It is also possible to add a logo image. + Change background and add logo text. body { background: #7c8391; @@ -48,6 +48,16 @@ Find the up-party login in [FoxIDs Control Client](control.md#foxids-control-cli ![Configure background and add logo with CSS](images/configure-login-css-backbround-logo.png) +It is also possible to use a logo image. + + .brand-content-text { + display: none; + } + + .brand-content-icon:before { + content:url('https://some-external-site.com/logo.png'); + } + Add a background image from an external site. body { @@ -61,6 +71,51 @@ Add a background image from an external site. ![Configure background image](images/configure-login-css-backbround-image.png) +Change button and link color, in this example CSS to green. + + label { + color: #a4c700 !important; + } + + .input:focus { + outline: none !important; + border:1px solid #a4c700; + box-shadow: 0 0 10px #a4c700; + } + + .btn-link, .btn-link:hover, a, a:hover { + color: #a4c700; + } + + .btn-primary.disabled, .btn-primary:disabled { + color: #fff; + background-color: #afc44f; + border-color: #afc44f; + } + + .btn-primary, .btn-primary:hover, .btn-primary:active, .btn-primary:focus, .btn-primary:active { + background-color: #a4c700; + border-color: #a4c700; + } + + .btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active, .show>.btn-primary.dropdown-toggle { + background-color: #7c9600; + border-color: #7c9600; + } + + .btn-link:not(:disabled):not(.disabled):active, .btn-link:not(:disabled):not(.disabled).active, .show>.btn-link.dropdown-toggle { + color: #a4c700; + } + + .btn:focus, .form-control:focus { + border-color: #a4c700; + box-shadow: 0 0 0 .2rem rgba(64,78,0,.25); + } + + .btn-primary:not(:disabled):not(.disabled).active:focus, .btn-primary:not(:disabled):not(.disabled):active:focus, .show>.btn-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 .2rem rgba(64,78,0,.25); + } + Add information to the login box. div.page-content:before { diff --git a/docs/oidc.md b/docs/oidc.md index a6b11898d..ac1400f1a 100644 --- a/docs/oidc.md +++ b/docs/oidc.md @@ -12,7 +12,8 @@ Configure [up-party OpenID Connect](up-party-oidc.md) which trust an external Op How to guides: -- Connect [FoxIDs](up-party-howto-oidc-foxids.md) +- Connect two FoxIDs tracks in a tenant with a [track link](howto-tracklink-foxids.md) +- Connect two FoxIDs tracks in the same or different tenants with [OpenID connect](howto-oidc-foxids.md) - Connect [Azure AD](up-party-howto-oidc-azure-ad.md) - Connect [Azure AD B2C](up-party-howto-oidc-azure-ad-b2c.md) - Connect [IdentityServer](up-party-howto-oidc-identityserver.md) diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index 108ce31eb..b32e1fca5 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -53,7 +53,7 @@ Configuration: - In the Networking section of the App Services. Enable access restriction to only allow traffic from Azure Front Door - Optionally add a Front Door endpoint for both the FoxIDs App Service and the FoxIDs Control App Service test slots - Restrict access to the App Services test slots -- Add the `Settings:TrustProxyHeaders` setting with the value `true` in the FoxIDs App Service (optionally also the test slot) configuration to support [custom domains](custom-domain.md) +- Add the `Settings:TrustProxyHeaders` setting with the value `true` and select Deployment slot setting in the FoxIDs App Service configuration to support [custom domains](custom-domain.md) (optionally also add the setting in the test slot) - Disable Session affinity - Optionally configure WAF policies diff --git a/docs/up-party-howto-saml-2.0-nemlogin.md b/docs/up-party-howto-saml-2.0-nemlogin.md index 2c0ecd311..f91d79a8e 100644 --- a/docs/up-party-howto-saml-2.0-nemlogin.md +++ b/docs/up-party-howto-saml-2.0-nemlogin.md @@ -25,7 +25,7 @@ NemLog-in documentation and configuration: NemLog-in requires the Relying Party (RP) to use a OSES certificate and a high level of logging. Therefore, consider connecting NemLog-in in a separate track where the OCES certificate and log level can be configured without affecting any other configuration. -Two FoxIDs tracks can be connected with OpenID Connect. Please see the [connect FoxIDs with OpenID Connect](up-party-howto-oidc-foxids.md) guide. The track with a up-party connected to NemLog-in is called the parallel FoxIDs track in the guide. +Two FoxIDs tracks can be connected with OpenID Connect. Please see the [connect FoxIDs with OpenID Connect](howto-oidc-foxids.md) guide. The track with a up-party connected to NemLog-in is called the parallel FoxIDs track in the guide. ## Certificate diff --git a/docs/up-party-oidc.md b/docs/up-party-oidc.md index bde326235..603b6db39 100644 --- a/docs/up-party-oidc.md +++ b/docs/up-party-oidc.md @@ -8,7 +8,8 @@ It is possible to configure multiple OpenID Connect up-parties which then can be How to guides: -- Connect [FoxIDs](up-party-howto-oidc-foxids.md) +- Connect two FoxIDs tracks in a tenant with a [track link](howto-tracklink-foxids.md) +- Connect two FoxIDs tracks in the same or different tenants with [OpenID connect](howto-oidc-foxids.md) - Connect [Azure AD](up-party-howto-oidc-azure-ad.md) - Connect [Azure AD B2C](up-party-howto-oidc-azure-ad-b2c.md) - Connect [IdentityServer](up-party-howto-oidc-identityserver.md) diff --git a/docs/Users.md b/docs/users.md similarity index 100% rename from docs/Users.md rename to docs/users.md diff --git a/src/FoxIDs.Control/Controllers/Master/MRiskPasswordFirstController.cs b/src/FoxIDs.Control/Controllers/Master/MRiskPasswordFirstController.cs new file mode 100644 index 000000000..014359fa5 --- /dev/null +++ b/src/FoxIDs.Control/Controllers/Master/MRiskPasswordFirstController.cs @@ -0,0 +1,45 @@ +using FoxIDs.Infrastructure; +using FoxIDs.Models; +using Api = FoxIDs.Models.Api; +using FoxIDs.Repository; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoMapper; + +namespace FoxIDs.Controllers +{ + public class MRiskPasswordFirstController : MasterApiController + { + private readonly TelemetryScopedLogger logger; + private readonly IMapper mapper; + private readonly IMasterRepository masterRepository; + + public MRiskPasswordFirstController(TelemetryScopedLogger logger, IMapper mapper, IMasterRepository masterRepository) : base(logger) + { + this.logger = logger; + this.mapper = mapper; + this.masterRepository = masterRepository; + } + + /// + /// Get the first 1000 risk password. Can be used query risk passwords before deleting them. + /// + /// Risk passwords. + [ProducesResponseType(typeof(HashSet), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetRiskPasswordFirst() + { + var mRiskPasswords = await masterRepository.GetListAsync(maxItemCount: 1000); + if (mRiskPasswords?.Count > 0) + { + return Ok(mapper.Map>(mRiskPasswords)); + } + else + { + return Ok(); + } + } + } +} diff --git a/src/FoxIDs.Control/Controllers/Parties/GenericPartyApiController.cs b/src/FoxIDs.Control/Controllers/Parties/GenericPartyApiController.cs index 09e1614ff..fa8aff0d7 100644 --- a/src/FoxIDs.Control/Controllers/Parties/GenericPartyApiController.cs +++ b/src/FoxIDs.Control/Controllers/Parties/GenericPartyApiController.cs @@ -8,7 +8,6 @@ using AutoMapper; using System; using FoxIDs.Logic; -using System.Linq; namespace FoxIDs.Controllers { @@ -57,16 +56,16 @@ protected async Task> Get(string name) } } - protected async Task> Post(AParty party, Func> apiModelActionAsync, Func> modelActionAsync) + protected async Task> Post(AParty party, Func> apiModelActionAsync = null, Func> modelActionAsync = null) { try { - if (!await ModelState.TryValidateObjectAsync(party) || !validateGenericPartyLogic.ValidateApiModelClaimTransforms(ModelState, party.ClaimTransforms) || !await apiModelActionAsync(party)) return BadRequest(ModelState); + if (!await ModelState.TryValidateObjectAsync(party) || !validateGenericPartyLogic.ValidateApiModelClaimTransforms(ModelState, party.ClaimTransforms) || (apiModelActionAsync != null &&!await apiModelActionAsync(party))) return BadRequest(ModelState); var mParty = mapper.Map(party); if (!(party is Api.IDownParty downParty ? await validateGenericPartyLogic.ValidateModelAllowUpPartiesAsync(ModelState, nameof(downParty.AllowUpPartyNames), mParty as DownParty) : true)) return BadRequest(ModelState); if (!validateGenericPartyLogic.ValidateModelClaimTransforms(ModelState, mParty)) return BadRequest(ModelState); - if (!(await modelActionAsync(party, mParty))) return BadRequest(ModelState); + if (modelActionAsync != null && !await modelActionAsync(party, mParty)) return BadRequest(ModelState); await tenantRepository.CreateAsync(mParty); @@ -97,16 +96,16 @@ protected async Task> Post(AParty party, Func> Put(AParty party, Func> apiModelActionAsync, Func> modelActionAsync) + protected async Task> Put(AParty party, Func> apiModelActionAsync = null, Func> modelActionAsync = null) { try { - if (!await ModelState.TryValidateObjectAsync(party) || !validateGenericPartyLogic.ValidateApiModelClaimTransforms(ModelState, party.ClaimTransforms) || !await apiModelActionAsync(party)) return BadRequest(ModelState); + if (!await ModelState.TryValidateObjectAsync(party) || !validateGenericPartyLogic.ValidateApiModelClaimTransforms(ModelState, party.ClaimTransforms) || (apiModelActionAsync != null && !await apiModelActionAsync(party))) return BadRequest(ModelState); var mParty = mapper.Map(party); if (!(party is Api.IDownParty downParty ? await validateGenericPartyLogic.ValidateModelAllowUpPartiesAsync(ModelState, nameof(downParty.AllowUpPartyNames), mParty as DownParty) : true)) return BadRequest(ModelState); if (!validateGenericPartyLogic.ValidateModelClaimTransforms(ModelState, mParty)) return BadRequest(ModelState); - if (!(await modelActionAsync(party, mParty))) return BadRequest(ModelState); + if (modelActionAsync != null && !await modelActionAsync(party, mParty)) return BadRequest(ModelState); if(party is Api.OidcDownParty) { diff --git a/src/FoxIDs.Control/Controllers/Parties/TTrackLinkDownPartyController.cs b/src/FoxIDs.Control/Controllers/Parties/TTrackLinkDownPartyController.cs new file mode 100644 index 000000000..bb627c109 --- /dev/null +++ b/src/FoxIDs.Control/Controllers/Parties/TTrackLinkDownPartyController.cs @@ -0,0 +1,56 @@ +using FoxIDs.Infrastructure; +using FoxIDs.Models; +using Api = FoxIDs.Models.Api; +using FoxIDs.Repository; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using AutoMapper; +using FoxIDs.Logic; + +namespace FoxIDs.Controllers +{ + /// + /// Track link down-party API. + /// + public class TTrackLinkDownPartyController : GenericPartyApiController + { + public TTrackLinkDownPartyController(TelemetryScopedLogger logger, IMapper mapper, ITenantRepository tenantRepository, DownPartyCacheLogic downPartyCacheLogic, UpPartyCacheLogic upPartyCacheLogic, DownPartyAllowUpPartiesQueueLogic downPartyAllowUpPartiesQueueLogic, ValidateGenericPartyLogic validateGenericPartyLogic) : base(logger, mapper, tenantRepository, downPartyCacheLogic, upPartyCacheLogic, downPartyAllowUpPartiesQueueLogic, validateGenericPartyLogic) + { } + + /// + /// Get track link down-party. + /// + /// Party name. + /// Track link down-party. + [ProducesResponseType(typeof(Api.TrackLinkDownParty), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetTrackLinkDownParty(string name) => await Get(name); + + /// + /// Create track link down-party. + /// + /// Track link down-party. + /// Track link down-party. + [ProducesResponseType(typeof(Api.TrackLinkDownParty), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task> PostTrackLinkDownParty([FromBody] Api.TrackLinkDownParty party) => await Post(party); + + /// + /// Update track link down-party. + /// + /// Track link down-party. + /// OIDC down-party. + [ProducesResponseType(typeof(Api.TrackLinkDownParty), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> PutTrackLinkDownParty([FromBody] Api.TrackLinkDownParty party) => await Put(party); + + /// + /// Delete track link down-party. + /// + /// Party name. + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteTrackLinkDownParty(string name) => await Delete(name); + } +} diff --git a/src/FoxIDs.Control/Controllers/Parties/TTrackLinkUpPartyController.cs b/src/FoxIDs.Control/Controllers/Parties/TTrackLinkUpPartyController.cs new file mode 100644 index 000000000..de09166d5 --- /dev/null +++ b/src/FoxIDs.Control/Controllers/Parties/TTrackLinkUpPartyController.cs @@ -0,0 +1,56 @@ +using FoxIDs.Infrastructure; +using FoxIDs.Models; +using Api = FoxIDs.Models.Api; +using FoxIDs.Repository; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using AutoMapper; +using FoxIDs.Logic; + +namespace FoxIDs.Controllers +{ + /// + /// Track link up-party API. + /// + public class TTrackLinkUpPartyController : GenericPartyApiController + { + public TTrackLinkUpPartyController(TelemetryScopedLogger logger, IMapper mapper, ITenantRepository tenantRepository, DownPartyCacheLogic downPartyCacheLogic, UpPartyCacheLogic upPartyCacheLogic, DownPartyAllowUpPartiesQueueLogic downPartyAllowUpPartiesQueueLogic, ValidateGenericPartyLogic validateGenericPartyLogic) : base(logger, mapper, tenantRepository, downPartyCacheLogic, upPartyCacheLogic, downPartyAllowUpPartiesQueueLogic, validateGenericPartyLogic) + { } + + /// + /// Get track link up-party. + /// + /// Party name. + /// Track link up-party. + [ProducesResponseType(typeof(Api.TrackLinkUpParty), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetTrackLinkUpParty(string name) => await Get(name); + + /// + /// Create track link up-party. + /// + /// Track link up-party. + /// Track link up-party. + [ProducesResponseType(typeof(Api.TrackLinkUpParty), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task> PostTrackLinkUpParty([FromBody] Api.TrackLinkUpParty party) => await Post(party); + + /// + /// Update track link up-party. + /// + /// Track link up-party. + /// Track link up-party. + [ProducesResponseType(typeof(Api.TrackLinkUpParty), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> PutTrackLinkUpParty([FromBody] Api.TrackLinkUpParty party) => await Put(party); + + /// + /// Delete track link up-party. + /// + /// Party name. + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteTrackLinkUpParty(string name) => await Delete(name); + } +} diff --git a/src/FoxIDs.Control/FoxIDs.Control.csproj b/src/FoxIDs.Control/FoxIDs.Control.csproj index e3394f650..9ebc09df6 100644 --- a/src/FoxIDs.Control/FoxIDs.Control.csproj +++ b/src/FoxIDs.Control/FoxIDs.Control.csproj @@ -2,7 +2,7 @@ net7.0 - 1.0.14.5 + 1.0.15.3 FoxIDs Anders Revsgaard ITfoxtec @@ -21,13 +21,13 @@ - - + + - - - + + + diff --git a/src/FoxIDs.Control/Infrastructure/Security/JwtBearerMultipleTenantsHandler.cs b/src/FoxIDs.Control/Infrastructure/Security/JwtBearerMultipleTenantsHandler.cs index 5155a4127..3e7231023 100644 --- a/src/FoxIDs.Control/Infrastructure/Security/JwtBearerMultipleTenantsHandler.cs +++ b/src/FoxIDs.Control/Infrastructure/Security/JwtBearerMultipleTenantsHandler.cs @@ -8,7 +8,7 @@ using System.Security.Authentication; using System.Text.Encodings.Web; using System.Threading.Tasks; -using UrlCombineLib; +using ITfoxtec.Identity.Util; using ITfoxtec.Identity.Tokens; using Microsoft.IdentityModel.Tokens; using System.Security.Claims; diff --git a/src/FoxIDs.Control/Logic/ValidateLoginPartyLogic.cs b/src/FoxIDs.Control/Logic/ValidateLoginPartyLogic.cs index 97c8b1dd0..a50bffa28 100644 --- a/src/FoxIDs.Control/Logic/ValidateLoginPartyLogic.cs +++ b/src/FoxIDs.Control/Logic/ValidateLoginPartyLogic.cs @@ -5,6 +5,9 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using System.ComponentModel.DataAnnotations; using System.IO; +using System.Linq; +using System.Collections.Generic; +using FoxIDs.Models; namespace FoxIDs.Logic { @@ -45,7 +48,7 @@ public bool ValidateApiModel(ModelStateDictionary modelState, Api.LoginUpParty p { isValid = false; logger.Warning(vex); - modelState.TryAddModelError($"{nameof(Api.LoginUpParty.IconUrl)}".ToCamelCase(), vex.Message); + modelState.TryAddModelError(nameof(Api.LoginUpParty.IconUrl).ToCamelCase(), vex.Message); } } @@ -54,6 +57,83 @@ public bool ValidateApiModel(ModelStateDictionary modelState, Api.LoginUpParty p party.TwoFactorAppName = RouteBinding.TenantName; } + if (party.CreateUser != null) + { + if (!party.EnableCreateUser || party.CreateUser.Elements?.Any() != true) + { + party.CreateUser = null; + } + else + { + if (!ValidateApiModelCreateUserElements(modelState, party.CreateUser.Elements)) + { + isValid = false; + } + + if (!ValidateApiModelCreateUserClaimTransforms(modelState, party.CreateUser.ClaimTransforms)) + { + isValid = false; + } + } + } + + return isValid; + } + + public bool ValidateApiModelCreateUserElements(ModelStateDictionary modelState, List createUserElements) + { + var isValid = true; + try + { + if (createUserElements?.Count() > 0) + { + var duplicatedOrderNumber = createUserElements.GroupBy(ct => ct.Order as int?).Where(g => g.Count() > 1).Select(g => g.Key).FirstOrDefault(); + if (duplicatedOrderNumber >= 0) + { + throw new ValidationException($"Duplicated create user dynamic element order number '{duplicatedOrderNumber}'"); + } + + if (createUserElements.Where(e => e.Type == Api.DynamicElementTypes.EmailAndPassword).Count() != 1) + { + throw new ValidationException("Exactly one create user dynamic element of type EmailAndPassword is required."); + } + + var duplicatedElementType = createUserElements.GroupBy(ct => ct.Type).Where(g => g.Count() > 1).Select(g => g.Key).FirstOrDefault(); + if (duplicatedElementType > 0) + { + throw new ValidationException($"Duplicated create user dynamic element type '{duplicatedElementType}'"); + } + } + } + catch (ValidationException vex) + { + isValid = false; + logger.Warning(vex); + modelState.TryAddModelError(nameof(Api.LoginUpParty.CreateUser.Elements).ToCamelCase(), vex.Message); + } + return isValid; + } + + public bool ValidateApiModelCreateUserClaimTransforms(ModelStateDictionary modelState, List claimTransforms) + { + var isValid = true; + try + { + if (claimTransforms?.Count() > 0) + { + var duplicatedOrderNumber = claimTransforms.GroupBy(ct => ct.Order as int?).Where(g => g.Count() > 1).Select(g => g.Key).FirstOrDefault(); + if (duplicatedOrderNumber >= 0) + { + throw new ValidationException($"Duplicated create user claim transform order number '{duplicatedOrderNumber}'"); + } + } + } + catch (ValidationException vex) + { + isValid = false; + logger.Warning(vex); + modelState.TryAddModelError(nameof(Api.LoginUpParty.CreateUser.ClaimTransforms).ToCamelCase(), vex.Message); + } return isValid; } } diff --git a/src/FoxIDs.Control/MappingProfiles/MasterMappingProfile.cs b/src/FoxIDs.Control/MappingProfiles/MasterMappingProfile.cs index 8a9874c24..6671918a4 100644 --- a/src/FoxIDs.Control/MappingProfiles/MasterMappingProfile.cs +++ b/src/FoxIDs.Control/MappingProfiles/MasterMappingProfile.cs @@ -33,6 +33,7 @@ private void Mapping() .ReverseMap(); CreateMap() + .ForMember(d => d.PasswordSha1Hash, opt => opt.MapFrom(s => s.Id.Substring(s.Id.LastIndexOf(':') + 1))) .ReverseMap(); } } diff --git a/src/FoxIDs.Control/MappingProfiles/TenantMappingProfiles.cs b/src/FoxIDs.Control/MappingProfiles/TenantMappingProfiles.cs index c67070a42..3c36a282b 100644 --- a/src/FoxIDs.Control/MappingProfiles/TenantMappingProfiles.cs +++ b/src/FoxIDs.Control/MappingProfiles/TenantMappingProfiles.cs @@ -69,6 +69,9 @@ private void Mapping() .ForMember(d => d.Action, opt => opt.MapFrom(s => MapAction(s))) .ReverseMap(); + CreateMap() + .ReverseMap(); + CreateMap() .ReverseMap(); @@ -130,6 +133,9 @@ private void UpPartyMapping() .ForMember(d => d.Name, opt => opt.MapFrom(s => s.Name.ToLower())) .ForMember(d => d.Id, opt => opt.MapFrom(s => UpParty.IdFormatAsync(RouteBinding, s.Name.ToLower()).GetAwaiter().GetResult())); + CreateMap() + .ReverseMap(); + CreateMap() .ReverseMap() .ForMember(d => d.Name, opt => opt.MapFrom(s => s.Name.ToLower())) @@ -162,6 +168,12 @@ private void UpPartyMapping() .ForMember(d => d.MetadataNameIdFormats, opt => opt.MapFrom(s => s.MetadataNameIdFormats.OrderBy(f => f))) .ForMember(d => d.MetadataAttributeConsumingServices, opt => opt.MapFrom(s => s.MetadataAttributeConsumingServices.OrderBy(a => a.ServiceName.Name))) .ForMember(d => d.MetadataContactPersons, opt => opt.MapFrom(s => s.MetadataContactPersons.OrderBy(c => c.ContactType))); + + CreateMap() + .ReverseMap() + .ForMember(d => d.Name, opt => opt.MapFrom(s => s.Name.ToLower())) + .ForMember(d => d.Id, opt => opt.MapFrom(s => UpParty.IdFormatAsync(RouteBinding, s.Name.ToLower()).GetAwaiter().GetResult())) + .ForMember(d => d.Claims, opt => opt.MapFrom(s => s.Claims.OrderBy(c => c))); } private void DownPartyMapping() @@ -230,6 +242,14 @@ private void DownPartyMapping() RequestBinding = s.LogoutRequestBinding.HasValue ? (SamlBindingTypes)s.LogoutRequestBinding.Value : SamlBindingTypes.Post, ResponseBinding = s.LogoutResponseBinding.HasValue ? (SamlBindingTypes)s.LogoutResponseBinding.Value : SamlBindingTypes.Post, })); + + CreateMap() + .ForMember(d => d.AllowUpPartyNames, opt => opt.MapFrom(s => s.AllowUpParties.Select(aup => aup.Name))) + .ReverseMap() + .ForMember(d => d.Name, opt => opt.MapFrom(s => s.Name.ToLower())) + .ForMember(d => d.Id, opt => opt.MapFrom(s => DownParty.IdFormatAsync(RouteBinding, s.Name.ToLower()).GetAwaiter().GetResult())) + .ForMember(d => d.ClaimTransforms, opt => opt.MapFrom(s => OrderClaimTransforms(s.ClaimTransforms))) + .ForMember(d => d.AllowUpParties, opt => opt.MapFrom(s => s.AllowUpPartyNames.Select(n => new UpPartyLink { Name = n.ToLower() }))); } private List OrderClaimTransforms(List claimTransforms) where T : Api.ClaimTransform diff --git a/src/FoxIDs.Control/Startup.cs b/src/FoxIDs.Control/Startup.cs index 2d1928f8e..acd8495e3 100644 --- a/src/FoxIDs.Control/Startup.cs +++ b/src/FoxIDs.Control/Startup.cs @@ -48,7 +48,11 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app) { - if (!CurrentEnvironment.IsDevelopment()) + if (CurrentEnvironment.IsDevelopment()) + { + app.UseWebAssemblyDebugging(); + } + else { app.UseHsts(); } diff --git a/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj b/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj index 94a48fbc8..2a346a51c 100644 --- a/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj +++ b/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj @@ -2,7 +2,7 @@ net7.0 - 1.0.14.5 + 1.0.15.3 FoxIDs.Client Anders Revsgaard ITfoxtec @@ -10,11 +10,11 @@ - + - - + + @@ -23,9 +23,6 @@ - - true - true PreserveNewest diff --git a/src/FoxIDs.ControlClient/Models/LoginTabTypes.cs b/src/FoxIDs.ControlClient/Models/LoginTabTypes.cs index 6b0b13c54..232272532 100644 --- a/src/FoxIDs.ControlClient/Models/LoginTabTypes.cs +++ b/src/FoxIDs.ControlClient/Models/LoginTabTypes.cs @@ -4,6 +4,7 @@ public enum LoginTabTypes { Login, ClaimsTransform, + CreateUser, Session, Hrd } diff --git a/src/FoxIDs.ControlClient/Models/TrackLinkTabTypes.cs b/src/FoxIDs.ControlClient/Models/TrackLinkTabTypes.cs new file mode 100644 index 000000000..43ff4a727 --- /dev/null +++ b/src/FoxIDs.ControlClient/Models/TrackLinkTabTypes.cs @@ -0,0 +1,10 @@ +namespace FoxIDs.Client.Models +{ + public enum TrackLinkTabTypes + { + TrackLink, + Session, + ClaimsTransform, + Hrd + } +} diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/CreateUserViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/CreateUserViewModel.cs new file mode 100644 index 000000000..a9e4afcff --- /dev/null +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/CreateUserViewModel.cs @@ -0,0 +1,18 @@ +using FoxIDs.Infrastructure.DataAnnotations; +using FoxIDs.Models.Api; +using System.Collections.Generic; + +namespace FoxIDs.Client.Models.ViewModels +{ + public class CreateUserViewModel : CreateUser, IOAuthClaimTransformViewModel, IDynamicElementsViewModel + { + [Length(Constants.Models.DynamicElements.ElementsMin, Constants.Models.DynamicElements.ElementsMax)] + public new List Elements { get; set; } + + /// + /// Claim transforms. + /// + [Length(Constants.Models.Claim.TransformsMin, Constants.Models.Claim.TransformsMax)] + public new List ClaimTransforms { get; set; } = new List(); + } +} diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/DynamicElementViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/DynamicElementViewModel.cs new file mode 100644 index 000000000..f6e345111 --- /dev/null +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/DynamicElementViewModel.cs @@ -0,0 +1,9 @@ +using FoxIDs.Models.Api; + +namespace FoxIDs.Client.Models.ViewModels +{ + public class DynamicElementViewModel : DynamicElement + { + public bool IsStaticRequired { get; set; } + } +} diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralLoginUpPartyViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralLoginUpPartyViewModel.cs index 6824cb7b3..b1dbaf645 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralLoginUpPartyViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralLoginUpPartyViewModel.cs @@ -17,7 +17,8 @@ public GeneralLoginUpPartyViewModel(UpParty upParty) : base(upParty) public PageEditForm Form { get; set; } public bool ShowLoginTab { get; set; } = true; - public bool ShowClaimTransformTab { get; set; } + public bool ShowClaimTransformTab { get; set; } + public bool ShowCreateUserTab { get; set; } public bool ShowSessionTab { get; set; } public bool ShowHrdTab { get; set; } } diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralTrackLinkDownPartyViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralTrackLinkDownPartyViewModel.cs new file mode 100644 index 000000000..0974e0393 --- /dev/null +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralTrackLinkDownPartyViewModel.cs @@ -0,0 +1,21 @@ +using FoxIDs.Client.Shared.Components; +using FoxIDs.Models.Api; + +namespace FoxIDs.Client.Models.ViewModels +{ + public class GeneralTrackLinkDownPartyViewModel : GeneralDownPartyViewModel + { + public GeneralTrackLinkDownPartyViewModel() : base(PartyTypes.TrackLink) + { } + + public GeneralTrackLinkDownPartyViewModel(DownParty downParty) : base(downParty) + { } + + public PageEditForm Form { get; set; } + + public SelectUpParty SelectAllowUpPartyName; + + public bool ShowTrackLinkTab { get; set; } = true; + public bool ShowClaimTransformTab { get; set; } + } +} diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralTrackLinkUpPartyViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralTrackLinkUpPartyViewModel.cs new file mode 100644 index 000000000..2079b7aa9 --- /dev/null +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralTrackLinkUpPartyViewModel.cs @@ -0,0 +1,24 @@ +using FoxIDs.Client.Shared.Components; +using FoxIDs.Models.Api; +using System.Collections.Generic; + +namespace FoxIDs.Client.Models.ViewModels +{ + public class GeneralTrackLinkUpPartyViewModel : GeneralUpPartyViewModel + { + public GeneralTrackLinkUpPartyViewModel() : base(PartyTypes.TrackLink) + { } + + public GeneralTrackLinkUpPartyViewModel(UpParty upParty) : base(upParty) + { } + + public PageEditForm Form { get; set; } + + public List KeyInfoList { get; set; } = new List(); + + public bool ShowTrackLinkTab { get; set; } = true; + public bool ShowSessionTab { get; set; } + public bool ShowClaimTransformTab { get; set; } + public bool ShowHrdTab { get; set; } + } +} diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/IDynamicElementsViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/IDynamicElementsViewModel.cs new file mode 100644 index 000000000..7f15e82b0 --- /dev/null +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/IDynamicElementsViewModel.cs @@ -0,0 +1,13 @@ +using FoxIDs.Infrastructure.DataAnnotations; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace FoxIDs.Client.Models.ViewModels +{ + public interface IDynamicElementsViewModel + { + [Length(Constants.Models.DynamicElements.ElementsMin, Constants.Models.DynamicElements.ElementsMax)] + [Display(Name = "Dynamic elements shown in order (use the move up and down arrows to change the order)")] + public List Elements { get; set; } + } +} diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/LoginUpPartyViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/LoginUpPartyViewModel.cs index d3d6df381..06ddd0827 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/LoginUpPartyViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/LoginUpPartyViewModel.cs @@ -128,5 +128,7 @@ public class LoginUpPartyViewModel : IOAuthClaimTransformViewModel, IUpPartySess [RegularExpression(Constants.Models.UpParty.HrdLogoUrlRegExPattern)] [Display(Name = "HRD logo URL")] public string HrdLogoUrl { get; set; } + + public CreateUserViewModel CreateUser { get; set; } = new CreateUserViewModel(); } } diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/TrackLinkDownPartyViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/TrackLinkDownPartyViewModel.cs new file mode 100644 index 000000000..a587e27b7 --- /dev/null +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/TrackLinkDownPartyViewModel.cs @@ -0,0 +1,59 @@ +using FoxIDs.Infrastructure.DataAnnotations; +using FoxIDs.Models.Api; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace FoxIDs.Client.Models.ViewModels +{ + public class TrackLinkDownPartyViewModel : IDownPartyName, IValidatableObject, IAllowUpPartyNames, IOAuthClaimTransformViewModel + { + [Required] + [MaxLength(Constants.Models.Party.NameLength)] + [RegularExpression(Constants.Models.Party.NameRegExPattern, ErrorMessage = "The field {0} can contain letters, numbers, '-' and '_'.")] + [Display(Name = "Down-party name (client ID / resource name)")] + public string Name { get; set; } + + [MaxLength(Constants.Models.Party.NoteLength)] + [Display(Name = "Your notes")] + public string Note { get; set; } + + [Required] + [MaxLength(Constants.Models.Track.NameLength)] + [RegularExpression(Constants.Models.Track.NameDbRegExPattern)] + [Display(Name = "To track name")] + public string ToUpTrackName { get; set; } + + [Required] + [MaxLength(Constants.Models.Party.NameLength)] + [RegularExpression(Constants.Models.Party.NameRegExPattern)] + [Display(Name = "To up-party name")] + public string ToUpPartyName { get; set; } + + [ValidateComplexType] + [Length(Constants.Models.DownParty.AllowUpPartyNamesMin, Constants.Models.DownParty.AllowUpPartyNamesMax, Constants.Models.Party.NameLength, Constants.Models.Party.NameRegExPattern)] + [Display(Name = "Allow up-party names")] + public List AllowUpPartyNames { get; set; } = new List(); + + [ValidateComplexType] + [Length(Constants.Models.OAuthDownParty.Client.ClaimsMin, Constants.Models.OAuthDownParty.Client.ClaimsMax)] + [Display(Name = "Issue claims (use * to issue all claims)")] + public List Claims { get; set; } + + /// + /// Claim transforms. + /// + [Length(Constants.Models.Claim.TransformsMin, Constants.Models.Claim.TransformsMax)] + public List ClaimTransforms { get; set; } = new List(); + + public IEnumerable Validate(ValidationContext validationContext) + { + var results = new List(); + if (AllowUpPartyNames?.Count <= 0) + { + results.Add(new ValidationResult($"At least one in the field {nameof(AllowUpPartyNames)} is required.", new[] { nameof(AllowUpPartyNames) })); + } + return results; + } + } +} diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/TrackLinkUpPartyViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/TrackLinkUpPartyViewModel.cs new file mode 100644 index 000000000..2b8e7cf87 --- /dev/null +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/TrackLinkUpPartyViewModel.cs @@ -0,0 +1,98 @@ +using FoxIDs.Infrastructure.DataAnnotations; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace FoxIDs.Client.Models.ViewModels +{ + public class TrackLinkUpPartyViewModel : IOAuthClaimTransformViewModel, IUpPartySessionLifetime, IUpPartyHrd + { + [Required] + [MaxLength(Constants.Models.Party.NameLength)] + [RegularExpression(Constants.Models.Party.NameRegExPattern, ErrorMessage = "The field {0} can contain letters, numbers, '-' and '_'.")] + [Display(Name = "Up-party name")] + public string Name { get; set; } + + [MaxLength(Constants.Models.Party.NoteLength)] + [Display(Name = "Your notes")] + public string Note { get; set; } + + [Required] + [MaxLength(Constants.Models.Track.NameLength)] + [RegularExpression(Constants.Models.Track.NameDbRegExPattern)] + [Display(Name = "To track name")] + public string ToDownTrackName { get; set; } + + [Required] + [MaxLength(Constants.Models.Party.NameLength)] + [RegularExpression(Constants.Models.Party.NameRegExPattern)] + [Display(Name = "To down-party name")] + public string ToDownPartyName { get; set; } + + [Length(Constants.Models.TrackLinkDownParty.SelectedUpPartiesMin, Constants.Models.TrackLinkDownParty.SelectedUpPartiesMax, Constants.Models.Party.NameLength, Constants.Models.TrackLinkDownParty.SelectedUpPartiesNameRegExPattern)] + [Display(Name = "Selected up-parties (use * to select all up-parties)")] + public List SelectedUpParties { get; set; } = new List(new[] { "*" }); + + [Length(Constants.Models.OAuthUpParty.Client.ClaimsMin, Constants.Models.OAuthUpParty.Client.ClaimsMax, Constants.Models.Claim.JwtTypeLength, Constants.Models.Claim.JwtTypeWildcardRegExPattern)] + [Display(Name = "Forward claims (use * to carried all claims forward)")] + public List Claims { get; set; } + + /// + /// Claim transforms. + /// + [Length(Constants.Models.Claim.TransformsMin, Constants.Models.Claim.TransformsMax)] + public List ClaimTransforms { get; set; } = new List(); + + + /// + /// Default 10 hours. + /// + [Range(Constants.Models.UpParty.SessionLifetimeMin, Constants.Models.UpParty.SessionLifetimeMax)] + public int SessionLifetime { get; set; } = 36000; + + /// + /// Default 24 hours. + /// + [Range(Constants.Models.UpParty.SessionAbsoluteLifetimeMin, Constants.Models.UpParty.SessionAbsoluteLifetimeMax)] + public int SessionAbsoluteLifetime { get; set; } = 86400; + + /// + /// Default 0 minutes. + /// + [Range(Constants.Models.UpParty.PersistentAbsoluteSessionLifetimeMin, Constants.Models.UpParty.PersistentAbsoluteSessionLifetimeMax)] + public int PersistentSessionAbsoluteLifetime { get; set; } = 0; + + /// + /// Default false. + /// + public bool PersistentSessionLifetimeUnlimited { get; set; } = false; + + [Display(Name = "Single logout")] + public bool EnableSingleLogout { get; set; } = true; + + /// + /// Home realm discovery (HRD) domains. + /// + [Length(Constants.Models.UpParty.HrdDomainMin, Constants.Models.UpParty.HrdDomainMax, Constants.Models.UpParty.HrdDomainLength, Constants.Models.UpParty.HrdDomainRegExPattern)] + [Display(Name = "HRD domains")] + public List HrdDomains { get; set; } + + [Display(Name = "Show HRD button with domain")] + public bool HrdShowButtonWithDomain { get; set; } + + /// + /// Home realm discovery (HRD) display name. + /// + [MaxLength(Constants.Models.UpParty.HrdDisplayNameLength)] + [RegularExpression(Constants.Models.UpParty.HrdDisplayNameRegExPattern)] + [Display(Name = "HRD display name")] + public string HrdDisplayName { get; set; } + + /// + /// Home realm discovery (HRD) logo URL. + /// + [MaxLength(Constants.Models.UpParty.HrdLogoUrlLength)] + [RegularExpression(Constants.Models.UpParty.HrdLogoUrlRegExPattern)] + [Display(Name = "HRD logo URL")] + public string HrdLogoUrl { get; set; } + } +} diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/UserViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/UserViewModel.cs index c18240e9c..1c10bba31 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/UserViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/UserViewModel.cs @@ -19,7 +19,7 @@ public UserViewModel() [Display(Name = "Email")] public string Email { get; set; } - [Display(Name = "Confirm account")] + [Display(Name = "User must confirm account")] public bool ConfirmAccount { get; set; } [Display(Name = "Email verified")] diff --git a/src/FoxIDs.ControlClient/Pages/ClaimMappings.cs b/src/FoxIDs.ControlClient/Pages/ClaimMappings.cs index f872fee9d..ba0b0c204 100644 --- a/src/FoxIDs.ControlClient/Pages/ClaimMappings.cs +++ b/src/FoxIDs.ControlClient/Pages/ClaimMappings.cs @@ -91,7 +91,7 @@ private async Task OnUpdateClaimMappingValidSubmitAsync(EditContext editContext) try { await TrackService.SaveTrackClaimMappingAsync(trackClaimMappingForm.Model.ClaimMappings); - toastService.ShowSuccess("Claim mappings updated.", "SUCCESS"); + toastService.ShowSuccess("Claim mappings updated."); } catch (Exception ex) { diff --git a/src/FoxIDs.ControlClient/Pages/Components/DownPartyBase.cs b/src/FoxIDs.ControlClient/Pages/Components/DownPartyBase.cs index 694edbfcb..a0c5658be 100644 --- a/src/FoxIDs.ControlClient/Pages/Components/DownPartyBase.cs +++ b/src/FoxIDs.ControlClient/Pages/Components/DownPartyBase.cs @@ -88,6 +88,23 @@ public void ShowSamlTab(GeneralSamlDownPartyViewModel downParty, SamlTabTypes sa } } + public void ShowTrackLinkTab(GeneralTrackLinkDownPartyViewModel downParty, TrackLinkTabTypes trackLinkTabTypes) + { + switch (trackLinkTabTypes) + { + case TrackLinkTabTypes.TrackLink: + downParty.ShowTrackLinkTab = true; + downParty.ShowClaimTransformTab = false; + break; + case TrackLinkTabTypes.ClaimsTransform: + downParty.ShowTrackLinkTab = false; + downParty.ShowClaimTransformTab = true; + break; + default: + throw new NotSupportedException(); + } + } + public void AddAllowUpPartyName((IAllowUpPartyNames model, string upPartyName) arg) { if (!arg.model.AllowUpPartyNames.Where(p => p.Equals(arg.upPartyName, StringComparison.OrdinalIgnoreCase)).Any()) diff --git a/src/FoxIDs.ControlClient/Pages/Components/ELoginUpParty.cs b/src/FoxIDs.ControlClient/Pages/Components/ELoginUpParty.cs index 804430163..cc8f8f053 100644 --- a/src/FoxIDs.ControlClient/Pages/Components/ELoginUpParty.cs +++ b/src/FoxIDs.ControlClient/Pages/Components/ELoginUpParty.cs @@ -63,6 +63,36 @@ private void LoginUpPartyViewModelAfterInit(LoginUpPartyViewModel model) { model.TwoFactorAppName = TenantName; } + if(model.CreateUser.Elements?.Any() != true) + { + model.CreateUser.Elements = new List + { + new DynamicElementViewModel + { + IsStaticRequired = true, + Type = DynamicElementTypes.EmailAndPassword, + Required = true + }, + new DynamicElementViewModel + { + Type = DynamicElementTypes.GivenName + }, + new DynamicElementViewModel + { + Type = DynamicElementTypes.FamilyName + } + }; + } + else + { + foreach(var element in model.CreateUser.Elements) + { + if (element.Type == DynamicElementTypes.EmailAndPassword) + { + element.IsStaticRequired = true; + } + } + } } private async Task OnEditLoginUpPartyValidSubmitAsync(GeneralLoginUpPartyViewModel generalLoginUpParty, EditContext editContext) @@ -95,10 +125,29 @@ private async Task OnEditLoginUpPartyValidSubmitAsync(GeneralLoginUpPartyViewMod claimTransform.Order = order++; } } + if (afterMap.CreateUser != null) + { + if (afterMap.CreateUser.Elements?.Count() > 0) + { + int order = 1; + foreach (var element in afterMap.CreateUser.Elements) + { + element.Order = order++; + } + } + if (afterMap.CreateUser.ClaimTransforms?.Count() > 0) + { + int order = 1; + foreach (var claimTransform in afterMap.CreateUser.ClaimTransforms) + { + claimTransform.Order = order++; + } + } + } })); generalLoginUpParty.Form.UpdateModel(ToViewModel(loginUpPartyResult)); generalLoginUpParty.CreateMode = false; - toastService.ShowSuccess("Login Up-party created.", "SUCCESS"); + toastService.ShowSuccess("Login up-party created."); } else { @@ -115,9 +164,28 @@ private async Task OnEditLoginUpPartyValidSubmitAsync(GeneralLoginUpPartyViewMod claimTransform.Order = order++; } } + if (afterMap.CreateUser != null) + { + if (afterMap.CreateUser.Elements?.Count() > 0) + { + int order = 1; + foreach (var element in afterMap.CreateUser.Elements) + { + element.Order = order++; + } + } + if (afterMap.CreateUser.ClaimTransforms?.Count() > 0) + { + int order = 1; + foreach (var claimTransform in afterMap.CreateUser.ClaimTransforms) + { + claimTransform.Order = order++; + } + } + } })); generalLoginUpParty.Form.UpdateModel(ToViewModel(loginUpParty)); - toastService.ShowSuccess("Login Up-party updated.", "SUCCESS"); + toastService.ShowSuccess("Login up-party updated."); } generalLoginUpParty.Name = generalLoginUpParty.Form.Model.Name; } diff --git a/src/FoxIDs.ControlClient/Pages/Components/ELoginUpParty.razor b/src/FoxIDs.ControlClient/Pages/Components/ELoginUpParty.razor index d6ee8748e..edd813284 100644 --- a/src/FoxIDs.ControlClient/Pages/Components/ELoginUpParty.razor +++ b/src/FoxIDs.ControlClient/Pages/Components/ELoginUpParty.razor @@ -1,7 +1,7 @@ @inherits UpPartyBase @{ - var loginUpParty = UpParty as GeneralLoginUpPartyViewModel; ; + var loginUpParty = UpParty as GeneralLoginUpPartyViewModel; } @@ -49,6 +49,17 @@ } + +