diff --git a/FoxIDs.sln b/FoxIDs.sln index 24e2225bb..debce1aa5 100644 --- a/FoxIDs.sln +++ b/FoxIDs.sln @@ -73,9 +73,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{CB5D86A0-D 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 @@ -116,6 +119,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{CB8812 docs\images\configure-plan.png = docs\images\configure-plan.png docs\images\configure-resource-scopes-client.png = docs\images\configure-resource-scopes-client.png docs\images\configure-resource-scopes-resource.png = docs\images\configure-resource-scopes-resource.png + docs\images\configure-reverse-proxy-secret-firewall.png = docs\images\configure-reverse-proxy-secret-firewall.png + docs\images\configure-reverse-proxy-secret-permissions.png = docs\images\configure-reverse-proxy-secret-permissions.png docs\images\configure-reverse-proxy-secret.png = docs\images\configure-reverse-proxy-secret.png docs\images\configure-saml-adfs-up-party.png = docs\images\configure-saml-adfs-up-party.png docs\images\configure-saml-down-party.png = docs\images\configure-saml-down-party.png diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 1493bef29..94fc9b6d9 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -2,6 +2,7 @@ - [Getting Started](getting-started.md) - [Parties](parties.md) - [Login & HRD & 2FA/MFA](login.md) + - [How to connect IdP](up-party-howto.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 f231daa21..037860898 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -47,11 +47,9 @@ The solution is to delete (purge) the old Key Vault, which will release the name ## Upload risk passwords -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. - -You can upload risk passwords with the FoxIDs seed tool. The seed tool is a console application. +You can increment the password security level by uploading risk passwords. -> The seed tool code can be [downloaded](https://github.com/ITfoxtec/FoxIDs/tree/master/tools/FoxIDs.SeedTool) and need to be compiled to run. +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). @@ -60,6 +58,8 @@ Download the `SHA-1` pwned passwords `ordered by prevalence` from [haveibeenpwne 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 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. + ### Configure the seed tool The seed tool is configured in the `appsettings.json` file. @@ -74,9 +74,9 @@ Create a seed tool OAuth 2.0 client in the [FoxIDs Control Client](control.md#fo 4. Remember the client secret. 5. In the resource and scopes section. Grant the sample seed client access to the FoxIDs Control API resource `foxids_control_api` with the scope `foxids:master`. 6. Click show advanced settings. -7. In the issue claims section. Add a claim with the name `role` and the value `foxids:tenant.admin`. This will granted the client the administrator role. +7. In the issue claims section. Add a claim with the name `role` and the value `foxids:tenant.admin`. This will grant the client the administrator role. -The seed tool client is thereby granted access to update to the master tenant. +The seed tool client is thereby granted access to update the master tenant. ![FoxIDs Control Client - seed tool client](images/upload-risk-passwords-seed-client.png) @@ -105,20 +105,20 @@ It is possible to run the sample applications after they are configured in a Fox ## Custom primary domains -The FoxIDs service and FoxIDs Control sites primary domains can be customized. +The FoxIDs service and FoxIDs Control sites primary domains can be customized. The new primary custom domains can be configured on the App Services or by using a [reverse proxy](reverse-proxy.md) > Important: change the primary domain before adding tenants. -- FoxIDs service default domain is `https://foxidsxxxx.azurewebsites.net` which can be changed to a custom primary domain like e.g., `https://somedomain.com` or `https://auth.somedomain.com` -- FoxIDs Control default domain is `https://foxidscontrolxxxx.azurewebsites.net` which can be changed to a custom primary domain like e.g., `https://control.somedomain.com` or `https://foxidscontrol.somedomain.com` +- FoxIDs service default domain is `https://foxidsxxxx.azurewebsites.net` which can be changed to a custom primary domain like e.g., `https://somedomain.com` or `https://id.somedomain.com` +- FoxIDs Control default domain is `https://foxidscontrolxxxx.azurewebsites.net` which can be changed to a custom primary domain like e.g., `https://control.somedomain.com` or `https://idcontrol.somedomain.com` The FoxIDs site support one primary domain and multiple [custom domains](custom-domain.md) which are connected to tenants, where the FoxIDs Control site only support one primary domain. Configure new primary custom domains: -1) Login to [FoxIDs Control Client](control.md#foxids-control-client) using the default/old primary domain. Select the `Parties` tab and under `Down-parties` select click `OpenID Connect - foxids_control_client` and click `Show advanced settings`. +1) Login to [FoxIDs Control Client](control.md#foxids-control-client) using the default/old primary domain. Select the `Parties` tab and `Down-parties` tap then click `OpenID Connect - foxids_control_client` and click `Show advanced settings`. - - Add the FoxIDs Control sites new primary custom domain to the `Allow CORS origins` list without a trailing slash. + - Add the FoxIDs Control sites new primary custom domain URL to the `Allow CORS origins` list without a trailing slash. - Add the FoxIDs Control Client sites new primary custom domain login and logout redirect URIs to the `Redirect URIs` list including the trailing `/master/authentication/login_callback` and `/master/authentication/logout_callback`. > If you have added tenants before changing the primary domain, the `OpenID Connect - foxids_control_client` configuration have to be done in each tenant. @@ -138,11 +138,22 @@ Depending on the reverse proxy your are using you might be required to also conf - The setting `Settings:FoxIDsEndpoint` is changed to the FoxIDs service sites new primary custom domain. - The setting `Settings:FoxIDsControlEndpoint` is changed to the FoxIDs Control sites new primary custom domain. -> You can create a `main` tenant and add the custom primary domain used on the FoxIDs service as a [custom domain](custom-domain.md) to remove the tenant element from the URL. +> Yo can achieve a shorter and prettier URL where the tenant element is removed from the URL. By creating a `main` tenant where the custom primary domain used on the FoxIDs service is set 92452093 +as a [custom domain](custom-domain.md). ## Reverse proxy It is recommended to place both the FoxIDs Azure App service and the FoxIDs Control Azure App service behind a [reverse proxy](reverse-proxy.md). +## Enable test slots for testing +Both the FoxIDs App Service and FoxIDs Control App service contain a test slots use for [updating](update.md) the sites without downtime. + +It is possible to do preliminary test in the test slots against the production data or create a new dataset for testing. + +Configuration to enable test with production data: +- In Key Vault. Grant the FoxIDs App Service and FoxIDs Control App service test slots access to call Key Vault with the same rights as the FoxIDs App Service and FoxIDs Control App service existing rights. +- In Log Analytics workspace. Grant the FoxIDs App Service and FoxIDs Control App service test slots read access. +- You can optionally add the two test slots behind a [reverse proxy](reverse-proxy.md) or restrict access otherwise + ## Specify default page An alternative default page can be configured for the FoxIDs site using the `Settings:WebsiteUrl` setting. If configured a full URL is required like e.g., `https://www.foxidsxxxx.com`. diff --git a/docs/images/configure-reverse-proxy-secret-firewall.png b/docs/images/configure-reverse-proxy-secret-firewall.png new file mode 100644 index 000000000..f91d3ede1 Binary files /dev/null and b/docs/images/configure-reverse-proxy-secret-firewall.png differ diff --git a/docs/images/configure-reverse-proxy-secret-permissions.png b/docs/images/configure-reverse-proxy-secret-permissions.png new file mode 100644 index 000000000..fa7e7ca7a Binary files /dev/null and b/docs/images/configure-reverse-proxy-secret-permissions.png differ diff --git a/docs/oidc.md b/docs/oidc.md index b5952b7a5..a6b11898d 100644 --- a/docs/oidc.md +++ b/docs/oidc.md @@ -16,6 +16,8 @@ How to guides: - 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) ## Down-party diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index 86386851f..108ce31eb 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -1,18 +1,27 @@ # Reverse proxy It is recommended to place both the FoxIDs Azure App service and the FoxIDs Control Azure App service behind a reverse proxy. +The [custom primary domains](deployment.md#custom-primary-domains) is exposed through the reverse proxy alongside optionally [custom domains](custom-domain.md). + The FoxIDs service support [custom domains](custom-domain.md) which is handled with domain rewrite through the reverse proxy. > FoxIDs only support [custom domains](custom-domain.md) if it is behind a reverse proxy and the access is restricted by the `X-FoxIDs-Secret` HTTP header or the `Settings:TrustProxyHeaders` setting is set to `true` in the FoxIDs App Service configuration. -The [custom primary domains](deployment.md#custom-primary-domains) is exposed through the reverse proxy alongside optionally [custom domains](custom-domain.md). - ## Restrict access Both the FoxIDs service and FoxIDs Control sites can restrict access based on the `X-FoxIDs-Secret` HTTP header. The access restriction is activated by adding a secret with the name `Settings--ProxySecret` in Key Vault. +1. Grant your IP address access through the Key Vault firewall +![Configure reverse proxy secret - firewall](images/configure-reverse-proxy-secret-firewall.png) + +2. Grant your user List and Set permissions in Access policies. +![Configure reverse proxy secret - permissions](images/configure-reverse-proxy-secret-permissions.png) + +3. Add the `Settings--ProxySecret` secret ![Configure reverse proxy secret](images/configure-reverse-proxy-secret.png) +4. After successfully configuration, remove you IP address and permissions. + > The sites needs to be restarted to read the secret. After the reverse proxy secret has been configured in Key Vault the reverse proxy needs to add the `X-FoxIDs-Secret` HTTP header in all backed calls to FoxIDs to get access. @@ -31,17 +40,25 @@ FoxIDs service support reading the [custom domain](custom-domain.md) (host name) > The host header is only read if access is restricted by the `X-FoxIDs-Secret` HTTP header. -## Tested reverse proxies -FoxIDs is tested with the following reverse proxies. +## Supported and tested reverse proxies +FoxIDs generally support all reverse proxies. The following reverse proxies is tested to work with FoxIDs. ### Azure Front Door -Azure Front Door can be configured as a reverse proxy with close to the default setup. Azure Front Door rewrite domains by default. -The `X-FoxIDs-Secret` HTTP header can optionally be added but is required to support [custom domain](custom-domain.md). +Azure Front Door can be configured as a reverse proxy. Azure Front Door rewrite domains by default. > Do NOT enable caching. The `Accept-Language` header is not forwarded if caching is enabled. The header is required by FoxIDs to support cultures. +Configuration: +- Add a Azure Front Door endpoint for both the FoxIDs App Service and the FoxIDs Control App Service +- 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) +- Disable Session affinity +- Optionally configure WAF policies + ### Cloudflare -Cloudflare can be configured as a reverse proxy. But Cloudflare require a Enterprise plan to rewrite domains (host headers). The `X-FoxIDs-Secret` HTTP header should can be added. +Cloudflare can be configured as a reverse proxy. But Cloudflare require a Enterprise plan to rewrite domains (host headers). The `X-FoxIDs-Secret` HTTP header should be added. ### IIS ARR Proxy Internet Information Services (IIS) Application Request Routing (ARR) Proxy require a Windows server. ARR Proxy rewrite domains with a rewrite rule. diff --git a/docs/standard-support.md b/docs/standard-support.md index 5f90488d9..7ac793a6a 100644 --- a/docs/standard-support.md +++ b/docs/standard-support.md @@ -16,5 +16,5 @@ - [SAML 2.0 metadata](https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf) - OAuth 2.0 limited to down-party [Client Credential Grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4) - [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) -- One-Time Password (OPT) supported by MFA +- Two-factor authentication (2FA) with One-Time Password (OPT) - [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238) \ No newline at end of file diff --git a/docs/up-party-howto-oidc-nets-eid-broker.md b/docs/up-party-howto-oidc-nets-eid-broker.md new file mode 100644 index 000000000..4bf3edbae --- /dev/null +++ b/docs/up-party-howto-oidc-nets-eid-broker.md @@ -0,0 +1,103 @@ +# Up-party - connect Nets eID Broker with OpenID Connect + +FoxIDs can be connected to Nets eID Broker with OpenID Connect and thereby authenticating end users with MitID and other credentials supported by Nets eID Broker. + +How to configure Nets eID Broker in +- [test environment](#configuring-nets-eid-broker-demotest-as-openid-provider-op) using Nets eID Broker demo +- [production environment](#configuring-nets-eid-broker-as-openid-provider-op) using Nets eID Broker admin portal + +> A connection to Nets eID Broker demo can be tested with the [samples](samples.md). E.g., with the [AspNetCoreOidcAuthCodeAllUpPartiesSample](https://github.com/ITfoxtec/FoxIDs.Samples/tree/master/src/AspNetCoreOidcAuthCodeAllUpPartiesSample) in the [sample solution](https://github.com/ITfoxtec/FoxIDs.Samples). + +## Configuring Nets eID Broker demo/test as OpenID Provider (OP) + +This guide describes how to connect a FoxIDs up-party to Nets eID Broker demo in the test environment. + +Nets eID Broker has a [MitID demo](https://broker.signaturgruppen.dk/en/technical-documentation/open-oidc-clients) where all clients can connect without prior registration. All redirect URIs are accepted. +Her you can find all needed to register a client with Nets eID Broker. + +This connection use OpenID Connect Authorization Code flow with PKCE, which is the recommended OpenID Connect flow. + +**Create an OpenID Connect up-party client in [FoxIDs Control Client](control.md#foxids-control-client)** + +1. Add the name +2. Add the Nets eID Broker demo authority `https://pp.netseidbroker.dk/op` in the Authority field +3. In the scopes list add `mitid` (to support MitID) and optionally `nemid` (to support the old NemID) +4. Add the Nets eID Broker demo secret `rnlguc7CM/wmGSti4KCgCkWBQnfslYr0lMDZeIFsCJweROTROy2ajEigEaPQFl76Py6AVWnhYofl/0oiSAgdtg==` in the Client secret field +5. Select show advanced settings +6. Add the Nets eID Broker demo client id `0a775a87-878c-4b83-abe3-ee29c720c3e7` in the Optional customer SP client ID field +7. Select use claims from ID token +8. Click create + +That's it, you are done. + +> The new up-party can now be selected as an allowed up-party in a down-party. +> The down-party can read the claims from the up-party. You can optionally add a `*` in the down-party Issue claims list to issue all the claims to your application. Or optionally define a [scope to issue claims](#scope-and-claims). + +## Configuring Nets eID Broker as OpenID Provider (OP) + +This guide describes how to connect a FoxIDs up-party to the Nets eID Broker in the production environment. + +You are granted access to the [Nets eID Broker admin portal](https://netseidbroker.dk/admin) by Nets. The Nets eID Broker [documentation](https://broker.signaturgruppen.dk/en/technical-documentation). + +This connection use OpenID Connect Authorization Code flow with PKCE, which is the recommended OpenID Connect flow. + +**1 - Start by creating an API client in [Nets eID Broker admin portal](https://netseidbroker.dk/admin)** + + 1. Navigate to Services & Clients + 2. Select the Service Provider + 3. Create or select a Service + 4. Click Add new client + 5. Add a Client name + 6. Select Web + 7. Click Create + 8. Copy the Client ID + 9. Click Create new Client Secret + 10. Select Based on password + 11. Add a name for the new client secret + 12. Click Generate on server + 13. Copy the Secret + 14. Click the IDP tab + 15. Select MitID and click `Add to pre-selected login options`, optionally select others + 16. Click the Advanced tab + 17. Set PKCE to Active + +**2 - Then create an OpenID Connect up-party client in [FoxIDs Control Client](control.md#foxids-control-client)** + +1. Add the name +2. Add the Nets eID Broker demo authority `https://netseidbroker.dk/op` in the Authority field +3. Copy the two URLs: `Redirect URL` and `Post logout redirect URL` +4. In the scopes list add `mitid` (to support MitID) and optionally other scopes like e.g, `nemid.pid` to request the NemID PID and/or `ssn` to request the CPR number +5. Add the Nets eID Broker secret in the Client secret field +6. Select show advanced settings +7. Add the Nets eID Broker client id in the Optional customer SP client ID field +8. Select use claims from ID token +9. Click create + + **3 - Go back to [Nets eID Broker admin portal](https://netseidbroker.dk/admin)** + + 1. Click the Endpoints tab + 2. Add the two URLs from the FoxIDs up-party client: `Redirect URL` and `Post logout redirect URL` in the fields `Login redirects` and `Logout redirects`. + +That's it, you are done. + +> The new up-party can now be selected as an allowed up-party in a down-party. +> The down-party can read the claims from the up-party. You can optionally add a `*` in the down-party Issue claims list to issue all the claims to your application. Or optionally define a [scope to issue claims](#scope-and-claims). + +## Scope and claims +You can optionally create a scope on the down-party with the Nets eID Broker claims as voluntary claims. The scope can then be used by a OpenID Connect client or another FoxIDs up-party acting as a OpenID Connect client. + +The name of the scope can e.g, be `nets_eid_broker` + +The most used Nets eID Broker claims: + +- `identity_type` +- `nemid.pid` +- `nemid.pid_status` +- `dk.cpr` +- `loa` +- `acr` +- `neb_sid` +- `idp` +- `idp_transaction_id` +- `transaction_id` +- `session_expiry` \ No newline at end of file diff --git a/docs/up-party-howto-oidc-signicat.md b/docs/up-party-howto-oidc-signicat.md new file mode 100644 index 000000000..beceaf4e9 --- /dev/null +++ b/docs/up-party-howto-oidc-signicat.md @@ -0,0 +1,47 @@ +# Up-party - connect Signicat with OpenID Connect + +FoxIDs can be connected to Signicat with OpenID Connect and thereby authenticating end users with MitID and all other credentials supported by Signicat. + +> A connection to Signicat Express can be tested with the [samples](samples.md). E.g., with the [AspNetCoreOidcAuthCodeAllUpPartiesSample](https://github.com/ITfoxtec/FoxIDs.Samples/tree/master/src/AspNetCoreOidcAuthCodeAllUpPartiesSample) in the [sample solution](https://github.com/ITfoxtec/FoxIDs.Samples). + +You can create a [free account](https://www.signicat.com/sign-up/express-api-onboarding) on [Signicat Express](https://developer.signicat.com/express/docs/) and get access to the [dashbord](https://dashboard-test.signicat.io/dashboard). +Her you have access to the test environment. + +This guide describes how to connect a FoxIDs up-party to the Signicat Express test environment. + +## Configuring Signicat as OpenID Provider (OP) + +This connection use OpenID Connect Authorization Code flow with PKCE, which is the recommended OpenID Connect flow. + +**1 - Start by creating an API client in [Signicat Express dashbord](https://dashboard-test.signicat.io/dashboard)** + + 1. Navigate to Account and then API Clients + 2. Add the Client name + 3. In Auth Flow / Grant Type select Authorization code + 4. Copy the Secret + 5. Click Create + 6. Copy the Client ID + +**2 - Then create an OpenID Connect up-party client in [FoxIDs Control Client](control.md#foxids-control-client)** + + 1. Add the name + 2. Add the Signicat Express test authority `https://login-test.signicat.io` in the Authority field + 3. Copy the three URLs: `Redirect URL`, `Post logout redirect URL` and `Front channel logout URL` + 4. In the scopes list add `profile` + 5. Add the Signicat Express secret in the Client secret field + 6. Select show advanced settings + 7. Add the Signicat Express client id in the Optional customer SP client ID field + 8. Click create + + **3 - Go back to [Signicat Express dashbord](https://dashboard-test.signicat.io/dashboard)** + + 1. Click OAuth / OpenID + 2. Click Edit + 3. Find the App URIs section + 4. Add the three URLs from the FoxIDs up-party client: `Redirect URL`, `Post logout redirect URL` and `Front channel logout URL` in the respectively fields + 5. Click Save + +That's it, you are done. + +> The new up-party can now be selected as an allowed up-party in a down-party. +> The down-party can read the claims from the up-party. You can optionally add a `*` in the down-party Issue claims list to issue all the claims to your application. diff --git a/docs/up-party-howto.md b/docs/up-party-howto.md new file mode 100644 index 000000000..afee3c501 --- /dev/null +++ b/docs/up-party-howto.md @@ -0,0 +1,29 @@ +# 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 + +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 + +Configure [SAML 2.0 up-party](up-party-saml-2.0.md) which trust an external SAML 2.0 Identity Provider (IdP). + +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 diff --git a/docs/up-party-oidc.md b/docs/up-party-oidc.md index 387c2980d..bde326235 100644 --- a/docs/up-party-oidc.md +++ b/docs/up-party-oidc.md @@ -12,6 +12,8 @@ How to guides: - 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) > It is recommended to use OpenID Connect Authorization Code flow with PKCE, because it is considered a secure flow. diff --git a/src/FoxIDs.Control/Controllers/Helpers/TReadCertificateController.cs b/src/FoxIDs.Control/Controllers/Helpers/TReadCertificateController.cs index 60d97895d..83ef3211b 100644 --- a/src/FoxIDs.Control/Controllers/Helpers/TReadCertificateController.cs +++ b/src/FoxIDs.Control/Controllers/Helpers/TReadCertificateController.cs @@ -39,9 +39,18 @@ public TReadCertificateController(TelemetryScopedLogger logger, IMapper mapper) false => new X509Certificate2(WebEncoders.Base64UrlDecode(certificateAndPassword.EncodeCertificate), certificateAndPassword.Password, keyStorageFlags: X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable), }; + if (!certificate.HasPrivateKey) + { + throw new ValidationException("Unable to read the certificates private key. E.g, try to convert the certificate and save the certificate with 'TripleDES-SHA1'."); + } + var jwt = await certificate.ToFTJsonWebKeyAsync(includePrivateKey: true); return Ok(mapper.Map(jwt)); } + catch (ValidationException) + { + throw; + } catch (Exception ex) { throw new ValidationException("Unable to read certificate.", ex); diff --git a/src/FoxIDs.Control/FoxIDs.Control.csproj b/src/FoxIDs.Control/FoxIDs.Control.csproj index 6b9bb531f..7ccbeae2d 100644 --- a/src/FoxIDs.Control/FoxIDs.Control.csproj +++ b/src/FoxIDs.Control/FoxIDs.Control.csproj @@ -2,7 +2,7 @@ net7.0 - 1.0.12 + 1.0.13 FoxIDs Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj b/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj index ca4bf9e80..7d5b65225 100644 --- a/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj +++ b/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj @@ -2,7 +2,7 @@ net7.0 - 1.0.12 + 1.0.13 FoxIDs.Client Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Tenants/CreateTenantViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Tenants/CreateTenantViewModel.cs index 8d5545f14..fdbe56257 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Tenants/CreateTenantViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Tenants/CreateTenantViewModel.cs @@ -23,12 +23,6 @@ public class CreateTenantViewModel [Display(Name = "Administrator email")] public string AdministratorEmail { get; set; } - /// - /// True if the administrator account should be confirmed. - /// - [Display(Name = "Confirm administrator account")] - public bool ConfirmAdministratorAccount { get; set; } - /// /// Administrator password. /// @@ -38,6 +32,18 @@ public class CreateTenantViewModel [Display(Name = "Administrator password")] public string AdministratorPassword { get; set; } + /// + /// True if the administrator account password should be changed on first login. Default true. + /// + [Display(Name = "Change administrator password")] + public bool ChangeAdministratorPassword { get; set; } = true; + + /// + /// True if the administrator account should be confirmed. Default true. + /// + [Display(Name = "Confirm administrator account")] + public bool ConfirmAdministratorAccount { get; set; } = true; + /// /// Plan (optional). /// diff --git a/src/FoxIDs.ControlClient/Shared/MainLayout.razor b/src/FoxIDs.ControlClient/Shared/MainLayout.razor index 55aa4f192..81e0a885f 100644 --- a/src/FoxIDs.ControlClient/Shared/MainLayout.razor +++ b/src/FoxIDs.ControlClient/Shared/MainLayout.razor @@ -133,6 +133,7 @@ + diff --git a/src/FoxIDs.ControlShared/FoxIDs.ControlShared.csproj b/src/FoxIDs.ControlShared/FoxIDs.ControlShared.csproj index 84519fb45..29199d555 100644 --- a/src/FoxIDs.ControlShared/FoxIDs.ControlShared.csproj +++ b/src/FoxIDs.ControlShared/FoxIDs.ControlShared.csproj @@ -2,7 +2,7 @@ net7.0 - 1.0.12 + 1.0.13 FoxIDs Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs.ControlShared/Models/Api/Tenants/CreateTenantRequest.cs b/src/FoxIDs.ControlShared/Models/Api/Tenants/CreateTenantRequest.cs index f43f99ed9..5d0a65338 100644 --- a/src/FoxIDs.ControlShared/Models/Api/Tenants/CreateTenantRequest.cs +++ b/src/FoxIDs.ControlShared/Models/Api/Tenants/CreateTenantRequest.cs @@ -23,13 +23,13 @@ public class CreateTenantRequest : Tenant public string AdministratorPassword { get; set; } /// - /// True if the administrator account password should be changed on first login. + /// True if the administrator account password should be changed on first login. Default true. /// [Display(Name = "Change administrator password")] public bool ChangeAdministratorPassword { get; set; } /// - /// True if the administrator account email should be confirmed. + /// True if the administrator account email should be confirmed. Default true. /// [Display(Name = "Confirm administrator account")] public bool ConfirmAdministratorAccount { get; set; } diff --git a/src/FoxIDs.Shared/FoxIDs.Shared.csproj b/src/FoxIDs.Shared/FoxIDs.Shared.csproj index 861cbfff6..b6011b955 100644 --- a/src/FoxIDs.Shared/FoxIDs.Shared.csproj +++ b/src/FoxIDs.Shared/FoxIDs.Shared.csproj @@ -2,7 +2,7 @@ net7.0 - 1.0.12 + 1.0.13 FoxIDs Anders Revsgaard ITfoxtec diff --git a/src/FoxIDs.Shared/Infrastructure/Hosting/RouteBindingMiddleware.cs b/src/FoxIDs.Shared/Infrastructure/Hosting/RouteBindingMiddleware.cs index 1f7f7157e..821bf9f5e 100644 --- a/src/FoxIDs.Shared/Infrastructure/Hosting/RouteBindingMiddleware.cs +++ b/src/FoxIDs.Shared/Infrastructure/Hosting/RouteBindingMiddleware.cs @@ -163,17 +163,17 @@ private async Task GetTrackAsync(Track.IdKey idKey, bool hasCustomDomain) { if (hasCustomDomain && idKey.TenantName.Equals(idKey.TrackName, StringComparison.OrdinalIgnoreCase)) { - throw new RouteCreationException($"Invalid tenant and track name '{idKey.TenantName}'. The URL for a custom domain has to be without the tenant element.", ex); + throw new RouteCreationException($"Invalid tenant and track '{idKey.TenantName}'. The URL for a custom domain has to be without the tenant element.", ex); } - throw new RouteCreationException($"Invalid tenant name '{idKey.TenantName}' and track name '{idKey.TrackName}'.", ex); + throw new RouteCreationException($"Invalid tenant '{idKey.TenantName}' and track '{idKey.TrackName}'.", ex); } } if (hasCustomDomain && idKey.TenantName.Equals(idKey.TrackName, StringComparison.OrdinalIgnoreCase)) { - throw new RouteCreationException($"Error loading tenant and track name '{idKey.TenantName}'.", ex); + throw new RouteCreationException($"Error loading tenant and track '{idKey.TenantName}'.", ex); } - throw new RouteCreationException($"Error loading tenant name '{idKey.TenantName}' and track name '{idKey.TrackName}'.", ex); + throw new RouteCreationException($"Error loading tenant '{idKey.TenantName}' and track '{idKey.TrackName}'.", ex); } } } diff --git a/src/FoxIDs.Shared/Logic/MasterTenantLogic.cs b/src/FoxIDs.Shared/Logic/MasterTenantLogic.cs index b49266c9e..5a9ca0e2e 100644 --- a/src/FoxIDs.Shared/Logic/MasterTenantLogic.cs +++ b/src/FoxIDs.Shared/Logic/MasterTenantLogic.cs @@ -66,6 +66,7 @@ public async Task CreateMasterLoginDocumentAsync(string tenantName EnableCreateUser = false, EnableCancelLogin = false, SessionLifetime = 0, + SessionAbsoluteLifetime = 0, PersistentSessionLifetimeUnlimited = false, LogoutConsent = LoginUpPartyLogoutConsent.IfRequired }; diff --git a/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj b/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj index 66fe013cf..1864f8c53 100644 --- a/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj +++ b/src/FoxIDs.SharedBase/FoxIDs.SharedBase.csproj @@ -2,7 +2,7 @@ net7.0 - 1.0.12 + 1.0.13 FoxIDs Anders Revsgaard ITfoxtec @@ -11,7 +11,7 @@ - + diff --git a/src/FoxIDs/FoxIDs.csproj b/src/FoxIDs/FoxIDs.csproj index 5fdec4cf0..aeb857748 100644 --- a/src/FoxIDs/FoxIDs.csproj +++ b/src/FoxIDs/FoxIDs.csproj @@ -1,7 +1,7 @@  net7.0 - 1.0.12 + 1.0.13 FoxIDs Anders Revsgaard ITfoxtec @@ -31,7 +31,7 @@ - + diff --git a/src/FoxIDs/Infrastructure/Hosting/FoxIDsRouteBindingMiddleware.cs b/src/FoxIDs/Infrastructure/Hosting/FoxIDsRouteBindingMiddleware.cs index b60d6a10c..9c45113a8 100644 --- a/src/FoxIDs/Infrastructure/Hosting/FoxIDsRouteBindingMiddleware.cs +++ b/src/FoxIDs/Infrastructure/Hosting/FoxIDsRouteBindingMiddleware.cs @@ -242,7 +242,7 @@ private async Task GetUpPartyAsync(Track.IdKey trackIdKey, Group upPart } catch (Exception ex) { - throw new RouteCreationException($"Invalid tenantName '{trackIdKey.TenantName}', trackName '{trackIdKey.TrackName}' and upPartyName '{upPartyGroup.Value}'.", ex); + throw new RouteCreationException($"Invalid tenant '{trackIdKey.TenantName}', track '{trackIdKey.TrackName}' and up-party '{upPartyGroup.Value}' combination.", ex); } } @@ -254,7 +254,7 @@ private async Task GetDownPartyAsync(Track.IdKey trackIdKey, Group do } catch (Exception ex) { - throw new RouteCreationException($"Invalid tenantName '{trackIdKey.TenantName}', trackName '{trackIdKey.TrackName}' and downPartyName '{downPartyGroup.Value}'.", ex); + throw new RouteCreationException($"Invalid tenant '{trackIdKey.TenantName}', track '{trackIdKey.TrackName}' and down-party '{downPartyGroup.Value}' combination.", ex); } } diff --git a/src/FoxIDs/Repository/TrackCookieRepository.cs b/src/FoxIDs/Repository/TrackCookieRepository.cs index 936ccc8ab..4a8631527 100644 --- a/src/FoxIDs/Repository/TrackCookieRepository.cs +++ b/src/FoxIDs/Repository/TrackCookieRepository.cs @@ -42,23 +42,24 @@ public Task DeleteAsync() private TMessage Get() { - if (RouteBindingDoNotExists()) return null; - CheckRouteBinding(); + var routeBinding = GetRouteBinding(); + if (RouteBindingDoNotExists(routeBinding)) return null; + CheckRouteBinding(routeBinding); - logger.ScopeTrace(() => $"Get track cookie '{typeof(TMessage).Name}', route '{RouteBinding.Route}'."); + logger.ScopeTrace(() => $"Get track cookie '{typeof(TMessage).Name}', route '{routeBinding.Route}'."); var cookie = httpContextAccessor.HttpContext.Request.Cookies[CookieName()]; if (!cookie.IsNullOrWhiteSpace()) { try { - var envelope = CookieEnvelope.FromCookieString(CreateProtector(), cookie); + var envelope = CookieEnvelope.FromCookieString(CreateProtector(routeBinding), cookie); return envelope.Message; } catch (CryptographicException ex) { logger.Warning(ex, $"Unable to unprotect track cookie '{typeof(TMessage).Name}', deleting cookie."); - DeleteByName(CookieName()); + DeleteByName(routeBinding, CookieName()); return null; } catch (Exception ex) @@ -74,10 +75,11 @@ private TMessage Get() private void Save(TMessage message) { - CheckRouteBinding(); + var routeBinding = GetRouteBinding(); + CheckRouteBinding(routeBinding); if (message == null) new ArgumentNullException(nameof(message)); - logger.ScopeTrace(() => $"Save track cookie '{typeof(TMessage).Name}', route '{RouteBinding.Route}'."); + logger.ScopeTrace(() => $"Save track cookie '{typeof(TMessage).Name}', route '{routeBinding.Route}'."); var cookieOptions = new CookieOptions { @@ -85,7 +87,7 @@ private void Save(TMessage message) HttpOnly = true, SameSite = message.SameSite, IsEssential = true, - Path = GetPath(), + Path = GetPath(routeBinding), }; httpContextAccessor.HttpContext.Response.Cookies.Append( @@ -93,37 +95,38 @@ private void Save(TMessage message) new CookieEnvelope { Message = message, - }.ToCookieString(CreateProtector()), + }.ToCookieString(CreateProtector(routeBinding)), cookieOptions); } private void Delete() { - if (RouteBindingDoNotExists()) return; - CheckRouteBinding(); + var routeBinding = GetRouteBinding(); + if (RouteBindingDoNotExists(routeBinding)) return; + CheckRouteBinding(routeBinding); - logger.ScopeTrace(() => $"Delete track cookie '{typeof(TMessage).Name}', route '{RouteBinding.Route}'."); + logger.ScopeTrace(() => $"Delete track cookie '{typeof(TMessage).Name}', route '{routeBinding.Route}'."); - DeleteByName(CookieName()); + DeleteByName(routeBinding, CookieName()); } - private void CheckRouteBinding() + private void CheckRouteBinding(RouteBinding routeBinding) { - if (RouteBinding == null) new ArgumentNullException(nameof(RouteBinding)); - if (RouteBinding.TenantName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(RouteBinding.TenantName), RouteBinding.GetTypeName()); - if (RouteBinding.TrackName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(RouteBinding.TrackName), RouteBinding.GetTypeName()); + if (routeBinding == null) new ArgumentNullException(nameof(routeBinding)); + if (routeBinding.TenantName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(routeBinding.TenantName), routeBinding.GetTypeName()); + if (routeBinding.TrackName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(routeBinding.TrackName), routeBinding.GetTypeName()); } - private bool RouteBindingDoNotExists() + private bool RouteBindingDoNotExists(RouteBinding routeBinding) { - if (RouteBinding == null) return true; - if (RouteBinding.TenantName.IsNullOrEmpty()) return true; - if (RouteBinding.TrackName.IsNullOrEmpty()) return true; + if (routeBinding == null) return true; + if (routeBinding.TenantName.IsNullOrEmpty()) return true; + if (routeBinding.TrackName.IsNullOrEmpty()) return true; return false; } - private void DeleteByName(string name) + private void DeleteByName(RouteBinding routeBinding, string name) { httpContextAccessor.HttpContext.Response.Cookies.Append( name, @@ -135,18 +138,18 @@ private void DeleteByName(string name) HttpOnly = true, SameSite = new TMessage().SameSite, IsEssential = true, - Path = GetPath(), + Path = GetPath(routeBinding), }); } - private string GetPath() + private string GetPath(RouteBinding routeBinding) { - return $"/{RouteBinding.TenantName}/{RouteBinding.TrackName}"; + return $"{(!routeBinding.HasCustomDomain ? $"/{routeBinding.TenantName}" : string.Empty)}/{routeBinding.TrackName}"; } - private IDataProtector CreateProtector() + private IDataProtector CreateProtector(RouteBinding routeBinding) { - return dataProtection.CreateProtector(new[] { RouteBinding.TenantName, RouteBinding.TrackName }); + return dataProtection.CreateProtector(new[] { routeBinding.TenantName, routeBinding.TrackName }); } private string CookieName() @@ -154,6 +157,6 @@ private string CookieName() return typeof(TMessage).Name.ToLower(); } - private RouteBinding RouteBinding => httpContextAccessor.HttpContext.GetRouteBinding(); + private RouteBinding GetRouteBinding() => httpContextAccessor.HttpContext.GetRouteBinding(); } } diff --git a/src/FoxIDs/Repository/UpPartyCookieRepository.cs b/src/FoxIDs/Repository/UpPartyCookieRepository.cs index 3ca80359b..a61dec6d6 100644 --- a/src/FoxIDs/Repository/UpPartyCookieRepository.cs +++ b/src/FoxIDs/Repository/UpPartyCookieRepository.cs @@ -42,22 +42,23 @@ public Task DeleteAsync(UpParty party, bool tryDelete = false) private TMessage Get(UpParty party, bool delete, bool tryGet = false) { - if (tryGet && RouteBindingDoNotExists()) return null; - CheckRouteBinding(); + var routeBinding = GetRouteBinding(); + if (tryGet && RouteBindingDoNotExists(routeBinding)) return null; + CheckRouteBinding(routeBinding); - logger.ScopeTrace(() => $"Get up-party cookie '{typeof(TMessage).Name}', route '{RouteBinding.Route}', delete '{delete}'."); + logger.ScopeTrace(() => $"Get up-party cookie '{typeof(TMessage).Name}', route '{routeBinding.Route}', delete '{delete}'."); var cookie = httpContextAccessor.HttpContext.Request.Cookies[CookieName()]; if (!cookie.IsNullOrWhiteSpace()) { try { - var envelope = CookieEnvelope.FromCookieString(CreateProtector(), cookie); + var envelope = CookieEnvelope.FromCookieString(CreateProtector(routeBinding), cookie); if (delete) { - logger.ScopeTrace(() => $"Delete up-party cookie, '{typeof(TMessage).Name}', route '{RouteBinding.Route}'."); - DeleteByName(party, CookieName()); + logger.ScopeTrace(() => $"Delete up-party cookie, '{typeof(TMessage).Name}', route '{routeBinding.Route}'."); + DeleteByName(routeBinding, party, CookieName()); } return envelope.Message; @@ -65,7 +66,7 @@ private TMessage Get(UpParty party, bool delete, bool tryGet = false) catch (CryptographicException ex) { logger.Warning(ex, $"Unable to unprotect up-party cookie '{typeof(TMessage).Name}', deleting cookie."); - DeleteByName(party, CookieName()); + DeleteByName(routeBinding, party, CookieName()); return null; } catch (Exception ex) @@ -81,10 +82,11 @@ private TMessage Get(UpParty party, bool delete, bool tryGet = false) private void Save(UpParty party, TMessage message, DateTimeOffset? persistentCookieExpires) { - CheckRouteBinding(); + var routeBinding = GetRouteBinding(); + CheckRouteBinding(routeBinding); if (message == null) new ArgumentNullException(nameof(message)); - logger.ScopeTrace(() => $"Save up-party cookie '{typeof(TMessage).Name}', route '{RouteBinding.Route}'."); + logger.ScopeTrace(() => $"Save up-party cookie '{typeof(TMessage).Name}', route '{routeBinding.Route}'."); var cookieOptions = new CookieOptions { @@ -92,7 +94,7 @@ private void Save(UpParty party, TMessage message, DateTimeOffset? persistentCoo HttpOnly = true, SameSite = message.SameSite, IsEssential = true, - Path = GetPath(party), + Path = GetPath(routeBinding, party), }; if (persistentCookieExpires != null) { @@ -104,39 +106,40 @@ private void Save(UpParty party, TMessage message, DateTimeOffset? persistentCoo new CookieEnvelope { Message = message, - }.ToCookieString(CreateProtector()), + }.ToCookieString(CreateProtector(routeBinding)), cookieOptions); } private void Delete(UpParty party, bool tryDelete = false) { - if (tryDelete && RouteBindingDoNotExists()) return; - CheckRouteBinding(); + var routeBinding = GetRouteBinding(); + if (tryDelete && RouteBindingDoNotExists(routeBinding)) return; + CheckRouteBinding(routeBinding); - logger.ScopeTrace(() => $"Delete up-party cookie '{typeof(TMessage).Name}', route '{RouteBinding.Route}'."); + logger.ScopeTrace(() => $"Delete up-party cookie '{typeof(TMessage).Name}', route '{routeBinding.Route}'."); - DeleteByName(party, CookieName()); + DeleteByName(routeBinding, party, CookieName()); } - private void CheckRouteBinding() + private void CheckRouteBinding(RouteBinding routeBinding) { - if (RouteBinding == null) new ArgumentNullException(nameof(RouteBinding)); - if (RouteBinding.TenantName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(RouteBinding.TenantName), RouteBinding.GetTypeName()); - if (RouteBinding.TrackName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(RouteBinding.TrackName), RouteBinding.GetTypeName()); - if (RouteBinding.UpParty == null) throw new ArgumentNullException(nameof(RouteBinding.UpParty), RouteBinding.GetTypeName()); + if (routeBinding == null) new ArgumentNullException(nameof(routeBinding)); + if (routeBinding.TenantName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(routeBinding.TenantName), routeBinding.GetTypeName()); + if (routeBinding.TrackName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(routeBinding.TrackName), routeBinding.GetTypeName()); + if (routeBinding.UpParty == null) throw new ArgumentNullException(nameof(routeBinding.UpParty), routeBinding.GetTypeName()); } - private bool RouteBindingDoNotExists() + private bool RouteBindingDoNotExists(RouteBinding routeBinding) { - if (RouteBinding == null) return true; - if (RouteBinding.TenantName.IsNullOrEmpty()) return true; - if (RouteBinding.TrackName.IsNullOrEmpty()) return true; - if (RouteBinding.UpParty == null) return true; + if (routeBinding == null) return true; + if (routeBinding.TenantName.IsNullOrEmpty()) return true; + if (routeBinding.TrackName.IsNullOrEmpty()) return true; + if (routeBinding.UpParty == null) return true; return false; } - private void DeleteByName(UpParty party, string name) + private void DeleteByName(RouteBinding routeBinding, UpParty party, string name) { httpContextAccessor.HttpContext.Response.Cookies.Append( name, @@ -148,18 +151,18 @@ private void DeleteByName(UpParty party, string name) HttpOnly = true, SameSite = new TMessage().SameSite, IsEssential = true, - Path = GetPath(party), + Path = GetPath(routeBinding, party), }); } - private string GetPath(UpParty party) + private string GetPath(RouteBinding routeBinding, UpParty party) { - return $"/{RouteBinding.TenantName}/{RouteBinding.TrackName}/{RouteBinding.UpParty.Name.ToUpPartyBinding(party.PartyBindingPattern)}"; + return $"{(!routeBinding.HasCustomDomain ? $"/{routeBinding.TenantName}" : string.Empty)}/{routeBinding.TrackName}/{routeBinding.UpParty.Name.ToUpPartyBinding(party.PartyBindingPattern)}"; } - private IDataProtector CreateProtector() + private IDataProtector CreateProtector(RouteBinding routeBinding) { - return dataProtection.CreateProtector(new[] { RouteBinding.TenantName, RouteBinding.TrackName, RouteBinding.UpParty.Name, typeof(TMessage).Name }); + return dataProtection.CreateProtector(new[] { routeBinding.TenantName, routeBinding.TrackName, routeBinding.UpParty.Name, typeof(TMessage).Name }); } private string CookieName() @@ -167,6 +170,6 @@ private string CookieName() return typeof(TMessage).Name.ToLower(); } - private RouteBinding RouteBinding => httpContextAccessor.HttpContext.GetRouteBinding(); + private RouteBinding GetRouteBinding() => httpContextAccessor.HttpContext.GetRouteBinding(); } }