diff --git a/docs/_sidebar.md b/docs/_sidebar.md index a24712241..18fb1e704 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -19,6 +19,7 @@ - [Email provider](email.md) - [Supported standards](standard-support.md) - [FoxIDs inside](foxids-inside.md) + - [Control Client & API](control#.md) - [Plans](plan.md) - [Development](development.md) - [Samples](samples.md) diff --git a/docs/control.md b/docs/control.md index a2f97f2bf..6d28772d3 100644 --- a/docs/control.md +++ b/docs/control.md @@ -60,13 +60,14 @@ The track properties can be configured by clicking the top right setting icon. ![Configure track settings](images/configure-track-setting.png) ## FoxIDs Control API -FoxIDs Control API is a REST API. The API expose a Swagger (OpenApi) interface document. +FoxIDs Control API is a REST API and has a Swagger (OpenApi) interface description. -FoxIDs Control API require that the client calling the API is granted the `foxids:master` scope to access master tenant data or the `foxids:tenant` scope access tenant data in a particular tenant. Normally only tenant data is accessed. +FoxIDs Control API require that the client calling the API is granted the `foxids:master` scope to access master tenant data or the `foxids:tenant` scope to access tenant data in a particular tenant. Normally only tenant data is accessed. - - The client can be an OAuth 2.0 client. Where the client is granted the administrator role `foxids:tenant.admin` acting as the client itself using client credentials grant. - Her is how the [sample seed tool](samples.md#configure-the-sample-seed-tool) client is granted access. - - Or a OpenID Connect client with an authenticated master track user. Where the user is granted the administrator role `foxids:tenant.admin`. + - The API can be accessed with a OAuth 2.0 client. Where the client is granted the administrator role `foxids:tenant.admin` acting as the client itself using client credentials grant. + It is probably helpful to take a look at how the [sample seed tool](samples.md#configure-the-sample-seed-tool) client is granted access. + - Or the API can be accessed with a OpenID Connect client with an authenticated master track user. Where the user is granted the administrator role `foxids:tenant.admin`. + *As an advanced option the mater user can also be granted access via a trust.* This shows the FoxIDs Control API configuration in a tenants master track with a scope that grants access to tenant data. @@ -76,5 +77,323 @@ FoxIDs Control API is called with an access token as described in the [OAuth 2.0 The Swagger (OpenApi) interface document is exposed on `.../api/swagger/v1/swagger.json`. -You can also find the FoxIDs.com Swagger (OpenApi) [interface document](https://control.foxids.com/api/swagger/v1/swagger.json) online. - +> FoxIDs.com Swagger (OpenApi) [https://control.foxids.com/api/swagger/v1/swagger.json](https://control.foxids.com/api/swagger/v1/swagger.json) + +The FoxIDs Control API URL contains the tenant name and track name on winch you want to operate `.../[tenant_name]/[track_name]/...`. +To call the API you replace the `[tenant_name]` element with your tenant name and the `[track_name]` element with the track name of the track you want to call. + +If you e.g. want read a OpenID Connect down-party on FoxIDs.com with the name `some_oidc_app` you do a HTTP GET call to `https://control.foxids.com/api/[tenant_name]/[track_name]/!oidcdownparty?name=some_oidc_app` - replaced with your tenant and track names. + +### API access rights +Access to FoxIDs Control API is limited by scopes and roles. There are two sets of scopes based on `foxids:master` which grant access to the master tenant data and `foxids:tenant` which grant access to tenant data. +The Control API resource `foxids_control_api` is defined in each tenant's master track and the configured set of scopes grant access the tenants data in the Control API. + +A scopes access is limited by adding more elements separated with semicolon and dot. The dot notation limits or grant a sub role, the notation is both used in scopes and roles. +To be granted access the caller is required to possess one or more matching scope(s) and role(s). + +Each access right is both defined as a scope and a role. This makes it possible to limit or grant access on both client and user level. The access rights are a hierarchy and the client and user do not need to be granted matching scopes and roles. + +The administrator role `foxids:tenant.admin` grants access to all data in a tenant and the master tenant data, it is the same as having the role `foxids:tenant` and `foxids:master`. + +> A client request a scope by requesting a scope on a resource, separating the resource and scope with a semicolon. E.g., to request the `foxids:tenant:track:party.create` scope the client request for `foxids_control_api:foxids:tenant:track:party.create`. + +#### Tenant access rights +The tenant access rights is at the same time both scopes and roles. + +The `:track[xxxx]` specifies a tenant e.g., the `dev` tenant is `:track[dev]`. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Scope / roleAccess
Access to everything in the tenant, not master tenant data.
foxids:tenantread, create, update, delete
foxids:tenant.readread
foxids:tenant.createcreate
foxids:tenant.updateupdate
foxids:tenant.deletedelete
Access to basic tenant elements: + +
  • My profile used in the Control Client.
  • +
  • Call the ReadCertificate API to get a JWT with certificate information from a X509 Certificate.
  • +
    +
    foxids:tenant:basicread, create, update, delete
    foxids:tenant:basic.readread
    foxids:tenant:basic.createcreate
    foxids:tenant:basic.updateupdate
    foxids:tenant:basic.deletedelete
    Access to everything in all tracks in a tenant, not including the master track.
    foxids:tenant:trackread, create, update, delete
    foxids:tenant:track.readread
    foxids:tenant:track.createcreate
    foxids:tenant:track.updateupdate
    foxids:tenant:track.deletedelete
    Access to everything in a specific track in a tenant.
    foxids:tenant:track[xxxx]read, create, update, delete
    foxids:tenant:track[xxxx].readread
    foxids:tenant:track[xxxx].createcreate
    foxids:tenant:track[xxxx].updateupdate
    foxids:tenant:track[xxxx].deletedelete
    All usage logs in all tracks in a tenant, not including the master track. Not applicable in the master tenant.
    foxids:tenant:track:usageread
    Usage logs in a specific track in a tenant. Not applicable in the master tenant.
    foxids:tenant:track[xxxx]:usageread
    All logs in all tracks in a tenant, not including the master track.
    foxids:tenant:track:logread, create, update, delete
    foxids:tenant:track:log.readread
    foxids:tenant:track:log.createcreate
    foxids:tenant:track:log.updateupdate
    foxids:tenant:track:log.deletedelete
    Logs in a specific tenant.
    foxids:tenant:track[xxxx]:logread, create, update, delete
    foxids:tenant:track[xxxx]:log.readread
    foxids:tenant:track[xxxx]:log.createcreate
    foxids:tenant:track[xxxx]:log.updateupdate
    foxids:tenant:track[xxxx]:log.deletedelete
    All users in all tracks in a tenant, not including the master track.
    foxids:tenant:track:userread, create, update, delete
    foxids:tenant:track:user.readread
    foxids:tenant:track:user.createcreate
    foxids:tenant:track:user.updateupdate
    foxids:tenant:track:user.deletedelete
    All users in a specific track in a tenant.
    foxids:tenant:track[xxxx]:userread, create, update, delete
    foxids:tenant:track[xxxx]:user.readread
    foxids:tenant:track[xxxx]:user.createcreate
    foxids:tenant:track[xxxx]:user.updateupdate
    foxids:tenant:track[xxxx]:user.deletedelete
    All down-parties and up-parties in all tracks in a tenant, not including the master track.
    foxids:tenant:track:partyread, create, update, delete
    foxids:tenant:track:party.readread
    foxids:tenant:track:party.createcreate
    foxids:tenant:track:party.updateupdate
    foxids:tenant:track:party.deletedelete
    All down-parties and up-parties in a specific track in a tenant.
    foxids:tenant:track[xxxx]:partyread, create, update, delete
    foxids:tenant:track[xxxx]:party.readread
    foxids:tenant:track[xxxx]:party.createcreate
    foxids:tenant:track[xxxx]:party.updateupdate
    foxids:tenant:track[xxxx]:party.deletedelete
    + +#### Master tenant access rights +The master tenant access rights is at the same time both scopes and roles. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Access to the master tenant data
    + Can list, create and delete tenants but not look into other tenants. +
    foxids:masterread, create, update, delete
    foxids:master.readread
    foxids:master.createcreate
    foxids:master.updateupdate
    foxids:master.deletedelete
    Usage log in the master tenant.
    foxids:master:usageread
    + +If the scope you need is not defined on the Control API `foxids_control_api` you can add the scope. The same goes for roles which has to be defined on the user or the calling client. \ No newline at end of file diff --git a/docs/faq.md b/docs/faq.md index b0bab9c8c..64a2b1f75 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -3,14 +3,18 @@ ##### Only the `sub`, `sid`, `acr` and `amr` claims are pass through. I get more claims from the up-party by using log claims trace. What am I doing wrong? By default an up-party should pass through all claims to the down-party if Forward Claims has a `*`. ![Up-party default pass through all claims to the down-party](images/faq-pass-through-all-claims-up-party.png) -You can also make the down-party (OpenID Connect client) add all claims in the access token issued to the application (not default). +You can also make the down-party (in this case a OpenID Connect client) add all claims to the access token issued to the application (not default). Navigating to the down-party then click Show advanced settings and add a `*` in the Issue claims field. Optionally also include all claims in the issued ID token. ![Make the down-party issue all claims](images/faq-pass-through-all-claims-down-party.png) +##### Is it possible to avoid the "Pick an account" dialog? +Yes FoxIDs support to forward the login hint from an up-party to an external IdP or another FoxIDs down-party. In OpenID Connect the login hint is forwarded in the `login_hint` parameter. +In SAML 2.0 the login hint is forwarded as a `NameID` with the Email Format `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` in the `Subject` element. + ##### Way am I unable to login for a moment when I change the certificate container types to 'Key Vault renewed self-signed certificates'? -The first certificate have to be generated by Key Vault before the track can perform logins again. Thereafter the certificate is renewed without seamlessly. +The first certificate have to be generated by Key Vault before the track can perform logins again. Thereafter the certificate is renewed seamlessly. ##### I am unable to logout of a client using OIDC if I login and theafter changed the certificate container type. -The problem occurs if the OIDC logout require an ID Token before accepting logout. In this case the ID Token is invalid because the container type and there by the signing certificate have changed. -The problem will occur in the FoxIDs Controle Client. You need to close the browser and start over. +The problem occurs if the OIDC logout require an ID Token before accepting logout. In this case the ID Token is invalid because the container type and there by the signing certificate have changed. +Solution: You need to close the browser and start over. diff --git a/docs/gs-context-handler.md b/docs/gs-context-handler.md index 4eadfdf06..b4b26ef5c 100644 --- a/docs/gs-context-handler.md +++ b/docs/gs-context-handler.md @@ -18,7 +18,7 @@ Test Context Handler with the Transform the [DK privilege XML claim](claim-transform-dk-privilege.md) to a JSON claim. diff --git a/docs/howto-saml-2.0-context-handler.md b/docs/howto-saml-2.0-context-handler.md index ca6f6f86e..d2088ef48 100644 --- a/docs/howto-saml-2.0-context-handler.md +++ b/docs/howto-saml-2.0-context-handler.md @@ -3,7 +3,7 @@ FoxIDs can be connected to Context Handler / Fælleskommunal Adgangsstyring (Danish identity broker) with a [SAML 2.0 up-party](up-party-saml-2.0.md). Context Handler is a Danish identity broker connecting the Danish municipalities in a common federation. -Context Handler is connected as a SAML 2.0 [Identity Provider (IdP)](#configuring-context-handler-as-identity-provider). +Context Handler is connected as a SAML 2.0 [Identity Provider (IdP)](#configuring-context-handler-as-identity-provider) based on OIOSAML 3 and OCES3 (RSASSA-PSS). ![Connect to Context Handler](images/how-to-context-handler.svg) @@ -14,7 +14,7 @@ In the test environment, FoxIDs can be connected to Context Handler as a test Id ![Connect to Context Handler RP](images/how-to-context-handler-rp.svg) -Context Handler can be configured based on either OIOSAML 2 or OIOSAML 3 and FoxIDs furthermore support the required certificates and it is possible to support NSIS. +Context Handler can be configured based on either OIOSAML 2 or OIOSAML 3 with OCES3 (RSASSA-PSS) and FoxIDs furthermore support the required certificates and it is possible to support NSIS. > You can test Context Handler login with the [online web app sample](https://aspnetcoreoidcallupsample.itfoxtec.com) ([sample docs](samples.md#aspnetcoreoidcauthcodealluppartiessample)) by clicking `Log in` and then `Danish Context Handler TEST` for the test environment (on `FoxIDs - test-corp` on Context Handler) or `Danish Context Handler` for production. > The sample is configured with a separate tracks for the Context Handler SAML 2.0 integration. diff --git a/docs/index.md b/docs/index.md index dd3d68c58..b64c70954 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,7 +17,7 @@ FoxIDs consist of two services: FoxIDs can be deployed and used by a single company or deployed as a shared cloud container and used by multiple organisations. You can select to use a shared cloud or a private cloud setup. -- FoxIDs is available at [FoxIDs.com](https://foxids.com) as an Identity Services (IDS) also called Identity as a Service (IDaaS). +- FoxIDs SaaS is available at [FoxIDs.com](https://foxids.com) as an Identity Services (IDS) also called Identity as a Service (IDaaS). FoxIDs.com is hosted in Europe and mainly in Microsoft Azure Holland, Netherlands. - You are free to [deploy](deployment.md) FoxIDs as your own private cloud on Microsoft Azure. diff --git a/docs/up-party-howto-saml-2.0-nemlogin.md b/docs/up-party-howto-saml-2.0-nemlogin.md index 3c1c68629..ee6be484a 100644 --- a/docs/up-party-howto-saml-2.0-nemlogin.md +++ b/docs/up-party-howto-saml-2.0-nemlogin.md @@ -7,7 +7,7 @@ FoxIDs will then handle the SAML 2.0 connection as a Relying Party (RP) / Servic ![Connect to NemLog-in](images/how-to-nemlogin.svg) -FoxIDs support NemLog-in and the SAML 2.0 based OIOSAML3 including single logout (SLO), logging, issuer naming, required OCES3 certificates and it is possible to support NSIS. +FoxIDs support NemLog-in and the SAML 2.0 based OIOSAML3 including single logout (SLO), logging, issuer naming, required OCES3 (RSASSA-PSS) certificates and it is possible to support NSIS. > You can test NemLog-in login with the [online web app sample](https://aspnetcoreoidcallupsample.itfoxtec.com) ([sample docs](samples.md#aspnetcoreoidcauthcodealluppartiessample)) by clicking `Log in` and then `Danish NemLog-in TEST` for the test environment or `Danish NemLog-in` for production. > The sample is configured with a separate track for the NemLog-in SAML 2.0 integration. diff --git a/src/FoxIDs.Control/BuildInfo.g.cs b/src/FoxIDs.Control/BuildInfo.g.cs new file mode 100644 index 000000000..0b5eadda3 --- /dev/null +++ b/src/FoxIDs.Control/BuildInfo.g.cs @@ -0,0 +1,9 @@ +using System; + +namespace FoxIDs +{ + public static partial class BuildInfo + { + public static DateTime CompilationTimestampUtc { get { return new DateTime(638430850953040022L, DateTimeKind.Utc); } } + } +} \ No newline at end of file diff --git a/src/FoxIDs.Control/BuildInfo.tt b/src/FoxIDs.Control/BuildInfo.tt new file mode 100644 index 000000000..82a1d0cb9 --- /dev/null +++ b/src/FoxIDs.Control/BuildInfo.tt @@ -0,0 +1,13 @@ +<#@ template debug="false" hostspecific="true" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System" #> +<#@ output extension=".g.cs" #> +using System; + +namespace FoxIDs +{ + public static partial class BuildInfo + { + public static DateTime CompilationTimestampUtc { get { return new DateTime(<# Write(DateTime.UtcNow.Ticks.ToString()); #>L, DateTimeKind.Utc); } } + } +} \ No newline at end of file diff --git a/src/FoxIDs.Control/Controllers/Base/ApiController.cs b/src/FoxIDs.Control/Controllers/Base/ApiController.cs index bb4f71149..d8b313843 100644 --- a/src/FoxIDs.Control/Controllers/Base/ApiController.cs +++ b/src/FoxIDs.Control/Controllers/Base/ApiController.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; -using Microsoft.IdentityModel.Tokens; using System; using System.Linq; diff --git a/src/FoxIDs.Control/Controllers/Base/TenantApiController.cs b/src/FoxIDs.Control/Controllers/Base/TenantApiController.cs deleted file mode 100644 index 92bac67ac..000000000 --- a/src/FoxIDs.Control/Controllers/Base/TenantApiController.cs +++ /dev/null @@ -1,12 +0,0 @@ -using FoxIDs.Infrastructure; -using FoxIDs.Infrastructure.Security; - -namespace FoxIDs.Controllers -{ - [TenantScopeAuthorize] - public abstract class TenantApiController : ApiController - { - public TenantApiController(TelemetryScopedLogger logger) : base(logger) - { } - } -} diff --git a/src/FoxIDs.Control/Controllers/Client/WController.cs b/src/FoxIDs.Control/Controllers/Client/WController.cs new file mode 100644 index 000000000..f7c8652d8 --- /dev/null +++ b/src/FoxIDs.Control/Controllers/Client/WController.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; + +namespace FoxIDs.Controllers.Client +{ + public class WController : Controller + { + private static string indexFile; + private readonly IWebHostEnvironment currentEnvironment; + + public WController(IWebHostEnvironment env) + { + currentEnvironment = env; + } + + [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Index() + { + return GetProcessedIndexFile(); + } + + private IActionResult GetProcessedIndexFile() + { + if (indexFile == null) + { + var file = currentEnvironment.WebRootFileProvider.GetFileInfo("index.html"); + indexFile = System.IO.File.ReadAllText(file.PhysicalPath); + indexFile = indexFile.Replace("{version}", BuildInfo.CompilationTimestampUtc.ToString("yyyyMMddHHmmss")); + indexFile = indexFile.Replace("{min}", currentEnvironment.IsDevelopment() ? string.Empty : ".min"); + } + return Content(indexFile, "text/html"); + } + } +} diff --git a/src/FoxIDs.Control/Controllers/Helpers/TReadCertificateController.cs b/src/FoxIDs.Control/Controllers/Helpers/TReadCertificateController.cs index 988bea5db..5f0efabd4 100644 --- a/src/FoxIDs.Control/Controllers/Helpers/TReadCertificateController.cs +++ b/src/FoxIDs.Control/Controllers/Helpers/TReadCertificateController.cs @@ -9,10 +9,12 @@ using Microsoft.AspNetCore.WebUtilities; using System; using System.ComponentModel.DataAnnotations; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TReadCertificateController : TenantApiController + [TenantScopeAuthorize(Constants.ControlApi.Segment.Base, Constants.ControlApi.Segment.Party)] + public class TReadCertificateController : ApiController { private readonly IMapper mapper; @@ -22,12 +24,12 @@ public TReadCertificateController(TelemetryScopedLogger logger, IMapper mapper) } /// - /// Read JWT with certificate information. + /// Read JWK with certificate information. /// /// Base64 URL encode certificate and optionally password. /// User. - [ProducesResponseType(typeof(Api.JwtWithCertificateInfo), StatusCodes.Status200OK)] - public async Task> PostReadCertificate([FromBody] Api.CertificateAndPassword certificateAndPassword) + [ProducesResponseType(typeof(Api.JwkWithCertificateInfo), StatusCodes.Status200OK)] + public async Task> PostReadCertificate([FromBody] Api.CertificateAndPassword certificateAndPassword) { if (!await ModelState.TryValidateObjectAsync(certificateAndPassword)) return BadRequest(ModelState); @@ -45,7 +47,7 @@ public TReadCertificateController(TelemetryScopedLogger logger, IMapper mapper) } var jwt = await certificate.ToFTJsonWebKeyAsync(includePrivateKey: true); - return Ok(mapper.Map(jwt)); + return Ok(mapper.Map(jwt)); } catch (ValidationException) { diff --git a/src/FoxIDs.Control/Controllers/Master/MFilterPlanController.cs b/src/FoxIDs.Control/Controllers/Master/MFilterPlanController.cs index f66c27cbc..9c55e7d38 100644 --- a/src/FoxIDs.Control/Controllers/Master/MFilterPlanController.cs +++ b/src/FoxIDs.Control/Controllers/Master/MFilterPlanController.cs @@ -10,10 +10,15 @@ using AutoMapper; using ITfoxtec.Identity; using System.Linq; +using FoxIDs.Infrastructure.Filters; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class MFilterPlanController : MasterApiController + [RequireMasterTenant] + [MasterScopeAuthorize] + + public class MFilterPlanController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Master/MPlanController.cs b/src/FoxIDs.Control/Controllers/Master/MPlanController.cs index 36724e97d..682210161 100644 --- a/src/FoxIDs.Control/Controllers/Master/MPlanController.cs +++ b/src/FoxIDs.Control/Controllers/Master/MPlanController.cs @@ -10,10 +10,14 @@ using System.Linq; using System; using FoxIDs.Logic; +using FoxIDs.Infrastructure.Filters; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class MPlanController : MasterApiController + [RequireMasterTenant] + [MasterScopeAuthorize] + public class MPlanController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Master/MRiskPasswordController.cs b/src/FoxIDs.Control/Controllers/Master/MRiskPasswordController.cs index 712dceba6..a1e30fde8 100644 --- a/src/FoxIDs.Control/Controllers/Master/MRiskPasswordController.cs +++ b/src/FoxIDs.Control/Controllers/Master/MRiskPasswordController.cs @@ -9,10 +9,14 @@ using System.Net; using System.Threading.Tasks; using AutoMapper; +using FoxIDs.Infrastructure.Filters; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class MRiskPasswordController : MasterApiController + [RequireMasterTenant] + [MasterScopeAuthorize] + public class MRiskPasswordController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Master/MRiskPasswordFirstController.cs b/src/FoxIDs.Control/Controllers/Master/MRiskPasswordFirstController.cs index 014359fa5..5bf23dbd3 100644 --- a/src/FoxIDs.Control/Controllers/Master/MRiskPasswordFirstController.cs +++ b/src/FoxIDs.Control/Controllers/Master/MRiskPasswordFirstController.cs @@ -7,10 +7,14 @@ using System.Collections.Generic; using System.Threading.Tasks; using AutoMapper; +using FoxIDs.Infrastructure.Filters; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class MRiskPasswordFirstController : MasterApiController + [RequireMasterTenant] + [MasterScopeAuthorize] + public class MRiskPasswordFirstController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Master/MRiskPasswordInfoController.cs b/src/FoxIDs.Control/Controllers/Master/MRiskPasswordInfoController.cs index de595d62a..50a66e00d 100644 --- a/src/FoxIDs.Control/Controllers/Master/MRiskPasswordInfoController.cs +++ b/src/FoxIDs.Control/Controllers/Master/MRiskPasswordInfoController.cs @@ -5,10 +5,14 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; +using FoxIDs.Infrastructure.Filters; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class MRiskPasswordInfoController : MasterApiController + [RequireMasterTenant] + [MasterScopeAuthorize] + public class MRiskPasswordInfoController : ApiController { private readonly IMasterRepository masterRepository; diff --git a/src/FoxIDs.Control/Controllers/Master/MRiskPasswordTestController.cs b/src/FoxIDs.Control/Controllers/Master/MRiskPasswordTestController.cs index 0254fceed..fd0aac954 100644 --- a/src/FoxIDs.Control/Controllers/Master/MRiskPasswordTestController.cs +++ b/src/FoxIDs.Control/Controllers/Master/MRiskPasswordTestController.cs @@ -1,4 +1,6 @@ using FoxIDs.Infrastructure; +using FoxIDs.Infrastructure.Filters; +using FoxIDs.Infrastructure.Security; using FoxIDs.Models; using FoxIDs.Repository; using Microsoft.AspNetCore.Http; @@ -8,7 +10,9 @@ namespace FoxIDs.Controllers { - public class MRiskPasswordTestController : MasterApiController + [RequireMasterTenant] + [MasterScopeAuthorize] + public class MRiskPasswordTestController : ApiController { private readonly IMasterRepository masterRepository; diff --git a/src/FoxIDs.Control/Controllers/Master/MasterApiController.cs b/src/FoxIDs.Control/Controllers/Master/MasterApiController.cs deleted file mode 100644 index fee052b01..000000000 --- a/src/FoxIDs.Control/Controllers/Master/MasterApiController.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FoxIDs.Infrastructure; -using FoxIDs.Infrastructure.Filters; -using FoxIDs.Infrastructure.Security; - -namespace FoxIDs.Controllers -{ - [RequireMasterTenant] - [MasterScopeAuthorize] - public abstract class MasterApiController : ApiController - { - private readonly TelemetryScopedLogger logger; - - public MasterApiController(TelemetryScopedLogger logger) : base(logger) - { - this.logger = logger; - } - } -} diff --git a/src/FoxIDs.Control/Controllers/Parties/GenericOAuthClientKeyUpPartyController.cs b/src/FoxIDs.Control/Controllers/Parties/GenericOAuthClientKeyUpPartyController.cs index 816d8a091..0584cddc7 100644 --- a/src/FoxIDs.Control/Controllers/Parties/GenericOAuthClientKeyUpPartyController.cs +++ b/src/FoxIDs.Control/Controllers/Parties/GenericOAuthClientKeyUpPartyController.cs @@ -13,13 +13,15 @@ using ITfoxtec.Identity; using System.Security.Cryptography.X509Certificates; using System; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { /// /// Abstract OAuth 2.0 import client key for up-party API. /// - public abstract class GenericOAuthClientKeyUpPartyController : TenantApiController where TParty : OAuthDownParty where TClient : OAuthDownClient where TScope : OAuthDownScope where TClaim : OAuthDownClaim + [TenantScopeAuthorize(Constants.ControlApi.Segment.Party)] + public abstract class GenericOAuthClientKeyUpPartyController : ApiController where TParty : OAuthUpParty where TClient : OAuthUpClient { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; @@ -43,7 +45,7 @@ public GenericOAuthClientKeyUpPartyController(TelemetryScopedLogger logger, IMap if (!ModelState.TryValidateRequiredParameter(partyName, nameof(partyName))) return BadRequest(ModelState); partyName = partyName?.ToLower(); - var oauthUpParty = await tenantRepository.GetAsync(await UpParty.IdFormatAsync(RouteBinding, partyName)); + var oauthUpParty = await tenantRepository.GetAsync(await UpParty.IdFormatAsync(RouteBinding, partyName)); if (oauthUpParty.Client.ClientKeys?.Count() > 0 && oauthUpParty.Client.ClientKeys.First().Type == ClientKeyTypes.KeyVaultImport) { var clientKey = oauthUpParty.Client.ClientKeys.First(); @@ -62,8 +64,8 @@ public GenericOAuthClientKeyUpPartyController(TelemetryScopedLogger logger, IMap { if (ex.StatusCode == HttpStatusCode.NotFound) { - logger.Warning(ex, $"NotFound, Get '{typeof(OAuthUpParty).Name}' client key by name '{partyName}'."); - return NotFound(typeof(OAuthUpParty).Name, partyName); + logger.Warning(ex, $"NotFound, Get '{typeof(TParty).Name}' client key by name '{partyName}'."); + return NotFound(typeof(TParty).Name, partyName); } throw; } @@ -82,7 +84,7 @@ public GenericOAuthClientKeyUpPartyController(TelemetryScopedLogger logger, IMap throw new Exception($"Key Vault and thereby client certificates is not supported in the '{plan.Name}' plan."); } - var oauthUpParty = await tenantRepository.GetAsync(await UpParty.IdFormatAsync(RouteBinding, keyRequest.PartyName)); + var oauthUpParty = await tenantRepository.GetAsync(await UpParty.IdFormatAsync(RouteBinding, keyRequest.PartyName)); (var externalName, var publicCertificate, var externalId) = await externalKeyLogic.ImportExternalKeyAsync(WebEncoders.Base64UrlDecode(keyRequest.Certificate), keyRequest.Password, upPartyName: keyRequest.PartyName); var publicKey = new X509Certificate2(publicCertificate).ToFTJsonWebKey(); @@ -117,8 +119,8 @@ public GenericOAuthClientKeyUpPartyController(TelemetryScopedLogger logger, IMap { if (ex.StatusCode == HttpStatusCode.Conflict) { - logger.Warning(ex, $"Conflict, Create client key on client '{typeof(OAuthUpParty).Name}' by name '{keyRequest.PartyName}'."); - return Conflict(typeof(OAuthUpParty).Name, keyRequest.PartyName, nameof(keyRequest.PartyName)); + logger.Warning(ex, $"Conflict, Create client key on client '{typeof(TParty).Name}' by name '{keyRequest.PartyName}'."); + return Conflict(typeof(TParty).Name, keyRequest.PartyName, nameof(keyRequest.PartyName)); } throw; } @@ -135,7 +137,7 @@ protected async Task Delete(string name) var externalName = name.GetLastInDotList(); if (!ModelState.TryValidateRequiredParameter(externalName, $"{nameof(name)}[1]")) return BadRequest(ModelState); - var oauthUpParty = await tenantRepository.GetAsync(await UpParty.IdFormatAsync(RouteBinding, partyName)); + var oauthUpParty = await tenantRepository.GetAsync(await UpParty.IdFormatAsync(RouteBinding, partyName)); var key = oauthUpParty.Client.ClientKeys?.Where(k => k.Type == ClientKeyTypes.KeyVaultImport && k.ExternalName == externalName).FirstOrDefault(); if (key != null) @@ -151,8 +153,8 @@ protected async Task Delete(string name) { if (ex.StatusCode == HttpStatusCode.NotFound) { - logger.Warning(ex, $"NotFound, Delete client key from client '{typeof(OAuthUpParty).Name}' by name '{name}'."); - return NotFound(typeof(OAuthUpParty).Name, name); + logger.Warning(ex, $"NotFound, Delete client key from client '{typeof(TParty).Name}' by name '{name}'."); + return NotFound(typeof(TParty).Name, name); } throw; } diff --git a/src/FoxIDs.Control/Controllers/Parties/GenericOAuthClientSecretDownPartyController.cs b/src/FoxIDs.Control/Controllers/Parties/GenericOAuthClientSecretDownPartyController.cs index fe091c0f0..fed17f395 100644 --- a/src/FoxIDs.Control/Controllers/Parties/GenericOAuthClientSecretDownPartyController.cs +++ b/src/FoxIDs.Control/Controllers/Parties/GenericOAuthClientSecretDownPartyController.cs @@ -9,13 +9,15 @@ using AutoMapper; using System.Collections.Generic; using FoxIDs.Logic; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { /// /// Abstract OAuth 2.0 client secret for down-party API. /// - public abstract class GenericOAuthClientSecretDownPartyController : TenantApiController where TParty : OAuthDownParty where TClient : OAuthDownClient where TScope : OAuthDownScope where TClaim : OAuthDownClaim + [TenantScopeAuthorize(Constants.ControlApi.Segment.Party)] + public abstract class GenericOAuthClientSecretDownPartyController : ApiController where TParty : OAuthDownParty where TClient : OAuthDownClient where TScope : OAuthDownScope where TClaim : OAuthDownClaim { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Parties/GenericOAuthClientSecretUpPartyController.cs b/src/FoxIDs.Control/Controllers/Parties/GenericOAuthClientSecretUpPartyController.cs new file mode 100644 index 000000000..0285e4164 --- /dev/null +++ b/src/FoxIDs.Control/Controllers/Parties/GenericOAuthClientSecretUpPartyController.cs @@ -0,0 +1,118 @@ +using FoxIDs.Infrastructure; +using FoxIDs.Models; +using Api = FoxIDs.Models.Api; +using FoxIDs.Repository; +using Microsoft.AspNetCore.Mvc; +using System.Net; +using System.Threading.Tasks; +using ITfoxtec.Identity; +using System; +using FoxIDs.Infrastructure.Security; + +namespace FoxIDs.Controllers +{ + /// + /// Abstract OAuth 2.0 import client secret for up-party API. + /// + [TenantScopeAuthorize(Constants.ControlApi.Segment.Party)] + public abstract class GenericOAuthClientSecretUpPartyController : ApiController where TParty : OAuthUpParty where TClient : OAuthUpClient + { + private readonly TelemetryScopedLogger logger; + private readonly ITenantRepository tenantRepository; + + public GenericOAuthClientSecretUpPartyController(TelemetryScopedLogger logger, ITenantRepository tenantRepository) : base(logger) + { + this.logger = logger; + this.tenantRepository = tenantRepository; + } + + protected async Task> Get(string partyName) + { + try + { + if (!ModelState.TryValidateRequiredParameter(partyName, nameof(partyName))) return BadRequest(ModelState); + partyName = partyName?.ToLower(); + + var oauthUpParty = await tenantRepository.GetAsync(await UpParty.IdFormatAsync(RouteBinding, partyName)); + if (!string.IsNullOrWhiteSpace(oauthUpParty?.Client?.ClientSecret)) + { + return Ok(new Api.OAuthClientSecretSingleResponse + { + Info = oauthUpParty.Client.ClientSecret.Length > 20 ? oauthUpParty.Client.ClientSecret.Substring(0, 3) : oauthUpParty.Client.ClientSecret, + }); + } + else + { + return Ok(new Api.OAuthClientSecretSingleResponse()); + } + } + catch (CosmosDataException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) + { + logger.Warning(ex, $"NotFound, Get '{typeof(TParty).Name}' client key by name '{partyName}'."); + return NotFound(typeof(TParty).Name, partyName); + } + throw; + } + } + + protected async Task> Put([FromBody] Api.OAuthClientSecretSingleRequest secretRequest) + { + try + { + if (!await ModelState.TryValidateObjectAsync(secretRequest)) return BadRequest(ModelState); + secretRequest.PartyName = secretRequest.PartyName?.ToLower(); + + var oauthUpParty = await tenantRepository.GetAsync(await UpParty.IdFormatAsync(RouteBinding, secretRequest.PartyName)); + if (oauthUpParty.Client.ClientAuthenticationMethod != ClientAuthenticationMethods.PrivateKeyJwt && secretRequest.Secret.IsNullOrEmpty()) + { + throw new Exception($"Client secret is require if 'ClientAuthenticationMethod' is different from '{ClientAuthenticationMethods.PrivateKeyJwt}'"); + } + + oauthUpParty.Client.ClientSecret = secretRequest.Secret; + await tenantRepository.UpdateAsync(oauthUpParty); + + return Created(new Api.OAuthUpParty { Name = secretRequest.PartyName }); + } + catch (CosmosDataException ex) + { + if (ex.StatusCode == HttpStatusCode.Conflict) + { + logger.Warning(ex, $"Conflict, Create client key on client '{typeof(TParty).Name}' by name '{secretRequest.PartyName}'."); + return Conflict(typeof(TParty).Name, secretRequest.PartyName, nameof(secretRequest.PartyName)); + } + throw; + } + } + + protected async Task Delete(string name) + { + try + { + if (!ModelState.TryValidateRequiredParameter(name, nameof(name))) return BadRequest(ModelState); + + var partyName = name?.ToLower(); + var oauthUpParty = await tenantRepository.GetAsync(await UpParty.IdFormatAsync(RouteBinding, partyName)); + if (oauthUpParty.Client.ClientAuthenticationMethod != ClientAuthenticationMethods.PrivateKeyJwt) + { + throw new Exception($"Client secret is require if 'ClientAuthenticationMethod' is different from '{ClientAuthenticationMethods.PrivateKeyJwt}'"); + } + + oauthUpParty.Client.ClientSecret = null; + await tenantRepository.UpdateAsync(oauthUpParty); + + return NoContent(); + } + catch (CosmosDataException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) + { + logger.Warning(ex, $"NotFound, Delete client secret from client '{typeof(TParty).Name}' by name '{name}'."); + return NotFound(typeof(TParty).Name, name); + } + throw; + } + } + } +} diff --git a/src/FoxIDs.Control/Controllers/Parties/GenericPartyApiController.cs b/src/FoxIDs.Control/Controllers/Parties/GenericPartyApiController.cs index 1d7c8884f..a81187b76 100644 --- a/src/FoxIDs.Control/Controllers/Parties/GenericPartyApiController.cs +++ b/src/FoxIDs.Control/Controllers/Parties/GenericPartyApiController.cs @@ -8,14 +8,15 @@ using AutoMapper; using System; using FoxIDs.Logic; -using System.ComponentModel.DataAnnotations; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { /// /// Abstract party API. /// - public abstract class GenericPartyApiController : TenantApiController where AParty : Api.INameValue, Api.IClaimTransform where MParty : Party where AClaimTransform : Api.ClaimTransform + [TenantScopeAuthorize(Constants.ControlApi.Segment.Party)] + public abstract class GenericPartyApiController : ApiController where AParty : Api.INameValue, Api.IClaimTransform where MParty : Party where AClaimTransform : Api.ClaimTransform { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; @@ -45,8 +46,8 @@ protected async Task> Get(string name) if (!ModelState.TryValidateRequiredParameter(name, nameof(name))) return BadRequest(ModelState); name = name?.ToLower(); - var MParty = await tenantRepository.GetAsync(await GetId(IsUpParty(), name)); - return Ok(mapper.Map(MParty)); + var mParty = await tenantRepository.GetAsync(await GetId(IsUpParty(), name)); + return base.Ok(ModelToApiMap(mParty)); } catch (CosmosDataException ex) { @@ -107,7 +108,7 @@ protected async Task> Post(AParty party, Func(mParty)); + return Created(ModelToApiMap(mParty)); } catch (CosmosDataException ex) { @@ -139,7 +140,7 @@ protected async Task> Put(AParty party, Func(mParty.Id); - if((tempMParty as OidcDownParty)?.Client?.Secrets?.Count > 0) + if((tempMParty as OidcDownParty).Client != null && (mParty as OidcDownParty).Client != null) { (mParty as OidcDownParty).Client.Secrets = (tempMParty as OidcDownParty).Client.Secrets; } @@ -147,7 +148,7 @@ protected async Task> Put(AParty party, Func(mParty.Id); - if ((tempMParty as OAuthDownParty)?.Client?.Secrets?.Count > 0) + if ((tempMParty as OAuthDownParty).Client != null && (mParty as OAuthDownParty).Client != null) { (mParty as OAuthDownParty).Client.Secrets = (tempMParty as OAuthDownParty).Client.Secrets; } @@ -155,10 +156,8 @@ protected async Task> Put(AParty party, Func(mParty.Id); - if ((tempMParty as OidcUpParty)?.Client?.ClientKeys?.Count > 0) - { - (mParty as OidcUpParty).Client.ClientKeys = (tempMParty as OidcUpParty).Client.ClientKeys; - } + (mParty as OidcUpParty).Client.ClientSecret = (tempMParty as OidcUpParty).Client.ClientSecret; + (mParty as OidcUpParty).Client.ClientKeys = (tempMParty as OidcUpParty).Client.ClientKeys; } var oldMUpParty = (mParty is UpParty mUpParty) ? await tenantRepository.GetAsync(await UpParty.IdFormatAsync(RouteBinding, mParty.Name)) : null; @@ -178,7 +177,7 @@ protected async Task> Put(AParty party, Func(mParty)); + return Ok(ModelToApiMap(mParty)); } catch (CosmosDataException ex) { @@ -229,6 +228,22 @@ protected async Task Delete(string name) } } + private AParty ModelToApiMap(MParty mParty) + { + var arParty = mapper.Map(mParty); + if (arParty is Api.OidcUpParty arOidcUpParty) + { + if (arOidcUpParty.Client?.ClientSecret != null) + { + if (arOidcUpParty.Client.ClientSecret.Length > 20) + { + arOidcUpParty.Client.ClientSecret = arOidcUpParty.Client.ClientSecret.Substring(0, 3); + } + } + } + return arParty; + } + private bool IsUpParty() { if (EqualsBaseType(0, typeof(MParty), (typeof(UpParty)))) diff --git a/src/FoxIDs.Control/Controllers/Parties/TFilterDownPartyController.cs b/src/FoxIDs.Control/Controllers/Parties/TFilterDownPartyController.cs index 5993ea3ce..ba1e3a5cc 100644 --- a/src/FoxIDs.Control/Controllers/Parties/TFilterDownPartyController.cs +++ b/src/FoxIDs.Control/Controllers/Parties/TFilterDownPartyController.cs @@ -11,10 +11,12 @@ using System.Linq; using ITfoxtec.Identity; using System; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TFilterDownPartyController : TenantApiController + [TenantScopeAuthorize(Constants.ControlApi.Segment.Party)] + public class TFilterDownPartyController : ApiController { private const string dataType = "party:down"; private readonly TelemetryScopedLogger logger; diff --git a/src/FoxIDs.Control/Controllers/Parties/TFilterUpPartyController.cs b/src/FoxIDs.Control/Controllers/Parties/TFilterUpPartyController.cs index fe5546780..c2de93ef0 100644 --- a/src/FoxIDs.Control/Controllers/Parties/TFilterUpPartyController.cs +++ b/src/FoxIDs.Control/Controllers/Parties/TFilterUpPartyController.cs @@ -11,10 +11,12 @@ using System.Linq; using ITfoxtec.Identity; using System; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TFilterUpPartyController : TenantApiController + [TenantScopeAuthorize(Constants.ControlApi.Segment.Party)] + public class TFilterUpPartyController : ApiController { private const string dataType = "party:up"; private readonly TelemetryScopedLogger logger; diff --git a/src/FoxIDs.Control/Controllers/Parties/TOidcClientKeyUpPartyController.cs b/src/FoxIDs.Control/Controllers/Parties/TOidcClientKeyUpPartyController.cs index 1b5913848..c26a076ba 100644 --- a/src/FoxIDs.Control/Controllers/Parties/TOidcClientKeyUpPartyController.cs +++ b/src/FoxIDs.Control/Controllers/Parties/TOidcClientKeyUpPartyController.cs @@ -14,7 +14,7 @@ namespace FoxIDs.Controllers /// /// OIDC import client key for up-party API. /// - public class TOidcClientKeyUpPartyController : GenericOAuthClientKeyUpPartyController + public class TOidcClientKeyUpPartyController : GenericOAuthClientKeyUpPartyController { public TOidcClientKeyUpPartyController(TelemetryScopedLogger logger, IMapper mapper, ITenantRepository tenantRepository, PlanCacheLogic planCacheLogic, ExternalKeyLogic externalKeyLogic) : base(logger, mapper, tenantRepository, planCacheLogic, externalKeyLogic) { } diff --git a/src/FoxIDs.Control/Controllers/Parties/TOidcClientSecretUpPartyController.cs b/src/FoxIDs.Control/Controllers/Parties/TOidcClientSecretUpPartyController.cs new file mode 100644 index 000000000..ffae6987c --- /dev/null +++ b/src/FoxIDs.Control/Controllers/Parties/TOidcClientSecretUpPartyController.cs @@ -0,0 +1,45 @@ +using FoxIDs.Infrastructure; +using FoxIDs.Models; +using Api = FoxIDs.Models.Api; +using FoxIDs.Repository; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; + +namespace FoxIDs.Controllers +{ + /// + /// OIDC import client secret for up-party API. + /// + public class TOidcClientSecretUpPartyController : GenericOAuthClientSecretUpPartyController + { + public TOidcClientSecretUpPartyController(TelemetryScopedLogger logger, ITenantRepository tenantRepository) : base(logger, tenantRepository) + { } + + /// + /// Get OIDC client secret for up-party. + /// + /// OIDC party name. + /// OIDC client secret for up-party. + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetOidcClientSecretUpParty(string partyName) => await Get(partyName); + + /// + /// Update OIDC client secret for up-party. + /// + /// OIDC client secret for up-party. + [ProducesResponseType(typeof(Api.OAuthUpParty), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task> PutOidcClientSecretUpParty([FromBody] Api.OAuthClientSecretSingleRequest secretRequest) => await Put(secretRequest); + + /// + /// Delete OIDC client secret for up-party. + /// + /// Party name. + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteOidcClientSecretUpParty(string name) => await Delete(name); + } +} diff --git a/src/FoxIDs.Control/Controllers/Parties/TOidcUpPartyController.cs b/src/FoxIDs.Control/Controllers/Parties/TOidcUpPartyController.cs index aeedff786..02b016854 100644 --- a/src/FoxIDs.Control/Controllers/Parties/TOidcUpPartyController.cs +++ b/src/FoxIDs.Control/Controllers/Parties/TOidcUpPartyController.cs @@ -16,11 +16,13 @@ namespace FoxIDs.Controllers public class TOidcUpPartyController : GenericPartyApiController { private readonly ValidateApiModelOAuthOidcPartyLogic validateApiModelOAuthOidcPartyLogic; + private readonly ValidateModelOAuthOidcPartyLogic validateModelOAuthOidcPartyLogic; private readonly OidcDiscoveryReadUpLogic oidcDiscoveryReadUpLogic; - public TOidcUpPartyController(TelemetryScopedLogger logger, IMapper mapper, ITenantRepository tenantRepository, DownPartyCacheLogic downPartyCacheLogic, UpPartyCacheLogic upPartyCacheLogic, DownPartyAllowUpPartiesQueueLogic downPartyAllowUpPartiesQueueLogic, ValidateApiModelGenericPartyLogic validateApiModelGenericPartyLogic, ValidateModelGenericPartyLogic validateModelGenericPartyLogic, ValidateApiModelOAuthOidcPartyLogic validateApiModelOAuthOidcPartyLogic, OidcDiscoveryReadUpLogic oidcDiscoveryReadUpLogic) : base(logger, mapper, tenantRepository, downPartyCacheLogic, upPartyCacheLogic, downPartyAllowUpPartiesQueueLogic, validateApiModelGenericPartyLogic, validateModelGenericPartyLogic) + public TOidcUpPartyController(TelemetryScopedLogger logger, IMapper mapper, ITenantRepository tenantRepository, DownPartyCacheLogic downPartyCacheLogic, UpPartyCacheLogic upPartyCacheLogic, DownPartyAllowUpPartiesQueueLogic downPartyAllowUpPartiesQueueLogic, ValidateApiModelGenericPartyLogic validateApiModelGenericPartyLogic, ValidateModelGenericPartyLogic validateModelGenericPartyLogic, ValidateApiModelOAuthOidcPartyLogic validateApiModelOAuthOidcPartyLogic, ValidateModelOAuthOidcPartyLogic validateModelOAuthOidcPartyLogic, OidcDiscoveryReadUpLogic oidcDiscoveryReadUpLogic) : base(logger, mapper, tenantRepository, downPartyCacheLogic, upPartyCacheLogic, downPartyAllowUpPartiesQueueLogic, validateApiModelGenericPartyLogic, validateModelGenericPartyLogic) { this.validateApiModelOAuthOidcPartyLogic = validateApiModelOAuthOidcPartyLogic; + this.validateModelOAuthOidcPartyLogic = validateModelOAuthOidcPartyLogic; this.oidcDiscoveryReadUpLogic = oidcDiscoveryReadUpLogic; } @@ -40,16 +42,17 @@ public TOidcUpPartyController(TelemetryScopedLogger logger, IMapper mapper, ITen /// OIDC up-party. [ProducesResponseType(typeof(Api.OidcUpParty), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task> PostOidcUpParty([FromBody] Api.OidcUpParty party) => await Post(party, ap => new ValueTask(validateApiModelOAuthOidcPartyLogic.ValidateApiModel(ModelState, ap)), async (ap, mp) => await oidcDiscoveryReadUpLogic.PopulateModelAsync(ModelState, mp)); + public async Task> PostOidcUpParty([FromBody] Api.OidcUpParty party) => await Post(party, ap => new ValueTask(validateApiModelOAuthOidcPartyLogic.ValidateApiModel(ModelState, ap)), async (ap, mp) => validateModelOAuthOidcPartyLogic.ValidateApiModel(ModelState, mp) && await oidcDiscoveryReadUpLogic.PopulateModelAsync(ModelState, mp)); /// /// Update OIDC up-party. + /// You cannot update the ClientSecret in this method. /// /// OIDC up-party. /// OIDC up-party. [ProducesResponseType(typeof(Api.OidcUpParty), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> PutOidcUpParty([FromBody] Api.OidcUpParty party) => await Put(party, ap => new ValueTask(validateApiModelOAuthOidcPartyLogic.ValidateApiModel(ModelState, ap)), async (ap, mp) => await oidcDiscoveryReadUpLogic.PopulateModelAsync(ModelState, mp)); + public async Task> PutOidcUpParty([FromBody] Api.OidcUpParty party) => await Put(party, ap => new ValueTask(validateApiModelOAuthOidcPartyLogic.ValidateApiModel(ModelState, ap)), async (ap, mp) => validateModelOAuthOidcPartyLogic.ValidateApiModel(ModelState, mp) && await oidcDiscoveryReadUpLogic.PopulateModelAsync(ModelState, mp)); /// /// Delete OIDC up-party. diff --git a/src/FoxIDs.Control/Controllers/Parties/TSamlUpPartyReadMetadataController.cs b/src/FoxIDs.Control/Controllers/Parties/TSamlUpPartyReadMetadataController.cs index 3106bbcce..123ac0b5d 100644 --- a/src/FoxIDs.Control/Controllers/Parties/TSamlUpPartyReadMetadataController.cs +++ b/src/FoxIDs.Control/Controllers/Parties/TSamlUpPartyReadMetadataController.cs @@ -8,10 +8,12 @@ using System.ComponentModel.DataAnnotations; using FoxIDs.Logic; using FoxIDs.Models; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TSamlUpPartyReadMetadataController : TenantApiController + [TenantScopeAuthorize(Constants.ControlApi.Segment.Party)] + public class TSamlUpPartyReadMetadataController : ApiController { private readonly IMapper mapper; private readonly SamlMetadataReadLogic samlMetadataReadLogic; diff --git a/src/FoxIDs.Control/Controllers/TenantResources/TFilterResourceNameController.cs b/src/FoxIDs.Control/Controllers/TenantResources/TFilterResourceNameController.cs index 450f18267..42d8038cf 100644 --- a/src/FoxIDs.Control/Controllers/TenantResources/TFilterResourceNameController.cs +++ b/src/FoxIDs.Control/Controllers/TenantResources/TFilterResourceNameController.cs @@ -11,10 +11,12 @@ using ITfoxtec.Identity; using FoxIDs.Logic; using System; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TFilterResourceNameController : TenantApiController + [TenantScopeAuthorize] + public class TFilterResourceNameController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Tenants/TMyTenantController.cs b/src/FoxIDs.Control/Controllers/Tenants/TMyTenantController.cs index 83a7a3283..d9bc83fed 100644 --- a/src/FoxIDs.Control/Controllers/Tenants/TMyTenantController.cs +++ b/src/FoxIDs.Control/Controllers/Tenants/TMyTenantController.cs @@ -10,10 +10,12 @@ using FoxIDs.Logic; using System; using ITfoxtec.Identity; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TMyTenantController : TenantApiController + [TenantScopeAuthorize] + public class TMyTenantController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Tenants/TMyTenantLogUsageController.cs b/src/FoxIDs.Control/Controllers/Tenants/TMyTenantLogUsageController.cs index 4b94ea59b..e43df5c59 100644 --- a/src/FoxIDs.Control/Controllers/Tenants/TMyTenantLogUsageController.cs +++ b/src/FoxIDs.Control/Controllers/Tenants/TMyTenantLogUsageController.cs @@ -5,10 +5,12 @@ using System.Threading.Tasks; using FoxIDs.Logic; using ITfoxtec.Identity; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TMyTenantLogUsageController : TenantApiController + [TenantScopeAuthorize(Constants.ControlApi.Segment.Usage)] + public class TMyTenantLogUsageController : ApiController { private readonly UsageLogLogic usageLogLogic; diff --git a/src/FoxIDs.Control/Controllers/Tenants/TTenantLogUsageController.cs b/src/FoxIDs.Control/Controllers/Tenants/TTenantLogUsageController.cs index e0e984d67..7737a17a2 100644 --- a/src/FoxIDs.Control/Controllers/Tenants/TTenantLogUsageController.cs +++ b/src/FoxIDs.Control/Controllers/Tenants/TTenantLogUsageController.cs @@ -11,7 +11,7 @@ namespace FoxIDs.Controllers { [RequireMasterTenant] - [MasterScopeAuthorize] + [MasterScopeAuthorize(Constants.ControlApi.Segment.Usage)] public class TTenantLogUsageController : ApiController { private readonly UsageLogLogic usageLogLogic; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TFilterTrackController.cs b/src/FoxIDs.Control/Controllers/Tracks/TFilterTrackController.cs index f6cd8ea01..17030acea 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TFilterTrackController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TFilterTrackController.cs @@ -11,10 +11,12 @@ using System.Linq; using ITfoxtec.Identity; using System; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TFilterTrackController : TenantApiController + [TenantScopeAuthorize] + public class TFilterTrackController : ApiController { private const string dataType = "track"; private readonly TelemetryScopedLogger logger; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TFilterUserController.cs b/src/FoxIDs.Control/Controllers/Tracks/TFilterUserController.cs index b6f59d317..738a3ea75 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TFilterUserController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TFilterUserController.cs @@ -11,10 +11,12 @@ using System.Linq; using ITfoxtec.Identity; using System; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TFilterUserController : TenantApiController + [TenantScopeAuthorize(Constants.ControlApi.Segment.User)] + public class TFilterUserController : ApiController { private const string dataType = "user"; private readonly TelemetryScopedLogger logger; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TMyUserController.cs b/src/FoxIDs.Control/Controllers/Tracks/TMyUserController.cs new file mode 100644 index 000000000..f139096c9 --- /dev/null +++ b/src/FoxIDs.Control/Controllers/Tracks/TMyUserController.cs @@ -0,0 +1,93 @@ +using AutoMapper; +using FoxIDs.Infrastructure; +using FoxIDs.Repository; +using FoxIDs.Models; +using Api = FoxIDs.Models.Api; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using System.Net; +using ITfoxtec.Identity; +using System; +using FoxIDs.Infrastructure.Security; + +namespace FoxIDs.Controllers +{ + [TenantScopeAuthorize(Constants.ControlApi.Segment.Base)] + public class TMyUserController : ApiController + { + private readonly TelemetryScopedLogger logger; + private readonly IMapper mapper; + private readonly ITenantRepository tenantRepository; + + public TMyUserController(TelemetryScopedLogger logger, IMapper mapper, ITenantRepository tenantRepository) : base(logger) + { + this.logger = logger; + this.mapper = mapper; + this.tenantRepository = tenantRepository; + } + + /// + /// Get my user. + /// + /// User. + [ProducesResponseType(typeof(Api.MyUser), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetMyUser() + { + var email = User.Claims.FindFirstOrDefaultValue(c => c.Type == JwtClaimTypes.Email); + try + { + if (email.IsNullOrWhiteSpace()) + { + throw new Exception("Authenticated users email claim is empty."); + } + var mUser = await tenantRepository.GetAsync(await Models.User.IdFormatAsync(RouteBinding, email)); + return Ok(mapper.Map(mUser)); + } + catch (CosmosDataException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) + { + logger.Warning(ex, $"NotFound, Get '{typeof(Api.MyUser).Name}' by email '{email}'."); + return NotFound(typeof(Api.MyUser).Name, email); + } + throw; + } + } + + /// + /// Update my user. + /// - It is only possible to set the ChangePassword property. + /// + /// My user. + /// My user. + [ProducesResponseType(typeof(Api.MyUser), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> PutMyUser([FromBody] Api.MyUser user) + { + var email = User.Claims.FindFirstOrDefaultValue(c => c.Type == JwtClaimTypes.Email); + try + { + if (email.IsNullOrWhiteSpace()) + { + throw new Exception("Authenticated users email claim is empty."); + } + var mUser = await tenantRepository.GetAsync(await Models.User.IdFormatAsync(RouteBinding, email)); + mUser.ChangePassword = user.ChangePassword; + await tenantRepository.UpdateAsync(mUser); + + return Ok(mapper.Map(mUser)); + } + catch (CosmosDataException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) + { + logger.Warning(ex, $"NotFound, Update '{typeof(Api.MyUser).Name}' by email '{email}'."); + return NotFound(typeof(Api.MyUser).Name, email, nameof(email)); + } + throw; + } + } + } +} diff --git a/src/FoxIDs.Control/Controllers/Tracks/TTrackClaimMappingController.cs b/src/FoxIDs.Control/Controllers/Tracks/TTrackClaimMappingController.cs index 0e0850f6f..567e479b0 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TTrackClaimMappingController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TTrackClaimMappingController.cs @@ -10,10 +10,12 @@ using System.Collections.Generic; using System.Linq; using FoxIDs.Logic; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TTrackClaimMappingController : TenantApiController + [TenantScopeAuthorize] + public class TTrackClaimMappingController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TTrackController.cs b/src/FoxIDs.Control/Controllers/Tracks/TTrackController.cs index de37ee59c..a5f6072b6 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TTrackController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TTrackController.cs @@ -10,10 +10,12 @@ using FoxIDs.Logic; using ITfoxtec.Identity; using System; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TTrackController : TenantApiController + [TenantScopeAuthorize] + public class TTrackController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TTrackKeyContainedController.cs b/src/FoxIDs.Control/Controllers/Tracks/TTrackKeyContainedController.cs index 4b475a8fd..ed4ca0b4a 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TTrackKeyContainedController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TTrackKeyContainedController.cs @@ -10,10 +10,12 @@ using System.ComponentModel.DataAnnotations; using ITfoxtec.Identity; using FoxIDs.Logic; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TTrackKeyContainedController : TenantApiController + [TenantScopeAuthorize] + public class TTrackKeyContainedController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TTrackKeyContainedSwapController.cs b/src/FoxIDs.Control/Controllers/Tracks/TTrackKeyContainedSwapController.cs index 4f6d0caf2..4228a8a38 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TTrackKeyContainedSwapController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TTrackKeyContainedSwapController.cs @@ -9,10 +9,12 @@ using System.Net; using System.ComponentModel.DataAnnotations; using FoxIDs.Logic; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TTrackKeyContainedSwapController : TenantApiController + [TenantScopeAuthorize] + public class TTrackKeyContainedSwapController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TTrackKeyTypeController.cs b/src/FoxIDs.Control/Controllers/Tracks/TTrackKeyTypeController.cs index 83d248447..b77971892 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TTrackKeyTypeController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TTrackKeyTypeController.cs @@ -12,10 +12,12 @@ using FoxIDs.Models.Config; using System.Collections.Generic; using FoxIDs.Logic; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TTrackKeyTypeController : TenantApiController + [TenantScopeAuthorize] + public class TTrackKeyTypeController : ApiController { private readonly TelemetryScopedLogger logger; private readonly FoxIDsControlSettings settings; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TTrackLogController.cs b/src/FoxIDs.Control/Controllers/Tracks/TTrackLogController.cs index 78b8291a9..a322a25ed 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TTrackLogController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TTrackLogController.cs @@ -13,10 +13,12 @@ using Azure.Monitor.Query; using Azure.Monitor.Query.Models; using Azure; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TTrackLogController : TenantApiController + [TenantScopeAuthorize(Constants.ControlApi.Segment.Log)] + public class TTrackLogController : ApiController { private const int maxQueryLogItems = 200; private const int maxResponseLogItems = 300; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TTrackLogSettingController.cs b/src/FoxIDs.Control/Controllers/Tracks/TTrackLogSettingController.cs index 0f4b793a9..8e0d48ac7 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TTrackLogSettingController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TTrackLogSettingController.cs @@ -8,10 +8,12 @@ using System.Threading.Tasks; using System.Net; using FoxIDs.Logic; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TTrackLogSettingController : TenantApiController + [TenantScopeAuthorize(Constants.ControlApi.Segment.Log)] + public class TTrackLogSettingController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TTrackLogStreamsSettingsController.cs b/src/FoxIDs.Control/Controllers/Tracks/TTrackLogStreamsSettingsController.cs index 0266ad76a..1721e66b1 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TTrackLogStreamsSettingsController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TTrackLogStreamsSettingsController.cs @@ -9,10 +9,12 @@ using System.Net; using System.Collections.Generic; using FoxIDs.Logic; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TTrackLogStreamsSettingsController : TenantApiController + [TenantScopeAuthorize(Constants.ControlApi.Segment.Log)] + public class TTrackLogStreamsSettingsController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TTrackLogUsageController.cs b/src/FoxIDs.Control/Controllers/Tracks/TTrackLogUsageController.cs index b6b8d6e04..e2f12d805 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TTrackLogUsageController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TTrackLogUsageController.cs @@ -4,10 +4,12 @@ using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using FoxIDs.Logic; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TTrackLogUsageController : TenantApiController + [TenantScopeAuthorize(Constants.ControlApi.Segment.Usage)] + public class TTrackLogUsageController : ApiController { private readonly UsageLogLogic usageLogLogic; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TTrackResourceController.cs b/src/FoxIDs.Control/Controllers/Tracks/TTrackResourceController.cs index 78c5781de..837e3b788 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TTrackResourceController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TTrackResourceController.cs @@ -12,10 +12,12 @@ using System.ComponentModel.DataAnnotations; using System; using FoxIDs.Logic; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TTrackResourceController : TenantApiController + [TenantScopeAuthorize] + public class TTrackResourceController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TTrackResourceSettingController.cs b/src/FoxIDs.Control/Controllers/Tracks/TTrackResourceSettingController.cs index a510e63f8..c0852d12b 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TTrackResourceSettingController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TTrackResourceSettingController.cs @@ -8,10 +8,12 @@ using System.Threading.Tasks; using System.Net; using FoxIDs.Logic; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TTrackResourceSettingController : TenantApiController + [TenantScopeAuthorize] + public class TTrackResourceSettingController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TTrackSendEmailController.cs b/src/FoxIDs.Control/Controllers/Tracks/TTrackSendEmailController.cs index 93dccb5de..16dff3468 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TTrackSendEmailController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TTrackSendEmailController.cs @@ -9,10 +9,12 @@ using System.Net; using System; using FoxIDs.Logic; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TTrackSendEmailController : TenantApiController + [TenantScopeAuthorize] + public class TTrackSendEmailController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/Controllers/Tracks/TUserControlProfileController.cs b/src/FoxIDs.Control/Controllers/Tracks/TUserControlProfileController.cs new file mode 100644 index 000000000..c1752ee3c --- /dev/null +++ b/src/FoxIDs.Control/Controllers/Tracks/TUserControlProfileController.cs @@ -0,0 +1,124 @@ +using AutoMapper; +using FoxIDs.Infrastructure; +using FoxIDs.Repository; +using FoxIDs.Models; +using Api = FoxIDs.Models.Api; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using System.Net; +using ITfoxtec.Identity; +using System; +using FoxIDs.Infrastructure.Security; + +namespace FoxIDs.Controllers +{ + [TenantScopeAuthorize(Constants.ControlApi.Segment.Base)] + public class TUserControlProfileController : ApiController + { + private readonly TelemetryScopedLogger logger; + private readonly IMapper mapper; + private readonly ITenantRepository tenantRepository; + + public TUserControlProfileController(TelemetryScopedLogger logger, IMapper mapper, ITenantRepository tenantRepository) : base(logger) + { + this.logger = logger; + this.mapper = mapper; + this.tenantRepository = tenantRepository; + } + + /// + /// Get user control profile. + /// + /// User control profile. + [ProducesResponseType(typeof(Api.UserControlProfile), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetUserControlProfile() + { + try + { + if(RouteBinding.TrackName != Constants.Routes.MasterTrackName) + { + throw new Exception("User control profile only supported in master track."); + } + + var userHashId = await User.Identity.Name.ToLower().Sha256HashBase64urlEncodedAsync(); + + var mUserControlProfile = await tenantRepository.GetAsync(await UserControlProfile.IdFormatAsync(RouteBinding, userHashId)); + return Ok(mapper.Map(mUserControlProfile)); + } + catch (CosmosDataException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) + { + logger.Warning(ex, $"NotFound, Get '{typeof(Api.UserControlProfile).Name}' by user sub '{User?.Identity?.Name}'."); + return NotFound(typeof(Api.UserControlProfile).Name, User?.Identity?.Name); + } + throw; + } + } + + /// + /// Update user control profile. + /// + /// User control profile. + /// User control profile. + [ProducesResponseType(typeof(Api.UserControlProfile), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> PutUserControlProfile([FromBody] Api.UserControlProfile userControlProfile) + { + try + { + if (RouteBinding.TrackName != Constants.Routes.MasterTrackName) + { + throw new Exception("User control profile only supported in master track."); + } + + var mUserControlProfile = mapper.Map(userControlProfile); + mUserControlProfile.Id = await UserControlProfile.IdFormatAsync(RouteBinding, await User.Identity.Name.ToLower().Sha256HashBase64urlEncodedAsync()); + await tenantRepository.SaveAsync(mUserControlProfile); + + return Ok(mapper.Map(mUserControlProfile)); + } + catch (CosmosDataException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) + { + logger.Warning(ex, $"NotFound, Update '{typeof(Api.UserControlProfile).Name}' by user sub '{User?.Identity?.Name}'."); + return NotFound(typeof(Api.UserControlProfile).Name, User?.Identity?.Name); + } + throw; + } + } + + /// + /// Delete user control profile. + /// + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteUserControlProfile() + { + try + { + if (RouteBinding.TrackName != Constants.Routes.MasterTrackName) + { + throw new Exception("User control profile only supported in master track."); + } + + var userHashId = await User.Identity.Name.ToLower().Sha256HashBase64urlEncodedAsync(); + + _ = await tenantRepository.DeleteAsync(await UserControlProfile.IdFormatAsync(RouteBinding, userHashId)); + return NoContent(); + } + catch (CosmosDataException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) + { + logger.Warning(ex, $"NotFound, Delete '{typeof(Api.UserControlProfile).Name}' by user sub '{User?.Identity?.Name}'."); + return NotFound(typeof(Api.UserControlProfile).Name, User?.Identity?.Name); + } + throw; + } + } + } +} diff --git a/src/FoxIDs.Control/Controllers/Tracks/TUserController.cs b/src/FoxIDs.Control/Controllers/Tracks/TUserController.cs index 575157eda..b0062ecff 100644 --- a/src/FoxIDs.Control/Controllers/Tracks/TUserController.cs +++ b/src/FoxIDs.Control/Controllers/Tracks/TUserController.cs @@ -13,10 +13,12 @@ using ITfoxtec.Identity; using System; using System.Linq.Expressions; +using FoxIDs.Infrastructure.Security; namespace FoxIDs.Controllers { - public class TUserController : TenantApiController + [TenantScopeAuthorize(Constants.ControlApi.Segment.User)] + public class TUserController : ApiController { private readonly TelemetryScopedLogger logger; private readonly IMapper mapper; diff --git a/src/FoxIDs.Control/FoxIDs.Control.csproj b/src/FoxIDs.Control/FoxIDs.Control.csproj index b83a91048..fbfaa1d62 100644 --- a/src/FoxIDs.Control/FoxIDs.Control.csproj +++ b/src/FoxIDs.Control/FoxIDs.Control.csproj @@ -2,7 +2,7 @@ net8.0 - 1.2.5.0 + 1.2.6.4 FoxIDs Anders Revsgaard ITfoxtec @@ -19,13 +19,13 @@ - + - + @@ -36,4 +36,23 @@ + + + TextTemplatingFileGenerator + BuildInfo.g.cs + + + + + + + + + + True + True + BuildInfo.tt + + + diff --git a/src/FoxIDs.Control/Infrastructure/Hosting/FoxIDsApiExceptionMiddleware.cs b/src/FoxIDs.Control/Infrastructure/Hosting/FoxIDsApiExceptionMiddleware.cs index 99740ab7d..6831d3dd8 100644 --- a/src/FoxIDs.Control/Infrastructure/Hosting/FoxIDsApiExceptionMiddleware.cs +++ b/src/FoxIDs.Control/Infrastructure/Hosting/FoxIDsApiExceptionMiddleware.cs @@ -70,7 +70,7 @@ private void LogError(TelemetryScopedLogger scopedLogger, Exception ex) scopedLogger.Error(ex); if (environment.IsDevelopment()) { - Debug.WriteLine(ex.ToString()); + Console.WriteLine(ex.ToString()); } } } diff --git a/src/FoxIDs.Control/Infrastructure/Hosting/ServiceCollectionExtensions.cs b/src/FoxIDs.Control/Infrastructure/Hosting/ServiceCollectionExtensions.cs index 73680692e..1fae588fc 100644 --- a/src/FoxIDs.Control/Infrastructure/Hosting/ServiceCollectionExtensions.cs +++ b/src/FoxIDs.Control/Infrastructure/Hosting/ServiceCollectionExtensions.cs @@ -43,6 +43,7 @@ public static IServiceCollection AddLogic(this IServiceCollection services) services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/FoxIDs.Control/Infrastructure/Security/BaseAuthorizationRequirement.cs b/src/FoxIDs.Control/Infrastructure/Security/BaseAuthorizationRequirement.cs new file mode 100644 index 000000000..31928c7f3 --- /dev/null +++ b/src/FoxIDs.Control/Infrastructure/Security/BaseAuthorizationRequirement.cs @@ -0,0 +1,73 @@ +using ITfoxtec.Identity; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace FoxIDs.Infrastructure.Security +{ + public abstract class BaseAuthorizationRequirement : AuthorizationHandler, IAuthorizationRequirement where Tar : IAuthorizationRequirement where Tsc : BaseScopeAuthorizeAttribute + { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, Tar requirement) + { + if (context.User != null && context.Resource is HttpContext httpContext) + { + var executingEnpoint = httpContext.GetEndpoint(); + var scopeAuthorizeAttribute = executingEnpoint.Metadata.OfType().FirstOrDefault(); + if (scopeAuthorizeAttribute != null) + { + var userScopes = context.User.Claims.Where(c => string.Equals(c.Type, JwtClaimTypes.Scope, StringComparison.OrdinalIgnoreCase)).Select(c => c.Value).FirstOrDefault().ToSpaceList(); + var userRoles = context.User.Claims.Where(c => string.Equals(c.Type, JwtClaimTypes.Role, StringComparison.OrdinalIgnoreCase)).Select(c => c.Value).ToList(); + if (userScopes?.Count() > 0 && userRoles?.Count > 0) + { + (var acceptedScopes, var acceptedRoles) = GetAcceptedScopesAndRoles(scopeAuthorizeAttribute.Segments, httpContext.GetRouteBinding()?.TrackName, httpContext.Request?.Method); + + if (userScopes.Where(us => acceptedScopes.Any(s => s.Equals(us, StringComparison.Ordinal))).Any() && userRoles.Where(ur => acceptedRoles.Any(r => r.Equals(ur, StringComparison.Ordinal))).Any()) + { + context.Succeed(requirement); + } + } + } + } + + return Task.CompletedTask; + } + + protected abstract (List acceptedScopes, List acceptedRoles) GetAcceptedScopesAndRoles(IEnumerable segments, string trackName, string httpMethod); + + protected void AddScopeAndRole(List acceptedScopes, List acceptedRoles, string httpMethod, string scope, string role, string segment = "") + { + acceptedScopes.Add(scope); + acceptedRoles.Add(role); + + if (segment != Constants.ControlApi.Segment.Usage) + { + if (httpMethod == HttpMethod.Get.Method) + { + acceptedScopes.Add($"{scope}{Constants.ControlApi.AccessElement.Read}"); + acceptedRoles.Add($"{role}{Constants.ControlApi.AccessElement.Read}"); + } + else if (httpMethod == HttpMethod.Post.Method) + { + acceptedScopes.Add($"{scope}{Constants.ControlApi.AccessElement.Create}"); + acceptedRoles.Add($"{role}{Constants.ControlApi.AccessElement.Create}"); + + } + else if (httpMethod == HttpMethod.Put.Method) + { + acceptedScopes.Add($"{scope}{Constants.ControlApi.AccessElement.Update}"); + acceptedRoles.Add($"{role}{Constants.ControlApi.AccessElement.Update}"); + + } + else if (httpMethod == HttpMethod.Delete.Method) + { + acceptedScopes.Add($"{scope}{Constants.ControlApi.AccessElement.Delete}"); + acceptedRoles.Add($"{role}{Constants.ControlApi.AccessElement.Delete}"); + } + } + } + } +} diff --git a/src/FoxIDs.Control/Infrastructure/Security/BaseScopeAuthorizeAttribute.cs b/src/FoxIDs.Control/Infrastructure/Security/BaseScopeAuthorizeAttribute.cs new file mode 100644 index 000000000..7c31ff038 --- /dev/null +++ b/src/FoxIDs.Control/Infrastructure/Security/BaseScopeAuthorizeAttribute.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Authorization; + +namespace FoxIDs.Infrastructure.Security +{ + public abstract class BaseScopeAuthorizeAttribute : AuthorizeAttribute + { + public BaseScopeAuthorizeAttribute(string name, params string[] subSegments) : base(name) + { + AuthenticationSchemes = JwtBearerMultipleTenantsHandler.AuthenticationScheme; + Segments = subSegments; + } + + public string[] Segments { get; } + } +} diff --git a/src/FoxIDs.Control/Infrastructure/Security/MasterAuthorizationRequirement.cs b/src/FoxIDs.Control/Infrastructure/Security/MasterAuthorizationRequirement.cs new file mode 100644 index 000000000..aba71bb01 --- /dev/null +++ b/src/FoxIDs.Control/Infrastructure/Security/MasterAuthorizationRequirement.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace FoxIDs.Infrastructure.Security +{ + public class MasterAuthorizationRequirement : BaseAuthorizationRequirement + { + protected override (List acceptedScopes, List acceptedRoles) GetAcceptedScopesAndRoles(IEnumerable segments, string trackName, string httpMethod) + { + var acceptedScopes = new List(); + var acceptedRoles = new List(); + + AddScopeAndRole(acceptedScopes, acceptedRoles, httpMethod, Constants.ControlApi.ResourceAndScope.Master, Constants.ControlApi.Access.Master); + acceptedRoles.Add(Constants.ControlApi.Access.TenantAdminRole); + + foreach (var segment in segments) + { + var scope = $"{Constants.ControlApi.ResourceAndScope.Master}{segment}"; + var role = $"{Constants.ControlApi.Access.Tenant}{segment}"; + AddScopeAndRole(acceptedScopes, acceptedRoles, httpMethod, scope, role, segment); + } + + return (acceptedScopes, acceptedRoles); + } + } +} diff --git a/src/FoxIDs.Control/Infrastructure/Security/MasterScopeAuthorizeAttribute.cs b/src/FoxIDs.Control/Infrastructure/Security/MasterScopeAuthorizeAttribute.cs index 4ac22fdf5..48a5af445 100644 --- a/src/FoxIDs.Control/Infrastructure/Security/MasterScopeAuthorizeAttribute.cs +++ b/src/FoxIDs.Control/Infrastructure/Security/MasterScopeAuthorizeAttribute.cs @@ -1,25 +1,19 @@ -using ITfoxtec.Identity; -using ITfoxtec.Identity.Models; -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; namespace FoxIDs.Infrastructure.Security { - public class MasterScopeAuthorizeAttribute : AuthorizeAttribute + public class MasterScopeAuthorizeAttribute : BaseScopeAuthorizeAttribute { public const string Name = nameof(MasterScopeAuthorizeAttribute); - public MasterScopeAuthorizeAttribute() : base(Name) - { - AuthenticationSchemes = JwtBearerMultipleTenantsHandler.AuthenticationScheme; - } + public MasterScopeAuthorizeAttribute(params string[] segments) : base(Name, segments) + { } public static void AddPolicy(AuthorizationOptions options) { options.AddPolicy(Name, policy => { - policy.RequireScopeAndRoles( - new ScopeAndRoles { Scope = Constants.ControlApi.ResourceAndScope.Master, Roles = new[] { Constants.ControlApi.Role.TenantAdmin } } - ); + policy.Requirements.Add(new MasterAuthorizationRequirement()); }); } } diff --git a/src/FoxIDs.Control/Infrastructure/Security/TenantAuthorizationRequirement.cs b/src/FoxIDs.Control/Infrastructure/Security/TenantAuthorizationRequirement.cs new file mode 100644 index 000000000..a7e1c4b5f --- /dev/null +++ b/src/FoxIDs.Control/Infrastructure/Security/TenantAuthorizationRequirement.cs @@ -0,0 +1,59 @@ +using ITfoxtec.Identity; +using System.Collections.Generic; +using System.Linq; + +namespace FoxIDs.Infrastructure.Security +{ + public class TenantAuthorizationRequirement : BaseAuthorizationRequirement + { + protected override (List acceptedScopes, List acceptedRoles) GetAcceptedScopesAndRoles(IEnumerable segments, string trackName, string httpMethod) + { + var acceptedScopes = new List(); + var acceptedRoles = new List(); + + AddScopeAndRole(acceptedScopes, acceptedRoles, httpMethod, Constants.ControlApi.ResourceAndScope.Tenant, Constants.ControlApi.Access.Tenant); + acceptedRoles.Add(Constants.ControlApi.Access.TenantAdminRole); + + if (!trackName.IsNullOrWhiteSpace()) + { + if (segments?.Count() > 0) + { + foreach (var segment in segments) + { + if (segment == Constants.ControlApi.Segment.Base) + { + AddScopeAndRole(acceptedScopes, acceptedRoles, httpMethod, + $"{Constants.ControlApi.ResourceAndScope.Tenant}{segment}", + $"{Constants.ControlApi.Access.Tenant}{segment}"); + } + else + { + AddScopeAndRoleByTrack(acceptedScopes, acceptedRoles, trackName, httpMethod, + $"{Constants.ControlApi.ResourceAndScope.Tenant}{Constants.ControlApi.AccessElement.Track}", + Constants.ControlApi.Access.Tenant, + segment); + } + } + } + else + { + AddScopeAndRoleByTrack(acceptedScopes, acceptedRoles, trackName, httpMethod, + $"{Constants.ControlApi.ResourceAndScope.Tenant}{Constants.ControlApi.AccessElement.Track}", + Constants.ControlApi.Access.Tenant); + } + } + + return (acceptedScopes, acceptedRoles); + } + + private void AddScopeAndRoleByTrack(List acceptedScopes, List acceptedRoles, string trackName, string httpMethod, string subScope, string subRole, string segment = "") + { + if (trackName != Constants.Routes.MasterTrackName) + { + AddScopeAndRole(acceptedScopes, acceptedRoles, httpMethod, $"{subScope}{segment}", $"{subRole}{segment}", segment); + } + + AddScopeAndRole(acceptedScopes, acceptedRoles, httpMethod, $"{subScope}[{trackName}]{segment}", $"{subRole}[{trackName}]{segment}", segment); + } + } +} diff --git a/src/FoxIDs.Control/Infrastructure/Security/TenantScopeAuthorizeAttribute.cs b/src/FoxIDs.Control/Infrastructure/Security/TenantScopeAuthorizeAttribute.cs index db9db6924..6ff61fa96 100644 --- a/src/FoxIDs.Control/Infrastructure/Security/TenantScopeAuthorizeAttribute.cs +++ b/src/FoxIDs.Control/Infrastructure/Security/TenantScopeAuthorizeAttribute.cs @@ -1,25 +1,19 @@ -using ITfoxtec.Identity; -using ITfoxtec.Identity.Models; -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; namespace FoxIDs.Infrastructure.Security { - public class TenantScopeAuthorizeAttribute : AuthorizeAttribute + public class TenantScopeAuthorizeAttribute : BaseScopeAuthorizeAttribute { public const string Name = nameof(TenantScopeAuthorizeAttribute); - public TenantScopeAuthorizeAttribute() : base(Name) - { - AuthenticationSchemes = JwtBearerMultipleTenantsHandler.AuthenticationScheme; - } + public TenantScopeAuthorizeAttribute(params string[] segments) : base(Name, segments) + { } public static void AddPolicy(AuthorizationOptions options) { options.AddPolicy(Name, policy => { - policy.RequireScopeAndRoles( - new ScopeAndRoles { Scope = Constants.ControlApi.ResourceAndScope.Tenant, Roles = new [] { Constants.ControlApi.Role.TenantAdmin } } - ); + policy.Requirements.Add(new TenantAuthorizationRequirement()); }); } } diff --git a/src/FoxIDs.Control/Logic/Seed/MasterTenantDocumentsSeedLogic.cs b/src/FoxIDs.Control/Logic/Seed/MasterTenantDocumentsSeedLogic.cs index fd3e51621..8f54862fb 100644 --- a/src/FoxIDs.Control/Logic/Seed/MasterTenantDocumentsSeedLogic.cs +++ b/src/FoxIDs.Control/Logic/Seed/MasterTenantDocumentsSeedLogic.cs @@ -32,8 +32,8 @@ public async Task SeedAsync() await masterTenantLogic.CreateMasterTrackDocumentAsync(Constants.Routes.MasterTenantName, TrackKeyTypes.KeyVaultRenewSelfSigned); var mLoginUpParty = await masterTenantLogic.CreateMasterLoginDocumentAsync(Constants.Routes.MasterTenantName); - await masterTenantLogic.CreateFirstAdminUserDocumentAsync(Constants.Routes.MasterTenantName, Constants.DefaultAdminAccount.Email, Constants.DefaultAdminAccount.Password, true, false, false); - await masterTenantLogic.CreateMasterFoxIDsControlApiResourceDocumentAsync(Constants.Routes.MasterTenantName, includeMasterTenantScope: true); + await masterTenantLogic.CreateFirstAdminUserDocumentAsync(Constants.Routes.MasterTenantName, Constants.DefaultAdminAccount.Email, Constants.DefaultAdminAccount.Password, true, false, false, isMasterTenant: true); + await masterTenantLogic.CreateMasterFoxIDsControlApiResourceDocumentAsync(Constants.Routes.MasterTenantName, isMasterTenant: true); await masterTenantLogic.CreateMasterControlClientDocmentAsync(Constants.Routes.MasterTenantName, settings.FoxIDsControlEndpoint, mLoginUpParty, includeMasterTenantScope: true); } catch (Exception ex) diff --git a/src/FoxIDs.Control/Logic/ValidateApiModelOAuthOidcPartyLogic.cs b/src/FoxIDs.Control/Logic/ValidateApiModelOAuthOidcPartyLogic.cs index f0dcccffd..ac43f6abb 100644 --- a/src/FoxIDs.Control/Logic/ValidateApiModelOAuthOidcPartyLogic.cs +++ b/src/FoxIDs.Control/Logic/ValidateApiModelOAuthOidcPartyLogic.cs @@ -76,6 +76,17 @@ public bool ValidateApiModel(ModelStateDictionary modelState, Api.OAuthDownParty { isValid = isValidRtResult; } + + if(party.Resource == null && party.Client.ResourceScopes?.Count() > 0) + { + foreach (var rs in party.Client.ResourceScopes) + { + if (rs.Resource == party.Name) + { + rs.Scopes = null; + } + } + } } return isValid; } @@ -94,6 +105,17 @@ public bool ValidateApiModel(ModelStateDictionary modelState, Api.OidcDownParty { isValid = isValidRtResult; } + + if (party.Resource == null && party.Client.ResourceScopes?.Count() > 0) + { + foreach (var rs in party.Client.ResourceScopes) + { + if (rs.Resource == party.Name) + { + rs.Scopes = null; + } + } + } } return isValid; } @@ -199,7 +221,7 @@ private List OrderResponseTypes(List responseTypes) private string OrderResponseType(string responseType) { var orderedResponseType = responseType.ToSpaceList() - .OrderBy(rt => Array.IndexOf(new string[] { IdentityConstants.ResponseTypes.Code, IdentityConstants.ResponseTypes.Token, IdentityConstants.ResponseTypes.IdToken }, rt)); + .OrderBy(rt => Array.IndexOf([IdentityConstants.ResponseTypes.Code, IdentityConstants.ResponseTypes.Token, IdentityConstants.ResponseTypes.IdToken], rt)); return orderedResponseType.ToSpaceList(); } @@ -217,7 +239,7 @@ private async Task ValidateClientResourceScopesAsync !rs.Resource.Equals(oauthDownParty.Name, System.StringComparison.Ordinal))) + foreach (var resourceScope in oauthDownParty.Client.ResourceScopes.Where(rs => !rs.Resource.Equals(oauthDownParty.Name, StringComparison.Ordinal))) { var duplicatedScope = resourceScope.Scopes?.GroupBy(s => s).Where(g => g.Count() > 1).FirstOrDefault(); if (duplicatedScope != null) @@ -232,7 +254,7 @@ private async Task ValidateClientResourceScopesAsync s.Equals(scope, System.StringComparison.Ordinal)).Count() > 0)) + if (!(resourceDownParty.Resource?.Scopes?.Where(s => s.Equals(scope, StringComparison.Ordinal)).Count() > 0)) { throw new ValidationException($"Resource '{resourceScope.Resource}' scope '{scope}' not found."); } diff --git a/src/FoxIDs.Control/Logic/ValidateApiModelSamlPartyLogic.cs b/src/FoxIDs.Control/Logic/ValidateApiModelSamlPartyLogic.cs index fc71e7104..743030f97 100644 --- a/src/FoxIDs.Control/Logic/ValidateApiModelSamlPartyLogic.cs +++ b/src/FoxIDs.Control/Logic/ValidateApiModelSamlPartyLogic.cs @@ -46,7 +46,7 @@ private bool ValidateSignatureAlgorithmAndSigningKeys(ModelStateDictionary model ValidateSigningKeys(modelState, nameof(samlDownParty.Keys), samlDownParty.Keys); } - private bool ValidateSigningKeys(ModelStateDictionary modelState, string propertyName, List keys) + private bool ValidateSigningKeys(ModelStateDictionary modelState, string propertyName, List keys) { var isValid = true; try diff --git a/src/FoxIDs.Control/Logic/ValidateModelOAuthOidcPartyLogic.cs b/src/FoxIDs.Control/Logic/ValidateModelOAuthOidcPartyLogic.cs new file mode 100644 index 000000000..de24b368b --- /dev/null +++ b/src/FoxIDs.Control/Logic/ValidateModelOAuthOidcPartyLogic.cs @@ -0,0 +1,45 @@ +using Api = FoxIDs.Models.Api; +using FoxIDs.Infrastructure; +using FoxIDs.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using ITfoxtec.Identity; +using System.ComponentModel.DataAnnotations; + +namespace FoxIDs.Logic +{ + public class ValidateModelOAuthOidcPartyLogic : LogicBase + { + private readonly TelemetryScopedLogger logger; + + public ValidateModelOAuthOidcPartyLogic(TelemetryScopedLogger logger, IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + this.logger = logger; + } + + public bool ValidateApiModel(ModelStateDictionary modelState, OidcUpParty party) + { + var isValid = true; + try + { + if (party.Client != null) + { + if (party.Client.ResponseType.Contains(IdentityConstants.ResponseTypes.Code) == true) + { + if (party.Client.ClientAuthenticationMethod != ClientAuthenticationMethods.PrivateKeyJwt && party.Client.ClientSecret.IsNullOrEmpty()) + { + throw new ValidationException($"Require '{nameof(OidcUpParty.Client)}.{nameof(party.Client.ClientSecret)}' or '{nameof(OidcUpParty.Client)}.{nameof(party.Client.ClientAuthenticationMethod)}={ClientAuthenticationMethods.PrivateKeyJwt}' to execute '{IdentityConstants.ResponseTypes.Code}' response type."); + } + } + } + } + catch (ValidationException vex) + { + isValid = false; + logger.Warning(vex); + modelState.TryAddModelError($"{nameof(Api.OidcUpParty.Client)}.{nameof(Api.OidcUpParty.Client.ClientSecret)}".ToCamelCase(), vex.Message); + } + return isValid; + } + } +} diff --git a/src/FoxIDs.Control/MappingProfiles/TenantMappingProfiles.cs b/src/FoxIDs.Control/MappingProfiles/TenantMappingProfiles.cs index 2064b377d..9ad8d9774 100644 --- a/src/FoxIDs.Control/MappingProfiles/TenantMappingProfiles.cs +++ b/src/FoxIDs.Control/MappingProfiles/TenantMappingProfiles.cs @@ -55,6 +55,11 @@ private void Mapping() .ForMember(d => d.Email, opt => opt.MapFrom(s => s.Email.ToLower())) .ForMember(d => d.Id, opt => opt.MapFrom(s => User.IdFormatAsync(RouteBinding, s.Email.ToLower()).GetAwaiter().GetResult())); + CreateMap(); + + CreateMap() + .ReverseMap(); + CreateMap() .ReverseMap(); @@ -78,7 +83,7 @@ private void Mapping() CreateMap() .ReverseMap(); - CreateMap() + CreateMap() .ForMember(d => d.CertificateInfo, opt => opt.MapFrom(s => GetCertificateInfo(s))); CreateMap(); diff --git a/src/FoxIDs.Control/Startup.cs b/src/FoxIDs.Control/Startup.cs index acd8495e3..8f82e83dd 100644 --- a/src/FoxIDs.Control/Startup.cs +++ b/src/FoxIDs.Control/Startup.cs @@ -96,7 +96,7 @@ public void Configure(IApplicationBuilder app) app.UseRouting(); app.UseEndpoints(endpoints => { - endpoints.MapFallbackToFile("index.html"); + endpoints.MapFallbackToController("Index", "W"); }); } } diff --git a/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj b/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj index 40c9e8114..e5ee42c3d 100644 --- a/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj +++ b/src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj @@ -2,7 +2,7 @@ net8.0 - 1.2.5.0 + 1.2.6.4 FoxIDs.Client Anders Revsgaard ITfoxtec @@ -12,10 +12,11 @@ - - - + + + + diff --git a/src/FoxIDs.ControlClient/Infrastructure/CheckResponseMessageHandler.cs b/src/FoxIDs.ControlClient/Infrastructure/CheckResponseMessageHandler.cs index 5fa50b4d5..8cd2c6fed 100644 --- a/src/FoxIDs.ControlClient/Infrastructure/CheckResponseMessageHandler.cs +++ b/src/FoxIDs.ControlClient/Infrastructure/CheckResponseMessageHandler.cs @@ -19,6 +19,10 @@ protected override async Task SendAsync(HttpRequestMessage { throw new FoxIDsApiException("Unauthorized", response.StatusCode, await GetResponseTextAsync(response), GetHeaders(response)); } + else if (response.StatusCode == HttpStatusCode.Forbidden) + { + throw new FoxIDsApiException("Forbidden, you do not possess the required scope and role.", response.StatusCode, await GetResponseTextAsync(response), GetHeaders(response)); + } else if (response.StatusCode == HttpStatusCode.BadRequest) { throw new FoxIDsApiException("Bad request", response.StatusCode, await GetResponseTextAsync(response), GetHeaders(response)); diff --git a/src/FoxIDs.ControlClient/Infrastructure/Hosting/ServiceCollectionExtensions.cs b/src/FoxIDs.ControlClient/Infrastructure/Hosting/ServiceCollectionExtensions.cs index c3d252082..da80dfe2a 100644 --- a/src/FoxIDs.ControlClient/Infrastructure/Hosting/ServiceCollectionExtensions.cs +++ b/src/FoxIDs.ControlClient/Infrastructure/Hosting/ServiceCollectionExtensions.cs @@ -25,6 +25,7 @@ public static IServiceCollection AddLogic(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/FoxIDs.ControlClient/Infrastructure/Security/TenantOpenidConnectPkce.cs b/src/FoxIDs.ControlClient/Infrastructure/Security/TenantOpenidConnectPkce.cs index 416122418..8e3e74660 100644 --- a/src/FoxIDs.ControlClient/Infrastructure/Security/TenantOpenidConnectPkce.cs +++ b/src/FoxIDs.ControlClient/Infrastructure/Security/TenantOpenidConnectPkce.cs @@ -24,7 +24,7 @@ public TenantOpenidConnectPkce(IServiceProvider serviceProvider, RouteBindingLog this.clientSettings = clientSettings; } - public async Task TenantLoginAsync() + public async Task TenantLoginAsync(string prompt = null) { await controlClientSettingLogic.InitLoadAsync(); @@ -38,7 +38,7 @@ public async Task TenantLoginAsync() LogoutCallBackPath = await ReplaceTenantNameAsync(clientSettings.LogoutCallBackPath) }; - await LoginAsync(openidConnectPkceSettings); + await LoginAsync(openidConnectPkceSettings, prompt: prompt); } public async Task TenantLogoutAsync() diff --git a/src/FoxIDs.ControlClient/Logic/TrackSelectedLogic.cs b/src/FoxIDs.ControlClient/Logic/TrackSelectedLogic.cs index 1cbe7ec4d..ee5cd4818 100644 --- a/src/FoxIDs.ControlClient/Logic/TrackSelectedLogic.cs +++ b/src/FoxIDs.ControlClient/Logic/TrackSelectedLogic.cs @@ -10,26 +10,25 @@ public class TrackSelectedLogic public bool IsTrackSelected => Track != null; + public event Func OnTrackSelectedAsync; + public event Func OnSelectTrackAsync; + public async Task TrackSelectedAsync(Track track) { Track = track; - if(OnTrackSelectedAsync != null) + if (OnTrackSelectedAsync != null) { await OnTrackSelectedAsync(track); } } - public event Func OnTrackSelectedAsync; - - public async Task ShowSelectTrackAsync() + public async Task SelectTrackAsync() { Track = null; - if (OnShowSelectTrackAsync != null) + if (OnSelectTrackAsync != null) { - await OnShowSelectTrackAsync(); + await OnSelectTrackAsync(); } } - - public event Func OnShowSelectTrackAsync; } } diff --git a/src/FoxIDs.ControlClient/Logic/UserProfileLogic.cs b/src/FoxIDs.ControlClient/Logic/UserProfileLogic.cs new file mode 100644 index 000000000..24c613873 --- /dev/null +++ b/src/FoxIDs.ControlClient/Logic/UserProfileLogic.cs @@ -0,0 +1,59 @@ +using FoxIDs.Client.Services; +using FoxIDs.Infrastructure; +using FoxIDs.Models.Api; +using System.Threading.Tasks; + +namespace FoxIDs.Client.Logic +{ + public class UserProfileLogic + { + private UserControlProfile userControlProfile; + private readonly UserService userService; + + public UserProfileLogic(UserService UserService) + { + userService = UserService; + } + + public async Task GetUserProfileAsync() + { + if (userControlProfile == null) + { + try + { + userControlProfile = await userService.GetUserControlProfileAsync(); + } + catch (FoxIDsApiException ex) + { + if (ex.StatusCode != System.Net.HttpStatusCode.NotFound) + { + throw; + } + } + } + + return userControlProfile; + } + + public async Task UpdateTrackAsync(string trackName) + { + if (userControlProfile != null && userControlProfile.LastTrackName == trackName) + { + return; + } + + if (userControlProfile == null) + { + userControlProfile = new UserControlProfile(); + } + + userControlProfile.LastTrackName = trackName; + await UpdateUserProfileAsync(); + } + + private async Task UpdateUserProfileAsync() + { + await userService.UpdateUserControlProfileAsync(userControlProfile); + } + } +} diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/DownPartyOAuthTypes.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/DownPartyOAuthTypes.cs new file mode 100644 index 000000000..6bf5d1c32 --- /dev/null +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/DownPartyOAuthTypes.cs @@ -0,0 +1,9 @@ +namespace FoxIDs.Client.Models.ViewModels +{ + public enum DownPartyOAuthTypes + { + Client, + Resource, + ClientAndResource + } +} diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralOAuthDownPartyViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralOAuthDownPartyViewModel.cs index 10a5d0bf0..cf3a19460 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralOAuthDownPartyViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralOAuthDownPartyViewModel.cs @@ -1,6 +1,7 @@ using FoxIDs.Client.Shared.Components; using FoxIDs.Models.Api; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; namespace FoxIDs.Client.Models.ViewModels { @@ -24,9 +25,8 @@ public GeneralOAuthDownPartyViewModel(DownParty downParty) : base(downParty) public OAuthSubPartyTypes SubPartyType { get; set; } - public bool EnableClientTab { get; set; } - - public bool EnableResourceTab { get; set; } = true; + [Display(Name = "Down-party type")] + public DownPartyOAuthTypes DownPartyType { get; set; } = DownPartyOAuthTypes.Resource; public bool ShowClientTab { get; set; } public bool ShowResourceTab { get; set; } = true; diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralOidcDownPartyViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralOidcDownPartyViewModel.cs index f0f56fbc1..0faffb3aa 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralOidcDownPartyViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/GeneralOidcDownPartyViewModel.cs @@ -1,6 +1,7 @@ using FoxIDs.Client.Shared.Components; using FoxIDs.Models.Api; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; namespace FoxIDs.Client.Models.ViewModels { @@ -22,9 +23,8 @@ public GeneralOidcDownPartyViewModel(DownParty downParty) : base(downParty) public string ClientCertificateFileStatus { get; set; } = DefaultClientCertificateFileStatus; - public bool EnableClientTab { get; set; } = true; - - public bool EnableResourceTab { get; set; } + [Display(Name = "Down-party type")] + public DownPartyOAuthTypes DownPartyType { get; set; } = DownPartyOAuthTypes.Client; public bool ShowClientTab { get; set; } = true; public bool ShowResourceTab { get; set; } diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/OAuthUpPartyViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/OAuthUpPartyViewModel.cs index 3ca14231a..b906927a1 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/OAuthUpPartyViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/OAuthUpPartyViewModel.cs @@ -50,7 +50,7 @@ public class OAuthUpPartyViewModel : IOAuthClaimTransformViewModel, IValidatable public string SpIssuer { get; set; } [Display(Name = "Keys")] - public List Keys { get; set; } + public List Keys { get; set; } /// /// OIDC up client. diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/OidcUpClientViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/OidcUpClientViewModel.cs index 888db48ef..57780dafa 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/OidcUpClientViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/OidcUpClientViewModel.cs @@ -67,6 +67,7 @@ public class OidcUpClientViewModel : IClientAdditionalParameters, IValidatableOb [MaxLength(Constants.Models.SecretHash.SecretLength)] [Display(Name = "Client secret")] public string ClientSecret { get; set; } + public string ClientSecretLoaded { get; set; } [Display(Name = "Client certificate")] public KeyInfoViewModel PublicClientKeyInfo { get; set; } diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/OidcUpPartyViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/OidcUpPartyViewModel.cs index 7a5b769a4..e4e8f784b 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/OidcUpPartyViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/OidcUpPartyViewModel.cs @@ -50,7 +50,7 @@ public class OidcUpPartyViewModel : IOAuthClaimTransformViewModel, IUpPartySessi public string SpIssuer { get; set; } [Display(Name = "Keys")] - public List Keys { get; set; } + public List Keys { get; set; } /// /// Default 10 hours. diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/SamlDownPartyViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/SamlDownPartyViewModel.cs index b8a8d4222..280887f4e 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/SamlDownPartyViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/SamlDownPartyViewModel.cs @@ -128,10 +128,10 @@ public class SamlDownPartyViewModel : IValidatableObject, IAllowUpPartyNames, ID [ValidateComplexType] [ListLength(Constants.Models.SamlParty.Down.KeysMin, Constants.Models.SamlParty.KeysMax)] [Display(Name = "Optional one or more signature validation certificates")] - public List Keys { get; set; } + public List Keys { get; set; } [Display(Name = "Optional encryption certificate")] - public JwtWithCertificateInfo EncryptionKey { get; set; } + public JwkWithCertificateInfo EncryptionKey { get; set; } [Display(Name = "Add logout response location URL in metadata")] public bool MetadataAddLogoutResponseLocation { get; set; } diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/SamlUpPartyViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/SamlUpPartyViewModel.cs index a55329ecc..87d68aa1d 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Parties/SamlUpPartyViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Parties/SamlUpPartyViewModel.cs @@ -105,7 +105,7 @@ public bool AutomaticUpdate [ValidateComplexType] [ListLength(0, Constants.Models.SamlParty.KeysMax)] [Display(Name = "One or more signature validation certificates")] - public List Keys { get; set; } + public List Keys { get; set; } [Display(Name = "Logout request binding")] public SamlBindingTypes LogoutRequestBinding { get; set; } = SamlBindingTypes.Post; diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/GeneralTrackCertificateViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/GeneralTrackCertificateViewModel.cs index 3a84157c5..787acf597 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/GeneralTrackCertificateViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/GeneralTrackCertificateViewModel.cs @@ -13,7 +13,7 @@ public GeneralTrackCertificateViewModel(bool isPrimary) IsPrimary = isPrimary; } - public GeneralTrackCertificateViewModel(JwtWithCertificateInfo key, bool isPrimary) : this(isPrimary) + public GeneralTrackCertificateViewModel(JwkWithCertificateInfo key, bool isPrimary) : this(isPrimary) { Subject = key.CertificateInfo.Subject; ValidFrom = key.CertificateInfo.ValidFrom; diff --git a/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/KeyInfoViewModel.cs b/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/KeyInfoViewModel.cs index 3e2fe6690..dd118adb6 100644 --- a/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/KeyInfoViewModel.cs +++ b/src/FoxIDs.ControlClient/Models/ViewModels/Tracks/KeyInfoViewModel.cs @@ -12,7 +12,7 @@ public class KeyInfoViewModel public bool IsValid { get; set; } public string Thumbprint { get; set; } public string KeyId { get; set; } - public JwtWithCertificateInfo Key { get; set; } + public JwkWithCertificateInfo Key { get; set; } [Display(Name = "Optional certificate password")] public string Password { get; set; } diff --git a/src/FoxIDs.ControlClient/Pages/Certificates.cs b/src/FoxIDs.ControlClient/Pages/Certificates.cs index 9dc2c604f..30d9a6c7a 100644 --- a/src/FoxIDs.ControlClient/Pages/Certificates.cs +++ b/src/FoxIDs.ControlClient/Pages/Certificates.cs @@ -195,9 +195,9 @@ private async Task OnCertificateFileSelectedAsync(GeneralTrackCertificateViewMod } var base64UrlEncodeCertificate = WebEncoders.Base64UrlEncode(certificateBytes); - var jwtWithCertificateInfo = await HelpersService.ReadCertificateAsync(new CertificateAndPassword { EncodeCertificate = base64UrlEncodeCertificate, Password = generalCertificate.Form.Model.Password }); + var jwkWithCertificateInfo = await HelpersService.ReadCertificateAsync(new CertificateAndPassword { EncodeCertificate = base64UrlEncodeCertificate, Password = generalCertificate.Form.Model.Password }); - if (!jwtWithCertificateInfo.HasPrivateKey()) + if (!jwkWithCertificateInfo.HasPrivateKey()) { generalCertificate.Form.Model.Subject = null; generalCertificate.Form.Model.Key = null; @@ -206,12 +206,12 @@ private async Task OnCertificateFileSelectedAsync(GeneralTrackCertificateViewMod return; } - generalCertificate.Form.Model.Subject = jwtWithCertificateInfo.CertificateInfo.Subject; - generalCertificate.Form.Model.ValidFrom = jwtWithCertificateInfo.CertificateInfo.ValidFrom; - generalCertificate.Form.Model.ValidTo = jwtWithCertificateInfo.CertificateInfo.ValidTo; - generalCertificate.Form.Model.IsValid = jwtWithCertificateInfo.CertificateInfo.IsValid(); - generalCertificate.Form.Model.Thumbprint = jwtWithCertificateInfo.CertificateInfo.Thumbprint; - generalCertificate.Form.Model.Key = jwtWithCertificateInfo; + generalCertificate.Form.Model.Subject = jwkWithCertificateInfo.CertificateInfo.Subject; + generalCertificate.Form.Model.ValidFrom = jwkWithCertificateInfo.CertificateInfo.ValidFrom; + generalCertificate.Form.Model.ValidTo = jwkWithCertificateInfo.CertificateInfo.ValidTo; + generalCertificate.Form.Model.IsValid = jwkWithCertificateInfo.CertificateInfo.IsValid(); + generalCertificate.Form.Model.Thumbprint = jwkWithCertificateInfo.CertificateInfo.Thumbprint; + generalCertificate.Form.Model.Key = jwkWithCertificateInfo; generalCertificate.CertificateFileStatus = GeneralTrackCertificateViewModel.DefaultCertificateFileStatus; } diff --git a/src/FoxIDs.ControlClient/Pages/Components/ELoginUpParty.razor b/src/FoxIDs.ControlClient/Pages/Components/ELoginUpParty.razor index dbfb1bbfa..8c64e6ab7 100644 --- a/src/FoxIDs.ControlClient/Pages/Components/ELoginUpParty.razor +++ b/src/FoxIDs.ControlClient/Pages/Components/ELoginUpParty.razor @@ -9,7 +9,7 @@
    Login
    - + }