Skip to content

Commit 7191fc5

Browse files
committed
PKCE Polish & Cleanup
1 parent fc87d75 commit 7191fc5

40 files changed

+353
-403
lines changed

samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Layout/MainLayout.razor

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
@using CodeBeam.UltimateAuth.Core.Abstractions
22
@using CodeBeam.UltimateAuth.Server.Infrastructure
33
@inherits LayoutComponentBase
4-
@inject IUAuthHubContextInitializer HubContextInitializer
5-
@inject UAuthHubContextAccessor HubContextAccessor
64

75
<UAuthClientProvider />
86
<MudThemeProvider />

samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Layout/MainLayout.razor.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,6 @@
22
{
33
public partial class MainLayout
44
{
5-
protected override async Task OnAfterRenderAsync(bool firstRender)
6-
{
7-
if (firstRender)
8-
{
9-
await HubContextInitializer.EnsureInitializedAsync();
10-
}
11-
}
5+
126
}
137
}

samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
@using CodeBeam.UltimateAuth.Client
44
@using CodeBeam.UltimateAuth.Client.Authentication
55
@using CodeBeam.UltimateAuth.Client.Diagnostics
6+
@using CodeBeam.UltimateAuth.Client.Utilities
67
@using CodeBeam.UltimateAuth.Core.Abstractions
78
@using CodeBeam.UltimateAuth.Core.Contracts
89
@using CodeBeam.UltimateAuth.Core.Domain
@@ -13,12 +14,12 @@
1314
@using CodeBeam.UltimateAuth.Server.Services
1415
@using CodeBeam.UltimateAuth.Server.Stores
1516
@inject IUAuthStateManager StateManager
16-
@inject IHubFlowStateReader HubStateReader
17+
@inject IHubFlowReader HubFlowReader
18+
@inject IHubCredentialResolver HubCredentialResolver
1719
@inject IAuthStore AuthStore
18-
@inject UAuthHubContextAccessor HubContextAccessor
20+
@inject IBrowserStorage BrowserStorage
1921
@inject IUAuthFlowService<UserId> Flow
2022
@inject ISnackbar Snackbar
21-
@inject ISessionQueryService<UserId> SessionQuery
2223
@inject IFlowCredentialResolver CredentialResolver
2324
@inject IUAuthClient UAuthClient
2425
@inject NavigationManager Nav
@@ -29,7 +30,15 @@
2930

3031
<div class="uauth-page d-flex align-center justify-center">
3132
<MudStack Class="uauth-stack">
32-
<UALoginForm @ref="_form" Identifier="@_username" Secret="@_password" LoginType="UAuthLoginType.Pkce" HubContextAccessor="@HubContextAccessor">
33+
@if (_state == null || !_state.IsActive)
34+
{
35+
<MudText>
36+
This page cannot be accessed directly.
37+
UAuthHub login flows can only be initiated by an authorized client application.
38+
</MudText>
39+
return;
40+
}
41+
<UALoginForm Identifier="@_username" Secret="@_password" LoginType="UAuthLoginType.Pkce">
3342
<MudStack>
3443
<MudText Typo="Typo.h4">Welcome to UltimateAuth!</MudText>
3544
<MudTextField @bind-Value="@_username" Variant="Variant.Outlined" Label="Username" Immediate="true" HelperText="Default: Admin" />
@@ -48,15 +57,12 @@
4857
</MudStack>
4958

5059
<MudStack Spacing="0">
51-
<MudText>Hub SessionId: @HubContextAccessor?.Current?.HubSessionId</MudText>
52-
<MudText>Client Profile: @HubContextAccessor?.Current?.ClientProfile</MudText>
53-
<MudText>Return Url: @HubContextAccessor?.Current?.ReturnUrl</MudText>
54-
<MudText>Flow Type: @HubContextAccessor?.Current?.FlowType</MudText>
55-
<MudText>Created At: @HubContextAccessor?.Current?.CreatedAt</MudText>
56-
<MudText>Completed: @HubContextAccessor?.Current?.IsCompleted</MudText>
57-
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="CompleteHubFlow">Complete Hub Flow</MudButton>
60+
<MudText>Hub SessionId: @_state?.HubSessionId</MudText>
61+
<MudText>Client Profile: @_state?.ClientProfile</MudText>
62+
<MudText>Return Url: @_state?.ReturnUrl</MudText>
63+
<MudText>Flow Type: @_state?.FlowType</MudText>
64+
<MudText>IsActive: @_state?.IsActive</MudText>
5865
</MudStack>
5966
</MudStack>
60-
6167
</div>
6268

samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Components/Pages/Home.razor.cs

