@@ -35,23 +35,23 @@ else
protected override async Task OnInitializedAsync()
{
- var state = await provider.GetAuthenticationStateAsync();
+ var state = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (state is not { User.Identity.IsAuthenticated: true })
{
- manager.NavigateTo($"login?returnUrl=/{Uri.EscapeDataString(manager.ToBaseRelativePath(manager.Uri))}", true);
+ NavigationManager.NavigateTo($"login?returnUrl={Uri.EscapeDataString("/" + NavigationManager.ToBaseRelativePath(NavigationManager.Uri))}", true);
return;
}
- client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", await runtime.InvokeAsync("getAntiForgeryToken"));
+ HttpClient.DefaultRequestHeaders.Add("X-XSRF-TOKEN", await JSRuntime.InvokeAsync("getAntiForgeryToken"));
try
{
- data = (await client.GetFromJsonAsync("api/local-api"))!;
+ data = (await HttpClient.GetFromJsonAsync("api/local-api"))!;
}
catch (HttpRequestException exception) when (exception.StatusCode is HttpStatusCode.Unauthorized)
{
- manager.NavigateTo($"login?returnUrl=/{Uri.EscapeDataString(manager.ToBaseRelativePath(manager.Uri))}", true);
+ NavigationManager.NavigateTo($"login?returnUrl={Uri.EscapeDataString("/" + NavigationManager.ToBaseRelativePath(NavigationManager.Uri))}", true);
return;
}
}
diff --git a/samples/Dantooine/Dantooine.WebAssembly.Client/Shared/LoginDisplay.razor b/samples/Dantooine/Dantooine.WebAssembly.Client/Shared/LoginDisplay.razor
index d4b1fb0d..5795a4f8 100644
--- a/samples/Dantooine/Dantooine.WebAssembly.Client/Shared/LoginDisplay.razor
+++ b/samples/Dantooine/Dantooine.WebAssembly.Client/Shared/LoginDisplay.razor
@@ -1,7 +1,7 @@
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
-@inject NavigationManager Navigation
+@inject NavigationManager NavigationManager
@@ -16,6 +16,6 @@
@code{
private void BeginLogout(MouseEventArgs args)
{
- Navigation.NavigateToLogout("authentication/logout");
+ NavigationManager.NavigateToLogout("authentication/logout");
}
}
diff --git a/samples/Geonosis/Geonosis.Api/Program.cs b/samples/Geonosis/Geonosis.Api/Program.cs
index 9c69e4e8..a81909c4 100644
--- a/samples/Geonosis/Geonosis.Api/Program.cs
+++ b/samples/Geonosis/Geonosis.Api/Program.cs
@@ -1,9 +1,6 @@
using OpenIddict.Abstractions;
using OpenIddict.Validation.AspNetCore;
-var issuerUrl = "https://localhost:7094";
-var weatherReadAuthPolicy = "Weather.Read";
-
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
@@ -13,7 +10,7 @@
.AddValidation(options =>
{
// Set the authority and the audience to validate the tokens.
- options.SetIssuer(issuerUrl);
+ options.SetIssuer("https://localhost:7094/");
options.AddAudiences("geonosis-api");
// Register the System.Net.Http integration.
@@ -29,7 +26,7 @@
// Add a policy that requires the "Weather.Read" scope.
builder.Services.AddAuthorizationBuilder()
- .AddPolicy(weatherReadAuthPolicy, policy => policy
+ .AddPolicy("Weather.Read", policy => policy
.RequireAuthenticatedUser()
.RequireAssertion(context => context.User.HasScope("Weather.Read")));
@@ -44,24 +41,24 @@
// A sample endpoint that requires the "Weather.Read" scope to be accessed.
app.MapGet("/weather-forecast", () =>
- {
- string[] summaries =
- [
- "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
- ];
-
- var forecast = Enumerable.Range(1, 5).Select(index =>
- new WeatherForecast
- (
- DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
- Random.Shared.Next(-20, 55),
- summaries[Random.Shared.Next(summaries.Length)]
- ))
- .ToArray();
-
- return forecast;
- })
- .RequireAuthorization(weatherReadAuthPolicy);
+{
+ string[] summaries =
+ [
+ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
+ ];
+
+ var forecast = Enumerable.Range(1, 5).Select(index =>
+ new WeatherForecast
+ (
+ DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
+ Random.Shared.Next(-20, 55),
+ summaries[Random.Shared.Next(summaries.Length)]
+ ))
+ .ToArray();
+
+ return forecast;
+})
+.RequireAuthorization("Weather.Read");
app.Run();
diff --git a/samples/Geonosis/Geonosis.AppHost/AppHost.cs b/samples/Geonosis/Geonosis.AppHost/AppHost.cs
index 0138ae88..d154f201 100644
--- a/samples/Geonosis/Geonosis.AppHost/AppHost.cs
+++ b/samples/Geonosis/Geonosis.AppHost/AppHost.cs
@@ -1,12 +1,13 @@
var builder = DistributedApplication.CreateBuilder(args);
-var geonosisAuth = builder.AddProject("geonosis-auth");
+var server = builder.AddProject("geonosis-auth");
-var geonosisApi = builder.AddProject("geonosis-api")
- .WaitFor(geonosisAuth);
+var resource = builder.AddProject("geonosis-api")
+ .WaitFor(server);
builder.AddProject("geonosis-ui")
- .WaitFor(geonosisAuth)
- .WaitFor(geonosisApi);
+ .WaitFor(server)
+ .WaitFor(resource);
-builder.Build().Run();
+var app = builder.Build();
+await app.RunAsync();
diff --git a/samples/Geonosis/Geonosis.Auth/Program.cs b/samples/Geonosis/Geonosis.Auth/Program.cs
index c98dc671..fec758de 100644
--- a/samples/Geonosis/Geonosis.Auth/Program.cs
+++ b/samples/Geonosis/Geonosis.Auth/Program.cs
@@ -19,7 +19,7 @@
builder.Services.AddDbContext(options =>
{
// Configure the context to use sqlite.
- options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "geonosis-auth.sqlite3")}");
+ options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "openiddict-geonosis-auth.sqlite3")}");
// Register the entity sets needed by OpenIddict.
// Note: use the generic overload if you need
@@ -222,8 +222,6 @@ static async Task SeedClientsAsync(IServiceProvider provider)
"""))
}
},
- // RedirectUris must match the URLs used by the Blazor Web application during the authentication process
- // These URLs are where the authorization server will redirect the user after login/logout back to the client application
RedirectUris =
{
new Uri("http://localhost:5027/authentication/login-callback/local"),
diff --git a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Constants.cs b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Constants.cs
deleted file mode 100644
index e800c441..00000000
--- a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Constants.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Geonosis.Ui.Client
-{
- public static class Constants
- {
- public const string LoginPath = "/authentication/login";
- public const string LogoutPath = "/authentication/logout";
- }
-}
diff --git a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Layout/MainLayout.razor b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Layout/MainLayout.razor
index d1590893..fc95aa04 100644
--- a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Layout/MainLayout.razor
+++ b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Layout/MainLayout.razor
@@ -1,4 +1,4 @@
-@inject NavigationManager Navigation
+@inject NavigationManager NavigationManager
@inherits LayoutComponentBase
@@ -14,7 +14,7 @@
@(authContext.User.Identity!.Name)
-
-
+
Login
@@ -48,18 +48,18 @@
protected override void OnInitialized()
{
- currentUrl = "/" + Navigation.ToBaseRelativePath(Navigation.Uri);
- Navigation.LocationChanged += OnLocationChanged;
+ currentUrl = "/" + NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
+ NavigationManager.LocationChanged += OnLocationChanged;
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
- currentUrl = "/" + Navigation.ToBaseRelativePath(e.Location);
+ currentUrl = "/" + NavigationManager.ToBaseRelativePath(e.Location);
StateHasChanged();
}
public void Dispose()
{
- Navigation.LocationChanged -= OnLocationChanged;
+ NavigationManager.LocationChanged -= OnLocationChanged;
}
}
\ No newline at end of file
diff --git a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Program.cs b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Program.cs
index e6a1cf2a..6435cbd3 100644
--- a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Program.cs
+++ b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Program.cs
@@ -7,11 +7,11 @@
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthenticationStateDeserialization();
-builder.Services.AddHttpClient(httpClient =>
+builder.Services.AddHttpClient(client =>
{
- httpClient.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
+ client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
});
var app = builder.Build();
-await app.RunAsync();
\ No newline at end of file
+await app.RunAsync();
diff --git a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/RedirectToLogin.razor b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/RedirectToLogin.razor
index 431b42c3..10cbb289 100644
--- a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/RedirectToLogin.razor
+++ b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/RedirectToLogin.razor
@@ -2,8 +2,6 @@
@code
{
- protected override void OnInitialized()
- {
- NavigationManager.NavigateTo($"{Constants.LoginPath}?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true);
- }
+ protected override void OnInitialized() => NavigationManager.NavigateTo(
+ $"/authentication/login?returnUrl={Uri.EscapeDataString("/" + NavigationManager.ToBaseRelativePath(NavigationManager.Uri))}", forceLoad: true);
}
\ No newline at end of file
diff --git a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/AuthenticationEndpointsExtensions.cs b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/AuthenticationEndpointsExtensions.cs
deleted file mode 100644
index ee0b1c63..00000000
--- a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/AuthenticationEndpointsExtensions.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-using System.Security.Claims;
-using Microsoft.AspNetCore.Antiforgery;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authentication.Cookies;
-using Microsoft.AspNetCore.Http.HttpResults;
-using Microsoft.AspNetCore.Mvc;
-using OpenIddict.Abstractions;
-using OpenIddict.Client.AspNetCore;
-using static OpenIddict.Abstractions.OpenIddictConstants;
-
-namespace Geonosis.Ui
-{
- ///
- /// Extension methods for mapping authentication-related endpoints including login, logout, and their callbacks.
- ///
- internal static class AuthenticationEndpointsExtensions
- {
- ///
- /// Maps all authentication endpoints: login, logout, and their OpenID Connect callbacks.
- ///
- internal static RouteGroupBuilder MapAuthenticationEndpoints(this IEndpointRouteBuilder routes)
- {
- var authGroup = routes.MapGroup("/authentication");
-
- RegisterLoginEndpoint(authGroup);
- RegisterLogoutEndpoint(authGroup);
- RegisterCallbackEndpoints(authGroup);
-
- return authGroup;
- }
-
- private static void RegisterLoginEndpoint(RouteGroupBuilder authGroup)
- {
- authGroup.MapGet("/login", (string? returnUrl) =>
- {
- return TypedResults.Challenge(
- BuildRedirectProperties(returnUrl),
- [
- CookieAuthenticationDefaults.AuthenticationScheme,
- OpenIddictClientAspNetCoreDefaults.AuthenticationScheme
- ]);
- }).AllowAnonymous();
- }
-
- private static void RegisterLogoutEndpoint(RouteGroupBuilder authGroup)
- {
- authGroup.MapPost("/logout", ([FromForm] string? returnUrl, HttpContext ctx, IAntiforgery antiforgery) =>
- {
- return TypedResults.SignOut(
- BuildRedirectProperties(returnUrl),
- [
- CookieAuthenticationDefaults.AuthenticationScheme,
- OpenIddictClientAspNetCoreDefaults.AuthenticationScheme
- ]);
- });
- }
-
- private static void RegisterCallbackEndpoints(RouteGroupBuilder authGroup)
- {
- authGroup.MapMethods(
- "/login-callback/{provider}",
- [HttpMethod.Get.Method, HttpMethod.Post.Method],
- async (string? provider, HttpContext ctx) => await HandleLoginCallback(ctx))
- .DisableAntiforgery();
-
- authGroup.MapMethods(
- "/logout-callback/{provider}",
- [HttpMethod.Get.Method, HttpMethod.Post.Method],
- async (string? provider, HttpContext ctx) => await HandleLogoutCallback(ctx))
- .DisableAntiforgery();
- }
-
- private static AuthenticationProperties BuildRedirectProperties(string? returnUrl)
- {
- const string baseRoute = "/";
-
- // Sanitize and validate return URL to prevent open redirects
- var sanitizedUrl = returnUrl switch
- {
- null or "" => baseRoute,
- _ when RedirectHttpResult.IsLocalUrl(returnUrl) => returnUrl,
- _ => new Uri(returnUrl, UriKind.Absolute).PathAndQuery,
- };
-
- return new AuthenticationProperties { RedirectUri = sanitizedUrl };
- }
-
- private static async Task HandleLoginCallback(HttpContext ctx)
- {
- var authResult = await ctx.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
-
- if (authResult is not { Succeeded: true, Principal.Identity.IsAuthenticated: true })
- {
- throw new InvalidOperationException("External authorization failed or user is not authenticated.");
- }
-
- var userIdentity = new ClaimsIdentity(
- authenticationType: "ExternalLogin",
- nameType: ClaimTypes.Name,
- roleType: ClaimTypes.Role);
-
- // Map essential user claims from the external provider
- userIdentity.SetClaim(ClaimTypes.Email, authResult.Principal.GetClaim(ClaimTypes.Email))
- .SetClaim(ClaimTypes.Name, authResult.Principal.GetClaim(ClaimTypes.Name))
- .SetClaim(ClaimTypes.NameIdentifier, authResult.Principal.GetClaim(ClaimTypes.NameIdentifier))
- .SetClaim(ClaimTypes.Role, authResult.Principal.GetClaim("Role"));
-
- // Store provider registration details
- userIdentity.SetClaim(Claims.Private.RegistrationId, authResult.Principal.GetClaim(Claims.Private.RegistrationId))
- .SetClaim(Claims.Private.ProviderName, authResult.Principal.GetClaim(Claims.Private.ProviderName));
-
- var authProps = new AuthenticationProperties(authResult.Properties.Items)
- {
- RedirectUri = authResult.Properties.RedirectUri ?? "/",
- IssuedUtc = null,
- ExpiresUtc = null,
- IsPersistent = true
- };
-
- // Filter and store only necessary tokens
- authProps.StoreTokens(authResult.Properties.GetTokens().Where(t => t.Name is
- OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken or
- OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessTokenExpirationDate or
- OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken or
- OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken));
-
- return TypedResults.SignIn(new ClaimsPrincipal(userIdentity), authProps);
- }
-
- private static async Task HandleLogoutCallback(HttpContext ctx)
- {
- var authResult = await ctx.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
- var redirectTarget = authResult?.Properties?.RedirectUri ?? "/";
-
- return TypedResults.Redirect(redirectTarget);
- }
- }
-}
diff --git a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Geonosis.Ui.csproj b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Geonosis.Ui.csproj
index eff7db66..29605c1d 100644
--- a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Geonosis.Ui.csproj
+++ b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Geonosis.Ui.csproj
@@ -12,8 +12,10 @@
+
+
diff --git a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Program.cs b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Program.cs
index 0fa9a905..e5f293d7 100644
--- a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Program.cs
+++ b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Program.cs
@@ -1,27 +1,43 @@
using System.Net.Http.Headers;
+using System.Security.Claims;
using System.Security.Cryptography;
-using Geonosis.Ui;
-using Geonosis.Ui.Client;
using Geonosis.Ui.Client.Weather;
using Geonosis.Ui.Components;
using Geonosis.Ui.Weather;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
+using OpenIddict.Abstractions;
using OpenIddict.Client;
using OpenIddict.Client.AspNetCore;
using Yarp.ReverseProxy.Transforms;
using static OpenIddict.Abstractions.OpenIddictConstants;
-var issuerUrl = "https://localhost:7094";
-var apiUrl = "https://localhost:7070";
-
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
-// Add OpenIddict services
+builder.Services.AddDbContext(options =>
+{
+ options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "openiddict-geonosis-ui.sqlite3")}");
+ options.UseOpenIddict();
+});
+
builder.Services.AddOpenIddict()
+
+ // Register the OpenIddict core components.
+ .AddCore(options =>
+ {
+ // Configure OpenIddict to use the Entity Framework Core stores and models.
+ // Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities.
+ options.UseEntityFrameworkCore()
+ .UseDbContext();
+ })
+
+ // Register the OpenIddict client components.
.AddClient(options =>
{
// Enable the authorization code flow, the refresh token flow and the token exchange flow.
@@ -47,12 +63,10 @@
options.UseSystemNetHttp()
.SetProductInformation(typeof(Program).Assembly);
- options.DisableTokenStorage();
-
// Add a client registration matching the client application definition in the server project.
options.AddRegistration(new OpenIddictClientRegistration
{
- Issuer = new Uri(issuerUrl, UriKind.Absolute),
+ Issuer = new Uri("https://localhost:7094/", UriKind.Absolute),
ClientId = "geonosis-ui",
@@ -80,33 +94,28 @@
});
});
-// Register the authentication and authorization services.
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
- options.LoginPath = Constants.LoginPath;
- options.LogoutPath = Constants.LogoutPath;
+ options.LoginPath = "/authentication/login";
+ options.LogoutPath = "/authentication/logout";
});
builder.Services.AddAuthorization();
-// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents()
- // Add authentication state serialization with all claims included
.AddAuthenticationStateSerialization(options => options.SerializeAllClaims = true);
-// Add authentication state provider
builder.Services.AddCascadingAuthenticationState();
-// Add HttpClient for weather forecaster with base address of the weather API
builder.Services.AddHttpForwarderWithServiceDiscovery();
builder.Services.AddHttpContextAccessor();
-builder.Services.AddHttpClient(httpClient =>
+builder.Services.AddHttpClient(client =>
{
- httpClient.BaseAddress = new(apiUrl);
+ client.BaseAddress = new("https://localhost:7070/");
});
var app = builder.Build();
@@ -140,47 +149,119 @@
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(Geonosis.Ui.Client._Imports).Assembly);
-// Map authentication endpoints used by the authentication state provider
-// - /authentication/login/local for handling the login and redirection to the Auth project for authentication
-// - /authentication/logout/local for handling the logout and redirection to the Auth project for logout
-// - /authentication/login-callback/local for handling the authentication response from the Auth project
-// - /authentication/logout-callback/local for handling the post-logout redirection from the Auth project
-//
-// Callback endpoint for handling authentication responses from Auth project
-// musth be registered in the OpenID ClientRegistration and
-// must be configured in the AddRegistration method above.
-app.MapAuthenticationEndpoints();
-
-// Map a reverse proxy endpoint for the weather API, which will forward requests to the Weather API project and add the access
-// token in the authorization header.
-// This is used by the client-side weather forecaster to retrieve weather forecasts from the Weather API project without
-// having to worry about authentication and token management.
-app.MapForwarder("/weather-forecast", apiUrl, transformBuilder =>
+// Register the endpoint responsible for redirecting the user to the authorization endpoint of the identity provider.
+app.MapGet("/authentication/login", (string? returnUrl) =>
+{
+ var properties = new AuthenticationProperties
{
- transformBuilder.AddRequestTransform(async transformContext =>
- {
- var openIddictClientService = transformContext.HttpContext.RequestServices.GetRequiredService();
- var accessToken = await transformContext.HttpContext.GetTokenAsync(OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken)
- ?? throw new InvalidOperationException("The access token cannot be retrieved.");
+ // Only allow local return URLs to prevent open redirect attacks.
+ RedirectUri = RedirectHttpResult.IsLocalUrl(returnUrl) ? returnUrl : "/"
+ };
- var exchangeResult = await openIddictClientService.AuthenticateWithTokenExchangeAsync(new()
- {
- SubjectToken = accessToken,
- SubjectTokenType = TokenTypeIdentifiers.AccessToken,
- RequestedTokenType = TokenTypeIdentifiers.AccessToken,
- Scopes = ["Weather.Read"],
- });
-
- //var accessToken = await transformContext.HttpContext.GetTokenAsync("access_token");
- transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", exchangeResult.IssuedToken);
+ return TypedResults.Challenge(properties, [OpenIddictClientAspNetCoreDefaults.AuthenticationScheme]);
+}).AllowAnonymous();
+
+// Register the endpoint responsible for redirecting the user to the end session endpoint of the identity provider.
+app.MapPost("/authentication/logout", ([FromForm] string? returnUrl, HttpContext context) =>
+{
+ var properties = new AuthenticationProperties
+ {
+ // Only allow local return URLs to prevent open redirect attacks.
+ RedirectUri = RedirectHttpResult.IsLocalUrl(returnUrl) ? returnUrl : "/"
+ };
+
+ return TypedResults.SignOut(properties, [CookieAuthenticationDefaults.AuthenticationScheme, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme]);
+}).AllowAnonymous();
+
+// Register the endpoint responsible for handling the authorization response returned by the identity provider.
+app.MapMethods("/authentication/login-callback/{provider}", [HttpMethods.Get, HttpMethods.Post], async (string? provider, HttpContext context) =>
+{
+ var result = await context.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
+ if (result is not { Succeeded: true, Principal.Identity.IsAuthenticated: true })
+ {
+ throw new InvalidOperationException("The external authorization data cannot be used for authentication.");
+ }
+
+ // Build an identity based on the external claims and that will be used to create the authentication cookie.
+ var identity = new ClaimsIdentity(
+ authenticationType: "ExternalLogin",
+ nameType: ClaimTypes.Name,
+ roleType: ClaimTypes.Role);
+
+ // By default, OpenIddict will automatically try to map the email/name and name identifier claims from
+ // their standard OpenID Connect or provider-specific equivalent, if available. If needed, additional
+ // claims can be resolved from the external identity and copied to the final authentication cookie.
+ identity.SetClaim(ClaimTypes.Email, result.Principal.GetClaim(ClaimTypes.Email))
+ .SetClaim(ClaimTypes.Name, result.Principal.GetClaim(ClaimTypes.Name))
+ .SetClaim(ClaimTypes.NameIdentifier, result.Principal.GetClaim(ClaimTypes.NameIdentifier))
+ .SetClaim(ClaimTypes.Role, result.Principal.GetClaim("Role"));
+
+ // Preserve the registration details to be able to resolve them later.
+ identity.SetClaim(Claims.Private.RegistrationId, result.Principal.GetClaim(Claims.Private.RegistrationId))
+ .SetClaim(Claims.Private.ProviderName, result.Principal.GetClaim(Claims.Private.ProviderName));
+
+ var properties = new AuthenticationProperties(result.Properties.Items)
+ {
+ RedirectUri = result.Properties.RedirectUri ?? "/",
+ IssuedUtc = null,
+ ExpiresUtc = null,
+ IsPersistent = true
+ };
+
+ // If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
+ // To make cookies less heavy, tokens that are not used are filtered out before creating the cookie.
+ properties.StoreTokens(result.Properties.GetTokens().Where(token => token.Name is
+ OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken or
+ OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessTokenExpirationDate or
+ OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken or
+ OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken));
+
+ return TypedResults.SignIn(new ClaimsPrincipal(identity), properties);
+}).DisableAntiforgery();
+
+// Register the endpoint responsible for handling the end session response returned by the identity provider.
+app.MapMethods("/authentication/logout-callback/{provider}", [HttpMethods.Get, HttpMethods.Post], async (string? provider, HttpContext context) =>
+{
+ var result = await context.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
+
+ return TypedResults.Redirect(result?.Properties?.RedirectUri ?? "/");
+})
+.DisableAntiforgery();
+
+app.MapForwarder("/weather-forecast", "https://localhost:7070/", builder =>
+{
+ builder.AddRequestTransform(async context =>
+ {
+ var service = context.HttpContext.RequestServices.GetRequiredService();
+ var token = await context.HttpContext.GetTokenAsync(OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken)
+ ?? throw new InvalidOperationException("The access token cannot be retrieved.");
+
+ var result = await service.AuthenticateWithTokenExchangeAsync(new()
+ {
+ SubjectToken = token,
+ SubjectTokenType = TokenTypeIdentifiers.AccessToken,
+ RequestedTokenType = TokenTypeIdentifiers.AccessToken,
+ Scopes = ["Weather.Read"]
});
- // Remove application cookies
- transformBuilder.RequestTransforms.Add(new RequestHeaderRemoveTransform("Cookie"));
- })
- .RequireAuthorization();
+ context.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.IssuedToken);
+ });
+
+ // Prevent application cookies from being sent to the downstream API.
+ builder.RequestTransforms.Add(new RequestHeaderRemoveTransform("Cookie"));
+})
+.RequireAuthorization();
+
+// Before starting the host, create the database used to store the application data.
+//
+// Note: in a real world application, this step should be part of a setup script.
+await using (var scope = app.Services.CreateAsyncScope())
+{
+ var context = scope.ServiceProvider.GetRequiredService();
+ await context.Database.EnsureCreatedAsync();
+}
-app.Run();
+await app.RunAsync();
static ECDsaSecurityKey GetECDsaSigningKey(ReadOnlySpan key)
{
diff --git a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Weather/ServerWeatherForecaster.cs b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Weather/ServerWeatherForecaster.cs
index 0c7b60c0..5a8cd805 100644
--- a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Weather/ServerWeatherForecaster.cs
+++ b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Weather/ServerWeatherForecaster.cs
@@ -6,26 +6,26 @@
namespace Geonosis.Ui.Weather
{
- internal sealed class ServerWeatherForecaster(HttpClient httpClient, IHttpContextAccessor httpContextAccessor) : IWeatherForecaster
+ internal sealed class ServerWeatherForecaster(
+ IHttpContextAccessor accessor, HttpClient client, OpenIddictClientService service) : IWeatherForecaster
{
public async Task> GetWeatherForecastAsync()
{
- var openIddictClientService = httpContextAccessor.HttpContext!.RequestServices.GetRequiredService();
- var accessToken = await httpContextAccessor.HttpContext!.GetTokenAsync(OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken)
+ var token = await accessor.HttpContext!.GetTokenAsync(OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken)
?? throw new InvalidOperationException("The access token cannot be retrieved.");
- var exchangeResult = await openIddictClientService.AuthenticateWithTokenExchangeAsync(new()
+ var result = await service.AuthenticateWithTokenExchangeAsync(new()
{
- SubjectToken = accessToken,
+ SubjectToken = token,
SubjectTokenType = TokenTypeIdentifiers.AccessToken,
RequestedTokenType = TokenTypeIdentifiers.AccessToken,
- Scopes = ["Weather.Read"],
+ Scopes = ["Weather.Read"]
});
using var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast");
- request.Headers.Authorization = new("Bearer", exchangeResult.IssuedToken);
+ request.Headers.Authorization = new("Bearer", result.IssuedToken);
- using var response = await httpClient.SendAsync(request);
+ using var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync() ?? [];
diff --git a/samples/Sorgan/Sorgan.BlazorHybrid.Client/Login.razor b/samples/Sorgan/Sorgan.BlazorHybrid.Client/Login.razor
index 51644aaf..b91c3499 100644
--- a/samples/Sorgan/Sorgan.BlazorHybrid.Client/Login.razor
+++ b/samples/Sorgan/Sorgan.BlazorHybrid.Client/Login.razor
@@ -4,7 +4,7 @@
@using System.Windows
@using static OpenIddict.Abstractions.OpenIddictExceptions
@using static OpenIddict.Abstractions.OpenIddictConstants
-@inject OpenIddictClientService service;
+@inject OpenIddictClientService ClientService;
@@ -26,13 +26,13 @@
try
{
// Ask OpenIddict to initiate the authentication flow (typically, by starting the system browser).
- var result = await service.ChallengeInteractivelyAsync(new()
+ var result = await ClientService.ChallengeInteractivelyAsync(new()
{
CancellationToken = source.Token
});
// Wait for the user to complete the authorization process.
- var principal = (await service.AuthenticateInteractivelyAsync(new()
+ var principal = (await ClientService.AuthenticateInteractivelyAsync(new()
{
CancellationToken = source.Token,
Nonce = result.Nonce