Skip to content

Commit edeb548

Browse files
committed
Update access token pattern
1 parent d17e5c3 commit edeb548

File tree

5 files changed

+140
-31
lines changed

5 files changed

+140
-31
lines changed

src/Components/Blazor/WebAssembly.Authentication/src/Services/AccessTokenResult.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
5+
46
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
57
{
68
/// <summary>
@@ -22,5 +24,24 @@ public class AccessTokenResult
2224
/// Gets or sets the URL to redirect to if <see cref="Status"/> is <see cref="AccessTokenResultStatus.RequiresRedirect"/>.
2325
/// </summary>
2426
public string RedirectUrl { get; set; }
27+
28+
/// <summary>
29+
/// Determines whether the token request was successful and makes the <see cref="AccessToken"/> available for use when it is.
30+
/// </summary>
31+
/// <param name="accessToken">The <see cref="AccessToken"/> if the request was successful.</param>
32+
/// <returns><c>true</c> when the token request is successful; <c>false</c> otherwise.</returns>
33+
public bool TryGetAccessToken(out AccessToken accessToken)
34+
{
35+
if (string.Equals(Status, AccessTokenResultStatus.Success, StringComparison.OrdinalIgnoreCase))
36+
{
37+
accessToken = Token;
38+
return true;
39+
}
40+
else
41+
{
42+
accessToken = null;
43+
return false;
44+
}
45+
}
2546
}
2647
}

src/Components/Blazor/WebAssembly.Authentication/src/Services/RemoteAuthenticationService.cs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ public class RemoteAuthenticationService<TRemoteAuthenticationState, TProviderOp
3333
/// </summary>
3434
protected readonly IJSRuntime _jsRuntime;
3535

36+
/// <summary>
37+
/// The <see cref="NavigationManager"/> used to compute absolute urls.
38+
/// </summary>
39+
protected readonly NavigationManager _navigation;
40+
3641
/// <summary>
3742
/// The options for the underlying JavaScript library handling the authentication operations.
3843
/// </summary>
@@ -45,9 +50,11 @@ public class RemoteAuthenticationService<TRemoteAuthenticationState, TProviderOp
4550
/// <param name="options">The options to be passed down to the underlying JavaScript library handling the authentication operations.</param>
4651
public RemoteAuthenticationService(
4752
IJSRuntime jsRuntime,
48-
IOptions<RemoteAuthenticationOptions<TProviderOptions>> options)
53+
IOptions<RemoteAuthenticationOptions<TProviderOptions>> options,
54+
NavigationManager navigation)
4955
{
5056
_jsRuntime = jsRuntime;
57+
_navigation = navigation;
5158
_options = options.Value;
5259
}
5360

@@ -120,8 +127,24 @@ public virtual async ValueTask<AccessTokenResult> GetAccessToken()
120127
/// <inheritdoc />
121128
public virtual async ValueTask<AccessTokenResult> GetAccessToken(AccessTokenRequestOptions options)
122129
{
130+
if (options is null)
131+
{
132+
throw new ArgumentNullException(nameof(options));
133+
}
134+
123135
await EnsureAuthService();
124-
return await _jsRuntime.InvokeAsync<AccessTokenResult>("AuthenticationService.getAccessToken", options);
136+
var result = await _jsRuntime.InvokeAsync<AccessTokenResult>("AuthenticationService.getAccessToken", options);
137+
138+
var returnUrl = options.ReturnUrl != null ? _navigation.ToAbsoluteUri(options.ReturnUrl).ToString() : null;
139+
var encodedReturnUrl = Uri.EscapeDataString(returnUrl ?? _navigation.Uri);
140+
var redirectUrl = _navigation.ToAbsoluteUri($"{_options.AuthenticationPaths.LogInPath}?returnUrl={encodedReturnUrl}");
141+
142+
if (string.Equals(result.Status, AccessTokenResultStatus.RequiresRedirect, StringComparison.OrdinalIgnoreCase))
143+
{
144+
result.RedirectUrl = redirectUrl.ToString();
145+
}
146+
147+
return result;
125148
}
126149

