diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/App.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/App.razor index 71805ad6..48e265d0 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/App.razor +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/App.razor @@ -6,11 +6,9 @@ - @* *@ - - + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Layout/MainLayout.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Layout/MainLayout.razor index d257eb7a..ac680f3b 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Layout/MainLayout.razor +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Layout/MainLayout.razor @@ -1,11 +1,101 @@ -@using CodeBeam.UltimateAuth.Core.Abstractions -@using CodeBeam.UltimateAuth.Server.Infrastructure -@inherits LayoutComponentBase +@inherits UAuthHubLayoutComponentBase +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject NavigationManager Nav -@Body +@if (!IsHubAuthorized) +{ + + + UltimateAuth + + UAuthHub Sample -
+ + + + + + + + + + + Access Denied + + + This page cannot be accessed directly. + UAuthHub login flows can only be initiated by an authorized client application. + + + + + + UltimateAuth protects this resource based on your session and permissions. + + + + + return; +} + + + + + UltimateAuth + + UAuthHub Sample + + + + + + + + + +
+ + + @((state.Identity?.DisplayName ?? "?").Trim() is var n ? (n.Length >= 2 ? n[..2] : n[..1]) : "?") + + +
+
+ + + @state.Identity?.DisplayName + @string.Join(", ", state.Claims.Roles) + + + + + + + + @if (state.Identity?.SessionState is not null && state.Identity.SessionState != SessionState.Active) + { + + + } + +
+
+ + + + +
+
+ + + @Body + +
+ + +
An unhandled error has occurred. - Reload - 🗙 + Reload + 🗙
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Layout/MainLayout.razor.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Layout/MainLayout.razor.cs index d9123d59..7242adbd 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Layout/MainLayout.razor.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Layout/MainLayout.razor.cs @@ -1,7 +1,130 @@ -namespace CodeBeam.UltimateAuth.Sample.UAuthHub.Components.Layout +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Errors; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Sample.UAuthHub.Infrastructure; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.Components.Layout; + +public partial class MainLayout { - public partial class MainLayout + [CascadingParameter] + public UAuthState UAuth { get; set; } = default!; + + [CascadingParameter] + public DarkModeManager DarkModeManager { get; set; } = default!; + + private async Task Refresh() { - + await UAuthClient.Flows.RefreshAsync(); + } + + private async Task Logout() + { + await UAuthClient.Flows.LogoutAsync(); + } + + private Color GetBadgeColor() + { + if (UAuth is null || !UAuth.IsAuthenticated) + return Color.Error; + + if (UAuth.IsStale) + return Color.Warning; + + var state = UAuth.Identity?.SessionState; + + if (state is null || state == SessionState.Active) + return Color.Success; + + if (state == SessionState.Invalid) + return Color.Error; + + return Color.Warning; + } + + private void HandleSignInClick() + { + var uri = Nav.ToAbsoluteUri(Nav.Uri); + + if (uri.AbsolutePath.EndsWith("/login", StringComparison.OrdinalIgnoreCase)) + { + Nav.NavigateTo("/login?focus=1", replace: true, forceLoad: true); + return; + } + + GoToLoginWithReturn(); + } + + private async Task Validate() + { + try + { + var result = await UAuthClient.Flows.ValidateAsync(); + + if (result.IsValid) + { + if (result.Snapshot?.Identity.UserStatus == UserStatus.SelfSuspended) + { + Snackbar.Add("Your account is suspended by you.", Severity.Warning); + return; + } + Snackbar.Add($"Session active • Tenant: {result.Snapshot?.Identity?.Tenant.Value} • User: {result.Snapshot?.Identity?.PrimaryUserName}", Severity.Success); + } + else + { + switch (result.State) + { + case SessionState.Expired: + Snackbar.Add("Session expired. Please sign in again.", Severity.Warning); + break; + + case SessionState.DeviceMismatch: + Snackbar.Add("Session invalid for this device.", Severity.Error); + break; + + default: + Snackbar.Add($"Session state: {result.State}", Severity.Error); + break; + } + } + } + catch (UAuthTransportException) + { + Snackbar.Add("Network error.", Severity.Error); + } + catch (UAuthProtocolException) + { + Snackbar.Add("Invalid response.", Severity.Error); + } + catch (UAuthException ex) + { + Snackbar.Add($"UAuth error: {ex.Message}", Severity.Error); + } + catch (Exception ex) + { + Snackbar.Add($"Unexpected error: {ex.Message}", Severity.Error); + } + } + + private void GoToLoginWithReturn() + { + var uri = Nav.ToAbsoluteUri(Nav.Uri); + + if (uri.AbsolutePath.EndsWith("/login", StringComparison.OrdinalIgnoreCase)) + { + Nav.NavigateTo("/login", replace: true); + return; + } + + var current = Nav.ToBaseRelativePath(uri.ToString()); + if (string.IsNullOrWhiteSpace(current)) + current = "home"; + + var returnUrl = Uri.EscapeDataString("/" + current.TrimStart('/')); + Nav.NavigateTo($"/login?returnUrl={returnUrl}", replace: true); } } diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor index 219617bf..b1720a39 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor @@ -1,82 +1,103 @@ @page "/" @page "/login" -@using CodeBeam.UltimateAuth.Client -@using CodeBeam.UltimateAuth.Client.Authentication -@using CodeBeam.UltimateAuth.Client.Diagnostics +@attribute [UAuthLoginPage] +@inherits UAuthHubPageBase + +@implements IDisposable @using CodeBeam.UltimateAuth.Client.Infrastructure +@using CodeBeam.UltimateAuth.Client.Options +@using CodeBeam.UltimateAuth.Client.Runtime @using CodeBeam.UltimateAuth.Core.Abstractions @using CodeBeam.UltimateAuth.Core.Contracts -@using CodeBeam.UltimateAuth.Core.Domain -@using CodeBeam.UltimateAuth.Core.Runtime -@using CodeBeam.UltimateAuth.Server.Abstractions -@using CodeBeam.UltimateAuth.Server.Infrastructure @using CodeBeam.UltimateAuth.Server.Services @using CodeBeam.UltimateAuth.Server.Stores -@inject IUAuthStateManager StateManager -@inject IHubFlowReader HubFlowReader -@inject IHubCredentialResolver HubCredentialResolver +@using Microsoft.Extensions.Options +@inject IUAuthClient UAuthClient @inject IAuthStore AuthStore +@inject IHubFlowService HubFlowService +@inject IPkceService PkceService +@inject IHubCredentialResolver HubCredentialResolver @inject IClientStorage BrowserStorage -@inject IUAuthFlowService Flow @inject ISnackbar Snackbar -@inject IFlowCredentialResolver CredentialResolver -@inject IUAuthClient UAuthClient -@inject NavigationManager Nav -@inject IUAuthProductInfoProvider ProductInfo -@inject AuthenticationStateProvider AuthStateProvider -@inject UAuthClientDiagnostics Diagnostics - - -
- - @if (_state == null || !_state.IsActive) - { - - - - +@inject IUAuthClientProductInfoProvider ClientProductInfoProvider +@inject IDeviceIdProvider DeviceIdProvider +@inject IDialogService DialogService +@inject IOptions Options - Access Denied - - - This page cannot be accessed directly. - UAuthHub login flows can only be initiated by an authorized client application. - + + + + + - - return; - } - - - Welcome to UltimateAuth! - - - Login - - + + + + -
+ @* *@ + + @* TODO: Enhance sample *@ + @* Forgot Password *@ + @* Don't have an account? SignUp *@ + + + + + + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs index dc0f988e..c3258e6a 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs @@ -1,75 +1,74 @@ -using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Blazor; +using CodeBeam.UltimateAuth.Client.Runtime; using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Server.Stores; -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.WebUtilities; using MudBlazor; namespace CodeBeam.UltimateAuth.Sample.UAuthHub.Components.Pages; public partial class Home { - [SupplyParameterFromQuery(Name = "hub")] - public string? HubKey { get; set; } - private string? _username; private string? _password; - private HubFlowState? _state; + private UAuthClientProductInfo? _productInfo; + private UAuthLoginForm _loginForm = null!; - protected override async Task OnParametersSetAsync() - { - if (string.IsNullOrWhiteSpace(HubKey)) - { - _state = null; - return; - } + private CancellationTokenSource? _lockoutCts; + private PeriodicTimer? _lockoutTimer; + private DateTimeOffset? _lockoutUntil; + private TimeSpan _remaining; + private bool _isLocked; + private DateTimeOffset? _lockoutStartedAt; + private TimeSpan _lockoutDuration; + private double _progressPercent; + private int? _remainingAttempts = null; + private bool _errorHandled; - if (HubSessionId.TryParse(HubKey, out var hubSessionId)) - _state = await HubFlowReader.GetStateAsync(hubSessionId); + protected override async Task OnInitializedAsync() + { + _productInfo = ClientProductInfoProvider.Get(); } protected override async Task OnAfterRenderAsync(bool firstRender) { - if (!firstRender) + if (string.IsNullOrWhiteSpace(HubKey)) return; - var currentError = await BrowserStorage.GetAsync(StorageScope.Session, "uauth:last_error"); - - if (!string.IsNullOrWhiteSpace(currentError)) + if (HubState is null || !HubState.Exists) { - Snackbar.Add(ResolveErrorMessage(currentError), Severity.Error); - await BrowserStorage.RemoveAsync(StorageScope.Session, "uauth:last_error"); + return; } - var uri = Nav.ToAbsoluteUri(Nav.Uri); - var query = QueryHelpers.ParseQuery(uri.Query); - - if (query.TryGetValue("__uauth_error", out var error)) - { - await BrowserStorage.SetAsync(StorageScope.Session, "uauth:last_error", error.ToString()); - } - - if (string.IsNullOrWhiteSpace(HubKey)) + if (HubState.IsExpired) { + await ContinuePkceAsync(); return; } - if (_state is null || !_state.Exists) - return; - - if (_state?.IsActive != true) + if (HubState.Error != null && !_errorHandled) { - await StartNewPkceAsync(); - return; + _errorHandled = true; + Snackbar.Add(ResolveErrorMessage(HubState.Error), Severity.Error); + await ContinuePkceAsync(); + + if (HubSessionId.TryParse(HubKey, out var hubSessionId)) + { + await ReloadState(); + } + + await _loginForm.ReloadAsync(); + + StateHasChanged(); } } // For testing & debugging private async Task ProgrammaticPkceLogin() { - var hub = _state; + var hub = HubState; if (hub is null) return; @@ -79,15 +78,54 @@ private async Task ProgrammaticPkceLogin() var credentials = await HubCredentialResolver.ResolveAsync(hubSessionId); - var request = new PkceLoginRequest + var request = new PkceCompleteRequest { Identifier = "admin", Secret = "admin", AuthorizationCode = credentials?.AuthorizationCode ?? string.Empty, CodeVerifier = credentials?.CodeVerifier ?? string.Empty, - ReturnUrl = _state?.ReturnUrl ?? string.Empty + ReturnUrl = HubState?.ReturnUrl ?? string.Empty, + HubSessionId = HubState?.HubSessionId.Value ?? hubSessionId.Value, }; - await UAuthClient.Flows.CompletePkceLoginAsync(request); + + await UAuthClient.Flows.TryCompletePkceLoginAsync(request, UAuthSubmitMode.TryAndCommit); + } + + private async Task HandleLoginResult(IUAuthTryResult result) + { + if (result is TryPkceLoginResult pkce) + { + if (!result.Success) + { + if (result.Reason == AuthFailureReason.LockedOut && result.LockoutUntilUtc is { } until) + { + _lockoutUntil = until; + StartCountdown(); + } + + _remainingAttempts = result.RemainingAttempts; + + ShowLoginError(result.Reason, result.RemainingAttempts); + await ContinuePkceAsync(); + } + } + } + + private HubCredentials? _pkce; + + private async Task ContinuePkceAsync() + { + if (string.IsNullOrWhiteSpace(HubKey)) + return; + + var key = new AuthArtifactKey(HubKey); + var artifact = await AuthStore.GetAsync(key) as HubFlowArtifact; + + if (artifact is null) + return; + + _pkce = await PkceService.RefreshAsync(artifact); + await HubFlowService.ContinuePkceAsync(HubKey, _pkce.AuthorizationCode, _pkce.CodeVerifier); } private async Task StartNewPkceAsync() @@ -98,7 +136,7 @@ private async Task StartNewPkceAsync() private async Task ResolveReturnUrlAsync() { - var fromContext = _state?.ReturnUrl; + var fromContext = HubState?.ReturnUrl; if (!string.IsNullOrWhiteSpace(fromContext)) return fromContext; @@ -115,18 +153,106 @@ private async Task ResolveReturnUrlAsync() return flow.ReturnUrl!; } - // Config default (recommend adding to options) - //if (!string.IsNullOrWhiteSpace(_options.Login.DefaultReturnUrl)) - // return _options.Login.DefaultReturnUrl!; - return Nav.Uri; } - - private string ResolveErrorMessage(string? errorKey) + + private async void StartCountdown() + { + if (_lockoutUntil is null) + return; + + _isLocked = true; + _lockoutStartedAt = DateTimeOffset.UtcNow; + _lockoutDuration = _lockoutUntil.Value - DateTimeOffset.UtcNow; + UpdateRemaining(); + + _lockoutCts?.Cancel(); + _lockoutCts = new CancellationTokenSource(); + + _lockoutTimer?.Dispose(); + _lockoutTimer = new PeriodicTimer(TimeSpan.FromSeconds(1)); + + try + { + while (await _lockoutTimer.WaitForNextTickAsync(_lockoutCts.Token)) + { + UpdateRemaining(); + + if (_remaining <= TimeSpan.Zero) + { + ResetLockoutState(); + await InvokeAsync(StateHasChanged); + break; + } + + await InvokeAsync(StateHasChanged); + } + } + catch (OperationCanceledException) + { + + } + } + + private void ResetLockoutState() + { + _isLocked = false; + _lockoutUntil = null; + _progressPercent = 0; + _remainingAttempts = null; + } + + private void UpdateRemaining() + { + if (_lockoutUntil is null || _lockoutStartedAt is null) + return; + + var now = DateTimeOffset.UtcNow; + + _remaining = _lockoutUntil.Value - now; + + if (_remaining <= TimeSpan.Zero) + { + _remaining = TimeSpan.Zero; + return; + } + + var elapsed = now - _lockoutStartedAt.Value; + + if (_lockoutDuration.TotalSeconds > 0) + { + var percent = 100 - (elapsed.TotalSeconds / _lockoutDuration.TotalSeconds * 100); + _progressPercent = Math.Max(0, percent); + } + } + + private void ShowLoginError(AuthFailureReason? reason, int? remainingAttempts) + { + string message = reason switch + { + AuthFailureReason.InvalidCredentials when remainingAttempts is > 0 + => $"Invalid username or password. {remainingAttempts} attempt(s) remaining.", + + AuthFailureReason.InvalidCredentials + => "Invalid username or password.", + + AuthFailureReason.RequiresMfa + => "Multi-factor authentication required.", + + AuthFailureReason.LockedOut + => "Your account is locked.", + + _ => "Login failed." + }; + + Snackbar.Add(message, Severity.Error); + } + + private string ResolveErrorMessage(HubErrorCode? errorCode) { - if (errorKey == "invalid") + if (errorCode == HubErrorCode.InvalidCredentials) { - return "Login failed."; + return "Invalid credentials."; } return "Failed attempt."; diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/_Imports.razor b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/_Imports.razor index aada4df3..f530884f 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/_Imports.razor +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/_Imports.razor @@ -10,6 +10,7 @@ @using CodeBeam.UltimateAuth.Sample.UAuthHub @using CodeBeam.UltimateAuth.Sample.UAuthHub.Components @using CodeBeam.UltimateAuth.Sample.UAuthHub.Components.Layout +@using CodeBeam.UltimateAuth.Core.Domain @using CodeBeam.UltimateAuth.Client @using CodeBeam.UltimateAuth.Client.Blazor diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Controllers/HubLoginController.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Controllers/HubLoginController.cs deleted file mode 100644 index 71cb29b3..00000000 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Controllers/HubLoginController.cs +++ /dev/null @@ -1,54 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Abstractions; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.Core.Options; -using CodeBeam.UltimateAuth.Server.Options; -using CodeBeam.UltimateAuth.Server.Stores; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace CodeBeam.UltimateAuth.Sample.UAuthHub.Controllers; - -[Route("auth/uauthhub")] -[IgnoreAntiforgeryToken] -public sealed class HubLoginController : Controller -{ - private readonly IAuthStore _authStore; - private readonly UAuthServerOptions _options; - private readonly IClock _clock; - - public HubLoginController(IAuthStore authStore, IOptions options, IClock clock) - { - _authStore = authStore; - _options = options.Value; - _clock = clock; - } - - [HttpPost("login")] - [IgnoreAntiforgeryToken] - public async Task BeginLogin( - [FromForm] string authorization_code, - [FromForm] string code_verifier, - [FromForm] UAuthClientProfile client_profile, - [FromForm] string? return_url) - { - var hubSessionId = HubSessionId.New(); - - var payload = new HubFlowPayload(); - payload.Set("authorization_code", authorization_code); - payload.Set("code_verifier", code_verifier); - - var artifact = new HubFlowArtifact( - hubSessionId: hubSessionId, - flowType: HubFlowType.Login, - clientProfile: client_profile, - tenant: TenantKeys.System, - returnUrl: return_url, - payload: payload, - expiresAt: _clock.UtcNow.Add(_options.Hub.FlowLifetime)); - - await _authStore.StoreAsync(new AuthArtifactKey(hubSessionId.Value), artifact, HttpContext.RequestAborted); - - return Redirect($"{_options.Hub.LoginPath}?hub={hubSessionId.Value}"); - } -} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/DefaultUAuthHubMarker.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/DefaultUAuthHubMarker.cs deleted file mode 100644 index eb5fe640..00000000 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/DefaultUAuthHubMarker.cs +++ /dev/null @@ -1,5 +0,0 @@ -using CodeBeam.UltimateAuth.Core.Runtime; - -internal sealed class DefaultUAuthHubMarker : IUAuthHubMarker -{ -} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs index d5c689a8..47a813c8 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs @@ -2,11 +2,9 @@ using CodeBeam.UltimateAuth.Client.Blazor.Extensions; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Infrastructure; -using CodeBeam.UltimateAuth.Core.Runtime; using CodeBeam.UltimateAuth.InMemory; using CodeBeam.UltimateAuth.Sample.UAuthHub.Components; using CodeBeam.UltimateAuth.Sample.UAuthHub.Infrastructure; -using CodeBeam.UltimateAuth.Security.Argon2; using CodeBeam.UltimateAuth.Server.Extensions; using MudBlazor.Services; using MudExtensions.Services; @@ -14,28 +12,34 @@ var builder = WebApplication.CreateBuilder(args); -// Add services to the container. builder.Services.AddRazorComponents() - .AddInteractiveServerComponents(); - -builder.Services.AddControllers(); + .AddInteractiveServerComponents() + .AddCircuitOptions(options => + { + options.DetailedErrors = true; + }); builder.Services.AddMudServices(o => { o.SnackbarConfiguration.PreventDuplicates = false; }); builder.Services.AddMudExtensions(); -//builder.Services.AddAuthorization(); - -//builder.Services.AddHttpContextAccessor(); +builder.Services.AddScoped(); builder.Services.AddUltimateAuthServer(o => { o.Diagnostics.EnableRefreshDetails = true; //o.Session.MaxLifetime = TimeSpan.FromSeconds(32); + //o.Session.Lifetime = TimeSpan.FromSeconds(32); //o.Session.TouchInterval = TimeSpan.FromSeconds(9); //o.Session.IdleTimeout = TimeSpan.FromSeconds(15); + //o.Token.AccessTokenLifetime = TimeSpan.FromSeconds(30); + //o.Token.RefreshTokenLifetime = TimeSpan.FromSeconds(32); + o.Login.MaxFailedAttempts = 2; + o.Login.LockoutDuration = TimeSpan.FromSeconds(10); + o.Identifiers.AllowMultipleUsernames = true; }) - .AddUltimateAuthInMemory(); + .AddUltimateAuthInMemory() + .AddUAuthHub(o => o.AllowedClientOrigins.Add("https://localhost:6130")); // Client sample's URL builder.Services.AddUltimateAuthClientBlazor(o => { @@ -43,28 +47,11 @@ o.Reauth.Behavior = ReauthBehavior.RaiseEvent; }); -builder.Services.AddSingleton(); -builder.Services.AddScoped(); - -builder.Services.AddCors(options => -{ - options.AddPolicy("WasmSample", policy => - { - policy - .WithOrigins("https://localhost:6130") - .AllowAnyHeader() - .AllowAnyMethod() - .AllowCredentials() - .WithExposedHeaders("X-UAuth-Refresh"); // TODO: Add exposed headers globally - }); -}); - var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error", createScopeForErrors: true); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } else @@ -78,26 +65,16 @@ } app.UseHttpsRedirection(); -app.UseCors("WasmSample"); app.UseUltimateAuthWithAspNetCore(); app.UseAntiforgery(); app.MapUltimateAuthEndpoints(); +app.MapUAuthHub(); app.MapStaticAssets(); -app.MapControllers(); app.MapRazorComponents() .AddInteractiveServerRenderMode() .AddUltimateAuthRoutes(UAuthAssemblies.BlazorClient()); -app.MapGet("/health", () => -{ - return Results.Ok(new - { - service = "UAuthHub", - status = "ok" - }); -}); - app.Run(); diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/wwwroot/app.css b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/wwwroot/app.css index 671b6199..17fcfd6a 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/wwwroot/app.css +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/wwwroot/app.css @@ -13,7 +13,7 @@ a, .btn-link { } .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { - box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; } .content { @@ -50,15 +50,6 @@ h1:focus { border-color: #929292; } -.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { - color: var(--bs-secondary-color); - text-align: end; -} - -.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { - text-align: start; -} - .uauth-stack { min-height: 60vh; max-height: calc(100vh - var(--mud-appbar-height)); @@ -79,6 +70,11 @@ h1:focus { color: white; } + .uauth-login-paper.mud-theme-secondary { + background: linear-gradient(145deg, var(--mud-palette-secondary), rgba(0, 0, 0, 0.85) ); + color: white; + } + .uauth-brand-glow { filter: drop-shadow(0 0 25px rgba(255,255,255,0.15)); } diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor index 7687854b..f1d587c7 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Login.razor @@ -73,7 +73,7 @@