Skip to content

Commit d530cba

Browse files
committed
Completed PKCE
1 parent 05b1acb commit d530cba

File tree

7 files changed

+117
-106
lines changed

7 files changed

+117
-106
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
@inject IUAuthClient UAuthClient
1616
@inject IAuthStore AuthStore
1717
@inject IHubFlowService HubFlowService
18+
@inject IPkceService PkceService
1819
@inject IHubFlowReader HubFlowReader
1920
@inject IHubCredentialResolver HubCredentialResolver
2021
@inject IClientStorage BrowserStorage
@@ -74,7 +75,7 @@
7475

7576
<MudItem Class="order-0 order-sm-1" xs="12" sm="6">
7677
<MudPaper Class="uauth-login-paper pa-8" Elevation="3">
77-
<UAuthLoginForm Identifier="@_username" Secret="@_password" LoginType="UAuthLoginType.Pkce" SubmitMode="UAuthSubmitMode.DirectCommit" OnTryResult="@HandleLoginResult">
78+
<UAuthLoginForm @ref="_loginForm" Identifier="@_username" Secret="@_password" LoginType="UAuthLoginType.Pkce" SubmitMode="UAuthSubmitMode.TryAndCommit" OnTryResult="@HandleLoginResult">
7879
<MudStack Class="mud-width-full">
7980
<MudStack Row="true" AlignItems="AlignItems.Center">
8081
<UAuthLogo Size="48" ShieldColor="var(--mud-palette-primary)" KeyColor="white" />

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

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using CodeBeam.UltimateAuth.Client;
2+
using CodeBeam.UltimateAuth.Client.Blazor;
23
using CodeBeam.UltimateAuth.Client.Runtime;
34
using CodeBeam.UltimateAuth.Core.Contracts;
45
using CodeBeam.UltimateAuth.Core.Domain;
56
using CodeBeam.UltimateAuth.Core.MultiTenancy;
67
using CodeBeam.UltimateAuth.Server.Contracts;
8+
using CodeBeam.UltimateAuth.Server.Services;
79
using CodeBeam.UltimateAuth.Server.Stores;
810
using Microsoft.AspNetCore.Components;
911
using MudBlazor;
@@ -21,6 +23,7 @@ public partial class Home
2123
private HubFlowState? _state;
2224

2325
private UAuthClientProductInfo? _productInfo;
26+
private UAuthLoginForm _loginForm = null!;
2427
private MudTextField<string> _usernameField = default!;
2528

2629
private CancellationTokenSource? _lockoutCts;
@@ -32,6 +35,7 @@ public partial class Home
3235
private TimeSpan _lockoutDuration;
3336
private double _progressPercent;
3437
private int? _remainingAttempts = null;
38+
private bool _errorHandled;
3539

3640
protected override async Task OnInitializedAsync()
3741
{
@@ -67,18 +71,24 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
6771
return;
6872
}
6973

70-
if (_state.Error != null)
74+
if (_state.Error != null && !_errorHandled)
7175
{
76+
_errorHandled = true;
77+
7278
Snackbar.Add(ResolveErrorMessage(_state.Error), Severity.Error);
73-
_state = _state.ClearError();
79+
//_state = _state.ClearError();
7480

75-
await Task.Delay(200);
81+
//await Task.Delay(200);
7682
await ContinuePkceAsync();
7783

7884
if (HubSessionId.TryParse(HubKey, out var hubSessionId))
7985
{
8086
_state = await HubFlowReader.GetStateAsync(hubSessionId);
8187
}
88+
89+
await _loginForm.RefreshAsync();
90+
91+
StateHasChanged();
8292
}
8393
}
8494

@@ -129,37 +139,20 @@ private async Task HandleLoginResult(IUAuthTryResult result)
129139
}
130140

131141
private PkceCredentials? _pkce;
132-
private string? _hubSessionId;
133142