127150
private async ValueTask<ClaimsPrincipal> GetUser(bool useCache = false)

src/Components/Blazor/WebAssembly.Authentication/test/RemoteAuthenticationServiceTests.cs

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ public async Task RemoteAuthenticationService_SignIn_UpdatesUserOnSuccess()
2020
var options = CreateOptions();
2121
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
2222
testJsRuntime,
23-
options);
23+
options,
24+
new TestNavigationManager());
2425

2526
var state = new RemoteAuthenticationState();
2627
testJsRuntime.SignInResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
@@ -49,7 +50,8 @@ public async Task RemoteAuthenticationService_SignIn_DoesNotUpdateUserOnOtherRes
4950
var options = CreateOptions();
5051
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
5152
testJsRuntime,
52-
options);
53+
options,
54+
new TestNavigationManager());
5355

5456
var state = new RemoteAuthenticationState();
5557
testJsRuntime.SignInResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
@@ -74,7 +76,8 @@ public async Task RemoteAuthenticationService_CompleteSignInAsync_UpdatesUserOnS
7476
var options = CreateOptions();
7577
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
7678
testJsRuntime,
77-
options);
79+
options,
80+
new TestNavigationManager());
7881

7982
var state = new RemoteAuthenticationState();
8083
testJsRuntime.CompleteSignInResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
@@ -103,7 +106,8 @@ public async Task RemoteAuthenticationService_CompleteSignInAsync_DoesNotUpdateU
103106
var options = CreateOptions();
104107
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
105108
testJsRuntime,
106-
options);
109+
options,
110+
new TestNavigationManager());
107111

108112
var state = new RemoteAuthenticationState();
109113
testJsRuntime.CompleteSignInResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
@@ -128,7 +132,8 @@ public async Task RemoteAuthenticationService_SignOut_UpdatesUserOnSuccess()
128132
var options = CreateOptions();
129133
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
130134
testJsRuntime,
131-
options);
135+
options,
136+
new TestNavigationManager());
132137

133138
var state = new RemoteAuthenticationState();
134139
testJsRuntime.SignOutResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
@@ -157,7 +162,8 @@ public async Task RemoteAuthenticationService_SignOut_DoesNotUpdateUserOnOtherRe
157162
var options = CreateOptions();
158163
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
159164
testJsRuntime,
160-
options);
165+
options,
166+
new TestNavigationManager());
161167

162168
var state = new RemoteAuthenticationState();
163169
testJsRuntime.SignOutResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
@@ -182,7 +188,8 @@ public async Task RemoteAuthenticationService_CompleteSignOutAsync_UpdatesUserOn
182188
var options = CreateOptions();
183189
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
184190
testJsRuntime,
185-
options);
191+
options,
192+
new TestNavigationManager());
186193

187194
var state = new RemoteAuthenticationState();
188195
testJsRuntime.CompleteSignOutResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
@@ -211,7 +218,8 @@ public async Task RemoteAuthenticationService_CompleteSignOutAsync_DoesNotUpdate
211218
var options = CreateOptions();
212219
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
213220
testJsRuntime,
214-
options);
221+
options,
222+
new TestNavigationManager());
215223

216224
var state = new RemoteAuthenticationState();
217225
testJsRuntime.CompleteSignOutResult = new RemoteAuthenticationResult<RemoteAuthenticationState>
@@ -236,7 +244,8 @@ public async Task RemoteAuthenticationService_GetAccessToken_ReturnsAccessTokenR
236244
var options = CreateOptions();
237245
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
238246
testJsRuntime,
239-
options);
247+
options,
248+
new TestNavigationManager());
240249

241250
var state = new RemoteAuthenticationState();
242251
testJsRuntime.GetAccessTokenResult = new AccessTokenResult
@@ -269,20 +278,60 @@ public async Task RemoteAuthenticationService_GetAccessToken_PassesDownOptions()
269278
var options = CreateOptions();
270279
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
271280
testJsRuntime,
272-
options);
281+
options,
282+
new TestNavigationManager());
273283

