From 8f5fd0e249e6f2c3a00276cd6331fea1f0c7f733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Wed, 1 Apr 2026 16:22:22 +0200 Subject: [PATCH] Remove the principal caching logic from HostAuthenticationStateProvider and enforce antiforgery for all requests --- OpenIddict.Samples.slnx | 2 - .../Balosar.Client/Balosar.Client.csproj | 4 - .../Balosar.Client/Pages/FetchData.razor | 11 ++- samples/Balosar/Balosar.Client/Program.cs | 10 +-- .../Balosar.Server/Balosar.Server.csproj | 1 - .../Controllers/AuthorizationController.cs | 6 +- .../Controllers/WeatherForecastController.cs | 2 +- .../Models}/WeatherForecast.cs | 2 +- .../Balosar.Shared/Balosar.Shared.csproj | 11 --- samples/Dantooine/Dantooine.Api/Program.cs | 2 +- .../Controllers/AuthorizationController.cs | 6 +- samples/Dantooine/Dantooine.Server/Program.cs | 11 ++- .../Dantooine.WebAssembly.Client.csproj | 4 - .../Pages/DantooineApi.razor | 43 ---------- .../Pages/DirectApi.razor | 43 ---------- .../Pages/DownstreamApi.razor | 58 +++++++++++++ .../Pages/LocalApi.razor | 59 +++++++++++++ .../Dantooine.WebAssembly.Client/Program.cs | 25 ++---- .../Services/AuthorizedHandler.cs | 39 --------- .../HostAuthenticationStateProvider.cs | 86 ++++--------------- .../Shared/NavMenu.razor | 8 +- .../wwwroot/antiForgeryToken.js | 2 +- .../Controllers/DirectApiController.cs | 18 ---- .../Controllers/UserController.cs | 58 ------------- .../Dantooine.WebAssembly.Server.csproj | 1 - .../Dantooine.WebAssembly.Server/Program.cs | 41 ++++++++- .../appsettings.json | 2 +- .../Authorization/ClaimValue.cs | 15 ---- .../Authorization/UserInfo.cs | 15 ---- .../Dantooine.WebAssembly.Shared.csproj | 10 --- .../Account/RegisterExternalLogin.aspx.cs | 2 +- .../Controllers/AuthorizationController.cs | 6 +- .../Controllers/AuthorizationController.cs | 2 +- .../Controllers/AuthorizationController.cs | 4 +- .../Controllers/AuthorizationController.cs | 4 +- samples/Mimban/Mimban.Server/Program.cs | 3 +- .../Controllers/AccountController.cs | 2 +- .../Controllers/AuthorizationController.cs | 6 +- samples/Zirku/Zirku.Api1/Program.cs | 2 +- samples/Zirku/Zirku.Api2/Program.cs | 2 +- samples/Zirku/Zirku.Server/Program.cs | 48 ++++------- 41 files changed, 245 insertions(+), 431 deletions(-) rename samples/Balosar/{Balosar.Shared => Balosar.Server/Models}/WeatherForecast.cs (86%) delete mode 100644 samples/Balosar/Balosar.Shared/Balosar.Shared.csproj delete mode 100644 samples/Dantooine/Dantooine.WebAssembly.Client/Pages/DantooineApi.razor delete mode 100644 samples/Dantooine/Dantooine.WebAssembly.Client/Pages/DirectApi.razor create mode 100644 samples/Dantooine/Dantooine.WebAssembly.Client/Pages/DownstreamApi.razor create mode 100644 samples/Dantooine/Dantooine.WebAssembly.Client/Pages/LocalApi.razor delete mode 100644 samples/Dantooine/Dantooine.WebAssembly.Client/Services/AuthorizedHandler.cs delete mode 100644 samples/Dantooine/Dantooine.WebAssembly.Server/Controllers/DirectApiController.cs delete mode 100644 samples/Dantooine/Dantooine.WebAssembly.Server/Controllers/UserController.cs delete mode 100644 samples/Dantooine/Dantooine.WebAssembly.Shared/Authorization/ClaimValue.cs delete mode 100644 samples/Dantooine/Dantooine.WebAssembly.Shared/Authorization/UserInfo.cs delete mode 100644 samples/Dantooine/Dantooine.WebAssembly.Shared/Dantooine.WebAssembly.Shared.csproj diff --git a/OpenIddict.Samples.slnx b/OpenIddict.Samples.slnx index 5c83e07c7..5c416e931 100644 --- a/OpenIddict.Samples.slnx +++ b/OpenIddict.Samples.slnx @@ -31,7 +31,6 @@ - @@ -43,7 +42,6 @@ - diff --git a/samples/Balosar/Balosar.Client/Balosar.Client.csproj b/samples/Balosar/Balosar.Client/Balosar.Client.csproj index 985594ed6..840f9a8d2 100644 --- a/samples/Balosar/Balosar.Client/Balosar.Client.csproj +++ b/samples/Balosar/Balosar.Client/Balosar.Client.csproj @@ -5,10 +5,6 @@ true - - - - diff --git a/samples/Balosar/Balosar.Client/Pages/FetchData.razor b/samples/Balosar/Balosar.Client/Pages/FetchData.razor index 881ba4fc9..a91e0d069 100644 --- a/samples/Balosar/Balosar.Client/Pages/FetchData.razor +++ b/samples/Balosar/Balosar.Client/Pages/FetchData.razor @@ -1,7 +1,6 @@ @page "/fetchdata" @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@using Balosar.Shared @attribute [Authorize] @inject HttpClient Http @@ -53,4 +52,14 @@ else } } + class WeatherForecast + { + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public required string Summary { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } } diff --git a/samples/Balosar/Balosar.Client/Program.cs b/samples/Balosar/Balosar.Client/Program.cs index 8447d551d..b8c8374b5 100644 --- a/samples/Balosar/Balosar.Client/Program.cs +++ b/samples/Balosar/Balosar.Client/Program.cs @@ -1,21 +1,15 @@ using Balosar.Client; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.Extensions.Options; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); -builder.Services.AddHttpClient("Balosar.ServerAPI") +builder.Services.AddHttpClient(Options.DefaultName) .ConfigureHttpClient(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)) .AddHttpMessageHandler(); -// Supply HttpClient instances that include access tokens when making requests to the server project. -builder.Services.AddScoped(provider => -{ - var factory = provider.GetRequiredService(); - return factory.CreateClient("Balosar.ServerAPI"); -}); - builder.Services.AddOidcAuthentication(options => { options.ProviderOptions.ClientId = "balosar-blazor-client"; diff --git a/samples/Balosar/Balosar.Server/Balosar.Server.csproj b/samples/Balosar/Balosar.Server/Balosar.Server.csproj index a2a564dad..1c2e43a16 100644 --- a/samples/Balosar/Balosar.Server/Balosar.Server.csproj +++ b/samples/Balosar/Balosar.Server/Balosar.Server.csproj @@ -7,7 +7,6 @@ - diff --git a/samples/Balosar/Balosar.Server/Controllers/AuthorizationController.cs b/samples/Balosar/Balosar.Server/Controllers/AuthorizationController.cs index 72a5fe0c0..60a093f1d 100644 --- a/samples/Balosar/Balosar.Server/Controllers/AuthorizationController.cs +++ b/samples/Balosar/Balosar.Server/Controllers/AuthorizationController.cs @@ -138,7 +138,7 @@ public async Task Authorize() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); // Note: in this sample, the granted scopes match the requested scope // but you may want to allow the user to uncheck specific scopes. @@ -232,7 +232,7 @@ public async Task Accept() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); // Note: in this sample, the granted scopes match the requested scope // but you may want to allow the user to uncheck specific scopes. @@ -332,7 +332,7 @@ public async Task Exchange() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); identity.SetDestinations(GetDestinations); diff --git a/samples/Balosar/Balosar.Server/Controllers/WeatherForecastController.cs b/samples/Balosar/Balosar.Server/Controllers/WeatherForecastController.cs index 75b3c6d74..9e5997f9e 100644 --- a/samples/Balosar/Balosar.Server/Controllers/WeatherForecastController.cs +++ b/samples/Balosar/Balosar.Server/Controllers/WeatherForecastController.cs @@ -1,4 +1,4 @@ -using Balosar.Shared; +using Balosar.Server.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using OpenIddict.Validation.AspNetCore; diff --git a/samples/Balosar/Balosar.Shared/WeatherForecast.cs b/samples/Balosar/Balosar.Server/Models/WeatherForecast.cs similarity index 86% rename from samples/Balosar/Balosar.Shared/WeatherForecast.cs rename to samples/Balosar/Balosar.Server/Models/WeatherForecast.cs index 25b2492c2..23cd83b10 100644 --- a/samples/Balosar/Balosar.Shared/WeatherForecast.cs +++ b/samples/Balosar/Balosar.Server/Models/WeatherForecast.cs @@ -1,4 +1,4 @@ -namespace Balosar.Shared; +namespace Balosar.Server.Models; public class WeatherForecast { diff --git a/samples/Balosar/Balosar.Shared/Balosar.Shared.csproj b/samples/Balosar/Balosar.Shared/Balosar.Shared.csproj deleted file mode 100644 index acda77487..000000000 --- a/samples/Balosar/Balosar.Shared/Balosar.Shared.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - $(NetCoreTargetFramework) - - - - - - - diff --git a/samples/Dantooine/Dantooine.Api/Program.cs b/samples/Dantooine/Dantooine.Api/Program.cs index 5859ce87b..0f6e119b4 100644 --- a/samples/Dantooine/Dantooine.Api/Program.cs +++ b/samples/Dantooine/Dantooine.Api/Program.cs @@ -46,7 +46,7 @@ app.UseAuthentication(); app.UseAuthorization(); -app.MapGet("api/DantooineApi", [Authorize] () => new string[] { "data1", "data2" }); +app.MapGet("api/downstream-api", () => new[] { "data1", "data2" }).RequireAuthorization(); app.Run(); diff --git a/samples/Dantooine/Dantooine.Server/Controllers/AuthorizationController.cs b/samples/Dantooine/Dantooine.Server/Controllers/AuthorizationController.cs index d0b55b57e..ee00e3a31 100644 --- a/samples/Dantooine/Dantooine.Server/Controllers/AuthorizationController.cs +++ b/samples/Dantooine/Dantooine.Server/Controllers/AuthorizationController.cs @@ -138,7 +138,7 @@ public async Task Authorize() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); // Note: in this sample, the granted scopes match the requested scope // but you may want to allow the user to uncheck specific scopes. @@ -232,7 +232,7 @@ public async Task Accept() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); // Note: in this sample, the granted scopes match the requested scope // but you may want to allow the user to uncheck specific scopes. @@ -332,7 +332,7 @@ public async Task Exchange() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); identity.SetDestinations(GetDestinations); diff --git a/samples/Dantooine/Dantooine.Server/Program.cs b/samples/Dantooine/Dantooine.Server/Program.cs index 9f2d22dc5..ef3309a15 100644 --- a/samples/Dantooine/Dantooine.Server/Program.cs +++ b/samples/Dantooine/Dantooine.Server/Program.cs @@ -177,7 +177,7 @@ static async Task RegisterApplicationsAsync(IServiceProvider provider) // Blazor Hosted if (await manager.FindByClientIdAsync("blazorcodeflowpkceclient") is null) { - await manager.CreateAsync(new OpenIddictApplicationDescriptor + var descriptor = new OpenIddictApplicationDescriptor { ClientId = "blazorcodeflowpkceclient", ConsentType = ConsentTypes.Explicit, @@ -217,14 +217,17 @@ await manager.CreateAsync(new OpenIddictApplicationDescriptor Permissions.ResponseTypes.Code, Permissions.Scopes.Email, Permissions.Scopes.Profile, - Permissions.Scopes.Roles, - Permissions.Prefixes.Scope + "api1" + Permissions.Scopes.Roles }, Requirements = { Requirements.Features.ProofKeyForCodeExchange } - }); + }; + + descriptor.AddScopePermissions("api1"); + + await manager.CreateAsync(descriptor); } } diff --git a/samples/Dantooine/Dantooine.WebAssembly.Client/Dantooine.WebAssembly.Client.csproj b/samples/Dantooine/Dantooine.WebAssembly.Client/Dantooine.WebAssembly.Client.csproj index 2888670e4..fd0745b25 100644 --- a/samples/Dantooine/Dantooine.WebAssembly.Client/Dantooine.WebAssembly.Client.csproj +++ b/samples/Dantooine/Dantooine.WebAssembly.Client/Dantooine.WebAssembly.Client.csproj @@ -12,8 +12,4 @@ - - - - diff --git a/samples/Dantooine/Dantooine.WebAssembly.Client/Pages/DantooineApi.razor b/samples/Dantooine/Dantooine.WebAssembly.Client/Pages/DantooineApi.razor deleted file mode 100644 index a30ea7d4d..000000000 --- a/samples/Dantooine/Dantooine.WebAssembly.Client/Pages/DantooineApi.razor +++ /dev/null @@ -1,43 +0,0 @@ -@page "/dantooineapi1" -@using Dantooine.WebAssembly.Shared -@inject IHttpClientFactory httpClientFactory -@inject IJSRuntime JSRuntime - -

