Skip to content

Commit 3e2dcb6

Browse files
committed
Update test app to cover more complex scenarios
1 parent 22934f9 commit 3e2dcb6

22 files changed

+553
-8
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.Text.Json.Serialization;
2+
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
3+
4+
namespace Wasm.Authentication.Client
5+
{
6+
public class OidcAccount : RemoteUserAccount
7+
{
8+
[JsonPropertyName("amr")]
9+
public string[] AuthenticationMethod { get; set; }
10+
}
11+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
@page "/admin-settings"
2+
@attribute [Authorize(Roles = "admin")]
3+
@inject IAccessTokenProvider TokenProvider
4+
@inject NavigationManager Navigation
5+
6+
@if(_error == null)
7+
{
8+
<button id="admin-action" @onclick="AdminAction">Perform admin action</button>
9+
}
10+
else if(_error == true)
11+
{
12+
<p>Could not get the access token.</p>
13+
}
14+
else if (_error == false)
15+
{
16+
<p id="admin-success">Successfully perfomed admin action.</p>
17+
}
18+
19+
@code {
20+
21+
private bool? _error;
22+
23+
public async Task AdminAction()
24+
{
25+
var tokenResult = await TokenProvider.RequestAccessToken();
26+
27+
if (tokenResult.TryGetToken(out var token))
28+
{
29+
var client = new HttpClient() { BaseAddress = new Uri(Navigation.BaseUri) };
30+
var request = new HttpRequestMessage(HttpMethod.Post, "Roles/AdminOnly");
31+
request.Headers.Add("Authorization", $"Bearer {token.Value}");
32+
var response = await client.SendAsync(request);
33+
if (response.StatusCode != System.Net.HttpStatusCode.OK)
34+
{
35+
_error = true;
36+
}
37+
else
38+
{
39+
_error = false;
40+
}
41+
}
42+
else
43+
{
44+
_error = true;
45+
}
46+
}
47+
}
Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,43 @@
11
@page "/authentication/{action}"
2+
@inject StateService State
3+
@inject AuthenticationStateProvider AuthenticationStateProvider
4+
@inject NavigationManager Navigation
25

3-
<RemoteAuthenticatorView Action="@Action" />
6+
<RemoteAuthenticatorViewCore TAuthenticationState="RemoteAppState"
7+
AuthenticationState="AppState"
8+
OnLogInSucceeded="CompleteLogin"
9+
Action="@Action" />
410

511
@code{
612
[Parameter] public string Action { get; set; }
13+
14+
public RemoteAppState AppState { get; set; } = new RemoteAppState();
15+
16+
protected override void OnInitialized()
17+
{
18+
if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn, Action))
19+
{
20+
AppState.State = State.GetCurrentState();
21+
}
22+
23+
base.OnInitialized();
24+
}
25+
26+
public async Task CompleteLogin(RemoteAppState remoteState)
27+
{
28+
if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogInCallback, Action))
29+
{
30+
State.RestoreCurrentState(remoteState.State);
31+
}
32+
33+
var userState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
34+
var user = userState.User;
35+
36+
if (user.HasClaim("NewUser", "true"))
37+
{
38+
var originalReturnUrl = remoteState.ReturnUrl;
39+
var preferencesUrl = Navigation.ToAbsoluteUri("preferences");
40+
remoteState.ReturnUrl = $"{preferencesUrl}?returnUrl={Uri.EscapeDataString(originalReturnUrl)}";
41+
}
42+
}
743
}
Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
@page "/"
2-
2+
@inject StateService State
3+
@inject IJSRuntime JS
34
<h1>Hello, world!</h1>
45

