Skip to content

main #1261

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Aug 12, 2025
Merged

main #1261

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch FoxIDs",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build FoxIDs",
"program": "${workspaceFolder}/src/FoxIDs/bin/Debug/net9.0/FoxIDs.dll",
"args": [],
"cwd": "${workspaceFolder}/src/FoxIDs",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
{
"name": "Launch FoxIDs.Control",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build FoxIDs.Control",
"program": "${workspaceFolder}/src/FoxIDs.Control/bin/Debug/net9.0/FoxIDs.Control.dll",
"args": [],
"cwd": "${workspaceFolder}/src/FoxIDs.Control",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
],
"compounds": [
{
"name": "Launch FoxIDs & FoxIDs.Control",
"configurations": [
"Launch FoxIDs",
"Launch FoxIDs.Control"
],
"stopAll": true
}
]
}
29 changes: 29 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build FoxIDs",
"type": "process",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/src/FoxIDs/FoxIDs.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "build FoxIDs.Control",
"type": "process",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/src/FoxIDs.Control/FoxIDs.Control.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
}
]
}
2 changes: 2 additions & 0 deletions docs/login.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ In this example the user is asked to do two-factor authentication with an authen

![2FA example](images/configure-login-2fa-example.png)

A phone number and email can either be configured as a user identifier or as a claim with the `phone_number` and `email` claim types.

The two-factor authentication type is selected as shown in this table.