Lines changed: 45 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
using CodeBeam.UltimateAuth.Client;
1+
using CodeBeam.UltimateAuth.Client.Contracts;
2+
using CodeBeam.UltimateAuth.Client.Utilities;
23
using CodeBeam.UltimateAuth.Core.Contracts;
34
using CodeBeam.UltimateAuth.Core.Domain;
4-
using CodeBeam.UltimateAuth.Server.Infrastructure;
55
using CodeBeam.UltimateAuth.Server.Stores;
66
using Microsoft.AspNetCore.Components;
7-
using Microsoft.AspNetCore.Components.Authorization;
87
using Microsoft.AspNetCore.WebUtilities;
98
using MudBlazor;
10-
using System;
119

1210
namespace CodeBeam.UltimateAuth.Sample.UAuthHub.Components.Pages
1311
{
@@ -16,68 +14,75 @@ public partial class Home
1614
[SupplyParameterFromQuery(Name = "hub")]
1715
public string? HubKey { get; set; }
1816

19-
private string _authorizationCode = default!;
20-
private string _codeVerifier = default!;
21-
2217
private string? _username;
2318
private string? _password;
2419

25-
private UALoginForm _form = null!;
20+
private HubFlowState? _state;
21+
22+
protected override async Task OnParametersSetAsync()
23+
{
24+
if (string.IsNullOrWhiteSpace(HubKey))
25+
{
26+
_state = null;
27+
return;
28+
}
2629

27-
protected bool Ready;
28-
protected string? Error;
30+
_state = await HubFlowReader.GetStateAsync(new HubSessionId(HubKey));
31+
}
2932

3033
protected override async Task OnAfterRenderAsync(bool firstRender)
3134
{
3235
if (!firstRender)
3336
return;
3437

35-
var hubKey = GetHubKeyFromQuery();
38+
var currentError = await BrowserStorage.GetAsync(StorageScope.Session, "uauth:last_error");
3639

37-
if (hubKey is null)
40+
if (!string.IsNullOrWhiteSpace(currentError))
3841
{
39-
await StartNewPkceAsync();
40-
return;
42+
Snackbar.Add(ResolveErrorMessage(currentError), Severity.Error);
43+
await BrowserStorage.RemoveAsync(StorageScope.Session, "uauth:last_error");
4144
}
4245

43-
var state = await HubStateReader.GetStateAsync(hubKey);
46+
var uri = Nav.ToAbsoluteUri(Nav.Uri);
47+
var query = QueryHelpers.ParseQuery(uri.Query);
4448

45-
if (!state.Exists || !state.IsActive)
49+
if (query.TryGetValue("__uauth_error", out var error))
50+
{
51+
await BrowserStorage.SetAsync(StorageScope.Session, "uauth:last_error", error.ToString());
52+
}
53+
54+
if (string.IsNullOrWhiteSpace(HubKey))
4655
{
47-
await StartNewPkceAsync();
4856
return;
4957
}
50-
}
5158

52-
private string? GetHubKeyFromQuery()
53-
{
54-
var uri = Nav.ToAbsoluteUri(Nav.Uri);
55-
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query);
56-
57-
if (query.TryGetValue("hub", out var hubKey) && !string.IsNullOrWhiteSpace(hubKey))
58-
return hubKey;
59+
if (_state is null || !_state.Exists)
60+
return;
5961

60-
return null;
62+
if (_state?.IsActive != true)
63+
{
64+
await StartNewPkceAsync();
65+
return;
66+
}
6167
}
6268

69+
// For testing & debugging
6370
private async Task ProgrammaticPkceLogin()
6471
{
65-
var hub = HubContextAccessor.Current;
72+
var hub = _state;
6673

6774
if (hub is null)
6875
return;
6976

70-
hub.Payload.TryGet("authorization_code", out string? authorizationCode);
71-
hub.Payload.TryGet("code_verifier", out string? codeVerifier);
72-
hub.Payload.TryGet("return_url", out string? returnUrl);
77+
var credentials = await HubCredentialResolver.ResolveAsync(new HubSessionId(HubKey));
7378

7479
var request = new PkceLoginRequest
7580
{
7681
Identifier = "Admin",
7782
Secret = "Password!",
78-
AuthorizationCode = authorizationCode ?? string.Empty,
79-
CodeVerifier = codeVerifier ?? string.Empty,
80-
ReturnUrl = hub.ReturnUrl ?? string.Empty
83+
AuthorizationCode = credentials?.AuthorizationCode ?? string.Empty,
84+
CodeVerifier = credentials?.CodeVerifier ?? string.Empty,
85+
ReturnUrl = _state?.ReturnUrl ?? string.Empty
8186
};
8287
await UAuthClient.CompletePkceLoginAsync(request);
8388
}
@@ -90,85 +95,38 @@ private async Task StartNewPkceAsync()
9095