56
Welcome to your new app.
7+
8+
Current state is:
9+
<p id="app-state">@State.GetCurrentState()</p>
10+
11+
<!-- Elements to help testing functionality -->
12+
<p id="test-helpers">
13+
<button id="test-clear-storage" @onclick="ClearStorage">Clear storage</button>
14+
<button id="test-refresh-page" @onclick="TriggerPageRefresh">Refresh page</button>
15+
</p>
16+
17+
@code{
18+
public async Task ClearStorage()
19+
{
20+
await JS.InvokeVoidAsync("sessionStorage.clear");
21+
}
22+
23+
public async Task TriggerPageRefresh()
24+
{
25+
await JS.InvokeVoidAsync("location.reload", true);
26+
}
27+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
@page "/new-admin"
2+
@attribute [Authorize]
3+
@inject IAccessTokenProvider TokenProvider
4+
@inject NavigationManager Navigation
5+
6+
@if (_error == true)
7+
{
8+
<p>Could not get the access token.</p>
9+
}
10+
else if (_error == false)
11+
{
12+
<p>Successfully added to the admin group.</p>
13+
}
14+
15+
@code {
16+
17+
private bool? _error;
18+
19+
protected override async Task OnInitializedAsync()
20+
{
21+
var tokenResult = await TokenProvider.RequestAccessToken();
22+
23+
if (tokenResult.TryGetToken(out var token))
24+
{
25+
var client = new HttpClient() { BaseAddress = new Uri(Navigation.BaseUri) };
26+
var request = new HttpRequestMessage(HttpMethod.Post, "Roles/MakeAdmin");
27+
request.Headers.Add("Authorization", $"Bearer {token.Value}");
28+
var response = await client.SendAsync(request);
29+
response.EnsureSuccessStatusCode();
30+
31+
}
32+
else
33+
{
34+
_error = true;
35+
}
36+
}
37+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
@page "/preferences"
2+
@attribute [Authorize]
3+
@using System.Text.Json;
4+
@using System.Text;
5+
@using System.Net.Http.Headers;
6+
7+
@inject NavigationManager Navigation
8+
@inject IAccessTokenProvider AccessTokenProvider
9+
10+
<p>User preferences</p>
11+
12+
<input id="color-preference" type="text" @bind="Color" />
13+
<button id="submit-preference" @onclick="SendPreferences">Send</button>
14+
15+
@code {
16+
public string Color { get; set; }
17+
18+
public async Task SendPreferences()
19+
{
20+
var content = new StringContent(JsonSerializer.Serialize(new UserPreferences { Color = Color }), Encoding.UTF8, "application/json");
21+
var tokenResponse = await AccessTokenProvider.RequestAccessToken();
22+
if (tokenResponse.TryGetToken(out var token))
23+
{
24+
var client = new HttpClient { BaseAddress = new Uri(Navigation.BaseUri) };
25+
var request = new HttpRequestMessage(HttpMethod.Post, "Preferences/AddPreferences");
26+
request.Content = content;
27+
28+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Value);
29+
30+
var response = await client.SendAsync(request);
31+
if (response.IsSuccessStatusCode)
32+
{
33+
var query = new Uri(Navigation.Uri).Query;
34+
var hasReturnUrl = System.Text.RegularExpressions.Regex.Match(query, ".*?returnUrl=([^&]+).*");
35+
if (hasReturnUrl.Success)
36+
{
37+
var returnUrl = hasReturnUrl.Groups[1];
38+
Navigation.NavigateTo(Uri.UnescapeDataString(returnUrl.Value));
39+
}
40+
}
41+
else
42+
{
43+
Navigation.NavigateTo("/");
44+
}
45+
}
46+
}
47+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System;
2+
using System.Net;
3+
using System.Net.Http;
4+
using System.Net.Http.Headers;
5+
using System.Security.Claims;
6+
using System.Text.Json;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Components;
9+
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
10+
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
11+
12+
namespace Wasm.Authentication.Client
13+
{
14+
public class PreferencesUserFactory : UserFactory<OidcAccount>
15+
{
16+
private readonly HttpClient _httpClient;
17+
18+
public PreferencesUserFactory(NavigationManager navigationManager, IAccessTokenProviderAccessor accessor)
19+
: base(accessor)
20+
{
21+
_httpClient = new HttpClient { BaseAddress = new Uri(navigationManager.BaseUri) };
22+
}
23+
24+
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
25+
OidcAccount account,
26+
RemoteAuthenticationUserOptions options)
27+
{
28+
var initialUser = await base.CreateUserAsync(account, options);
29+
30+
if (initialUser.Identity.IsAuthenticated)
31+
{
32+
foreach (var value in account.AuthenticationMethod)
33+
{
34+
((ClaimsIdentity)initialUser.Identity).AddClaim(new Claim("amr", value));
35+
}
36+
37+
var tokenResponse = await TokenProvider.RequestAccessToken();
38+
if (tokenResponse.TryGetToken(out var token))
39+
{
40+
var request = new HttpRequestMessage(HttpMethod.Get, "Preferences/HasCompletedAdditionalInformation");
41+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Value);
42+
43+
var response = await _httpClient.SendAsync(request);
44+
if (response.StatusCode != HttpStatusCode.OK)
45+
{
46+
throw new InvalidOperationException("Error accessing additional user info.");
47+
}
48+
49+
var hasInfo = JsonSerializer.Deserialize<bool>(await response.Content.ReadAsStringAsync());
50+
if (!hasInfo)
51+
{
52+
// The actual pattern would be to cache this info to avoid constant queries to the server per auth update.
53+
// (By default once every minute)
54+
((ClaimsIdentity)initialUser.Identity).AddClaim(new Claim("NewUser", "true"));
55+
}
56+
}
57+
}
58+
59+
return initialUser;
60+
}
61+
}
62+
}

src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Program.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
56
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
67
using Microsoft.Extensions.DependencyInjection;
78

@@ -13,7 +14,10 @@ public static async Task Main(string[] args)
1314
{
1415
var builder = WebAssemblyHostBuilder.CreateDefault(args);
1516

16-
builder.Services.AddApiAuthorization();
17+
builder.Services.AddApiAuthorization<RemoteAppState, OidcAccount>()
18+
.AddUserFactory<RemoteAppState, OidcAccount, PreferencesUserFactory>();
19+
20+
builder.Services.AddSingleton<StateService>();
1721

1822
builder.RootComponents.Add<App>("app");
1923

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
6+
7+
namespace Wasm.Authentication.Client
8+
{
9+
public class RemoteAppState : RemoteAuthenticationState
10+
{
11+
public string State { get; set; }
12+
}
13+
}

src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Shared/NavMenu.razor

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@
2727
<span class="oi oi-list-rich" aria-hidden="true"></span> User
2828
</NavLink>
2929
</li>
30+
<li class="nav-item px-3">
31+
<NavLink class="nav-link" href="new-admin">
32+
<span class="oi oi-list-rich" aria-hidden="true"></span> Make admin
33+
</NavLink>
34+
</li>
35+
<li class="nav-item px-3">
36+
<NavLink class="nav-link" href="admin-settings">
37+
<span class="oi oi-list-rich" aria-hidden="true"></span> Settings
38+
</NavLink>
39+
</li>
40+
3041
</ul>
3142
</div>
3243

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
6+
namespace Wasm.Authentication.Client
7+
{
8+
public class StateService
9+
{
10+
private string _state;
11+
12+
public string GetCurrentState() => _state != null ? _state : (_state = Guid.NewGuid().ToString());
13+
14+
public void RestoreCurrentState(string state) => _state = state;
15+
}
16+
}

src/Components/WebAssembly/testassets/Wasm.Authentication.Client/wwwroot/index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,4 @@
2121
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
2222
<script src="_framework/blazor.webassembly.js"></script>
2323
</body>
24-
2524
</html>

0 commit comments

Comments
 (0)