<table>
Expand Down
2 changes: 1 addition & 1 deletion docs/reverse-proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Optionally both requiring (`secret1`) and sending (`secret2`) in a `X-FoxIDs-Sec
</globalRules>

## Read HTTP headers
The FoxIDs site support reading the client IP address in the following HTTP headers in order of priority:
The FoxIDs site support reading the forwarded client IP address in the following HTTP headers in order of priority:

1. `CF-Connecting-IP`
2. `X-Azure-ClientIP`
Expand Down
4 changes: 2 additions & 2 deletions docs/users-upload.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Upload users
# Upload many users

Provisioning your users in an environment, with or without a password:

- You can upload the users with there password, if you know the users' passwords.
- Otherwise, you can upload the users without a password and the users are then requested to set a password with an email or SMS conformation code. Require the users to have either an email or phone number.

The users are bulk uploaded to an environment with 1,000 users at the time and supporting upload of millions of users. You can either user the [FoxIDs Control API](control.md#foxids-control-api) directly or use the [seed tool](#upload-with-seed-tool).
The users are bulk uploaded to an environment with 1,000 users at the time and supporting multiple upload of millions of users. You can either user the [FoxIDs Control API](control.md#foxids-control-api) directly or use the [seed tool](#upload-with-seed-tool).

## Upload with seed tool

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ public TReadCertificateController(TelemetryScopedLogger logger, IMapper mapper)
this.mapper = mapper;
}

/// <summary>
/// Read JWK with certificate information.
/// </summary>
/// <param name="certificateAndPassword">Base64 URL encode certificate and optionally password.</param>
/// <returns>User.</returns>
[ProducesResponseType(typeof(Api.JwkWithCertificateInfo), StatusCodes.Status200OK)]
public async Task<ActionResult<Api.JwkWithCertificateInfo>> PostReadCertificate([FromBody] Api.CertificateAndPassword certificateAndPassword)
/// <summary>
/// Read JWK with certificate information from PFX.
/// </summary>
/// <param name="certificateAndPassword">Base64 URL encode certificate and optionally password.</param>
[ProducesResponseType(typeof(Api.JwkWithCertificateInfo), StatusCodes.Status200OK)]
public async Task<ActionResult<Api.JwkWithCertificateInfo>> PostReadCertificate([FromBody] Api.CertificateAndPassword certificateAndPassword)
{
if (!await ModelState.TryValidateObjectAsync(certificateAndPassword)) return BadRequest(ModelState);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using AutoMapper;
using FoxIDs.Infrastructure;
using Api = FoxIDs.Models.Api;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using ITfoxtec.Identity;
using System.Security.Cryptography.X509Certificates;
using System;
using System.ComponentModel.DataAnnotations;
using FoxIDs.Infrastructure.Security;

namespace FoxIDs.Controllers
{
[TenantScopeAuthorize(Constants.ControlApi.Segment.Basic, Constants.ControlApi.Segment.Party)]
public class TReadCertificateFromPemController : ApiController
{
private readonly IMapper mapper;

public TReadCertificateFromPemController(TelemetryScopedLogger logger, IMapper mapper) : base(logger)
{
this.mapper = mapper;
}

/// <summary>
/// Read JWK with certificate information from PEM certificate (.crt) and private key (.key).
/// </summary>
/// <param name="certificateCrtAndKey">PEM certificate and private key.</param>
[ProducesResponseType(typeof(Api.JwkWithCertificateInfo), StatusCodes.Status200OK)]
public async Task<ActionResult<Api.JwkWithCertificateInfo>> PostReadCertificateFromPem([FromBody] Api.CertificateCrtAndKey certificateCrtAndKey)
{
if (!await ModelState.TryValidateObjectAsync(certificateCrtAndKey)) return BadRequest(ModelState);

try
{
var certificate = X509Certificate2.CreateFromPem(certificateCrtAndKey.CertificatePemCrt, certificateCrtAndKey.CertificatePemKey);
var jwt = await certificate.ToFTJsonWebKeyAsync(includePrivateKey: certificate.HasPrivateKey);
return Ok(mapper.Map<Api.JwkWithCertificateInfo>(jwt));
}
catch (ValidationException)
{
throw;
}
catch (Exception ex)
{
throw new ValidationException("Unable to read PEM certificate and key.", ex);
}
}
}
}
124 changes: 124 additions & 0 deletions src/FoxIDs.Control/Controllers/Tracks/TTrackSendSmsController.cs
Original file line number Diff line number Diff line change
@@ -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;
using FoxIDs.Logic;
using FoxIDs.Infrastructure.Security;

namespace FoxIDs.Controllers
{
[TenantScopeAuthorize]
public class TTrackSendSmsController : ApiController
{
private readonly TelemetryScopedLogger logger;
private readonly IMapper mapper;
private readonly ITenantDataRepository tenantDataRepository;
private readonly TrackCacheLogic trackCacheLogic;

public TTrackSendSmsController(TelemetryScopedLogger logger, IMapper mapper, ITenantDataRepository tenantDataRepository, TrackCacheLogic trackCacheLogic) : base(logger)
{
this.logger = logger;
this.mapper = mapper;
this.tenantDataRepository = tenantDataRepository;
this.trackCacheLogic = trackCacheLogic;
}

/// <summary>
/// Get environment send SMS settings.
/// </summary>
/// <returns>Send SMS settings.</returns>
[ProducesResponseType(typeof(Api.ResourceItem), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Api.SendSms>> GetTrackSendSms()
{
try
{
var mTrack = await tenantDataRepository.GetTrackByNameAsync(new Track.IdKey { TenantName = RouteBinding.TenantName, TrackName = RouteBinding.TrackName });
if (mTrack.SendSms == null)
{
return NoContent();
}
return Ok(mapper.Map<Api.SendSms>(mTrack.SendSms));
}
catch (FoxIDsDataException ex)
{
if (ex.StatusCode == DataStatusCode.NotFound)
{
logger.Warning(ex, $"NotFound, Get Track.SendSms by environment name '{RouteBinding.TrackName}'.");
return NotFound("Track.SendSms", RouteBinding.TrackName);
}
throw;
}
}

/// <summary>
/// Update environment send SMS settings.
/// </summary>
/// <param name="sendSms">Send SMS settings.</param>
/// <returns>Send SMS settings.</returns>
[ProducesResponseType(typeof(Api.TrackResourceItem), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Api.SendSms>> PutTrackSendSms([FromBody] Api.SendSms sendSms)
{
try
{
if (!await ModelState.TryValidateObjectAsync(sendSms)) return BadRequest(ModelState);

var trackIdKey = new Track.IdKey { TenantName = RouteBinding.TenantName, TrackName = RouteBinding.TrackName };
var mTrack = await tenantDataRepository.GetTrackByNameAsync(trackIdKey);

mTrack.SendSms = mapper.Map<SendSms>(sendSms);
await tenantDataRepository.UpdateAsync(mTrack);

await trackCacheLogic.InvalidateTrackCacheAsync(trackIdKey);

return Ok(mapper.Map<Api.SendSms>(mTrack.SendSms));
}
catch (FoxIDsDataException ex)
{
if (ex.StatusCode == DataStatusCode.NotFound)
{
logger.Warning(ex, $"NotFound, Update Track.SendSms by environment name '{RouteBinding.TrackName}'.");
return NotFound("Track.SendSms", Convert.ToString(RouteBinding.TrackName));
}
throw;
}
}

/// <summary>
/// Delete environment send SMS settings.
/// </summary>
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> DeleteTrackSendSms()
{
try
{
var trackIdKey = new Track.IdKey { TenantName = RouteBinding.TenantName, TrackName = RouteBinding.TrackName };
var mTrack = await tenantDataRepository.GetTrackByNameAsync(trackIdKey);

mTrack.SendSms = null;
await tenantDataRepository.UpdateAsync(mTrack);

await trackCacheLogic.InvalidateTrackCacheAsync(trackIdKey);

return NoContent();
}
catch (FoxIDsDataException ex)
{
if (ex.StatusCode == DataStatusCode.NotFound)
{
logger.Warning(ex, $"NotFound, Delete Track.SendSms by environment name '{RouteBinding.TrackName}'.");
return NotFound("Track.SendSms", Convert.ToString(RouteBinding.TrackName));
}
throw;
}
}
}
}
14 changes: 7 additions & 7 deletions src/FoxIDs.Control/FoxIDs.Control.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>2.0.13</Version>
<Version>2.1.2</Version>
<RootNamespace>FoxIDs</RootNamespace>
<Authors>Anders Revsgaard</Authors>
<Company>FoxIDs</Company>
Expand All @@ -20,21 +20,21 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="AutoMapper" Version="15.0.1" />
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.2" />
<PackageReference Include="Azure.Identity" Version="1.13.1" />
<PackageReference Include="Azure.Security.KeyVault.Certificates" Version="4.7.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
<PackageReference Include="Azure.Monitor.Query" Version="1.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="9.0.6">
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="9.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.24" />
<PackageReference Include="Mollie.Api" Version="4.4.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="9.0.1" />
<PackageReference Include="Mollie.Api" Version="4.12.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="9.0.3" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using Mollie.Api;
using Mollie.Api.Framework;
Expand Down Expand Up @@ -255,14 +256,15 @@ public static IServiceCollection AddAutoMapper(this IServiceCollection services)
services.AddSingleton(serviceProvider =>
{
var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
var mappingConfig = new MapperConfiguration(mc =>
{
mc.AllowNullCollections = true;

mc.AddProfile(new MasterMappingProfile());
mc.AddProfile(new TenantMappingProfiles(httpContextAccessor));
mc.AddProfile(new ExternalMappingProfile());
});
}, loggerFactory);

return mappingConfig.CreateMapper();
});
Expand Down
6 changes: 3 additions & 3 deletions src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>2.0.13</Version>
<Version>2.1.2</Version>
<RootNamespace>FoxIDs.Client</RootNamespace>
<Authors>Anders Revsgaard</Authors>
<Company>FoxIDs</Company>
Expand All @@ -12,8 +12,8 @@
<ItemGroup>
<PackageReference Include="Blazored.Toast" Version="4.2.1" />
<PackageReference Include="ITfoxtec.Identity.BlazorWebAssembly.OpenidConnect" Version="1.7.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.7" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading