Skip to content

Commit

Permalink
Use options pattern for magic links and application overrides (#613)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonashendrickx authored Jun 12, 2024
1 parent 04440ec commit 4e8eb5c
Show file tree
Hide file tree
Showing 7 changed files with 41 additions and 37 deletions.
12 changes: 5 additions & 7 deletions src/Api/Endpoints/Magic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using Passwordless.Api.Authorization;
using Passwordless.Api.OpenApi;
using Passwordless.Common.MagicLinks.Models;
Expand All @@ -12,7 +13,6 @@
using Passwordless.Service.Helpers;
using Passwordless.Service.MagicLinks;
using Passwordless.Service.MagicLinks.Extensions;
using Passwordless.Service.Models;
using static Microsoft.AspNetCore.Http.Results;

namespace Passwordless.Api.Endpoints;
Expand All @@ -32,13 +32,11 @@ public static void AddMagicRateLimiterPolicy(this RateLimiterOptions builder) =>
{
var tenant = context.User.FindFirstValue(CustomClaimTypes.AccountName) ?? "<global>";
var isRateLimitBypassed = context.RequestServices
.GetRequiredService<IConfiguration>()
.GetSection("ApplicationOverrides")
.GetApplicationOverrides(tenant)
.IsRateLimitBypassEnabled;
var applicationOverridesOptions = context.RequestServices
.GetRequiredService<IOptionsSnapshot<ApplicationOverridesOptions>>();
if (isRateLimitBypassed)
var applicationOverrides = applicationOverridesOptions.Value.GetApplication(tenant);
if (applicationOverrides.IsRateLimitBypassEnabled)
return RateLimitPartition.GetNoLimiter(tenant);
return RateLimitPartition.GetFixedWindowLimiter(tenant, _ =>
Expand Down
8 changes: 5 additions & 3 deletions src/Api/Extensions/MagicLinkBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ namespace Passwordless.Api.Extensions;

public static class MagicLinkBootstrap
{
public static IServiceCollection AddMagicLinks(this IServiceCollection serviceCollection) =>
serviceCollection
.AddScoped<MagicLinkService>();
public static void AddMagicLinks(this WebApplicationBuilder builder)
{
builder.Services.AddOptions<MagicLinksOptions>().BindConfiguration("MagicLinks");
builder.Services.AddScoped<MagicLinkService>();
}
}
4 changes: 3 additions & 1 deletion src/Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Passwordless.Common.HealthChecks;
using Passwordless.Common.Logging;
using Passwordless.Common.Middleware.SelfHosting;
using Passwordless.Common.Overrides;
using Passwordless.Common.Services.Mail;
using Passwordless.Service;
using Passwordless.Service.EventLog;
Expand Down Expand Up @@ -102,7 +103,8 @@

builder.AddPasswordlessHealthChecks();

builder.Services.AddMagicLinks();
builder.Services.AddOptions<ApplicationOverridesOptions>().BindConfiguration("ApplicationOverrides");
builder.AddMagicLinks();

WebApplication app = builder.Build();

Expand Down
17 changes: 0 additions & 17 deletions src/Common/Overrides/ApplicationOverridesExtensions.cs

This file was deleted.

15 changes: 15 additions & 0 deletions src/Common/Overrides/ApplicationOverridesOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Passwordless.Common.Overrides;

public class ApplicationOverridesOptions : Dictionary<string, ApplicationOverrides>
{
/// <summary>
/// Gets the application overrides for the specified tenant. If the application is not found, an instance with
/// default values is returned.
/// </summary>
/// <param name="tenant"></param>
/// <returns></returns>
public ApplicationOverrides GetApplication(string tenant)
{
return this.TryGetValue(tenant, out var overrides) ? overrides : new ApplicationOverrides();
}
}
16 changes: 7 additions & 9 deletions src/Service/MagicLinks/MagicLinkService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Passwordless.Common.MagicLinks.Models;
using Passwordless.Common.Overrides;
using Passwordless.Common.Services.Mail;
Expand All @@ -11,33 +11,31 @@ namespace Passwordless.Service.MagicLinks;

public class MagicLinkService(
TimeProvider timeProvider,
IConfiguration configuration,
IOptionsSnapshot<MagicLinksOptions> magicLinksOptions,
IOptionsSnapshot<ApplicationOverridesOptions> applicationOverridesOptions,
ITenantStorage tenantStorage,
IFido2Service fido2Service,
IMailProvider mailProvider,
IEventLogger eventLogger)
{
private TimeSpan NewAccountTimeout { get; } =
configuration.GetSection("MagicLinks").GetValue<TimeSpan?>(nameof(NewAccountTimeout)) ??
TimeSpan.FromHours(24);

private async Task EnforceQuotaAsync(MagicLinkTokenRequest request)
{
var now = timeProvider.GetUtcNow().UtcDateTime;
var account = await tenantStorage.GetAccountInformation();
var accountAge = now - account.CreatedAt;

// Check bypass
if (configuration.GetApplicationOverrides(account.AcountName).IsMagicLinkQuotaBypassEnabled)
var applicationOverrides = applicationOverridesOptions.Value.GetApplication(account.AcountName);
if (applicationOverrides.IsMagicLinkQuotaBypassEnabled)
return;

// Newly created accounts can only send magic links to the admin email address
if (accountAge < NewAccountTimeout &&
if (accountAge < magicLinksOptions.Value.NewAccountTimeout &&
!account.AdminEmails.Contains(request.EmailAddress.Address, StringComparer.OrdinalIgnoreCase))
{
throw new ApiException(
"magic_link_email_admin_address_only",
$"Because your application has been created less than {(int)NewAccountTimeout.TotalHours} hours ago, " +
$"Because your application has been created less than {(int)magicLinksOptions.Value.NewAccountTimeout.TotalHours} hours ago, " +
"you can only request magic links to the admin email address.",
403
);
Expand Down
6 changes: 6 additions & 0 deletions src/Service/MagicLinks/MagicLinksOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Passwordless.Service.MagicLinks;

public class MagicLinksOptions
{
public TimeSpan NewAccountTimeout { get; set; } = TimeSpan.FromHours(24);
}

0 comments on commit 4e8eb5c

Please sign in to comment.