Skip to content

Commit 38b4f19

Browse files
committed
Added Device Id Support For PKCE Login
1 parent c8dcbad commit 38b4f19

File tree

15 files changed

+199
-28
lines changed

15 files changed

+199
-28
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ protected override async Task OnParametersSetAsync()
2626
return;
2727
}
2828

29-
if (!HubSessionId.TryParse(HubKey, out var hubSessionId))
29+
if (HubSessionId.TryParse(HubKey, out var hubSessionId))
3030
_state = await HubFlowReader.GetStateAsync(hubSessionId);
3131
}
3232

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
@inject NavigationManager Nav
2+
3+
<MudPage Class="d-flex align-center justify-center" FullScreen="FullScreen.FullWithoutAppbar" Column="1" Row="1">
4+
<MudPaper Class="pa-8" Elevation="4" Style="max-width: 520px; width: 100%;">
5+
<MudStack Spacing="3" AlignItems="AlignItems.Center">
6+
<UAuthLogo Size="72" />
7+
8+
<MudText Typo="Typo.h5"><b>Access Denied</b></MudText>
9+
10+
<MudText Typo="Typo.body2" Align="Align.Center" Color="Color.Secondary">
11+
You don’t have permission to view this page.
12+
If you think this is a mistake, sign in with a different account or request access.
13+
</MudText>
14+
15+
<MudStack Row="true" Spacing="2" Class="mt-2">
16+
<MudButton Href="@LoginHref" Color="Color.Primary" Variant="Variant.Filled">Sign In</MudButton>
17+
<MudButton OnClick="@GoBack" Color="Color.Primary" Variant="Variant.Outlined">Go Back</MudButton>
18+
</MudStack>
19+
20+
<MudDivider Class="my-2" />
21+
22+
<MudText Typo="Typo.caption" Color="Color.Secondary">
23+
UltimateAuth protects this resource based on your session and permissions.
24+
</MudText>
25+
</MudStack>
26+
</MudPaper>
27+
</MudPage>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace CodeBeam.UltimateAuth.Sample.UAuthHub.Components.Pages;
2+
3+
public partial class NotAuthorized
4+
{
5+
private string LoginHref
6+
{
7+
get
8+
{
9+
var returnUrl = Uri.EscapeDataString(Nav.ToBaseRelativePath(Nav.Uri));
10+
return $"/login?returnUrl=/{returnUrl}";
11+
}
12+
}
13+
14+
private void GoBack() => Nav.NavigateTo("/", replace: false);
15+
}
Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,59 @@
1-
<UAuthApp>
2-
3-
<MudThemeProvider />
4-
<MudPopoverProvider />
5-
<MudDialogProvider />
6-
<MudSnackbarProvider />
7-
8-
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
9-
<Found Context="routeData">
10-
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
11-
<FocusOnNavigate RouteData="routeData" Selector="h1" />
12-
</Found>
13-
</Router>
1+
@using CodeBeam.UltimateAuth.Sample.UAuthHub.Components.Pages
2+
@using CodeBeam.UltimateAuth.Sample.UAuthHub.Infrastructure
3+
@inject ISnackbar Snackbar
4+
@inject DarkModeManager DarkModeManager
5+
6+
<UAuthApp OnReauthRequired="HandleReauth">
7+
<CascadingValue Value="DarkModeManager" IsFixed="true">
8+
<MudThemeProvider IsDarkMode="@DarkModeManager.IsDarkMode" />
9+
<MudPopoverProvider />
10+
<MudDialogProvider />
11+
<MudSnackbarProvider />
12+
13+
<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(UAuthClientMarker).Assembly }">
14+
<Found Context="routeData">
15+
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
16+
<NotAuthorized>
17+
<NotAuthorized />
18+
</NotAuthorized>
19+
</AuthorizeRouteView>
20+
<FocusOnNavigate RouteData="routeData" Selector="h1" />
21+
</Found>
22+
</Router>
23+
</CascadingValue>
1424
</UAuthApp>
25+
26+
@code {
27+
private async Task HandleReauth()
28+
{
29+
Snackbar.Add("Reauthentication required. Please log in again.", Severity.Warning);
30+
}
31+
32+
#region DarkMode
33+
34+
protected override void OnInitialized()
35+
{
36+
DarkModeManager.Changed += OnThemeChanged;
37+
}
38+
39+
private void OnThemeChanged()
40+
{
41+
InvokeAsync(StateHasChanged);
42+
}
43+
44+
protected override async Task OnAfterRenderAsync(bool firstRender)
45+
{
46+
if (firstRender)
47+
{
48+
await DarkModeManager.InitializeAsync();
49+
StateHasChanged();
50+
}
51+
}
52+
53+
public void Dispose()
54+
{
55+
DarkModeManager.Changed -= OnThemeChanged;
56+
}
57+
58+
#endregion
59+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using CodeBeam.UltimateAuth.Client.Contracts;
2+
using CodeBeam.UltimateAuth.Client.Infrastructure;
3+
4+
namespace CodeBeam.UltimateAuth.Sample.UAuthHub.Infrastructure;
5+
6+
public sealed class DarkModeManager
7+
{
8+
private const string StorageKey = "uauth:theme:dark";
9+
10+
private readonly IBrowserStorage _storage;
11+
12+
public DarkModeManager(IBrowserStorage storage)
13+
{
14+
_storage = storage;
15+
}
16+
17+
public async Task InitializeAsync()
18+
{
19+
var value = await _storage.GetAsync(StorageScope.Local, StorageKey);
20+
21+
if (bool.TryParse(value, out var parsed))
22+
IsDarkMode = parsed;
23+
}
24+
25+
public bool IsDarkMode { get; set; }
26+
27+
public event Action? Changed;
28+
29+
public async Task ToggleAsync()
30+
{
31+
IsDarkMode = !IsDarkMode;
32+
33+
await _storage.SetAsync(StorageScope.Local, StorageKey, IsDarkMode.ToString());
34+
Changed?.Invoke();
35+
}
36+
37+
public void Set(bool value)
38+
{
39+
if (IsDarkMode == value)
40+
return;
41+
42+
IsDarkMode = value;
43+
Changed?.Invoke();
44+
}
45+
}

samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using CodeBeam.UltimateAuth.Authentication.InMemory;
2-
using CodeBeam.UltimateAuth.Authorization.InMemory;
32
using CodeBeam.UltimateAuth.Authorization.InMemory.Extensions;
43
using CodeBeam.UltimateAuth.Authorization.Reference.Extensions;
54
using CodeBeam.UltimateAuth.Client;
@@ -10,6 +9,7 @@
109
using CodeBeam.UltimateAuth.Credentials.InMemory.Extensions;
1110
using CodeBeam.UltimateAuth.Credentials.Reference;
1211
using CodeBeam.UltimateAuth.Sample.UAuthHub.Components;
12+
using CodeBeam.UltimateAuth.Sample.UAuthHub.Infrastructure;
1313
using CodeBeam.UltimateAuth.Security.Argon2;
1414
using CodeBeam.UltimateAuth.Server.Extensions;
1515
using CodeBeam.UltimateAuth.Sessions.InMemory;
@@ -62,6 +62,7 @@
6262
});
6363