274284
var state = new RemoteAuthenticationState();
275285
testJsRuntime.GetAccessTokenResult = new AccessTokenResult
276286
{
277287
Status = AccessTokenResultStatus.RequiresRedirect,
278-
RedirectUrl = "https://www.example.com/base/auth/login"
279288
};
280289

281290
var tokenOptions = new AccessTokenRequestOptions
282291
{
283292
Scopes = new[] { "something" }
284293
};
285294

295+
var expectedRedirectUrl = "https://www.example.com/base/login?returnUrl=https%3A%2F%2Fwww.example.com%2Fbase%2Fadd-product";
296+
297+
// Act
298+
var result = await runtime.GetAccessToken(tokenOptions);
299+
300+
// Assert
301+
Assert.Equal(
302+
new[] { "AuthenticationService.init", "AuthenticationService.getAccessToken" },
303+
testJsRuntime.PastInvocations.Select(i => i.identifier).ToArray());
304+
305+
Assert.Equal(result, testJsRuntime.GetAccessTokenResult);
306+
Assert.Equal(expectedRedirectUrl, result.RedirectUrl);
307+
Assert.Equal(tokenOptions, (AccessTokenRequestOptions)testJsRuntime.PastInvocations[^1].args[0]);
308+
}
309+
310+
[Fact]
311+
public async Task RemoteAuthenticationService_GetAccessToken_ComputesDefaultReturnUrlOnRequiresRedirect()
312+
{
313+
// Arrange
314+
var testJsRuntime = new TestJsRuntime();
315+
var options = CreateOptions();
316+
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
317+
testJsRuntime,
318+
options,
319+
new TestNavigationManager());
320+
321+
var state = new RemoteAuthenticationState();
322+
testJsRuntime.GetAccessTokenResult = new AccessTokenResult
323+
{
324+
Status = AccessTokenResultStatus.RequiresRedirect,
325+
};
326+
327+
var tokenOptions = new AccessTokenRequestOptions
328+
{
329+
Scopes = new[] { "something" },
330+
ReturnUrl = "https://www.example.com/base/add-saved-product/123413241234"
331+
};
332+
333+
var expectedRedirectUrl = "https://www.example.com/base/login?returnUrl=https%3A%2F%2Fwww.example.com%2Fbase%2Fadd-saved-product%2F123413241234";
334+
286335
// Act
287336
var result = await runtime.GetAccessToken(tokenOptions);
288337

@@ -292,6 +341,7 @@ public async Task RemoteAuthenticationService_GetAccessToken_PassesDownOptions()
292341
testJsRuntime.PastInvocations.Select(i => i.identifier).ToArray());
293342

294343
Assert.Equal(result, testJsRuntime.GetAccessTokenResult);
344+
Assert.Equal(expectedRedirectUrl, result.RedirectUrl);
295345
Assert.Equal(tokenOptions, (AccessTokenRequestOptions)testJsRuntime.PastInvocations[^1].args[0]);
296346
}
297347

@@ -303,7 +353,8 @@ public async Task RemoteAuthenticationService_GetUser_ReturnsAnonymousClaimsPrin
303353
var options = CreateOptions();
304354
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
305355
testJsRuntime,
306-
options);
356+
options,
357+
new TestNavigationManager());
307358

308359
testJsRuntime.GetUserResult = null;
309360

@@ -328,7 +379,8 @@ public async Task RemoteAuthenticationService_GetUser_ReturnsUser_ForAuthenticat
328379
var options = CreateOptions();
329380
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
330381
testJsRuntime,
331-
options);
382+
options,
383+
new TestNavigationManager());
332384

333385
var serializationOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true };
334386
var serializedUser = JsonSerializer.Serialize(new
@@ -360,7 +412,8 @@ public async Task RemoteAuthenticationService_GetUser_DoesNotMapScopesToRoles()
360412
var options = CreateOptions("scope");
361413
var runtime = new RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>(
362414
testJsRuntime,
363-
options);
415+
options,
416+
new TestNavigationManager());
364417