134143
private async Task ContinuePkceAsync()
135144
{
136-
if (string.IsNullOrWhiteSpace(_hubSessionId))
145+
if (string.IsNullOrWhiteSpace(HubKey))
137146
return;
138147

139-
_pkce = await UAuthClient.Flows.BeginPkceSilentAsync();
140-
await HubFlowService.ContinuePkceAsync(_hubSessionId, _pkce.AuthorizationCode, _pkce.CodeVerifier);
141-
}
148+
var key = new AuthArtifactKey(HubKey);
149+
var artifact = await AuthStore.GetAsync(key) as HubFlowArtifact;
142150

143-
private async Task StartNewPkceSilentAsync()
144-
{
145-
var returnUrl = await ResolveReturnUrlAsync();
146-
147-
_pkce = await UAuthClient.Flows.BeginPkceSilentAsync();
148-
149-
HubBeginRequest request = new HubBeginRequest()
150-
{
151-
AuthorizationCode = _pkce.AuthorizationCode,
152-
CodeVerifier = _pkce.CodeVerifier,
153-
ClientProfile = Options.Value.ClientProfile,
154-
Tenant = TenantKeys.Single, // TODO
155-
ReturnUrl = returnUrl,
156-
PreviousHubSessionId = _hubSessionId
157-
//deviceId: _deviceId
158-
};
159-
160-
var result = await HubFlowService.BeginLoginAsync(request);
151+
if (artifact is null)
152+
return;
161153

162-
_hubSessionId = result.HubSessionId;
154+
_pkce = await PkceService.RefreshAsync(artifact);
155+
await HubFlowService.ContinuePkceAsync(HubKey, _pkce.AuthorizationCode, _pkce.CodeVerifier);
163156
}
164157

165158
private async Task StartNewPkceAsync()

src/CodeBeam.UltimateAuth.Server/Services/Abstractions/IPkceService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using CodeBeam.UltimateAuth.Core.Contracts;
2+
using CodeBeam.UltimateAuth.Core.Domain;
23
using CodeBeam.UltimateAuth.Server.Auth;
34

45
namespace CodeBeam.UltimateAuth.Server.Services;
@@ -7,4 +8,5 @@ public interface IPkceService
78
{
89
Task<PkceAuthorizeResponse> AuthorizeAsync(PkceAuthorizeCommand command, CancellationToken ct = default);
910
Task<PkceCompleteResult> CompleteAsync(AuthFlowContext auth, PkceCompleteRequest request, CancellationToken ct = default);
11+
Task<PkceCredentials> RefreshAsync(HubFlowArtifact hub, CancellationToken ct = default);
1012
}

src/CodeBeam.UltimateAuth.Server/Services/PkceService.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
using CodeBeam.UltimateAuth.Server.Options;
77
using CodeBeam.UltimateAuth.Server.Stores;
88
using Microsoft.Extensions.Options;
9+
using System.Security.Cryptography;
10+
using System.Text;
911

1012
namespace CodeBeam.UltimateAuth.Server.Services;
1113