6464
builder.Services.AddSingleton<IUAuthHubMarker, DefaultUAuthHubMarker>();
65+
builder.Services.AddScoped<DarkModeManager>();
6566

6667
builder.Services.AddCors(options =>
6768
{

samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
o.Endpoints.BasePath = "https://localhost:6110/auth";
2121
o.Reauth.Behavior = ReauthBehavior.RaiseEvent;
2222
o.Login.AllowCredentialPost = true;
23+
o.Pkce.ReturnUrl = "https://localhost:6130/home";
2324
});
2425

2526
//builder.Services.AddScoped<AuthenticationStateProvider, UAuthAuthenticationStateProvider>();

src/CodeBeam.UltimateAuth.Client/Options/UAuthClientPkceLoginFlowOptions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ public sealed class UAuthClientPkceLoginFlowOptions
99
/// </summary>
1010
public bool Enabled { get; set; } = true;
1111

12-
public string? ReturnUrl { get; init; }
12+
public string? ReturnUrl { get; set; }
1313

1414
/// <summary>
1515
/// Called after authorization_code is issued,
1616
/// before redirecting to the Hub.
1717
/// </summary>
18-
public Func<PkceAuthorizeResponse, Task>? OnAuthorized { get; init; }
18+
public Func<PkceAuthorizeResponse, Task>? OnAuthorized { get; set; }
1919

