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
@@ -0,0 +1,98 @@
@using CodeBeam.UltimateAuth.Credentials.Contracts

@if (Policy is not null)
{
<div class="lab-password-policy">
<div class="lab-password-policy-header">
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Small" Color="Color.Primary" />
<span>Password requirements</span>

<MudTooltip Text="UltimateAuth password policy preset. This is the configured default for this application and can be customized by developers through PasswordPolicyOptions.">
<MudChip T="string"
Size="Size.Small"
Variant="Variant.Outlined"
Color="Color.Primary">
@Policy.Level
</MudChip>
</MudTooltip>
</div>

<div class="lab-password-policy-rules">
@foreach (var rule in Rules)
{
<div class="lab-password-policy-rule"
@key="rule.Label">
<MudIcon Icon="@(rule.Passed ? Icons.Material.Filled.CheckCircle : Icons.Material.Outlined.RadioButtonUnchecked)"
Size="Size.Small"
Color="@(rule.Passed ? Color.Success : Color.Default)" />
<span class="@(rule.Passed ? "rule-passed" : "")">
@rule.Label
</span>
</div>
}
</div>

@if (Policy.CheckCommonPasswords ||
Policy.CheckUserSimilarity ||
Policy.CheckPasswordHistory)
{
<MudText Typo="Typo.caption" Color="Color.Secondary">
Additional security checks are performed when you submit.
</MudText>
}
</div>
}