@@ -119,4 +121,59 @@ public async Task<PkceCompleteResult> CompleteAsync(AuthFlowContext auth, PkceCo
119121
LoginResult = result
120122
};
121123
}
124+
125+
public async Task<PkceCredentials> RefreshAsync(HubFlowArtifact hub, CancellationToken ct = default)
126+
{
127+
if (!hub.Payload.TryGet<string>("device_id", out var deviceId) || string.IsNullOrWhiteSpace(deviceId))
128+
throw new InvalidOperationException("HubFlow missing device_id.");
129+
130+
var verifier = CreateVerifier();
131+
var challenge = CreateChallenge(verifier);
132+
133+
var authorizationCode = AuthArtifactKey.New();
134+
135+
var snapshot = new PkceContextSnapshot(
136+
clientProfile: hub.ClientProfile,
137+
tenant: hub.Tenant,
138+
redirectUri: null,
139+
deviceId: deviceId
140+
);
141+
142+
var expiresAt = _clock.UtcNow.AddSeconds(_options.Pkce.AuthorizationCodeLifetimeSeconds);
143+
144+
var artifact = new PkceAuthorizationArtifact(
145+
authorizationCode,
146+
challenge,
147+
PkceChallengeMethod.S256,
148+
expiresAt,
149+
snapshot
150+
);
151+
152+
await _authStore.StoreAsync(authorizationCode, artifact, ct);
153+
154+
return new PkceCredentials
155+
{
156+
AuthorizationCode = authorizationCode.Value,
157+
CodeVerifier = verifier
158+
};
159+
}
160+
161+
private static string CreateVerifier()
162+
{
163+
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(32))
164+
.TrimEnd('=')
165+
.Replace('+', '-')
166+
.Replace('/', '_');
167+
}
168+
169+
private static string CreateChallenge(string verifier)
170+
{
171+
using var sha256 = SHA256.Create();
172+
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(verifier));
173+
174+
return Convert.ToBase64String(bytes)
175+
.TrimEnd('=')
176+
.Replace('+', '-')
177+
.Replace('/', '_');
178+
}
122179
}

src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthLoginForm.razor.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,13 @@ private async Task EmitResultAsync(IUAuthTryResult result)
209209
await OnTryResult.InvokeAsync(result);
210210
}
211211

212+
public async Task RefreshAsync()
213+
{
214+
await ReloadCredentialsAsync();
215+
await ReloadStateAsync();
216+
await InvokeAsync(StateHasChanged);
217+
}
218+
212219
private string ClientProfileValue => Options.Value.ClientProfile.ToString();
213220

214221
private string EffectiveEndpoint => LoginType == UAuthLoginType.Pkce

src/client/CodeBeam.UltimateAuth.Client/Services/Abstractions/IFlowClient.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ public interface IFlowClient
1717
Task<AuthValidationResult> ValidateAsync();
1818

1919
Task BeginPkceAsync(string? returnUrl = null);
20-
Task<PkceCredentials> ContinuePkceAsync(HubFlowArtifact hub);
21-
Task<PkceCredentials> BeginPkceSilentAsync();
20+
//Task<PkceCredentials> ContinuePkceAsync(HubFlowArtifact hub);
2221
Task<TryPkceLoginResult> TryCompletePkceLoginAsync(PkceCompleteRequest request, UAuthSubmitMode mode);
2322
Task CompletePkceLoginAsync(PkceCompleteRequest request);
2423

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

Lines changed: 28 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -241,85 +241,46 @@ public async Task BeginPkceAsync(string? returnUrl = null)
241241
}
242242
}
243243