2020
/// <summary>
2121
/// If false, BeginPkceAsync will NOT redirect automatically.
2222
/// Caller is responsible for navigation.
2323
/// </summary>
24-
public bool AutoRedirect { get; init; } = true;
24+
public bool AutoRedirect { get; set; } = true;
2525
}

src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ internal class UAuthFlowClient : IFlowClient
2222
{
2323
private readonly IUAuthRequestClient _post;
2424
private readonly IUAuthClientEvents _events;
25+
private readonly IDeviceIdProvider _deviceIdProvider;
2526
private readonly UAuthClientOptions _options;
2627
private readonly UAuthClientDiagnostics _diagnostics;
2728
private readonly NavigationManager _nav;
2829

29-
public UAuthFlowClient(IUAuthRequestClient post, IUAuthClientEvents events, IOptions<UAuthClientOptions> options, UAuthClientDiagnostics diagnostics, NavigationManager nav)
30+
public UAuthFlowClient(IUAuthRequestClient post, IUAuthClientEvents events, IDeviceIdProvider deviceIdProvider, IOptions<UAuthClientOptions> options, UAuthClientDiagnostics diagnostics, NavigationManager nav)
3031
{
3132
_post = post;
3233
_events = events;
34+
_deviceIdProvider = deviceIdProvider;
3335
_options = options.Value;
3436
_diagnostics = diagnostics;
3537
_nav = nav;
@@ -168,6 +170,7 @@ public async Task<AuthValidationResult> ValidateAsync()
168170
public async Task BeginPkceAsync(string? returnUrl = null)
169171
{
170172
var pkce = _options.Pkce;
173+
var deviceId = await _deviceIdProvider.GetOrCreateAsync();
171174

172175
if (!pkce.Enabled)
173176
throw new InvalidOperationException("PKCE login is disabled by configuration.");
@@ -182,7 +185,8 @@ public async Task BeginPkceAsync(string? returnUrl = null)
182185
new Dictionary<string, string>
183186
{
184187
["code_challenge"] = challenge,
185-
["challenge_method"] = "S256"
188+
["challenge_method"] = "S256",
189+
["device_id"] = deviceId.Value
186190
});
187191

188192
if (!raw.Ok || raw.Body is null)
@@ -205,7 +209,7 @@ public async Task BeginPkceAsync(string? returnUrl = null)
205209

206210
if (pkce.AutoRedirect)
207211
{
208-
await NavigateToHubLoginAsync(response.AuthorizationCode, verifier, resolvedReturnUrl);
212+
await NavigateToHubLoginAsync(response.AuthorizationCode, verifier, resolvedReturnUrl, deviceId.Value);
209213
}
210214
}
211215

@@ -229,7 +233,7 @@ public async Task CompletePkceLoginAsync(PkceLoginRequest request)
229233
["return_url"] = request.ReturnUrl,
230234

231235
["Identifier"] = request.Identifier ?? string.Empty,
232-
["Secret"] = request.Secret ?? string.Empty
236+
["Secret"] = request.Secret ?? string.Empty,
233237
};
234238

235239
await _post.NavigateAsync(url, payload);
@@ -289,7 +293,7 @@ public async Task<UAuthResult> LogoutAllDevicesAdminAsync(UserKey userKey)
289293
}
290294

291295

292-
private Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifier, string returnUrl)
296+
private Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifier, string returnUrl, string deviceId)
293297
{
294298
var hubLoginUrl = Url(_options.Endpoints.HubLoginPath);
295299

@@ -298,7 +302,8 @@ private Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifi
298302
["authorization_code"] = authorizationCode,
299303
["code_verifier"] = codeVerifier,
300304
["return_url"] = returnUrl,
301-
["client_profile"] = _options.ClientProfile.ToString()
305+
["client_profile"] = _options.ClientProfile.ToString(),
306+
["device_id"] = deviceId
302307
};
303308

304309
return _post.NavigateAsync(hubLoginUrl, data);
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
using CodeBeam.UltimateAuth.Core.Options;
1+
using CodeBeam.UltimateAuth.Core.Domain;
2+
using CodeBeam.UltimateAuth.Core.Options;
23

34
namespace CodeBeam.UltimateAuth.Server.Auth;
45

56
public sealed record AuthExecutionContext
67
{
78
public required UAuthClientProfile? EffectiveClientProfile { get; init; }
9+
public DeviceContext? Device { get; init; }
810
}

0 commit comments

Comments
 (0)