@code {
[Parameter, EditorRequired]
public UAuthPasswordPolicyInfo? Policy { get; set; }

[Parameter]
public string? Password { get; set; }


private IEnumerable<(string Label, bool Passed)> Rules
{
get
{
if (Policy is null)
yield break;

var password = Password ?? string.Empty;
var hasPassword = password.Length > 0;

yield return (
$"At least {Policy.MinLength} characters",
hasPassword && password.Length >= Policy.MinLength);

yield return (
$"At most {Policy.MaxLength} characters",
hasPassword && password.Length <= Policy.MaxLength);

if (Policy.RequireLetter)
yield return (
"Contains a letter",
hasPassword && password.Any(char.IsLetter));

if (Policy.RequireDigit)
yield return (
"Contains a number",
hasPassword && password.Any(char.IsDigit));

if (Policy.RequireUppercase)
yield return (
"Contains an uppercase letter",
hasPassword && password.Any(char.IsUpper));

if (Policy.RequireLowercase)
yield return (
"Contains a lowercase letter",
hasPassword && password.Any(char.IsLower));

if (Policy.RequireSpecialCharacter)
yield return (
"Contains a special character",
hasPassword && password.Any(c => !char.IsLetterOrDigit(c)));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.lab-password-policy {
padding: 14px 16px;
border: 1px solid var(--mud-palette-lines-default);
border-radius: 12px;
background: var(--mud-palette-background-gray);
}

.lab-password-policy-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 13px;
font-weight: 700;
color: var(--mud-palette-text-primary);
}

.lab-password-policy-header .mud-chip {
margin-left: auto;
}

.lab-password-policy-rules {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 9px 12px;
margin-bottom: 10px;
}

.lab-password-policy-rule {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
font-size: 12px;
color: var(--mud-palette-text-secondary);
}

.lab-password-policy-rule .mud-icon-root {
flex-shrink: 0;
}

.rule-passed {
color: var(--mud-palette-success);
}

@media (max-width: 480px) {
.lab-password-policy-rules {
grid-template-columns: 1fr;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,18 @@
"Change Password",
new DialogParameters<CredentialDialog>
{
{ x => x.PreviewMode, false }
{ x => x.PreviewMode, false },
{
x => x.PasswordRequirementsContent,
(RenderFragment<PasswordRequirementsContext>)(context =>
builder =>
{
builder.OpenComponent<LabPasswordRequirements>(0);
builder.AddAttribute(1, "Policy", context.Policy);
builder.AddAttribute(2, "Password", context.Password);
builder.CloseComponent();
})
}
},
UAuthDialog.GetDialogOptions(MaxWidth.Small));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,18 @@
var parameters = new DialogParameters<CreateUserDialog>
{
{ x => x.PreviewMode, false },
{ x => x.OnCreateUserAsync, CreateSandboxUserAsync }
{ x => x.OnCreateUserAsync, CreateSandboxUserAsync },
{
x => x.PasswordRequirementsContent,
(RenderFragment<PasswordRequirementsContext>)(context =>
builder =>
{
builder.OpenComponent<LabPasswordRequirements>(0);
builder.AddAttribute(1, "Policy", context.Policy);
builder.AddAttribute(2, "Password", context.Password);
builder.CloseComponent();
})
}
};

var dialog = await DialogService.ShowAsync<CreateUserDialog>(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
@page "/register"
@using CodeBeam.UltimateAuth.Core.Errors

@layout PublicLayout
@implements IDisposable

@inject LabRegistrationService RegistrationService
@inject LabRegistrationAvailabilityService AvailabilityService
@inject IUAuthClient UAuthClient
@inject ISnackbar Snackbar
@inject NavigationManager Navigation

Expand Down Expand Up @@ -122,6 +125,23 @@
autocomplete="new-password"
Validation="@(new Func<string, string?>(ValidatePassword))"
Disabled="_isSubmitting" />

@if (_passwordPolicy is not null)
{
<LabPasswordRequirements Policy="_passwordPolicy" Password="@_password" />
}
else if (_policyLoading)
{
<MudText Typo="Typo.caption" Color="Color.Secondary">
Loading password requirements...
</MudText>
}
else
{
<MudText Typo="Typo.caption" Color="Color.Secondary">
Password requirements will be verified by the server.
</MudText>
}

<MudPasswordField @bind-Value="_confirmPassword"
Label="Confirm password"
Expand Down Expand Up @@ -310,6 +330,9 @@
private string? _errorMessage;
private MudForm? _form;

private UAuthPasswordPolicyInfo? _passwordPolicy;
private bool _policyLoading = true;

private LabIdentifierAvailability _usernameAvailability = LabIdentifierAvailability.Unknown;

private LabIdentifierAvailability _emailAvailability = LabIdentifierAvailability.Unknown;
Expand All @@ -320,6 +343,26 @@
private int _usernameVersion;
private int _emailVersion;

protected override async Task OnInitializedAsync()
{
try
{
var result = await UAuthClient.Credentials.GetPasswordPolicyAsync();

if (result.IsSuccess)
_passwordPolicy = result.Value;
}
catch (Exception)
{
// Policy availability must not prevent registration.
// Server-side validation remains authoritative.
}
finally
{
_policyLoading = false;
}
}

private static string? ValidateEmail(string value)
{
if (string.IsNullOrWhiteSpace(value))
Expand All @@ -331,15 +374,50 @@
: "Enter a valid email address.";
}

private static string? ValidatePassword(string value)
private string? ValidatePassword(string value)
{
if (string.IsNullOrWhiteSpace(value))
if (string.IsNullOrEmpty(value))
return "Password is required.";

if (value.Length < 8)
return "Password must be at least 8 characters.";
if (value.Length > PasswordPolicyLimits.AbsoluteMaxLength)
return "Password exceeds the maximum allowed length.";

if (_passwordPolicy is null)
return null;

var violations = UAuthPasswordClientValidator.Validate(value, _passwordPolicy);

if (violations.Count == 0)
return null;

return violations[0] switch
{
PasswordPolicyErrorCodes.TooShort =>
$"Password must contain at least {_passwordPolicy.MinLength} characters.",

PasswordPolicyErrorCodes.TooLong =>
$"Password cannot exceed {_passwordPolicy.MaxLength} characters.",

PasswordPolicyErrorCodes.LetterRequired =>
"Password must contain a letter.",

PasswordPolicyErrorCodes.DigitRequired =>
"Password must contain a number.",

PasswordPolicyErrorCodes.UppercaseRequired =>
"Password must contain an uppercase letter.",

PasswordPolicyErrorCodes.LowercaseRequired =>
"Password must contain a lowercase letter.",

return null;
PasswordPolicyErrorCodes.SpecialCharacterRequired =>
"Password must contain a special character.",

PasswordPolicyErrorCodes.InputTooLong =>
"Password exceeds the maximum allowed length.",

_ => "Password does not meet the requirements."
};
}

private string? ValidateConfirmPassword(string value)
Expand Down Expand Up @@ -436,10 +514,7 @@
return Task.CompletedTask;
}

private async Task CheckUsernameAsync(
string value,
int version,
CancellationTokenSource cts)
private async Task CheckUsernameAsync(string value, int version, CancellationTokenSource cts)
{
var token = cts.Token;

Expand Down Expand Up @@ -495,10 +570,7 @@
}
}

private async Task CheckEmailAsync(
string value,
int version,
CancellationTokenSource cts)
private async Task CheckEmailAsync(string value, int version, CancellationTokenSource cts)
{
var token = cts.Token;

Expand Down Expand Up @@ -567,6 +639,14 @@
if (!_form.IsValid)
return;

var passwordError = ValidatePassword(_password);

if (passwordError is not null)
{
_errorMessage = passwordError;
return;
}

if (_birthDate is { } birthDate &&
birthDate.Date > DateTime.Today)
{
Expand Down Expand Up @@ -630,6 +710,10 @@

_registered = true;
}
catch (UAuthValidationException ex)
{
_errorMessage = LabPasswordErrorMessages.Get(ex.Code);
}
catch (Exception)
{
_errorMessage = "An unexpected error occurred. If you already submitted your registration, try signing in before registering again.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
@using CodeBeam.UltimateAuth.Client.Blazor
@using CodeBeam.UltimateAuth.Client.Diagnostics
@using CodeBeam.UltimateAuth.Users.Contracts
@using CodeBeam.UltimateAuth.Credentials.Contracts
@using CodeBeam.UltimateAuth.Authorization.Contracts

@using CodeBeam.UltimateAuth.Sample.Shared.Common
Expand Down
Loading
Loading