Data retrieved from Dantooine API via YARP

- -@if (apiData == null) -{ -

Loading...

-} -else -{ - - - - - - - - @foreach (var data in apiData) - { - - - - } - -
Name
@data
-} - -@code { - private string[] apiData = default!; - - protected override async Task OnInitializedAsync() - { - var token = await JSRuntime.InvokeAsync("getAntiForgeryToken"); - - var client = httpClientFactory.CreateClient("authorizedClient"); - client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", token); - - apiData = (await client.GetFromJsonAsync("api/DantooineApi"))!; - } -} diff --git a/samples/Dantooine/Dantooine.WebAssembly.Client/Pages/DirectApi.razor b/samples/Dantooine/Dantooine.WebAssembly.Client/Pages/DirectApi.razor deleted file mode 100644 index e13995564..000000000 --- a/samples/Dantooine/Dantooine.WebAssembly.Client/Pages/DirectApi.razor +++ /dev/null @@ -1,43 +0,0 @@ -@page "/directapi" -@inject IHttpClientFactory httpClientFactory -@inject IJSRuntime JSRuntime - -

Data from Direct API

- -@if (apiData == null) -{ -

Loading...

-} -else -{ - - - - - - - - @foreach (var data in apiData) - { - - - - } - -
Data
@data
-} - -@code { - private string[] apiData = default!; - - protected override async Task OnInitializedAsync() - { - var token = await JSRuntime.InvokeAsync("getAntiForgeryToken"); - - var client = httpClientFactory.CreateClient("authorizedClient"); - client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", token); - - apiData = (await client.GetFromJsonAsync("api/DirectApi"))!; - } - -} \ No newline at end of file diff --git a/samples/Dantooine/Dantooine.WebAssembly.Client/Pages/DownstreamApi.razor b/samples/Dantooine/Dantooine.WebAssembly.Client/Pages/DownstreamApi.razor new file mode 100644 index 000000000..0813a4249 --- /dev/null +++ b/samples/Dantooine/Dantooine.WebAssembly.Client/Pages/DownstreamApi.razor @@ -0,0 +1,58 @@ +@page "/downstream-api" +@using System.Net +@inject HttpClient client +@inject NavigationManager manager +@inject AuthenticationStateProvider provider +@inject IJSRuntime runtime + +

Data retrieved from downstream API via YARP

+ +@if (data is null) +{ +

Loading...

+} +else +{ + + + + + + + + @for (var index = 0; index < data.Length; index++) + { + + + + } + +
Name
@data[index]
+} + +@code { + private string[] data = default!; + + protected override async Task OnInitializedAsync() + { + var state = await provider.GetAuthenticationStateAsync(); + if (state is not { User.Identity.IsAuthenticated: true }) + { + manager.NavigateTo($"login?returnUrl=/{Uri.EscapeDataString(manager.ToBaseRelativePath(manager.Uri))}", true); + return; + } + + client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", await runtime.InvokeAsync("getAntiForgeryToken")); + + try + { + data = (await client.GetFromJsonAsync("api/downstream-api"))!; + } + + catch (HttpRequestException exception) when (exception.StatusCode is HttpStatusCode.Unauthorized) + { + manager.NavigateTo($"login?returnUrl=/{Uri.EscapeDataString(manager.ToBaseRelativePath(manager.Uri))}", true); + return; + } + } +} diff --git a/samples/Dantooine/Dantooine.WebAssembly.Client/Pages/LocalApi.razor b/samples/Dantooine/Dantooine.WebAssembly.Client/Pages/LocalApi.razor new file mode 100644 index 000000000..e2e1270f3 --- /dev/null +++ b/samples/Dantooine/Dantooine.WebAssembly.Client/Pages/LocalApi.razor @@ -0,0 +1,59 @@ +@page "/local-api" +@using System.Net +@inject HttpClient client +@inject NavigationManager manager +@inject AuthenticationStateProvider provider +@inject IJSRuntime runtime + +

Data from local API

+ +@if (data is null) +{ +

Loading...

+} +else +{ + + + + + + + + @for (var index = 0; index < data.Length; index++) + { + + + + } + +
Data
@data[index]
+} + +@code { + private string[] data = default!; + + protected override async Task OnInitializedAsync() + { + var state = await provider.GetAuthenticationStateAsync(); + if (state is not { User.Identity.IsAuthenticated: true }) + { + manager.NavigateTo($"login?returnUrl=/{Uri.EscapeDataString(manager.ToBaseRelativePath(manager.Uri))}", true); + return; + } + + client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", await runtime.InvokeAsync("getAntiForgeryToken")); + + try + { + data = (await client.GetFromJsonAsync("api/local-api"))!; + } + + catch (HttpRequestException exception) when (exception.StatusCode is HttpStatusCode.Unauthorized) + { + manager.NavigateTo($"login?returnUrl=/{Uri.EscapeDataString(manager.ToBaseRelativePath(manager.Uri))}", true); + return; + } + } + +} \ No newline at end of file diff --git a/samples/Dantooine/Dantooine.WebAssembly.Client/Program.cs b/samples/Dantooine/Dantooine.WebAssembly.Client/Program.cs index edc88dfd0..d931d7206 100644 --- a/samples/Dantooine/Dantooine.WebAssembly.Client/Program.cs +++ b/samples/Dantooine/Dantooine.WebAssembly.Client/Program.cs @@ -1,34 +1,21 @@ -using System.Net.Http.Headers; -using Dantooine.WebAssembly.Client; +using Dantooine.WebAssembly.Client; using Dantooine.WebAssembly.Client.Services; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.Services.AddOptions(); builder.Services.AddAuthorizationCore(); -builder.Services.TryAddSingleton(); -builder.Services.TryAddSingleton(provider => (HostAuthenticationStateProvider) provider.GetRequiredService()); -builder.Services.AddTransient(); - -builder.RootComponents.Add("#app"); -builder.Services.AddHttpClient("default", client => -{ - client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); -}); +builder.Services.AddHttpClient(Options.DefaultName) + .ConfigureHttpClient(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)); -builder.Services.AddHttpClient("authorizedClient", client => -{ - client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); -}).AddHttpMessageHandler(); +builder.Services.TryAddSingleton(); -builder.Services.AddTransient(provider => provider.GetRequiredService().CreateClient("default")); +builder.RootComponents.Add("#app"); var host = builder.Build(); - await host.RunAsync(); \ No newline at end of file diff --git a/samples/Dantooine/Dantooine.WebAssembly.Client/Services/AuthorizedHandler.cs b/samples/Dantooine/Dantooine.WebAssembly.Client/Services/AuthorizedHandler.cs deleted file mode 100644 index 98bf6c0f0..000000000 --- a/samples/Dantooine/Dantooine.WebAssembly.Client/Services/AuthorizedHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Net; - -namespace Dantooine.WebAssembly.Client.Services; - -// Original source: https://github.com/berhir/BlazorWebAssemblyCookieAuth. -public class AuthorizedHandler : DelegatingHandler -{ - private readonly HostAuthenticationStateProvider _authenticationStateProvider; - - public AuthorizedHandler(HostAuthenticationStateProvider authenticationStateProvider) - { - _authenticationStateProvider = authenticationStateProvider; - } - - protected override async Task SendAsync( - HttpRequestMessage request, - CancellationToken cancellationToken) - { - var authState = await _authenticationStateProvider.GetAuthenticationStateAsync(); - HttpResponseMessage responseMessage; - if (authState.User is not { Identity.IsAuthenticated: true }) - { - // if user is not authenticated, immediately set response status to 401 Unauthorized - responseMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized); - } - else - { - responseMessage = await base.SendAsync(request, cancellationToken); - } - - if (responseMessage.StatusCode == HttpStatusCode.Unauthorized) - { - // if server returned 401 Unauthorized, redirect to login page - _authenticationStateProvider.SignIn(); - } - - return responseMessage; - } -} diff --git a/samples/Dantooine/Dantooine.WebAssembly.Client/Services/HostAuthenticationStateProvider.cs b/samples/Dantooine/Dantooine.WebAssembly.Client/Services/HostAuthenticationStateProvider.cs index 900d220c2..f9d11b7de 100644 --- a/samples/Dantooine/Dantooine.WebAssembly.Client/Services/HostAuthenticationStateProvider.cs +++ b/samples/Dantooine/Dantooine.WebAssembly.Client/Services/HostAuthenticationStateProvider.cs @@ -1,92 +1,36 @@ -using System.Net.Http.Json; +using System.Net; using System.Security.Claims; -using Dantooine.WebAssembly.Shared.Authorization; -using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; namespace Dantooine.WebAssembly.Client.Services; -// Original source: https://github.com/berhir/BlazorWebAssemblyCookieAuth. -public class HostAuthenticationStateProvider : AuthenticationStateProvider +public class HostAuthenticationStateProvider(HttpClient client) : AuthenticationStateProvider { - private static readonly TimeSpan _userCacheRefreshInterval = TimeSpan.FromSeconds(60); - - private const string LogInPath = "login"; - - private readonly NavigationManager _navigation; - private readonly HttpClient _client; - private readonly ILogger _logger; - - private DateTimeOffset _userLastCheck = DateTimeOffset.FromUnixTimeSeconds(0); - private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity()); - - public HostAuthenticationStateProvider(NavigationManager navigation, HttpClient client, ILogger logger) - { - _navigation = navigation; - _client = client; - _logger = logger; - } - public override async Task GetAuthenticationStateAsync() { - return new AuthenticationState(await GetUser(useCache: true)); - } + // Note: the resulting authentication state is deliberately not cached, to ensure that the logged-in + // user information is refreshed when navigating to a different page. While a caching strategy can be + // implemented, doing so must be done with care to ensure that the cached authentication state is + // invalidated when the user logs out, and that the cache is refreshed when the user logs in again. - public void SignIn(string? customReturnUrl = null) - { - var returnUrl = customReturnUrl != null ? _navigation.ToAbsoluteUri(customReturnUrl).ToString() : null; - var encodedReturnUrl = Uri.EscapeDataString(_navigation.ToBaseRelativePath(returnUrl ?? _navigation.Uri)); - var logInUrl = _navigation.ToAbsoluteUri($"{LogInPath}?returnUrl=/{encodedReturnUrl}"); - _navigation.NavigateTo(logInUrl.ToString(), true); - } - - private async ValueTask GetUser(bool useCache = false) - { - var now = DateTimeOffset.Now; - if (useCache && now < _userLastCheck + _userCacheRefreshInterval) - { - _logger.LogDebug("Taking user from cache"); - return _cachedUser; - } - - _logger.LogDebug("Fetching user"); - _cachedUser = await FetchUser(); - _userLastCheck = now; - - return _cachedUser; - } - - private async Task FetchUser() - { - UserInfo? user = null; + string name; try { - user = await _client.GetFromJsonAsync("api/User"); - } - catch (Exception exc) - { - _logger.LogWarning(exc, "Fetching user failed."); + name = await client.GetStringAsync("api/current-user-name"); } - if (user == null || !user.IsAuthenticated) + catch (HttpRequestException exception) when (exception.StatusCode is HttpStatusCode.Unauthorized) { - return new ClaimsPrincipal(new ClaimsIdentity()); + return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } - var identity = new ClaimsIdentity( - nameof(HostAuthenticationStateProvider), - user.NameClaimType, - user.RoleClaimType); + var identity = new ClaimsIdentity("Cookies"); + identity.AddClaim(new Claim(ClaimTypes.Name, name)); - if (user.Claims != null) - { - foreach (var claim in user.Claims) - { - identity.AddClaim(new Claim(claim.Type, claim.Value)); - } - } + var state = new AuthenticationState(new ClaimsPrincipal(identity)); + NotifyAuthenticationStateChanged(Task.FromResult(state)); - return new ClaimsPrincipal(identity); + return state; } } diff --git a/samples/Dantooine/Dantooine.WebAssembly.Client/Shared/NavMenu.razor b/samples/Dantooine/Dantooine.WebAssembly.Client/Shared/NavMenu.razor index 7ae0e8a17..ceac80124 100644 --- a/samples/Dantooine/Dantooine.WebAssembly.Client/Shared/NavMenu.razor +++ b/samples/Dantooine/Dantooine.WebAssembly.Client/Shared/NavMenu.razor @@ -16,13 +16,13 @@ diff --git a/samples/Dantooine/Dantooine.WebAssembly.Client/wwwroot/antiForgeryToken.js b/samples/Dantooine/Dantooine.WebAssembly.Client/wwwroot/antiForgeryToken.js index ad81e5ad7..335582d4c 100644 --- a/samples/Dantooine/Dantooine.WebAssembly.Client/wwwroot/antiForgeryToken.js +++ b/samples/Dantooine/Dantooine.WebAssembly.Client/wwwroot/antiForgeryToken.js @@ -5,6 +5,6 @@ function getAntiForgeryToken() { return elements[0].value } - console.warn('no anti forgery token found!'); + console.warn('No anti forgery token was found in the document.'); return null; } \ No newline at end of file diff --git a/samples/Dantooine/Dantooine.WebAssembly.Server/Controllers/DirectApiController.cs b/samples/Dantooine/Dantooine.WebAssembly.Server/Controllers/DirectApiController.cs deleted file mode 100644 index 34b83ccf8..000000000 --- a/samples/Dantooine/Dantooine.WebAssembly.Server/Controllers/DirectApiController.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Dantooine.WebAssembly.Server.Controllers; - -[ValidateAntiForgeryToken] -[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)] -[ApiController] -[Route("api/[controller]")] -public class DirectApiController : ControllerBase -{ - [HttpGet] - public IEnumerable Get() - { - return new List { "some data", "more data", "loads of data" }; - } -} diff --git a/samples/Dantooine/Dantooine.WebAssembly.Server/Controllers/UserController.cs b/samples/Dantooine/Dantooine.WebAssembly.Server/Controllers/UserController.cs deleted file mode 100644 index f6c9a5b2e..000000000 --- a/samples/Dantooine/Dantooine.WebAssembly.Server/Controllers/UserController.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Security.Claims; -using Dantooine.WebAssembly.Shared.Authorization; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using static OpenIddict.Abstractions.OpenIddictConstants; - -namespace Dantooine.WebAssembly.Server.Controllers; - -// Original source: https://github.com/berhir/BlazorWebAssemblyCookieAuth. -[Route("api/[controller]")] -[ApiController] -public class UserController : ControllerBase -{ - [HttpGet] - [AllowAnonymous] - public IActionResult GetCurrentUser() - { - return Ok(User.Identity is { IsAuthenticated: true } ? CreateUserInfo(User) : UserInfo.Anonymous); - } - - private static UserInfo CreateUserInfo(ClaimsPrincipal principal) - { - if (principal.Identity is not { IsAuthenticated: true }) - { - return UserInfo.Anonymous; - } - - var userinfo = new UserInfo - { - IsAuthenticated = true - }; - - if (principal.Identity is ClaimsIdentity identity) - { - userinfo.NameClaimType = identity.NameClaimType; - userinfo.RoleClaimType = identity.RoleClaimType; - } - else - { - userinfo.NameClaimType = Claims.Name; - userinfo.RoleClaimType = Claims.Role; - } - - if (principal.Claims.Any()) - { - var claims = new List(); - - foreach (var claim in principal.FindAll(userinfo.NameClaimType)) - { - claims.Add(new ClaimValue(userinfo.NameClaimType, claim.Value)); - } - - userinfo.Claims = claims; - } - - return userinfo; - } -} diff --git a/samples/Dantooine/Dantooine.WebAssembly.Server/Dantooine.WebAssembly.Server.csproj b/samples/Dantooine/Dantooine.WebAssembly.Server/Dantooine.WebAssembly.Server.csproj index eecd78aa4..a7440c4cd 100644 --- a/samples/Dantooine/Dantooine.WebAssembly.Server/Dantooine.WebAssembly.Server.csproj +++ b/samples/Dantooine/Dantooine.WebAssembly.Server/Dantooine.WebAssembly.Server.csproj @@ -18,7 +18,6 @@ - diff --git a/samples/Dantooine/Dantooine.WebAssembly.Server/Program.cs b/samples/Dantooine/Dantooine.WebAssembly.Server/Program.cs index 8b5775e20..f4cc146d6 100644 --- a/samples/Dantooine/Dantooine.WebAssembly.Server/Program.cs +++ b/samples/Dantooine/Dantooine.WebAssembly.Server/Program.cs @@ -1,7 +1,9 @@ using System.Globalization; +using System.Security.Claims; using System.Security.Cryptography; using Dantooine.WebAssembly.Server.Helpers; using Dantooine.WebAssembly.Server.Models; +using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.EntityFrameworkCore; @@ -255,6 +257,34 @@ app.UseAuthentication(); app.UseAuthorization(); +app.Use(async (context, next) => +{ + // Note: the antiforgery middleware only validates the antiforgery token for POST, PUT and PATCH requests. + // In this sample, the antiforgery token is validated for all requests sent to /api (except the special + // /api/current-user-name endpoint) to detect cases where the user logged in under a different identity in + // another tab and tries to perform actions in a tab that wasn't refreshed: since the antiforgery token + // is always bound to the user's identity contained in the authentication cookie, the validation will fail + // and the user agent will be automatically redirected to the login page to refresh the user information. + + if (context.Request.Path.StartsWithSegments("/api") && context.Request.Path != "/api/current-user-name") + { + var service = context.RequestServices.GetRequiredService(); + + try + { + await service.ValidateRequestAsync(context); + } + + catch (AntiforgeryValidationException) + { + await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme); + return; + } + } + + await next(context); +}); + app.MapRazorPages(); app.MapControllers(); @@ -262,9 +292,18 @@ // the user agent to the login page configured in the cookie authentication options. // In this case, this behavior is not desirable as an HTTP 401 response MUST be // returned to the WASM client to automatically redirect the user agent to the -// login page. As such, this logic is disabled for all proxied requests. +// login page. As such, this logic is disabled for all local and forwarded requests. + app.MapReverseProxy(ConfigureProxyPipeline).DisableCookieRedirect(); +app.MapGet("/api/local-api", () => new[] { "some data", "more data", "loads of data" }) + .RequireAuthorization() + .DisableCookieRedirect(); + +app.MapGet("/api/current-user-name", (ClaimsPrincipal user) => user.Identity!.Name!) + .RequireAuthorization() + .DisableCookieRedirect(); + app.MapFallbackToPage("/_Host"); // Before starting the host, create the database used to store the application data. diff --git a/samples/Dantooine/Dantooine.WebAssembly.Server/appsettings.json b/samples/Dantooine/Dantooine.WebAssembly.Server/appsettings.json index 274c8032a..daa1ba7ea 100644 --- a/samples/Dantooine/Dantooine.WebAssembly.Server/appsettings.json +++ b/samples/Dantooine/Dantooine.WebAssembly.Server/appsettings.json @@ -15,7 +15,7 @@ "ClusterId": "cluster1", "AuthorizationPolicy": "CookieAuthenticationPolicy", "Match": { - "Path": "api/DantooineApi" + "Path": "api/downstream-api" } } }, diff --git a/samples/Dantooine/Dantooine.WebAssembly.Shared/Authorization/ClaimValue.cs b/samples/Dantooine/Dantooine.WebAssembly.Shared/Authorization/ClaimValue.cs deleted file mode 100644 index 0b6808770..000000000 --- a/samples/Dantooine/Dantooine.WebAssembly.Shared/Authorization/ClaimValue.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Dantooine.WebAssembly.Shared.Authorization; - -// Original source: https://github.com/berhir/BlazorWebAssemblyCookieAuth. -public class ClaimValue -{ - public ClaimValue(string type, string value) - { - Type = type; - Value = value; - } - - public string Type { get; } - - public string Value { get; } -} diff --git a/samples/Dantooine/Dantooine.WebAssembly.Shared/Authorization/UserInfo.cs b/samples/Dantooine/Dantooine.WebAssembly.Shared/Authorization/UserInfo.cs deleted file mode 100644 index a7cf2b3c4..000000000 --- a/samples/Dantooine/Dantooine.WebAssembly.Shared/Authorization/UserInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Dantooine.WebAssembly.Shared.Authorization; - -// Original source: https://github.com/berhir/BlazorWebAssemblyCookieAuth. -public class UserInfo -{ - public static readonly UserInfo Anonymous = new UserInfo(); - - public bool IsAuthenticated { get; set; } - - public string? NameClaimType { get; set; } - - public string? RoleClaimType { get; set; } - - public ICollection Claims { get; set; } = []; -} diff --git a/samples/Dantooine/Dantooine.WebAssembly.Shared/Dantooine.WebAssembly.Shared.csproj b/samples/Dantooine/Dantooine.WebAssembly.Shared/Dantooine.WebAssembly.Shared.csproj deleted file mode 100644 index 70c17b8f7..000000000 --- a/samples/Dantooine/Dantooine.WebAssembly.Shared/Dantooine.WebAssembly.Shared.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - $(NetCoreTargetFramework) - - - - - - diff --git a/samples/Fornax/Fornax.Server/Account/RegisterExternalLogin.aspx.cs b/samples/Fornax/Fornax.Server/Account/RegisterExternalLogin.aspx.cs index ab4b72bc3..689586dfe 100644 --- a/samples/Fornax/Fornax.Server/Account/RegisterExternalLogin.aspx.cs +++ b/samples/Fornax/Fornax.Server/Account/RegisterExternalLogin.aspx.cs @@ -24,7 +24,7 @@ protected string ProviderAccountKey private void RedirectOnFail() { - Response.Redirect((User.Identity.IsAuthenticated) ? "~/Account/Manage" : "~/Account/Login"); + Response.Redirect(User.Identity.IsAuthenticated ? "~/Account/Manage" : "~/Account/Login"); } protected void Page_Load() diff --git a/samples/Geonosis/Geonosis.Auth/Controllers/AuthorizationController.cs b/samples/Geonosis/Geonosis.Auth/Controllers/AuthorizationController.cs index 1ddd98d18..45d1f375b 100644 --- a/samples/Geonosis/Geonosis.Auth/Controllers/AuthorizationController.cs +++ b/samples/Geonosis/Geonosis.Auth/Controllers/AuthorizationController.cs @@ -139,7 +139,7 @@ public async Task Authorize() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); // Note: in this sample, the granted scopes match the requested scope // but you may want to allow the user to uncheck specific scopes. @@ -234,7 +234,7 @@ public async Task Accept() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); // Note: in this sample, the granted scopes match the requested scope // but you may want to allow the user to uncheck specific scopes. @@ -335,7 +335,7 @@ public async Task Exchange() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); identity.SetDestinations(GetDestinations); diff --git a/samples/Hollastin/Hollastin.Server/Controllers/AuthorizationController.cs b/samples/Hollastin/Hollastin.Server/Controllers/AuthorizationController.cs index 12bbc9510..a240bac1f 100644 --- a/samples/Hollastin/Hollastin.Server/Controllers/AuthorizationController.cs +++ b/samples/Hollastin/Hollastin.Server/Controllers/AuthorizationController.cs @@ -76,7 +76,7 @@ public async Task Exchange() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); // Set the list of scopes granted to the client application. identity.SetScopes(new[] diff --git a/samples/Imynusoph/Imynusoph.Server/Controllers/AuthorizationController.cs b/samples/Imynusoph/Imynusoph.Server/Controllers/AuthorizationController.cs index 03b69e433..56699c1f1 100644 --- a/samples/Imynusoph/Imynusoph.Server/Controllers/AuthorizationController.cs +++ b/samples/Imynusoph/Imynusoph.Server/Controllers/AuthorizationController.cs @@ -76,7 +76,7 @@ public async Task Exchange() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); // Note: in this sample, the granted scopes match the requested scope // but you may want to allow the user to uncheck specific scopes. @@ -128,7 +128,7 @@ public async Task Exchange() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); identity.SetDestinations(GetDestinations); diff --git a/samples/Matty/Matty.Server/Controllers/AuthorizationController.cs b/samples/Matty/Matty.Server/Controllers/AuthorizationController.cs index 7aba032b5..4bb5b4025 100644 --- a/samples/Matty/Matty.Server/Controllers/AuthorizationController.cs +++ b/samples/Matty/Matty.Server/Controllers/AuthorizationController.cs @@ -98,7 +98,7 @@ public async Task VerifyAccept() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); // Note: in this sample, the granted scopes match the requested scope // but you may want to allow the user to uncheck specific scopes. @@ -184,7 +184,7 @@ public async Task Exchange() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); identity.SetDestinations(GetDestinations); diff --git a/samples/Mimban/Mimban.Server/Program.cs b/samples/Mimban/Mimban.Server/Program.cs index 73690af51..7c6ee1cec 100644 --- a/samples/Mimban/Mimban.Server/Program.cs +++ b/samples/Mimban/Mimban.Server/Program.cs @@ -123,8 +123,7 @@ app.UseAuthentication(); app.UseAuthorization(); -app.MapGet("api", [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] - (ClaimsPrincipal user) => user.Identity!.Name); +app.MapGet("api", (ClaimsPrincipal user) => user.Identity!.Name).RequireAuthorization(); app.MapMethods("callback/login/github", [HttpMethods.Get, HttpMethods.Post], async (HttpContext context) => { diff --git a/samples/Mortis/Mortis.Server/Controllers/AccountController.cs b/samples/Mortis/Mortis.Server/Controllers/AccountController.cs index e4faa8788..0f63618fd 100644 --- a/samples/Mortis/Mortis.Server/Controllers/AccountController.cs +++ b/samples/Mortis/Mortis.Server/Controllers/AccountController.cs @@ -200,7 +200,7 @@ public async Task ForgotPassword(ForgotPasswordViewModel model) if (ModelState.IsValid) { var user = await UserManager.FindByNameAsync(model.Email); - if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id))) + if (user == null || !await UserManager.IsEmailConfirmedAsync(user.Id)) { // Ne révélez pas que l'utilisateur n'existe pas ou qu'il n'est pas confirmé return View("ForgotPasswordConfirmation"); diff --git a/samples/Velusia/Velusia.Server/Controllers/AuthorizationController.cs b/samples/Velusia/Velusia.Server/Controllers/AuthorizationController.cs index 1cda38148..e4dba6052 100644 --- a/samples/Velusia/Velusia.Server/Controllers/AuthorizationController.cs +++ b/samples/Velusia/Velusia.Server/Controllers/AuthorizationController.cs @@ -138,7 +138,7 @@ public async Task Authorize() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); // Note: in this sample, the granted scopes match the requested scope // but you may want to allow the user to uncheck specific scopes. @@ -232,7 +232,7 @@ public async Task Accept() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); // Note: in this sample, the granted scopes match the requested scope // but you may want to allow the user to uncheck specific scopes. @@ -332,7 +332,7 @@ public async Task Exchange() .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. (await _userManager.GetRolesAsync(user))]); + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); identity.SetDestinations(GetDestinations); diff --git a/samples/Zirku/Zirku.Api1/Program.cs b/samples/Zirku/Zirku.Api1/Program.cs index 5fdf818b7..4cfc97a42 100644 --- a/samples/Zirku/Zirku.Api1/Program.cs +++ b/samples/Zirku/Zirku.Api1/Program.cs @@ -105,7 +105,7 @@ app.UseAuthentication(); app.UseAuthorization(); -app.MapGet("api", [Authorize] (ClaimsPrincipal user) => $"{user.Identity!.Name} is allowed to access Api1."); +app.MapGet("api", (ClaimsPrincipal user) => $"{user.Identity!.Name} is allowed to access Api1.").RequireAuthorization(); app.UseWelcomePage("/"); diff --git a/samples/Zirku/Zirku.Api2/Program.cs b/samples/Zirku/Zirku.Api2/Program.cs index 9941ed075..86e86ad9f 100644 --- a/samples/Zirku/Zirku.Api2/Program.cs +++ b/samples/Zirku/Zirku.Api2/Program.cs @@ -98,7 +98,7 @@ app.UseAuthentication(); app.UseAuthorization(); -app.MapGet("api", [Authorize] (ClaimsPrincipal user) => $"{user.Identity!.Name} is allowed to access Api2."); +app.MapGet("api", (ClaimsPrincipal user) => $"{user.Identity!.Name} is allowed to access Api2.").RequireAuthorization(); app.UseWelcomePage("/"); diff --git a/samples/Zirku/Zirku.Server/Program.cs b/samples/Zirku/Zirku.Server/Program.cs index 95be3b706..1ffad32b3 100644 --- a/samples/Zirku/Zirku.Server/Program.cs +++ b/samples/Zirku/Zirku.Server/Program.cs @@ -5,14 +5,12 @@ using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; -using OpenIddict.Validation.AspNetCore; using Quartz; using static OpenIddict.Abstractions.OpenIddictConstants; @@ -111,16 +109,6 @@ // options.UseAspNetCore() .EnableAuthorizationEndpointPassthrough(); - }) - - // Register the OpenIddict validation components. - .AddValidation(options => - { - // Import the configuration from the local OpenIddict server instance. - options.UseLocalServer(); - - // Register the ASP.NET Core host. - options.UseAspNetCore(); }); // Configure Kestrel to listen on the 44319 port and configure it to enforce mTLS. @@ -176,24 +164,18 @@ .AllowAnyMethod() .WithOrigins("http://localhost:5112"))); -builder.Services.AddAuthorization(); - var app = builder.Build(); app.UseCors(); app.UseHttpsRedirection(); app.UseAuthentication(); -app.UseAuthorization(); - -app.MapGet("api", - [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] (ClaimsPrincipal user) => user.Identity!.Name); app.MapMethods("connect/authorize", [HttpMethods.Get, HttpMethods.Post], async (HttpContext context, IOpenIddictScopeManager manager) => { // Retrieve the OpenIddict server request from the HTTP context. var request = context.GetOpenIddictServerRequest() ?? - throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); var identifier = (int?) request["hardcoded_identity_id"]; if (identifier is not (1 or 2)) @@ -266,7 +248,7 @@ async Task CreateApplicationsAsync() if (await manager.FindByClientIdAsync("console_app") is null) { - await manager.CreateAsync(new OpenIddictApplicationDescriptor + var descriptor = new OpenIddictApplicationDescriptor { ApplicationType = ApplicationTypes.Native, ClientId = "console_app", @@ -283,16 +265,18 @@ await manager.CreateAsync(new OpenIddictApplicationDescriptor Permissions.ResponseTypes.Code, Permissions.Scopes.Email, Permissions.Scopes.Profile, - Permissions.Scopes.Roles, - Permissions.Prefixes.Scope + "api1", - Permissions.Prefixes.Scope + "api2" + Permissions.Scopes.Roles } - }); + }; + + descriptor.AddScopePermissions("api1", "api2"); + + await manager.CreateAsync(descriptor); } if (await manager.FindByClientIdAsync("spa") is null) { - await manager.CreateAsync(new OpenIddictApplicationDescriptor + var descriptor = new OpenIddictApplicationDescriptor { ClientId = "spa", ClientType = ClientTypes.Public, @@ -312,15 +296,17 @@ await manager.CreateAsync(new OpenIddictApplicationDescriptor Permissions.ResponseTypes.Code, Permissions.Scopes.Email, Permissions.Scopes.Profile, - Permissions.Scopes.Roles, - Permissions.Prefixes.Scope + "api1", - Permissions.Prefixes.Scope + "api2" + Permissions.Scopes.Roles }, Requirements = { - Requirements.Features.ProofKeyForCodeExchange, - }, - }); + Requirements.Features.ProofKeyForCodeExchange + } + }; + + descriptor.AddScopePermissions("api1", "api2"); + + await manager.CreateAsync(descriptor); } if (await manager.FindByClientIdAsync("resource_server_1") is null)