Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="RequestResetAsync">Request Reset</MudButton>
@if (_resetRequested)
{
<MudText Align="Align.Center">Your reset code is: (Copy it before next step)</MudText>
<MudText Align="Align.Center" Typo="Typo.h6"><b>@_resetCode</b></MudText>
<MudText Align="Align.Center">Your reset code is on the server console.</MudText>
<MudText Align="Align.Center"><MudLink Class="cursor-pointer" Href="@($"/reset?identifier={_identifier}")">Use Reset Code</MudLink></MudText>
}
</MudStack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs;
public partial class ResetDialog
{
private bool _resetRequested = false;
private string? _resetCode;
private string? _identifier;

[CascadingParameter]
Expand All @@ -34,7 +33,6 @@ private async Task RequestResetAsync()
return;
}

_resetCode = result.Value.Token;
_resetRequested = true;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
@using CodeBeam.UltimateAuth.Core.Contracts
@using CodeBeam.UltimateAuth.Users.Contracts
@inject IUAuthClient UAuthClient
@inject IUAuthClient UAuthClient
@inject ISnackbar Snackbar
@inject IDialogService DialogService

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
@using CodeBeam.UltimateAuth.Core.Contracts
@using CodeBeam.UltimateAuth.Credentials.Contracts
@using CodeBeam.UltimateAuth.Users.Contracts
@inject IUAuthClient UAuthClient
@inject IUAuthClient UAuthClient
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject IUAuthStateManager StateManager
Expand All @@ -15,8 +12,8 @@
<DialogContent>
<MudStack Class="mud-width-full">
<MudAlert Severity="Severity.Info">
This is a demonstration of how to implement a credential reset flow.
In a production application, you should use reset token or code in email, SMS etc. verification steps.
In this development sample, reset codes are written to the server console by a custom ICredentialResetNotifier.
Production applications should send them through a trusted email or SMS channel.
</MudAlert>
<MudAlert Severity="Severity.Warning">
Reset request always returns ok even with not found users due to security reasons.
Expand All @@ -25,9 +22,8 @@
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="RequestResetAsync">Request Reset</MudButton>
@if (_resetRequested)
{
<MudText Align="Align.Center">Your reset code is: (Copy it before next step)</MudText>
<MudText Align="Align.Center" Typo="Typo.h6"><b>@_resetCode</b></MudText>
<MudText Align="Align.Center"><MudLink Class="cursor-pointer" Href="@($"/reset?identifier={_identifier}")">Use Reset Code</MudLink></MudText>
<MudText Align="Align.Center">Your reset code is on the server console.</MudText>
<MudText Align="Align.Center"><MudLink Class="cursor-pointer" Href="@($"/reset?uauth_identifier={_identifier}")">Use Reset Code</MudLink></MudText>
}
</MudStack>
</DialogContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace CodeBeam.UltimateAuth.Sample.BlazorServer.Components.Dialogs;
public partial class ResetDialog
{
private bool _resetRequested = false;
private string? _resetCode;
private string? _identifier;

[CascadingParameter]
Expand All @@ -34,7 +33,6 @@ private async Task RequestResetAsync()
return;
}

_resetCode = result.Value.Token;
_resetRequested = true;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using CodeBeam.UltimateAuth.Credentials;

namespace CodeBeam.UltimateAuth.Sample.BlazorServer.Infrastructure;