9196
private async Task<string> ResolveReturnUrlAsync()
9297
{
93-
// 1) Live hub context
94-
var fromContext = HubContextAccessor.Current?.ReturnUrl;
98+
var fromContext = _state?.ReturnUrl;
9599
if (!string.IsNullOrWhiteSpace(fromContext))
96100
return fromContext;
97101

98-
// 2) Query return_url (optional)
99102
var uri = Nav.ToAbsoluteUri(Nav.Uri);
100103
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query);
101104

102105
if (query.TryGetValue("return_url", out var ru) && !string.IsNullOrWhiteSpace(ru))
103106
return ru!;
104107

105-
// 3) Query hub -> store lookup
106108
if (query.TryGetValue("hub", out var hubKey) && !string.IsNullOrWhiteSpace(hubKey))
107109
{
108110
var artifact = await AuthStore.GetAsync(new AuthArtifactKey(hubKey!));
109111
if (artifact is HubFlowArtifact flow && !string.IsNullOrWhiteSpace(flow.ReturnUrl))
110112
return flow.ReturnUrl!;
111113
}
112114

113-
// 4) Config default (recommend adding to options)
115+
// Config default (recommend adding to options)
114116
//if (!string.IsNullOrWhiteSpace(_options.Login.DefaultReturnUrl))
115117
// return _options.Login.DefaultReturnUrl!;
116118

117-
// 5) Final fallback (Hub URL)
118119
return Nav.Uri;
119120
}
120-
121-
private void CompleteHubFlow()
121+
122+
private string ResolveErrorMessage(string? errorKey)
122123
{
123-
HubContextAccessor.Complete();
124-
StateHasChanged();
125-
}
126-
127-
private void HandleErrorFromQuery()
128-
{
129-
var uri = Nav.ToAbsoluteUri(Nav.Uri);
130-
var query = QueryHelpers.ParseQuery(uri.Query);
131-
132-
if (query.TryGetValue("error", out var error))
124+
if (errorKey == "invalid")
133125
{
134-
Snackbar.Add("Login failed.", Severity.Error);
135-
Nav.NavigateTo(uri.GetLeftPart(UriPartial.Path), replace: true);
126+
return "Login failed.";
136127
}
137-
}
138-
139-
//protected override void OnAfterRender(bool firstRender)
140-
//{
141-
// if (firstRender)
142-
// {
143-
// var uri = Nav.ToAbsoluteUri(Nav.Uri);
144-
// var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query);
145-
146-
// if (query.TryGetValue("error", out var error))
147-
// {
148-
// ShowLoginError(error.ToString());
149-
// ClearQueryString();
150-
// }
151-
// }
152-
//}
153-
154-
private void ShowLoginError(string code)
155-
{
156-
var message = code switch
157-
{
158-
"invalid" => "Invalid username or password.",
159-
"locked" => "Your account is locked.",
160-
"mfa" => "Multi-factor authentication required.",
161-
_ => "Login failed."
162-
};
163128

164-
Snackbar.Add(message, Severity.Error);
165-
}
166-
167-
private void ClearQueryString()
168-
{
169-
var uri = new Uri(Nav.Uri);
170-
var clean = uri.GetLeftPart(UriPartial.Path);
171-
Nav.NavigateTo(clean, replace: true);
129+
return "Failed attempt.";
172130
}
173131

174132
}

src/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@
1313
<PackageReference Include="Microsoft.AspNetCore.Components" Version="8.0.21" />
1414
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.21" />
1515
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.21" />
16+
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.21" />
1617
</ItemGroup>
1718
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
1819
<PackageReference Include="Microsoft.AspNetCore.Components" Version="9.0.11" />
1920
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.11" />
2021
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.11" />
22+
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="9.0.11" />
2123
</ItemGroup>
2224
<ItemGroup Condition=" '$(TargetFramework)' == 'net10.0' ">
2325
<PackageReference Include="Microsoft.AspNetCore.Components" Version="10.0.1" />
2426
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.1" />
2527
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1" />
28+
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.1" />
2629
</ItemGroup>
2730

2831
<ItemGroup>

src/CodeBeam.UltimateAuth.Client/Components/UALoginForm.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717

1818
@if (LoginType == UAuthLoginType.Pkce)
1919
{
20-
<input type="hidden" name="authorization_code" value="@AuthorizationCode" />
21-
<input type="hidden" name="code_verifier" value="@CodeVerifier" />
20+
<input type="hidden" name="authorization_code" value="@_credentials?.AuthorizationCode" />
21+
<input type="hidden" name="code_verifier" value="@_credentials?.CodeVerifier" />
2222
<input type="hidden" name="return_url" value="@EffectiveReturnUrl" />
2323
}
2424

0 commit comments

Comments
 (0)