244-
public async Task<PkceCredentials> BeginPkceSilentAsync()
245-
{
246-
var pkce = _options.Pkce;
247-
248-
if (!pkce.Enabled)
249-
throw new InvalidOperationException("PKCE login is disabled.");
250-
251-
var verifier = CreateVerifier();
252-
var challenge = CreateChallenge(verifier);
253-
254-
var authorizeUrl = Url(_options.Endpoints.PkceAuthorize);
255-
256-
var raw = await _post.SendFormAsync(
257-
authorizeUrl,
258-
new Dictionary<string, string>
259-
{
260-
["code_challenge"] = challenge,
261-
["challenge_method"] = "S256",
262-
});
263-
264-
if (!raw.Ok || raw.Body is null)
265-
throw new InvalidOperationException("PKCE authorize failed.");
266-
267-
var response = raw.Body.Value.Deserialize<PkceAuthorizeResponse>(
268-
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
269-
270-
if (response is null || string.IsNullOrWhiteSpace(response.AuthorizationCode))
271-
throw new InvalidOperationException("Invalid PKCE authorize response.");
272-
273-
if (pkce.OnAuthorized is not null)
274-
await pkce.OnAuthorized(response);
244+
//public async Task<PkceCredentials> ContinuePkceAsync(HubFlowArtifact hub)
245+
//{
246+
// var pkce = _options.Pkce;
275247

276-
return new PkceCredentials
277-
{
278-
AuthorizationCode = response.AuthorizationCode,
279-
CodeVerifier = verifier
280-
};
281-
}
248+
// if (!pkce.Enabled)
249+
// throw new InvalidOperationException("PKCE disabled.");
282250

283-
public async Task<PkceCredentials> ContinuePkceAsync(HubFlowArtifact hub)
284-
{
285-
var pkce = _options.Pkce;
251+
// var deviceId = hub.Payload.GetRequired<string>("device_id");
252+
// var clientProfile = hub.ClientProfile;
286253

287-
if (!pkce.Enabled)
288-
throw new InvalidOperationException("PKCE disabled.");
254+
// var verifier = CreateVerifier();
255+
// var challenge = CreateChallenge(verifier);
289256

290-
var deviceId = hub.Payload.GetRequired<string>("device_id");
291-
var clientProfile = hub.ClientProfile;
257+
// var authorizeUrl = Url(_options.Endpoints.PkceAuthorize);
292258

293-
var verifier = CreateVerifier();
294-
var challenge = CreateChallenge(verifier);
295-
296-
var authorizeUrl = Url(_options.Endpoints.PkceAuthorize);
297-
298-
var raw = await _post.SendFormAsync(
299-
authorizeUrl,
300-
new Dictionary<string, string>
301-
{
302-
["code_challenge"] = challenge,
303-
["challenge_method"] = "S256",
304-
["device_id"] = deviceId
305-
});
259+
// var raw = await _post.SendFormAsync(
260+
// authorizeUrl,
261+
// new Dictionary<string, string>
262+
// {
263+
// ["code_challenge"] = challenge,
264+
// ["challenge_method"] = "S256",
265+
// ["device_id"] = deviceId
266+
// });
306267

307-
var response = raw.Body.Value.Deserialize<PkceAuthorizeResponse>(
308-
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
268+
// var response = raw.Body.Value.Deserialize<PkceAuthorizeResponse>(
269+
// new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
309270

310-
return new PkceCredentials
311-
{
312-
AuthorizationCode = response.AuthorizationCode,
313-
CodeVerifier = verifier
314-
};
315-
}
271+
// return new PkceCredentials
272+
// {
273+
// AuthorizationCode = response.AuthorizationCode,
274+
// CodeVerifier = verifier
275+
// };
276+
//}
316277

317278
public async Task<TryPkceLoginResult> TryCompletePkceLoginAsync(PkceCompleteRequest request, UAuthSubmitMode mode)
318279
{
319280
if (mode == UAuthSubmitMode.DirectCommit)
320281
{
321282
await CompletePkceLoginAsync(request);
322-
return new TryPkceLoginResult();
283+
return new TryPkceLoginResult { Success = true };
323284
}
324285

325286
if (request is null)
@@ -359,19 +320,10 @@ public async Task<TryPkceLoginResult> TryCompletePkceLoginAsync(PkceCompleteRequ
359320
return parsed;
360321
}
361322

362-
case UAuthSubmitMode.DirectCommit:
363-
{
364-
await _post.NavigateAsync(commitUrl, payload);
365-
return new TryPkceLoginResult { Success = true };
366-
}
367-
368323
case UAuthSubmitMode.TryAndCommit:
369324
default:
370325
{
371-
var result = await _post.TryAndCommitAsync<TryPkceLoginResult>(
372-
tryUrl,
373-
commitUrl,
374-
payload);
326+
var result = await _post.TryAndCommitAsync<TryPkceLoginResult>(tryUrl, commitUrl, payload);
375327

376328
if (result is null)
377329
throw new UAuthProtocolException("Invalid PKCE try result.");

0 commit comments

Comments
 (0)