internal sealed class DevelopmentCredentialResetNotifier(ILogger<DevelopmentCredentialResetNotifier> logger) : ICredentialResetNotifier
{
public Task NotifyAsync(CredentialResetNotification notification, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();

logger.LogWarning(
"DEVELOPMENT ONLY — Credential reset for tenant {Tenant}, " +
"user {UserKey}. Code: {Code}. Expires at: {ExpiresAt}",
notification.Tenant,
notification.UserKey,
notification.Token,
notification.ExpiresAt);

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.Infrastructure;
using CodeBeam.UltimateAuth.Credentials;
using CodeBeam.UltimateAuth.InMemory;
using CodeBeam.UltimateAuth.Sample.BlazorServer.Components;
using CodeBeam.UltimateAuth.Sample.BlazorServer.Infrastructure;
Expand Down Expand Up @@ -63,6 +64,11 @@

builder.Services.AddUltimateAuthSampleSeed();

if (builder.Environment.IsDevelopment())
{
builder.Services.AddScoped<ICredentialResetNotifier, DevelopmentCredentialResetNotifier>();
}

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="RequestResetAsync">Request Reset</MudButton>
@if (_resetRequested)
{
<MudText Align="Align.Center">Your reset code is: (Copy it before next step)</MudText>
<MudText Align="Align.Center" Typo="Typo.h6"><b>@_resetCode</b></MudText>
<MudText Align="Align.Center">Your reset code is on the server console.</MudText>
<MudText Align="Align.Center"><MudLink Class="cursor-pointer" Href="@($"/reset?identifier={_identifier}")">Use Reset Code</MudLink></MudText>
}
</MudStack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm.Components.Dialogs;
public partial class ResetDialog
{
private bool _resetRequested = false;
private string? _resetCode;
private string? _identifier;

[CascadingParameter]
Expand All @@ -34,7 +33,6 @@ private async Task RequestResetAsync()
return;
}

_resetCode = result.Value.Token;
_resetRequested = true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="RequestResetAsync">Request Reset</MudButton>
@if (_resetRequested)
{
<MudText Align="Align.Center">Your reset code is: (Copy it before next step)</MudText>
<MudText Align="Align.Center" Typo="Typo.h6"><b>@_resetCode</b></MudText>
<MudText Align="Align.Center">Your reset code is on the server console.</MudText>
<MudText Align="Align.Center"><MudLink Class="cursor-pointer" Href="@($"/reset?identifier={_identifier}")">Use Reset Code</MudLink></MudText>
}
</MudStack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace CodeBeam.UAuth.Sample.IntWasm.Client.Components.Dialogs;
public partial class ResetDialog
{
private bool _resetRequested = false;
private string? _resetCode;
private string? _identifier;

[CascadingParameter]
Expand All @@ -34,7 +33,6 @@ private async Task RequestResetAsync()
return;
}

_resetCode = result.Value.Token;
_resetRequested = true;
}

Expand Down
1 change: 1 addition & 0 deletions src/CodeBeam.UltimateAuth.Server/AssemblyVisibility.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Credentials.Reference")]
[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Tests.Unit")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ internal static IServiceCollection AddUsersInternal(IServiceCollection services)
internal static IServiceCollection AddCredentialsInternal(IServiceCollection services)
{
services.TryAddScoped<ICredentialValidator, CredentialValidator>();
services.TryAddScoped<ICredentialResetNotifier, NotConfiguredCredentialResetNotifier>();
return services;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using CodeBeam.UltimateAuth.Credentials;

namespace CodeBeam.UltimateAuth.Server.Infrastructure;

internal sealed class NotConfiguredCredentialResetNotifier : ICredentialResetNotifier
{
internal static InvalidOperationException CreateException() => new(
"Credential reset notification is not configured. " +
"Implement ICredentialResetNotifier and register it with " +
"services.AddScoped<ICredentialResetNotifier, " +
"YourCredentialResetNotifier>(). " +
"The implementation must send the reset code or token " +
"through a trusted channel associated with the user's account.");

public Task NotifyAsync(CredentialResetNotification context, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
throw CreateException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@

public sealed record BeginCredentialResetResult
{
public string? Token { get; init; }
public DateTimeOffset ExpiresAt { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ internal sealed class CredentialManagementService : ICredentialManagementService
private readonly ITokenHasher _tokenHasher;
private readonly ILoginIdentifierResolver _identifierResolver;
private readonly ISessionStoreFactory _sessionFactory;
private readonly ICredentialResetNotifier _resetNotifier;
private readonly UAuthServerOptions _options;
private readonly IClock _clock;

Expand All @@ -37,6 +38,7 @@ public CredentialManagementService(
ITokenHasher tokenHasher,
ILoginIdentifierResolver identifierResolver,
ISessionStoreFactory sessionFactory,
ICredentialResetNotifier resetNotifier,
IOptions<UAuthServerOptions> options,
IClock clock)
{
Expand All @@ -49,6 +51,7 @@ public CredentialManagementService(
_tokenHasher = tokenHasher;
_identifierResolver = identifierResolver;
_sessionFactory = sessionFactory;
_resetNotifier = resetNotifier;
_options = options.Value;
_clock = clock;
}
Expand Down Expand Up @@ -199,22 +202,28 @@ public async Task<BeginCredentialResetResult> BeginResetAsync(AccessContext cont

var cmd = new AccessCommand<BeginCredentialResetResult>(async innerCt =>
{

if (_resetNotifier is NotConfiguredCredentialResetNotifier)
throw NotConfiguredCredentialResetNotifier.CreateException();

if (string.IsNullOrWhiteSpace(request.Identifier))
throw new UAuthValidationException("identifier_required");

var now = _clock.UtcNow;
var validity = request.Validity ?? _options.ResetCredential.TokenValidity;
// Anonymous callers must not control the proof's lifetime.
var validity = _options.ResetCredential.TokenValidity;
var expiresAt = now.Add(validity);

var result = new BeginCredentialResetResult
{
ExpiresAt = expiresAt
};


var resolution = await _identifierResolver.ResolveAsync(context.ResourceTenant, request.Identifier, innerCt);

if (resolution?.UserKey is not UserKey userKey)
{
return new BeginCredentialResetResult
{
Token = null,
ExpiresAt = now.Add(validity)
};
}
return result; // Enumeration protection

var state = await _authenticationSecurityManager
.GetOrCreateFactorAsync(context.ResourceTenant, userKey, request.CredentialType, innerCt);
Expand All @@ -239,11 +248,17 @@ public async Task<BeginCredentialResetResult> BeginResetAsync(AccessContext cont
var updatedState = state.BeginReset(tokenHash, now, validity);
await _authenticationSecurityManager.UpdateAsync(updatedState, state.SecurityVersion, innerCt);

return new BeginCredentialResetResult
{
Token = token,
ExpiresAt = now.Add(validity)
};
await _resetNotifier.NotifyAsync(
new CredentialResetNotification(
Tenant: context.ResourceTenant,
UserKey: userKey,
CredentialType: request.CredentialType,
CodeType: request.ResetCodeType,
Token: token,
ExpiresAt: expiresAt),
innerCt);

return result;
});

return await _accessOrchestrator.ExecuteAsync(context, cmd, ct);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.MultiTenancy;

namespace CodeBeam.UltimateAuth.Credentials;

public interface ICredentialResetNotifier
{
Task NotifyAsync(CredentialResetNotification notification, CancellationToken ct = default);
}

public sealed record CredentialResetNotification(
TenantKey Tenant,
UserKey UserKey,
CredentialType CredentialType,
ResetCodeType CodeType,
string Token,
DateTimeOffset ExpiresAt);
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.Errors;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
using CodeBeam.UltimateAuth.Credentials;
using CodeBeam.UltimateAuth.Credentials.Contracts;
using CodeBeam.UltimateAuth.Credentials.Reference;
using CodeBeam.UltimateAuth.Credentials.Reference.Internal;
Expand Down Expand Up @@ -746,6 +747,9 @@ private sealed class Fixture
public Mock<ILoginIdentifierResolver> IdentifierResolver { get; }
= new(MockBehavior.Strict);

public Mock<ICredentialResetNotifier> ResetNotifier { get; }
= new(MockBehavior.Strict);

public Mock<ISessionStoreFactory> SessionStoreFactory { get; }
= new(MockBehavior.Strict);

Expand Down Expand Up @@ -830,6 +834,7 @@ public Fixture()
TokenHasher.Object,
IdentifierResolver.Object,
SessionStoreFactory.Object,
ResetNotifier.Object,
options,
Clock.Object);
}
Expand Down
Loading
Loading