365418
var serializationOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true };
366419
var serializedUser = JsonSerializer.Serialize(new
@@ -402,7 +455,7 @@ private static IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> Create
402455
{
403456
AuthenticationPaths = new RemoteAuthenticationApplicationPathsOptions
404457
{
405-
LogInPath = "a",
458+
LogInPath = "login",
406459
LogInCallbackPath = "a",
407460
LogInFailedPath = "a",
408461
RegisterPath = "a",
@@ -489,4 +542,12 @@ private object GetInvocationResult<TValue>(string identifier)
489542
}
490543
}
491544
}
545+
546+
internal class TestNavigationManager : NavigationManager
547+
{
548+
public TestNavigationManager() =>
549+
Initialize("https://www.example.com/base/", "https://www.example.com/base/add-product");
550+
551+
protected override void NavigateToCore(string uri, bool forceLoad) => throw new NotImplementedException();
552+
}
492553
}

src/Components/Blazor/WebAssembly.Authentication/test/RemoteAuthenticatorCoreTests.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -535,15 +535,19 @@ private static
535535
var remoteAuthenticator = new RemoteAuthenticatorViewCore<RemoteAuthenticationState>();
536536
renderer.Attach(remoteAuthenticator);
537537

538-
remoteAuthenticator.Navigation = new TestNavigationManager(
538+
var navigationManager = new TestNavigationManager(
539539
baseUri,
540540
currentUri);
541+
remoteAuthenticator.Navigation = navigationManager;
541542

542543
remoteAuthenticator.AuthenticationState = new RemoteAuthenticationState();
543544
remoteAuthenticator.ApplicationPaths = new RemoteAuthenticationApplicationPathsOptions();
544545

545546
var jsRuntime = new TestJsRuntime();
546-
var authenticationServiceMock = new TestRemoteAuthenticationService(jsRuntime, Mock.Of<IOptions<RemoteAuthenticationOptions<OidcProviderOptions>>>());
547+
var authenticationServiceMock = new TestRemoteAuthenticationService(
548+
jsRuntime,
549+
Mock.Of<IOptions<RemoteAuthenticationOptions<OidcProviderOptions>>>(),
550+
navigationManager);
547551

548552
remoteAuthenticator.AuthenticationService = authenticationServiceMock;
549553
remoteAuthenticator.AuthenticationProvider = authenticationServiceMock;
@@ -598,7 +602,11 @@ protected override Task OnParametersSetAsync()
598602

599603
private class TestRemoteAuthenticationService : RemoteAuthenticationService<RemoteAuthenticationState, OidcProviderOptions>
600604
{
601-
public TestRemoteAuthenticationService(IJSRuntime jsRuntime, IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> options) : base(jsRuntime, options)
605+
public TestRemoteAuthenticationService(
606+
IJSRuntime jsRuntime,
607+
IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> options,
608+
TestNavigationManager navigationManager) :
609+
base(jsRuntime, options, navigationManager)
602610
{
603611
}
604612

src/Components/Blazor/testassets/Wasm.Authentication.Client/Pages/FetchData.razor

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,14 @@ else
4747

4848
var tokenResult = await AuthenticationService.GetAccessToken();
4949

50-
switch (tokenResult.Status)
50+
if (tokenResult.TryGetAccessToken(out var token))
5151
{
52-
case AccessTokenResultStatus.Success:
53-
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {tokenResult.Token.Value}");
54-
forecasts = await httpClient.GetJsonAsync<WeatherForecast[]>("WeatherForecast");
55-
56-
break;
57-
case AccessTokenResultStatus.RequiresRedirect:
58-
Navigation.NavigateTo(tokenResult.RedirectUrl);
59-
break;
60-
default:
61-
break;
52+
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {tokenResult.Token.Value}");
53+
forecasts = await httpClient.GetJsonAsync<WeatherForecast[]>("WeatherForecast");
54+
}
55+
else
56+
{
57+
Navigation.NavigateTo(tokenResult.RedirectUrl);
6258
}
6359
}
6460
}

0 commit comments

Comments
 (0)