mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
cleanup (#2074)
This commit is contained in:
parent
7702c8d758
commit
75e88f6863
109 changed files with 1173 additions and 958 deletions
|
|
@ -39,7 +39,7 @@ public class Index : PageModel
|
|||
TestUserStore users = null)
|
||||
{
|
||||
// this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity)
|
||||
_users = users ?? throw new Exception("Please call 'AddTestUsers(TestUsers.Users)' on the IIdentityServerBuilder in Startup or remove the TestUserStore from the AccountController.");
|
||||
_users = users ?? throw new InvalidOperationException("Please call 'AddTestUsers(TestUsers.Users)' on the IIdentityServerBuilder in Startup or remove the TestUserStore from the AccountController.");
|
||||
|
||||
_interaction = interaction;
|
||||
_clientStore = clientStore;
|
||||
|
|
@ -146,7 +146,7 @@ public class Index : PageModel
|
|||
else
|
||||
{
|
||||
// user might have clicked on a malicious link - should be logged
|
||||
throw new Exception("invalid return URL");
|
||||
throw new InvalidOperationException("invalid return URL");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ public class Callback : PageModel
|
|||
TestUserStore users = null)
|
||||
{
|
||||
// this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity)
|
||||
_users = users ?? throw new Exception("Please call 'AddTestUsers(TestUsers.Users)' on the IIdentityServerBuilder in Startup or remove the TestUserStore from the AccountController.");
|
||||
_users = users ?? throw new InvalidOperationException("Please call 'AddTestUsers(TestUsers.Users)' on the IIdentityServerBuilder in Startup or remove the TestUserStore from the AccountController.");
|
||||
|
||||
_interaction = interaction;
|
||||
_logger = logger;
|
||||
|
|
@ -43,7 +43,7 @@ public class Callback : PageModel
|
|||
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
|
||||
if (result?.Succeeded != true)
|
||||
{
|
||||
throw new Exception("External authentication error");
|
||||
throw new InvalidOperationException("External authentication error");
|
||||
}
|
||||
|
||||
var externalUser = result.Principal;
|
||||
|
|
@ -60,7 +60,7 @@ public class Callback : PageModel
|
|||
// depending on the external provider, some other claim type might be used
|
||||
var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ??
|
||||
externalUser.FindFirst(ClaimTypes.NameIdentifier) ??
|
||||
throw new Exception("Unknown userid");
|
||||
throw new InvalidOperationException("Unknown userid");
|
||||
|
||||
var provider = result.Properties.Items["scheme"];
|
||||
var providerUserId = userIdClaim.Value;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public class Challenge : PageModel
|
|||
if (Url.IsLocalUrl(returnUrl) == false && _interactionService.IsValidReturnUrl(returnUrl) == false)
|
||||
{
|
||||
// user might have clicked on a malicious link - should be logged
|
||||
throw new Exception("invalid return URL");
|
||||
throw new InvalidOperationException("invalid return URL");
|
||||
}
|
||||
|
||||
// start challenge and roundtrip the return URL and scheme
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
|
|||
{
|
||||
if (options.EventsType != null && !typeof(DPoPJwtBearerEvents).IsAssignableFrom(options.EventsType))
|
||||
{
|
||||
throw new Exception("EventsType on JwtBearerOptions must derive from DPoPJwtBearerEvents to work with the DPoP support.");
|
||||
throw new InvalidOperationException("EventsType on JwtBearerOptions must derive from DPoPJwtBearerEvents to work with the DPoP support.");
|
||||
}
|
||||
if (options.Events != null && !typeof(DPoPJwtBearerEvents).IsAssignableFrom(options.Events.GetType()))
|
||||
{
|
||||
throw new Exception("Events on JwtBearerOptions must derive from DPoPJwtBearerEvents to work with the DPoP support.");
|
||||
throw new InvalidOperationException("Events on JwtBearerOptions must derive from DPoPJwtBearerEvents to work with the DPoP support.");
|
||||
}
|
||||
|
||||
if (options.Events == null && options.EventsType == null)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using Microsoft.Extensions.Options;
|
|||
|
||||
namespace Duende.Bff.Blazor.Client.Internals;
|
||||
|
||||
internal class BffClientAuthenticationStateProvider : AuthenticationStateProvider
|
||||
internal class BffClientAuthenticationStateProvider : AuthenticationStateProvider, IDisposable
|
||||
{
|
||||
public const string HttpClientName = "Duende.Bff.Blazor.Client:StateProvider";
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ internal class BffClientAuthenticationStateProvider : AuthenticationStateProvide
|
|||
private ClaimsPrincipal? _user;
|
||||
private readonly ILogger<BffClientAuthenticationStateProvider> _logger;
|
||||
|
||||
private SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
|
||||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="AuthenticationStateProvider"/> intended for use in Blazor
|
||||
|
|
@ -90,7 +90,9 @@ internal class BffClientAuthenticationStateProvider : AuthenticationStateProvide
|
|||
try
|
||||
{
|
||||
await _semaphore.WaitAsync();
|
||||
#pragma warning disable CA1508 // this is a false positive. It's a double locking pattern.
|
||||
if (_user == null)
|
||||
#pragma warning restore CA1508
|
||||
{
|
||||
_user = await RefreshUser();
|
||||
}
|
||||
|
|
@ -103,4 +105,10 @@ internal class BffClientAuthenticationStateProvider : AuthenticationStateProvide
|
|||
|
||||
return new AuthenticationState(_user);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
_semaphore.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Duende.Bff.Blazor.Client.Internals;
|
||||
|
|
@ -10,7 +11,7 @@ namespace Duende.Bff.Blazor.Client.Internals;
|
|||
/// <summary>
|
||||
/// Internal service that retrieves user info from the /bff/user endpoint.
|
||||
/// </summary>
|
||||
internal class FetchUserService
|
||||
internal class FetchUserService : IDisposable
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly ILogger<FetchUserService> _logger;
|
||||
|
|
@ -33,7 +34,9 @@ internal class FetchUserService
|
|||
internal FetchUserService()
|
||||
{
|
||||
_client = new HttpClient();
|
||||
#pragma warning disable CA2000 // This is a test-only ctor, so we don't want to dispose the client here.
|
||||
_logger = new Logger<FetchUserService>(new LoggerFactory());
|
||||
#pragma warning restore CA2000
|
||||
}
|
||||
|
||||
public virtual async ValueTask<ClaimsPrincipal> FetchUserAsync()
|
||||
|
|
@ -58,10 +61,17 @@ internal class FetchUserService
|
|||
|
||||
return new ClaimsPrincipal(identity);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.FetchingUserFailed(ex);
|
||||
return new ClaimsPrincipal(new ClaimsIdentity());
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.FetchingUserFailed(ex);
|
||||
return new ClaimsPrincipal(new ClaimsIdentity());
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => _client.Dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,24 +83,24 @@ public static class ServiceCollectionExtensions
|
|||
|
||||
private static Action<IServiceProvider, HttpClient> SetRemoteApiBaseAddress(
|
||||
Action<IServiceProvider, HttpClient>? configureClient) => (sp, client) =>
|
||||
{
|
||||
SetRemoteApiBaseAddress(sp, client);
|
||||
configureClient?.Invoke(sp, client);
|
||||
};
|
||||
{
|
||||
SetRemoteApiBaseAddress(sp, client);
|
||||
configureClient?.Invoke(sp, client);
|
||||
};
|
||||
|
||||
private static Action<IServiceProvider, HttpClient> SetRemoteApiBaseAddress(
|
||||
Action<HttpClient>? configureClient) => (sp, client) =>
|
||||
{
|
||||
SetRemoteApiBaseAddress(sp, client);
|
||||
configureClient?.Invoke(client);
|
||||
};
|
||||
{
|
||||
SetRemoteApiBaseAddress(sp, client);
|
||||
configureClient?.Invoke(client);
|
||||
};
|
||||
|
||||
private static void SetLocalApiBaseAddress(IServiceProvider sp, HttpClient client)
|
||||
{
|
||||
var baseAddress = GetLocalBaseAddress(sp);
|
||||
if (!baseAddress.EndsWith("/"))
|
||||
if (!baseAddress.EndsWith('/'))
|
||||
{
|
||||
baseAddress += "/";
|
||||
baseAddress += '/';
|
||||
}
|
||||
|
||||
client.BaseAddress = new Uri(baseAddress);
|
||||
|
|
@ -108,37 +108,37 @@ public static class ServiceCollectionExtensions
|
|||
|
||||
private static Action<IServiceProvider, HttpClient> SetLocalApiBaseAddress(
|
||||
Action<HttpClient>? configureClient) => (sp, client) =>
|
||||
{
|
||||
SetLocalApiBaseAddress(sp, client);
|
||||
configureClient?.Invoke(client);
|
||||
};
|
||||
{
|
||||
SetLocalApiBaseAddress(sp, client);
|
||||
configureClient?.Invoke(client);
|
||||
};
|
||||
|
||||
private static Action<IServiceProvider, HttpClient> SetLocalApiBaseAddress(
|
||||
Action<IServiceProvider, HttpClient>? configureClient) => (sp, client) =>
|
||||
{
|
||||
SetLocalApiBaseAddress(sp, client);
|
||||
configureClient?.Invoke(sp, client);
|
||||
};
|
||||
{
|
||||
SetLocalApiBaseAddress(sp, client);
|
||||
configureClient?.Invoke(sp, client);
|
||||
};
|
||||
|
||||
private static void SetRemoteApiBaseAddress(IServiceProvider sp, HttpClient client)
|
||||
{
|
||||
var baseAddress = GetRemoteBaseAddress(sp);
|
||||
if (!baseAddress.EndsWith("/"))
|
||||
if (!baseAddress.EndsWith('/'))
|
||||
{
|
||||
baseAddress += "/";
|
||||
baseAddress += '/';
|
||||
}
|
||||
|
||||
var remoteApiPath = GetRemoteApiPath(sp);
|
||||
if (!string.IsNullOrEmpty(remoteApiPath))
|
||||
{
|
||||
if (remoteApiPath.StartsWith("/"))
|
||||
if (remoteApiPath.StartsWith('/'))
|
||||
{
|
||||
remoteApiPath = remoteApiPath.Substring(1);
|
||||
}
|
||||
|
||||
if (!remoteApiPath.EndsWith("/"))
|
||||
if (!remoteApiPath.EndsWith('/'))
|
||||
{
|
||||
remoteApiPath += "/";
|
||||
remoteApiPath += '/';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,8 +159,9 @@ public static class ServiceCollectionExtensions
|
|||
/// <param name="configureClient">A configuration callback used to set up
|
||||
/// the <see cref="HttpClient"/>.</param>
|
||||
public static IHttpClientBuilder AddLocalApiHttpClient(this IServiceCollection services, string clientName,
|
||||
Action<HttpClient> configureClient) => services.AddHttpClient(clientName, SetLocalApiBaseAddress(configureClient))
|
||||
.AddHttpMessageHandler<AntiForgeryHandler>();
|
||||
Action<HttpClient> configureClient) => services
|
||||
.AddHttpClient(clientName, SetLocalApiBaseAddress(configureClient))
|
||||
.AddHttpMessageHandler<AntiForgeryHandler>();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a named <see cref="HttpClient"/> for use when invoking local APIs
|
||||
|
|
@ -175,8 +176,9 @@ public static class ServiceCollectionExtensions
|
|||
/// <param name="configureClient">A configuration callback used to set up
|
||||
/// the <see cref="HttpClient"/>.</param>
|
||||
public static IHttpClientBuilder AddLocalApiHttpClient(this IServiceCollection services, string clientName,
|
||||
Action<IServiceProvider, HttpClient>? configureClient = null) => services.AddHttpClient(clientName, SetLocalApiBaseAddress(configureClient))
|
||||
.AddHttpMessageHandler<AntiForgeryHandler>();
|
||||
Action<IServiceProvider, HttpClient>? configureClient = null) => services
|
||||
.AddHttpClient(clientName, SetLocalApiBaseAddress(configureClient))
|
||||
.AddHttpMessageHandler<AntiForgeryHandler>();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a typed <see cref="HttpClient"/> for use when invoking remote APIs
|
||||
|
|
@ -189,7 +191,7 @@ public static class ServiceCollectionExtensions
|
|||
public static IHttpClientBuilder AddLocalApiHttpClient<T>(this IServiceCollection services,
|
||||
Action<IServiceProvider, HttpClient>? configureClient = null)
|
||||
where T : class => services.AddHttpClient<T>(SetLocalApiBaseAddress(configureClient))
|
||||
.AddHttpMessageHandler<AntiForgeryHandler>();
|
||||
.AddHttpMessageHandler<AntiForgeryHandler>();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a named <see cref="HttpClient"/> for use when invoking remote APIs
|
||||
|
|
@ -204,8 +206,9 @@ public static class ServiceCollectionExtensions
|
|||
/// <param name="configureClient">A configuration callback used to set up
|
||||
/// the <see cref="HttpClient"/>.</param>
|
||||
public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollection services, string clientName,
|
||||
Action<HttpClient> configureClient) => services.AddHttpClient(clientName, SetRemoteApiBaseAddress(configureClient))
|
||||
.AddHttpMessageHandler<AntiForgeryHandler>();
|
||||
Action<HttpClient> configureClient) => services
|
||||
.AddHttpClient(clientName, SetRemoteApiBaseAddress(configureClient))
|
||||
.AddHttpMessageHandler<AntiForgeryHandler>();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a named <see cref="HttpClient"/> for use when invoking remote APIs
|
||||
|
|
@ -221,8 +224,9 @@ public static class ServiceCollectionExtensions
|
|||
/// <param name="configureClient">A configuration callback used to set up
|
||||
/// the <see cref="HttpClient"/>.</param>
|
||||
public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollection services, string clientName,
|
||||
Action<IServiceProvider, HttpClient>? configureClient = null) => services.AddHttpClient(clientName, SetRemoteApiBaseAddress(configureClient))
|
||||
.AddHttpMessageHandler<AntiForgeryHandler>();
|
||||
Action<IServiceProvider, HttpClient>? configureClient = null) => services
|
||||
.AddHttpClient(clientName, SetRemoteApiBaseAddress(configureClient))
|
||||
.AddHttpMessageHandler<AntiForgeryHandler>();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a typed <see cref="HttpClient"/> for use when invoking remote APIs
|
||||
|
|
@ -234,7 +238,7 @@ public static class ServiceCollectionExtensions
|
|||
public static IHttpClientBuilder AddRemoteApiHttpClient<T>(this IServiceCollection services,
|
||||
Action<HttpClient> configureClient)
|
||||
where T : class => services.AddHttpClient<T>(SetRemoteApiBaseAddress(configureClient))
|
||||
.AddHttpMessageHandler<AntiForgeryHandler>();
|
||||
.AddHttpMessageHandler<AntiForgeryHandler>();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a typed <see cref="HttpClient"/> for use when invoking remote APIs
|
||||
|
|
@ -247,5 +251,5 @@ public static class ServiceCollectionExtensions
|
|||
public static IHttpClientBuilder AddRemoteApiHttpClient<T>(this IServiceCollection services,
|
||||
Action<IServiceProvider, HttpClient>? configureClient = null)
|
||||
where T : class => services.AddHttpClient<T>(SetRemoteApiBaseAddress(configureClient))
|
||||
.AddHttpMessageHandler<AntiForgeryHandler>();
|
||||
.AddHttpMessageHandler<AntiForgeryHandler>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ namespace Duende.Bff.Blazor;
|
|||
public static class
|
||||
BffBuilderExtensions
|
||||
{
|
||||
public static BffBuilder AddBlazorServer(this BffBuilder builder, Action<BffBlazorServerOptions>? configureOptions = null)
|
||||
public static BffBuilder AddBlazorServer(this BffBuilder builder,
|
||||
Action<BffBlazorServerOptions>? configureOptions = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
builder.Services
|
||||
.AddOpenIdConnectAccessTokenManagement()
|
||||
.AddBlazorServerAccessTokenManagement<ServerSideTokenStore>()
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ internal sealed class BffServerAuthenticationStateProvider : RevalidatingServerA
|
|||
Claims = claims.ToArray()
|
||||
};
|
||||
|
||||
_logger.LogDebug("Persisting Authentication State");
|
||||
_logger.PersistingAuthenticationState(LogLevel.Debug);
|
||||
|
||||
_state.PersistAsJson(nameof(ClaimsPrincipalRecord), principal);
|
||||
}
|
||||
|
|
@ -143,7 +143,7 @@ internal sealed class BffServerAuthenticationStateProvider : RevalidatingServerA
|
|||
SessionId = sid,
|
||||
SubjectId = sub
|
||||
},
|
||||
ct);
|
||||
ct);
|
||||
return sessions.Count != 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
bff/src/Bff.Blazor/LogMessages.cs
Normal file
13
bff/src/Bff.Blazor/LogMessages.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Duende.Bff.Blazor;
|
||||
|
||||
internal static partial class LogMessages
|
||||
{
|
||||
[LoggerMessage(
|
||||
Message = "Persisting authentication state")]
|
||||
public static partial void PersistingAuthenticationState(this ILogger logger, LogLevel logLevel);
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ internal class ServerSideTokenStore(
|
|||
public async Task<TokenResult<TokenForParameters>> GetTokenAsync(ClaimsPrincipal user, UserTokenRequestParameters? parameters = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
logger.RetrievingTokenForUser(user.Identity?.Name);
|
||||
logger.RetrievingTokenForUser(LogLevel.Debug, user.Identity?.Name);
|
||||
var session = await GetSession(user);
|
||||
if (session == null)
|
||||
{
|
||||
|
|
@ -58,7 +58,7 @@ internal class ServerSideTokenStore(
|
|||
var sub = user.FindFirst("sub")?.Value ?? throw new InvalidOperationException("no sub claim");
|
||||
var sid = user.FindFirst("sid")?.Value ?? throw new InvalidOperationException("no sid claim");
|
||||
|
||||
logger.RetrievingSession(sid, sub);
|
||||
logger.RetrievingSession(LogLevel.Debug, sid, sub);
|
||||
|
||||
var sessions = await sessionStore.GetUserSessionsAsync(new UserSessionsFilter
|
||||
{
|
||||
|
|
@ -82,7 +82,7 @@ internal class ServerSideTokenStore(
|
|||
public async Task StoreTokenAsync(ClaimsPrincipal user, UserToken token,
|
||||
UserTokenRequestParameters? parameters = null, CT ct = default)
|
||||
{
|
||||
logger.StoringTokenForUser(user.Identity?.Name);
|
||||
logger.StoringTokenForUser(LogLevel.Debug, user.Identity?.Name);
|
||||
await UpdateTicket(user,
|
||||
async ticket => { await tokensInAuthProperties.SetUserTokenAsync(token, ticket.Properties, parameters, ct); });
|
||||
}
|
||||
|
|
@ -90,7 +90,7 @@ internal class ServerSideTokenStore(
|
|||
|
||||
public async Task ClearTokenAsync(ClaimsPrincipal user, UserTokenRequestParameters? parameters = null, CT ct = default)
|
||||
{
|
||||
logger.RemovingTokenForUser(user.Identity?.Name);
|
||||
logger.RemovingTokenForUser(LogLevel.Debug, user.Identity?.Name);
|
||||
await UpdateTicket(user, ticket =>
|
||||
{
|
||||
tokensInAuthProperties.RemoveUserToken(ticket.Properties, parameters);
|
||||
|
|
@ -103,7 +103,7 @@ internal class ServerSideTokenStore(
|
|||
var session = await GetSession(user);
|
||||
if (session == null)
|
||||
{
|
||||
logger.FailedToFindSessionToUpdate();
|
||||
logger.FailedToFindSessionToUpdate(LogLevel.Debug);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ public static class BffBuilderExtensions
|
|||
public static BffBuilder AddEntityFrameworkServerSideSessions<TContext>(this BffBuilder bffBuilder, Action<IServiceProvider, DbContextOptionsBuilder> action)
|
||||
where TContext : DbContext, ISessionDbContext
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bffBuilder);
|
||||
bffBuilder.Services.AddDbContext<TContext>(action);
|
||||
return bffBuilder.AddEntityFrameworkServerSideSessionsServices<TContext>();
|
||||
}
|
||||
|
|
@ -50,6 +51,7 @@ public static class BffBuilderExtensions
|
|||
public static BffBuilder AddEntityFrameworkServerSideSessions<TContext>(this BffBuilder bffBuilder, Action<DbContextOptionsBuilder> action)
|
||||
where TContext : DbContext, ISessionDbContext
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bffBuilder);
|
||||
bffBuilder.Services.AddDbContext<TContext>(action);
|
||||
return bffBuilder.AddEntityFrameworkServerSideSessionsServices<TContext>();
|
||||
}
|
||||
|
|
@ -64,6 +66,7 @@ public static class BffBuilderExtensions
|
|||
public static BffBuilder AddEntityFrameworkServerSideSessionsServices<TContext>(this BffBuilder bffBuilder)
|
||||
where TContext : ISessionDbContext
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bffBuilder);
|
||||
bffBuilder.Services.AddTransient<IUserSessionStoreCleanup, UserSessionStore>();
|
||||
bffBuilder.Services.AddTransient<ISessionDbContext>(svcs => svcs.GetRequiredService<TContext>());
|
||||
return bffBuilder.AddServerSideSessions<UserSessionStore>();
|
||||
|
|
@ -74,6 +77,7 @@ public static class BffBuilderExtensions
|
|||
/// </summary>
|
||||
public static BffBuilder ConfigureEntityFrameworkSessionStoreOptions(this BffBuilder bffBuilder, Action<SessionStoreOptions> action)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bffBuilder);
|
||||
bffBuilder.Services.Configure(action);
|
||||
return bffBuilder;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,14 +13,16 @@ namespace Duende.Bff.EntityFramework;
|
|||
/// <summary>
|
||||
/// Entity framework core implementation of IUserSessionStore
|
||||
/// </summary>
|
||||
internal class UserSessionStore(IOptions<DataProtectionOptions> options, ISessionDbContext sessionDbContext, ILogger<UserSessionStore> logger) : IUserSessionStore, IUserSessionStoreCleanup
|
||||
#pragma warning disable CA1812 // internal class never instantiated? It is, but via DI
|
||||
internal sealed class UserSessionStore(IOptions<DataProtectionOptions> options, ISessionDbContext sessionDbContext, ILogger<UserSessionStore> logger) : IUserSessionStore, IUserSessionStoreCleanup
|
||||
#pragma warning restore CA1812
|
||||
{
|
||||
private readonly string? _applicationDiscriminator = options.Value.ApplicationDiscriminator;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task CreateUserSessionAsync(UserSession session, CT ct)
|
||||
{
|
||||
LogMessages.CreatingUserSession(logger, session.SubjectId, session.SessionId);
|
||||
logger.CreatingUserSession(LogLevel.Debug, session.SubjectId, session.SessionId);
|
||||
|
||||
var item = new UserSessionEntity()
|
||||
{
|
||||
|
|
@ -46,13 +48,13 @@ internal class UserSessionStore(IOptions<DataProtectionOptions> options, ISessio
|
|||
// SQL Server would send: ---> Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot insert duplicate key row in object 'Session.UserSessions' with unique index 'IX_UserSessions_ApplicationName_SessionId'. The duplicate key value is (<AppName>, <SessionIdValue>).
|
||||
// Postgres would send: ---> Npgsql.PostgresException (0x80004005): 23505: duplicate key value violates unique constraint "IX_UserSessions_ApplicationName_SessionId"
|
||||
// MySQL would send: ---> MySql.Data.MySqlClient.MySqlException (0x80004005): Duplicate entry '<AppName>-<SessionIdValue>' for key 'IX_UserSessions_ApplicationName_SessionId'
|
||||
if (exception.Contains("UNIQUE", StringComparison.OrdinalIgnoreCase) || exception.Contains("IX_UserSessions_ApplicationName_SessionId"))
|
||||
if (exception.Contains("UNIQUE", StringComparison.OrdinalIgnoreCase) || exception.Contains("IX_UserSessions_ApplicationName_SessionId", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
LogMessages.DuplicateSessionInsertDetected(logger, ex);
|
||||
logger.DuplicateSessionInsertDetected(LogLevel.Debug, ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMessages.ExceptionCreatingSession(logger, ex, ex.Message);
|
||||
logger.ExceptionCreatingSession(LogLevel.Warning, ex, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -65,11 +67,11 @@ internal class UserSessionStore(IOptions<DataProtectionOptions> options, ISessio
|
|||
|
||||
if (item == null)
|
||||
{
|
||||
LogMessages.NoRecordFoundForKey(logger, key);
|
||||
logger.NoRecordFoundForKey(LogLevel.Debug, key);
|
||||
return;
|
||||
}
|
||||
|
||||
LogMessages.DeletingUserSession(logger, item.SubjectId, item.SessionId);
|
||||
logger.DeletingUserSession(LogLevel.Debug, item.SubjectId, item.SessionId);
|
||||
|
||||
sessionDbContext.UserSessions.Remove(item);
|
||||
try
|
||||
|
|
@ -80,7 +82,7 @@ internal class UserSessionStore(IOptions<DataProtectionOptions> options, ISessio
|
|||
{
|
||||
// suppressing exception for concurrent deletes
|
||||
// https://github.com/DuendeSoftware/BFF/issues/63
|
||||
LogMessages.DbUpdateConcurrencyException(logger, ex.Message);
|
||||
logger.DbUpdateConcurrencyException(LogLevel.Debug, ex.Message);
|
||||
|
||||
foreach (var entry in ex.Entries)
|
||||
{
|
||||
|
|
@ -117,7 +119,7 @@ internal class UserSessionStore(IOptions<DataProtectionOptions> options, ISessio
|
|||
items = items.Where(x => x.SessionId == filter.SessionId).ToArray();
|
||||
}
|
||||
|
||||
LogMessages.DeletingUserSessions(logger, items.Length, filter.SubjectId, filter.SessionId);
|
||||
logger.DeletingUserSessions(LogLevel.Debug, items.Length, filter.SubjectId, filter.SessionId);
|
||||
|
||||
sessionDbContext.UserSessions.RemoveRange(items);
|
||||
|
||||
|
|
@ -129,7 +131,7 @@ internal class UserSessionStore(IOptions<DataProtectionOptions> options, ISessio
|
|||
{
|
||||
// suppressing exception for concurrent deletes
|
||||
// https://github.com/DuendeSoftware/BFF/issues/63
|
||||
LogMessages.DbUpdateConcurrencyException(logger, ex.Message);
|
||||
logger.DbUpdateConcurrencyException(LogLevel.Debug, ex.Message);
|
||||
|
||||
foreach (var entry in ex.Entries)
|
||||
{
|
||||
|
|
@ -148,11 +150,11 @@ internal class UserSessionStore(IOptions<DataProtectionOptions> options, ISessio
|
|||
UserSession? result = null;
|
||||
if (item == null)
|
||||
{
|
||||
LogMessages.NoRecordFoundForKey(logger, key);
|
||||
logger.NoRecordFoundForKey(LogLevel.Debug, key);
|
||||
return null;
|
||||
}
|
||||
|
||||
LogMessages.GettingUserSession(logger, item.SubjectId, item.SessionId);
|
||||
logger.GettingUserSession(LogLevel.Debug, item.SubjectId, item.SessionId);
|
||||
|
||||
result = new UserSession();
|
||||
item.CopyTo(result);
|
||||
|
|
@ -194,7 +196,7 @@ internal class UserSessionStore(IOptions<DataProtectionOptions> options, ISessio
|
|||
return item;
|
||||
}).ToArray();
|
||||
|
||||
LogMessages.GettingUserSessions(logger, results.Length, filter.SubjectId, filter.SessionId);
|
||||
logger.GettingUserSessions(LogLevel.Debug, results.Length, filter.SubjectId, filter.SessionId);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
|
@ -206,11 +208,11 @@ internal class UserSessionStore(IOptions<DataProtectionOptions> options, ISessio
|
|||
var item = items.SingleOrDefault(x => x.Key == key && x.ApplicationName == _applicationDiscriminator);
|
||||
if (item == null)
|
||||
{
|
||||
LogMessages.NoRecordFoundForKey(logger, key);
|
||||
logger.NoRecordFoundForKey(LogLevel.Debug, key);
|
||||
return;
|
||||
}
|
||||
|
||||
LogMessages.UpdatingUserSession(logger, item.SubjectId, item.SessionId);
|
||||
logger.UpdatingUserSession(LogLevel.Debug, item.SubjectId, item.SessionId);
|
||||
|
||||
session.CopyTo(item);
|
||||
await sessionDbContext.SaveChangesAsync(ct);
|
||||
|
|
@ -239,7 +241,7 @@ internal class UserSessionStore(IOptions<DataProtectionOptions> options, ISessio
|
|||
continue;
|
||||
}
|
||||
|
||||
LogMessages.RemovingServerSideSessions(logger, found);
|
||||
logger.RemovingServerSideSessions(LogLevel.Debug, found);
|
||||
|
||||
sessionDbContext.UserSessions.RemoveRange(expired);
|
||||
removed += found;
|
||||
|
|
@ -250,7 +252,7 @@ internal class UserSessionStore(IOptions<DataProtectionOptions> options, ISessio
|
|||
catch (DbUpdateConcurrencyException ex)
|
||||
{
|
||||
// suppressing exception for concurrent deletes
|
||||
LogMessages.DbUpdateConcurrencyException(logger, ex.Message);
|
||||
logger.DbUpdateConcurrencyException(LogLevel.Debug, ex.Message);
|
||||
|
||||
foreach (var entry in ex.Entries)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ public static class ModelBuilderExtensions
|
|||
/// <param name="storeOptions">The store options.</param>
|
||||
public static void ConfigureSessionContext(this ModelBuilder modelBuilder, SessionStoreOptions storeOptions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(modelBuilder);
|
||||
ArgumentNullException.ThrowIfNull(storeOptions);
|
||||
if (!string.IsNullOrWhiteSpace(storeOptions.DefaultSchema))
|
||||
{
|
||||
modelBuilder.HasDefaultSchema(storeOptions.DefaultSchema);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ public static class BffBuilderExtensions
|
|||
/// <returns></returns>
|
||||
public static BffBuilder AddRemoteApis(this BffBuilder builder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
builder.RegisterConfigurationLoader((services, config) =>
|
||||
{
|
||||
services.Configure<ProxyConfiguration>(config);
|
||||
|
|
@ -45,6 +47,8 @@ public static class BffBuilderExtensions
|
|||
public static IReverseProxyBuilder AddYarpConfig(this BffBuilder builder, RouteConfig[] routes,
|
||||
ClusterConfig[] clusters)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
var yarpBuilder = builder.Services.AddReverseProxy()
|
||||
.AddBffExtensions();
|
||||
|
||||
|
|
@ -55,6 +59,8 @@ public static class BffBuilderExtensions
|
|||
|
||||
public static IReverseProxyBuilder AddYarpConfig(this BffBuilder builder, IConfiguration config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
var yarpBuilder = builder.Services.AddReverseProxy()
|
||||
.AddBffExtensions();
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public static class DefaultBffYarpTransformerBuilders
|
|||
/// Build a default 'direct proxy' transformer. This removes the 'cookie' header, removes the local path prefix,
|
||||
/// and adds an access token to the request. The type of access token is determined by the <see cref="BffRemoteApiEndpointMetadata"/>.
|
||||
/// </summary>
|
||||
public static BffYarpTransformBuilder DirectProxyWithAccessToken =
|
||||
public static readonly BffYarpTransformBuilder DirectProxyWithAccessToken =
|
||||
(localPath, context) =>
|
||||
{
|
||||
context.AddRequestHeaderRemove("Cookie");
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Duende.Bff.Yarp;
|
||||
|
||||
internal static class EventIds
|
||||
{
|
||||
public static readonly EventId ProxyError = new(5, "ProxyError");
|
||||
}
|
||||
|
|
@ -100,11 +100,11 @@ internal class AccessTokenRequestTransform(
|
|||
{
|
||||
if (!options.Value.RemoveSessionAfterRefreshTokenExpiration)
|
||||
{
|
||||
Otel.LogMessages.FailedToRequestNewUserAccessToken(logger, tokenError.Error);
|
||||
logger.FailedToRequestNewUserAccessToken(LogLevel.Warning, tokenError.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
Otel.LogMessages.UserSessionRevoked(logger, tokenError.Error);
|
||||
logger.UserSessionRevoked(LogLevel.Warning, tokenError.Error);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ internal class AccessTokenRequestTransform(
|
|||
tokenError.Error);
|
||||
}
|
||||
|
||||
private void ApplyBearerToken(RequestTransformContext context, BearerTokenResult token) => context.ProxyRequest.Headers.Authorization =
|
||||
private static void ApplyBearerToken(RequestTransformContext context, BearerTokenResult token) => context.ProxyRequest.Headers.Authorization =
|
||||
new AuthenticationHeaderValue(OidcConstants.AuthenticationSchemes.AuthorizationHeaderBearer, token.AccessToken.ToString());
|
||||
|
||||
private async Task ApplyDPoPToken(RequestTransformContext context, DPoPTokenResult token)
|
||||
|
|
|
|||
|
|
@ -45,8 +45,8 @@ internal class RemoteRouteHandler(
|
|||
|
||||
foreach (var route in frontend.GetRemoteApis())
|
||||
{
|
||||
|
||||
if (context.Request.Path.StartsWithSegments(route.LocalPath.ToString()))
|
||||
// Path matching must be case insensitive
|
||||
if (context.Request.Path.StartsWithSegments(route.LocalPath.ToString(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var bffRemoteApiEndpointMetadata = new BffRemoteApiEndpointMetadata()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Duende.Bff.Yarp;
|
||||
|
||||
internal static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, string, string, Exception?> ProxyResponseErrorMessage = LoggerMessage.Define<string, string>(
|
||||
LogLevel.Information,
|
||||
EventIds.ProxyError,
|
||||
"Proxy response error. local path: '{localPath}', error: '{error}'");
|
||||
|
||||
public static void ProxyResponseError(this ILogger logger, string localPath, string error) => ProxyResponseErrorMessage(logger, localPath, error, null);
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ internal class MapRemoteRoutesMiddleware(RequestDelegate next, RemoteRouteHandle
|
|||
await next(context);
|
||||
}
|
||||
|
||||
private bool ShouldMapRemoteRoutes(HttpContext context)
|
||||
private static bool ShouldMapRemoteRoutes(HttpContext context)
|
||||
{
|
||||
var selectedFrontend = context.RequestServices.GetRequiredService<SelectedFrontend>();
|
||||
|
||||
|
|
@ -31,6 +31,6 @@ internal class MapRemoteRoutesMiddleware(RequestDelegate next, RemoteRouteHandle
|
|||
return false;
|
||||
}
|
||||
|
||||
return frontend.GetRemoteApis().Any();
|
||||
return frontend.GetRemoteApis().Length != 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@ public static class ProxyConfigExtensions
|
|||
/// <param name="config"></param>
|
||||
/// <param name="requiredTokenType"></param>
|
||||
/// <returns></returns>
|
||||
public static RouteConfig WithAccessToken(this RouteConfig config, RequiredTokenType requiredTokenType) => config.WithMetadata(Constants.Yarp.TokenTypeMetadata, requiredTokenType.ToString());
|
||||
public static RouteConfig WithAccessToken(this RouteConfig config, RequiredTokenType requiredTokenType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
return config.WithMetadata(Constants.Yarp.TokenTypeMetadata, requiredTokenType.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds BFF access token metadata to a route configuration, indicating that
|
||||
|
|
@ -27,17 +31,27 @@ public static class ProxyConfigExtensions
|
|||
/// <param name="config"></param>
|
||||
/// <returns></returns>
|
||||
[Obsolete("Use TokenRequirement.OptionalUserOrNone")]
|
||||
public static RouteConfig WithOptionalUserAccessToken(this RouteConfig config) => WithAccessToken(config, RequiredTokenType.UserOrNone);
|
||||
public static RouteConfig WithOptionalUserAccessToken(this RouteConfig config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
return WithAccessToken(config, RequiredTokenType.UserOrNone);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds anti-forgery metadata to a route configuration
|
||||
/// </summary>
|
||||
/// <param name="config"></param>
|
||||
/// <returns></returns>
|
||||
public static RouteConfig WithAntiforgeryCheck(this RouteConfig config) => config.WithMetadata(Constants.Yarp.AntiforgeryCheckMetadata, "true");
|
||||
public static RouteConfig WithAntiforgeryCheck(this RouteConfig config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
return config.WithMetadata(Constants.Yarp.AntiforgeryCheckMetadata, "true");
|
||||
}
|
||||
|
||||
private static RouteConfig WithMetadata(this RouteConfig config, string key, string value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
|
||||
Dictionary<string, string> metadata;
|
||||
|
||||
if (config.Metadata != null)
|
||||
|
|
@ -63,6 +77,7 @@ public static class ProxyConfigExtensions
|
|||
/// <returns></returns>
|
||||
public static ClusterConfig WithAccessToken(this ClusterConfig config, RequiredTokenType requiredTokenType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
Dictionary<string, string> metadata;
|
||||
|
||||
if (config.Metadata != null)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ public static class ProxyFrontendExtensionExtensions
|
|||
{
|
||||
public static BffFrontend WithRemoteApis(this BffFrontend frontend, params RemoteApi[] config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frontend);
|
||||
|
||||
// Remove existing ProxyFrontendExtension if present
|
||||
var newExtensions = frontend.DataExtensions
|
||||
.Where(e => e is not ProxyBffPlugin)
|
||||
|
|
@ -29,8 +31,13 @@ public static class ProxyFrontendExtensionExtensions
|
|||
/// </summary>
|
||||
/// <param name="frontend"></param>
|
||||
/// <returns></returns>
|
||||
public static RemoteApi[] GetRemoteApis(this BffFrontend frontend) => frontend.DataExtensions
|
||||
.OfType<ProxyBffPlugin>()
|
||||
.SelectMany(e => e.RemoteApis)
|
||||
.ToArray();
|
||||
public static RemoteApi[] GetRemoteApis(this BffFrontend frontend)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frontend);
|
||||
|
||||
return frontend.DataExtensions
|
||||
.OfType<ProxyBffPlugin>()
|
||||
.SelectMany(e => e.RemoteApis)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ public static class RouteBuilderExtensions
|
|||
ForwarderRequestConfig? requestConfig = null
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
ArgumentNullException.ThrowIfNull(apiAddress);
|
||||
endpoints.CheckLicense();
|
||||
|
||||
// See if a default request config is registered in DI, otherwise use an empty one
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ public static class YarpTransformExtensions
|
|||
/// </summary>
|
||||
public static TransformBuilderContext AddBffAccessToken(this TransformBuilderContext context, PathString localPath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
var proofService = context.Services.GetRequiredService<IDPoPProofService>();
|
||||
|
||||
var logger = context.Services.GetRequiredService<ILogger<AccessTokenRequestTransform>>();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using Duende.Bff.Internal;
|
||||
|
||||
using AtmAccessToken = Duende.AccessTokenManagement.AccessToken;
|
||||
|
||||
namespace Duende.Bff.AccessTokenManagement;
|
||||
|
|
@ -20,7 +19,8 @@ public readonly record struct AccessToken : IStronglyTypedValue<AccessToken>
|
|||
// Officially, there's no max length for JWTs, but 32k is a good limit
|
||||
public const int MaxLength = 32 * 1024; // 32k
|
||||
|
||||
private static readonly ValidationRule<string>[] Validators = [
|
||||
private static readonly ValidationRule<string>[] Validators =
|
||||
[
|
||||
ValidationRules.MaxLength(MaxLength)
|
||||
];
|
||||
|
||||
|
|
@ -29,13 +29,17 @@ public readonly record struct AccessToken : IStronglyTypedValue<AccessToken>
|
|||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public static implicit operator string(AccessToken value) => value.ToString();
|
||||
|
||||
#pragma warning disable CA2225 // (OperatorOverloadsHaveNamedAlternates) Intentionally not using named alternative for this, becuase we don't want to expose the ATM type in the BFF API.
|
||||
public static implicit operator AtmAccessToken(AccessToken value) => AtmAccessToken.Parse(value.ToString());
|
||||
#pragma warning restore CA2225
|
||||
|
||||
/// <summary>
|
||||
/// You can't directly create this type.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public AccessToken() => throw new InvalidOperationException("Can't create null value");
|
||||
|
||||
private AccessToken(string value) => Value = value;
|
||||
|
||||
private string Value { get; }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Duende.Bff.Internal;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
|
@ -16,13 +17,15 @@ public readonly record struct DPoPProofKey : IStronglyTypedValue<DPoPProofKey>
|
|||
{
|
||||
public bool Equals(DPoPProofKey other) => Value == other.Value;
|
||||
|
||||
public override int GetHashCode() => Value.GetHashCode();
|
||||
public override int GetHashCode() => Value.GetHashCode(StringComparison.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts DPOPProofKey to same type from access token management.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
#pragma warning disable CA2225 // (OperatorOverloadsHaveNamedAlternates) Intentionally not using named alternative for this, becuase we don't want to expose the ATM type in the BFF API.
|
||||
public static implicit operator AtmDPoPProofKey(DPoPProofKey value) => AtmDPoPProofKey.Parse(value.ToString());
|
||||
#pragma warning restore CA2225
|
||||
|
||||
/// <summary>
|
||||
/// Convenience method for converting a <see cref="DPoPProofKey"/> into a string.
|
||||
|
|
@ -34,7 +37,8 @@ public readonly record struct DPoPProofKey : IStronglyTypedValue<DPoPProofKey>
|
|||
|
||||
public override string ToString() => Value;
|
||||
|
||||
private static readonly ValidationRule<string>[] Validators = [
|
||||
private static readonly ValidationRule<string>[] Validators =
|
||||
[
|
||||
// Officially, there's no max length for JWTs, but 32k is a good limit
|
||||
ValidationRules.MaxLength(32 * 1024),
|
||||
IsValidJsonWebKey()
|
||||
|
|
@ -49,7 +53,7 @@ public readonly record struct DPoPProofKey : IStronglyTypedValue<DPoPProofKey>
|
|||
JsonWebKey.Create(value);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (JsonException e)
|
||||
{
|
||||
message = "String is not a valid json web key: " + e.Message;
|
||||
return false;
|
||||
|
|
@ -61,6 +65,7 @@ public readonly record struct DPoPProofKey : IStronglyTypedValue<DPoPProofKey>
|
|||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public DPoPProofKey() => throw new InvalidOperationException("Can't create null value");
|
||||
|
||||
private DPoPProofKey(string value)
|
||||
{
|
||||
Value = value;
|
||||
|
|
@ -95,5 +100,4 @@ public readonly record struct DPoPProofKey : IStronglyTypedValue<DPoPProofKey>
|
|||
/// contain null or whitespace strings.
|
||||
/// </summary>
|
||||
public static DPoPProofKey? ParseOrDefault(string? value) => StringParsers<DPoPProofKey>.ParseOrDefault(value);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ using System.Diagnostics.CodeAnalysis;
|
|||
using System.Text.Json.Serialization;
|
||||
using Duende.Bff.Internal;
|
||||
using Duende.IdentityModel;
|
||||
|
||||
using AtmScheme = Duende.AccessTokenManagement.Scheme;
|
||||
|
||||
namespace Duende.Bff.AccessTokenManagement;
|
||||
|
|
@ -27,11 +26,15 @@ public readonly record struct Scheme : IStronglyTypedValue<Scheme>
|
|||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public static implicit operator string(Scheme value) => value.ToString();
|
||||
|
||||
#pragma warning disable CA2225 // (OperatorOverloadsHaveNamedAlternates) Intentionally not using named alternative for this, becuase we don't want to expose the ATM type in the BFF API.
|
||||
public static implicit operator AtmScheme(Scheme value) => AtmScheme.Parse(value.ToString());
|
||||
#pragma warning restore CA2225
|
||||
|
||||
public override string ToString() => Value;
|
||||
|
||||
private static readonly ValidationRule<string>[] Validators = [
|
||||
private static readonly ValidationRule<string>[] Validators =
|
||||
[
|
||||
ValidationRules.MaxLength(MaxLength),
|
||||
];
|
||||
|
||||
|
|
@ -46,7 +49,6 @@ public readonly record struct Scheme : IStronglyTypedValue<Scheme>
|
|||
|
||||
private Scheme(string value)
|
||||
{
|
||||
|
||||
// Some target systems are case-sensitive in their scheme handling. This code normalizes
|
||||
// the casing.
|
||||
|
||||
|
|
@ -56,14 +58,17 @@ public readonly record struct Scheme : IStronglyTypedValue<Scheme>
|
|||
|
||||
//IE: if Scheme == BeAReR => "Bearer"
|
||||
//IE: if Scheme == DpoP => "DPoP"
|
||||
if (value.Equals(OidcConstants.AuthenticationSchemes.AuthorizationHeaderBearer, StringComparison.OrdinalIgnoreCase))
|
||||
if (value.Equals(OidcConstants.AuthenticationSchemes.AuthorizationHeaderBearer,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = OidcConstants.AuthenticationSchemes.AuthorizationHeaderBearer;
|
||||
}
|
||||
else if (value.Equals(OidcConstants.AuthenticationSchemes.AuthorizationHeaderDPoP, StringComparison.OrdinalIgnoreCase))
|
||||
else if (value.Equals(OidcConstants.AuthenticationSchemes.AuthorizationHeaderDPoP,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = OidcConstants.AuthenticationSchemes.AuthorizationHeaderDPoP;
|
||||
}
|
||||
|
||||
Value = value;
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +92,6 @@ public readonly record struct Scheme : IStronglyTypedValue<Scheme>
|
|||
/// Parses a value to a <see cref="Scheme"/>. This will throw an exception if the string is not valid.
|
||||
/// </summary>
|
||||
public static Scheme Parse(string value) => StringParsers<Scheme>.Parse(value);
|
||||
public static Scheme? ParseOrDefault(string? value) => StringParsers<Scheme>.ParseOrDefault(value);
|
||||
|
||||
public static Scheme? ParseOrDefault(string? value) => StringParsers<Scheme>.ParseOrDefault(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,12 @@ public static class AuthenticationPropertiesExtensions
|
|||
/// <summary>
|
||||
/// Determines if this AuthenticationProperties represents a BFF silent login.
|
||||
/// </summary>
|
||||
public static bool IsSilentLogin(this AuthenticationProperties props) => props.TryGetPrompt(out var prompt) && prompt == "none";
|
||||
public static bool IsSilentLogin(this AuthenticationProperties props) =>
|
||||
props.TryGetPrompt(out var prompt) && prompt == "none";
|
||||
|
||||
public static bool TryGetPrompt(this AuthenticationProperties props, [NotNullWhen(true)] out string? prompt) => props.Items.TryGetValue(Constants.BffFlags.Prompt, out prompt);
|
||||
public static bool TryGetPrompt(this AuthenticationProperties props, [NotNullWhen(true)] out string? prompt)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(props);
|
||||
return props.Items.TryGetValue(Constants.BffFlags.Prompt, out prompt);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Duende.Bff.Internal;
|
||||
using Duende.Bff.Otel;
|
||||
using Duende.Bff.SessionManagement.SessionStore;
|
||||
using Duende.IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
|
@ -29,6 +31,7 @@ public static class AuthenticationTicketExtensions
|
|||
/// </summary>
|
||||
public static string GetSubjectId(this AuthenticationTicket ticket)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ticket);
|
||||
var subjectId = ticket.Principal.FindFirst(JwtClaimTypes.Subject)?.Value ??
|
||||
ticket.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value ??
|
||||
// for the mfa remember me cookie, ASP.NET Identity uses the 'name' claim for the subject id (for some reason)
|
||||
|
|
@ -41,23 +44,37 @@ public static class AuthenticationTicketExtensions
|
|||
/// <summary>
|
||||
/// Extracts the session ID
|
||||
/// </summary>
|
||||
public static string? GetSessionId(this AuthenticationTicket ticket) => ticket.Principal.FindFirst(JwtClaimTypes.SessionId)?.Value;
|
||||
public static string? GetSessionId(this AuthenticationTicket ticket)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ticket);
|
||||
return ticket.Principal.FindFirst(JwtClaimTypes.SessionId)?.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the issuance time
|
||||
/// </summary>
|
||||
public static DateTime GetIssued(this AuthenticationTicket ticket) => ticket.Properties.IssuedUtc?.UtcDateTime ?? DateTime.UtcNow;
|
||||
public static DateTime GetIssued(this AuthenticationTicket ticket)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ticket);
|
||||
return ticket.Properties.IssuedUtc?.UtcDateTime ?? DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the expiration time
|
||||
/// </summary>
|
||||
public static DateTime? GetExpiration(this AuthenticationTicket ticket) => ticket.Properties.ExpiresUtc?.UtcDateTime;
|
||||
public static DateTime? GetExpiration(this AuthenticationTicket ticket)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ticket);
|
||||
return ticket.Properties.ExpiresUtc?.UtcDateTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes and AuthenticationTicket to a string
|
||||
/// </summary>
|
||||
public static string Serialize(this AuthenticationTicket ticket, IDataProtector protector)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ticket);
|
||||
ArgumentNullException.ThrowIfNull(protector);
|
||||
var data = new AuthenticationTicketLite
|
||||
{
|
||||
Scheme = ticket.AuthenticationScheme,
|
||||
|
|
@ -79,12 +96,16 @@ public static class AuthenticationTicketExtensions
|
|||
/// </summary>
|
||||
public static AuthenticationTicket? Deserialize(this UserSession session, IDataProtector protector, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(session);
|
||||
ArgumentNullException.ThrowIfNull(protector);
|
||||
try
|
||||
{
|
||||
var envelope = JsonSerializer.Deserialize<Envelope>(session.Ticket, JsonOptions);
|
||||
if (envelope == null || envelope.Version != 1)
|
||||
{
|
||||
logger.LogDebug("Deserializing AuthenticationTicket envelope failed or found incorrect version for key {key}.", session.Key);
|
||||
logger.AuthenticationTicketEnvelopeVersionInvalid(
|
||||
LogLevel.Debug,
|
||||
session.Key);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -93,16 +114,16 @@ public static class AuthenticationTicketExtensions
|
|||
{
|
||||
payload = protector.Unprotect(envelope.Payload);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
logger.LogDebug(ex, "Failed to unprotect AuthenticationTicket payload for key {key}", session.Key);
|
||||
logger.AuthenticationTicketPayloadInvalid(ex, LogLevel.Warning, session.Key);
|
||||
return null;
|
||||
}
|
||||
|
||||
var ticket = JsonSerializer.Deserialize<AuthenticationTicketLite>(payload, JsonOptions);
|
||||
if (ticket == null)
|
||||
{
|
||||
logger.LogDebug("Deserializing AuthenticationTicket failed for key {key}.", session.Key);
|
||||
logger.AuthenticationTicketPayloadInvalid(ex: null, LogLevel.Warning, session.Key);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -121,10 +142,10 @@ public static class AuthenticationTicketExtensions
|
|||
|
||||
return new AuthenticationTicket(user, properties, ticket.Scheme);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (JsonException ex)
|
||||
{
|
||||
// failed deserialize
|
||||
logger.LogError(ex, "Failed to deserialize UserSession payload for key {key}", session.Key);
|
||||
logger.AuthenticationTicketFailedToDeserialize(ex, LogLevel.Warning, session.Key);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -93,10 +93,10 @@ public sealed class BffBuilder(IServiceCollection services)
|
|||
|
||||
// Add 'default' configure methods that would have been added by
|
||||
// .AddAuthentication().AddCookie().AddOpenIdConnect()
|
||||
Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIdConnectOptions>, OpenIdConnectPostConfigureOptions>());
|
||||
Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
|
||||
|
||||
Services.AddTransient<PathMapper>();
|
||||
Services.TryAddEnumerable(ServiceDescriptor
|
||||
.Singleton<IPostConfigureOptions<OpenIdConnectOptions>, OpenIdConnectPostConfigureOptions>());
|
||||
Services.TryAddEnumerable(ServiceDescriptor
|
||||
.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
|
||||
|
||||
Services.TryAddSingleton<IIndexHtmlClient, IndexHtmlHttpClient>();
|
||||
|
||||
|
|
@ -124,7 +124,9 @@ public sealed class BffBuilder(IServiceCollection services)
|
|||
/// <returns></returns>
|
||||
public BffBuilder AddServerSideSessions()
|
||||
{
|
||||
Services.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureApplicationCookieTicketStore>();
|
||||
Services
|
||||
.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>,
|
||||
PostConfigureApplicationCookieTicketStore>();
|
||||
Services.AddTransient<IServerTicketStore, ServerSideTicketStore>();
|
||||
Services.AddTransient<ISessionRevocationService, SessionRevocationService>();
|
||||
Services.AddSingleton<IHostedService, SessionCleanupHost>();
|
||||
|
|
@ -162,6 +164,7 @@ public sealed class BffBuilder(IServiceCollection services)
|
|||
{
|
||||
configLoader(Services, section);
|
||||
}
|
||||
|
||||
// We no longer need them.
|
||||
_pluginConfigurationLoaders.Clear();
|
||||
|
||||
|
|
@ -170,6 +173,8 @@ public sealed class BffBuilder(IServiceCollection services)
|
|||
|
||||
public BffBuilder AddFrontends(params BffFrontend[] frontends)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frontends);
|
||||
|
||||
// Check for duplicate frontend names
|
||||
var duplicateNames = frontends
|
||||
.GroupBy(f => f.Name)
|
||||
|
|
@ -177,9 +182,10 @@ public sealed class BffBuilder(IServiceCollection services)
|
|||
.Select(g => g.Key)
|
||||
.ToList();
|
||||
|
||||
if (duplicateNames.Any())
|
||||
if (duplicateNames.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Duplicate frontend names detected: {string.Join(", ", duplicateNames.Select(n => n))}");
|
||||
throw new InvalidOperationException(
|
||||
$"Duplicate frontend names detected: {string.Join(", ", duplicateNames.Select(n => n))}");
|
||||
}
|
||||
|
||||
foreach (var frontend in frontends)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using Duende.Bff.Configuration;
|
||||
using Duende.Bff.Endpoints;
|
||||
using Duende.Bff.Endpoints.Internal;
|
||||
using Duende.Bff.Otel;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
|
@ -35,6 +36,7 @@ public static class BffEndpointRouteBuilderExtensions
|
|||
/// <param name="endpoints"></param>
|
||||
public static void MapBffManagementEndpoints(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
var options = endpoints.ServiceProvider.GetRequiredService<IOptions<BffOptions>>().Value;
|
||||
if (endpoints.AlreadyMappedManagementEndpoint(options.LoginPath, "Login"))
|
||||
{
|
||||
|
|
@ -57,6 +59,7 @@ public static class BffEndpointRouteBuilderExtensions
|
|||
/// <param name="endpoints"></param>
|
||||
public static void MapBffManagementLoginEndpoint(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
endpoints.CheckLicense();
|
||||
|
||||
var options = endpoints.ServiceProvider.GetRequiredService<IOptions<BffOptions>>().Value;
|
||||
|
|
@ -66,12 +69,16 @@ public static class BffEndpointRouteBuilderExtensions
|
|||
.AllowAnonymous();
|
||||
}
|
||||
|
||||
internal static bool AlreadyMappedManagementEndpoint(this IEndpointRouteBuilder endpoints, PathString route, string name)
|
||||
internal static bool AlreadyMappedManagementEndpoint(
|
||||
this IEndpointRouteBuilder endpoints,
|
||||
PathString route,
|
||||
string name)
|
||||
{
|
||||
if (endpoints.DataSources.Any(x =>
|
||||
x.Endpoints.OfType<RouteEndpoint>().Any(x => x.RoutePattern.RawText == route.ToString())))
|
||||
{
|
||||
endpoints.ServiceProvider.GetRequiredService<ILogger<BffBuilder>>().LogWarning("Already mapped {name} endpoint, so the call to MapBffManagementEndpoints will be ignored. If you're using BffOptions.AutomaticallyRegisterBffMiddleware, you don't need to call endpoints.MapBffManagementEndpoints()", name);
|
||||
var logger = endpoints.ServiceProvider.GetRequiredService<ILogger<BffBuilder>>();
|
||||
logger.AlreadyMappedManagementEndpoint(LogLevel.Warning, name);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -82,9 +89,11 @@ public static class BffEndpointRouteBuilderExtensions
|
|||
/// Adds the silent login BFF management endpoints
|
||||
/// </summary>
|
||||
/// <param name="endpoints"></param>
|
||||
[Obsolete("The silent login endpoint will be removed in a future version. Silent login is now handled by passing the prompt=none parameter to the login endpoint.")]
|
||||
[Obsolete(
|
||||
"The silent login endpoint will be removed in a future version. Silent login is now handled by passing the prompt=none parameter to the login endpoint.")]
|
||||
public static void MapBffManagementSilentLoginEndpoints(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
endpoints.CheckLicense();
|
||||
|
||||
var options = endpoints.ServiceProvider.GetRequiredService<IOptions<BffOptions>>().Value;
|
||||
|
|
@ -105,6 +114,7 @@ public static class BffEndpointRouteBuilderExtensions
|
|||
/// <param name="endpoints"></param>
|
||||
public static void MapBffManagementLogoutEndpoint(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
endpoints.CheckLicense();
|
||||
|
||||
var options = endpoints.ServiceProvider.GetRequiredService<IOptions<BffOptions>>().Value;
|
||||
|
|
@ -121,6 +131,7 @@ public static class BffEndpointRouteBuilderExtensions
|
|||
/// <param name="endpoints"></param>
|
||||
public static void MapBffManagementUserEndpoint(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
endpoints.CheckLicense();
|
||||
|
||||
var options = endpoints.ServiceProvider.GetRequiredService<IOptions<BffOptions>>().Value;
|
||||
|
|
@ -136,6 +147,7 @@ public static class BffEndpointRouteBuilderExtensions
|
|||
/// <param name="endpoints"></param>
|
||||
public static void MapBffManagementBackchannelEndpoint(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
endpoints.CheckLicense();
|
||||
|
||||
var options = endpoints.ServiceProvider.GetRequiredService<IOptions<BffOptions>>().Value;
|
||||
|
|
@ -150,6 +162,7 @@ public static class BffEndpointRouteBuilderExtensions
|
|||
/// <param name="endpoints"></param>
|
||||
public static void MapBffDiagnosticsEndpoint(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
endpoints.CheckLicense();
|
||||
|
||||
var options = endpoints.ServiceProvider.GetRequiredService<IOptions<BffOptions>>().Value;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ public static class BffRemoteApiEndpointExtensions
|
|||
public static IEndpointConventionBuilder WithAccessTokenRetriever<TRetriever>(this IEndpointConventionBuilder builder)
|
||||
where TRetriever : IAccessTokenRetriever
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
builder.Add(endpointBuilder =>
|
||||
{
|
||||
var metadata = endpointBuilder.GetBffRemoteApiEndpointMetadata();
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ public sealed class BffOptions
|
|||
/// The ASP.NET environment names that enable the diagnostics endpoint.
|
||||
/// Defaults to "Development".
|
||||
/// </summary>
|
||||
public ICollection<string> DiagnosticsEnvironments { get; set; } = new HashSet<string>()
|
||||
public ICollection<string> DiagnosticsEnvironments { get; } = new HashSet<string>()
|
||||
{
|
||||
Environments.Development
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ public sealed class BffRemoteApiEndpointMetadata : IBffApiMetadata
|
|||
/// <summary>
|
||||
/// Required token type (if any)
|
||||
/// </summary>
|
||||
public RequiredTokenType? TokenType;
|
||||
public RequiredTokenType? TokenType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maps to UserAccessTokenParameters and included if set
|
||||
|
|
@ -31,13 +31,15 @@ public sealed class BffRemoteApiEndpointMetadata : IBffApiMetadata
|
|||
get => _accessTokenRetriever;
|
||||
set
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
if (value.IsAssignableTo(typeof(IAccessTokenRetriever)))
|
||||
{
|
||||
_accessTokenRetriever = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Attempt to assign a AccessTokenRetriever type that cannot be assigned to IAccessTokenTokenRetriever");
|
||||
throw new InvalidOperationException(
|
||||
"Attempt to assign a AccessTokenRetriever type that cannot be assigned to IAccessTokenTokenRetriever");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ internal sealed record OidcConfiguration
|
|||
{
|
||||
if (Authority != null)
|
||||
{
|
||||
options.Authority = Authority?.ToString();
|
||||
options.Authority = Authority.ToString();
|
||||
}
|
||||
|
||||
if (ClientId != null)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ public static class Constants
|
|||
public const string AntiforgeryCheckMetadata = "Duende.Bff.Yarp.AntiforgeryCheck";
|
||||
}
|
||||
|
||||
#pragma warning disable CA1724 // CA1724: Type names should not match namespaces
|
||||
public static class Cookies
|
||||
#pragma warning restore CA1724
|
||||
{
|
||||
public const string HostPrefix = "__Host";
|
||||
public const string SecurePrefix = "__Secure";
|
||||
|
|
@ -129,7 +131,7 @@ public static class Constants
|
|||
public const string Prompt = "bff-prompt";
|
||||
}
|
||||
|
||||
public class HttpClientNames
|
||||
public static class HttpClientNames
|
||||
{
|
||||
public const string IndexHtmlHttpClient = "Duende.Bff.IndexHtmlClient";
|
||||
|
||||
|
|
|
|||
|
|
@ -8,33 +8,54 @@ namespace Duende.Bff.DynamicFrontends;
|
|||
|
||||
public static class BffFrontendExtensions
|
||||
{
|
||||
public static BffFrontend WithIndexHtmlUrl(this BffFrontend frontend, Uri? url) => frontend with
|
||||
public static BffFrontend WithIndexHtmlUrl(this BffFrontend frontend, Uri? url)
|
||||
{
|
||||
IndexHtmlUrl = url
|
||||
};
|
||||
public static BffFrontend WithOpenIdConnectOptions(this BffFrontend frontend, Action<OpenIdConnectOptions> options) => frontend with
|
||||
{
|
||||
ConfigureOpenIdConnectOptions = options
|
||||
};
|
||||
|
||||
public static BffFrontend WithCookieOptions(this BffFrontend frontend, Action<CookieAuthenticationOptions> options) => frontend with
|
||||
{
|
||||
ConfigureCookieOptions = options
|
||||
};
|
||||
|
||||
public static BffFrontend MappedToOrigin(this BffFrontend frontend, Origin origin) => frontend with
|
||||
{
|
||||
SelectionCriteria = frontend.SelectionCriteria with
|
||||
ArgumentNullException.ThrowIfNull(frontend);
|
||||
return frontend with
|
||||
{
|
||||
MatchingOrigin = origin
|
||||
}
|
||||
};
|
||||
IndexHtmlUrl = url
|
||||
};
|
||||
}
|
||||
|
||||
public static BffFrontend MappedToPath(this BffFrontend frontend, LocalPath path) => frontend with
|
||||
public static BffFrontend WithOpenIdConnectOptions(this BffFrontend frontend, Action<OpenIdConnectOptions> options)
|
||||
{
|
||||
SelectionCriteria = frontend.SelectionCriteria with
|
||||
ArgumentNullException.ThrowIfNull(frontend);
|
||||
return frontend with
|
||||
{
|
||||
MatchingPath = path
|
||||
}
|
||||
};
|
||||
ConfigureOpenIdConnectOptions = options
|
||||
};
|
||||
}
|
||||
|
||||
public static BffFrontend WithCookieOptions(this BffFrontend frontend, Action<CookieAuthenticationOptions> options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frontend);
|
||||
return frontend with
|
||||
{
|
||||
ConfigureCookieOptions = options
|
||||
};
|
||||
}
|
||||
|
||||
public static BffFrontend MappedToOrigin(this BffFrontend frontend, Origin origin)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frontend);
|
||||
return frontend with
|
||||
{
|
||||
SelectionCriteria = frontend.SelectionCriteria with
|
||||
{
|
||||
MatchingOrigin = origin
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static BffFrontend MappedToPath(this BffFrontend frontend, LocalPath path)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frontend);
|
||||
return frontend with
|
||||
{
|
||||
SelectionCriteria = frontend.SelectionCriteria with
|
||||
{
|
||||
MatchingPath = path
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public sealed record FrontendSelectionCriteria
|
|||
get => _matchingPath;
|
||||
init
|
||||
{
|
||||
if (value == string.Empty)
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_matchingPath = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ namespace Duende.Bff.DynamicFrontends;
|
|||
/// - Any changes are not expected to be persisted across application restarts; they are transient and in-memory.
|
||||
/// - This is not an extensibility point and implementors of this library cannot replace it with a different implementation.
|
||||
/// </summary>
|
||||
public interface IFrontendCollection
|
||||
public interface IFrontendCollection : IEnumerable<BffFrontend>
|
||||
{
|
||||
void AddOrUpdate(BffFrontend frontend);
|
||||
void Remove(BffFrontendName frontendName);
|
||||
IReadOnlyList<BffFrontend> GetAll();
|
||||
|
||||
public int Count { get; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ internal class BffAuthenticationSchemeProvider(
|
|||
return scheme ?? GetBffAuthenticationScheme(name);
|
||||
}
|
||||
|
||||
private AuthenticationScheme? GetBffAuthenticationScheme(string name)
|
||||
private BffAuthenticationScheme? GetBffAuthenticationScheme(string name)
|
||||
{
|
||||
selectedFrontend.TryGet(out var frontend);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Collections;
|
||||
using Duende.Bff.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ internal class FrontendCollection : IDisposable, IFrontendCollection
|
|||
IOptionsMonitor<BffConfiguration> bffConfiguration,
|
||||
IEnumerable<IBffPluginLoader> plugins,
|
||||
IEnumerable<BffFrontend>? frontendsConfiguredDuringStartup = null
|
||||
)
|
||||
)
|
||||
{
|
||||
_plugins = plugins.ToArray();
|
||||
_frontends = ReadFrontends(bffConfiguration.CurrentValue, frontendsConfiguredDuringStartup ?? []);
|
||||
|
|
@ -83,7 +84,6 @@ internal class FrontendCollection : IDisposable, IFrontendCollection
|
|||
IndexHtmlUrl = frontend.IndexHtmlUrl,
|
||||
ConfigureOpenIdConnectOptions = opt =>
|
||||
{
|
||||
// Then apply any explicitly configured options for the frontend
|
||||
frontend.Oidc?.ApplyTo(opt);
|
||||
},
|
||||
ConfigureCookieOptions = opt =>
|
||||
|
|
@ -93,7 +93,9 @@ internal class FrontendCollection : IDisposable, IFrontendCollection
|
|||
SelectionCriteria = new FrontendSelectionCriteria()
|
||||
{
|
||||
// todo: parse or default
|
||||
MatchingOrigin = string.IsNullOrEmpty(frontend.MatchingOrigin) ? null : Origin.Parse(frontend.MatchingOrigin),
|
||||
MatchingOrigin = string.IsNullOrEmpty(frontend.MatchingOrigin)
|
||||
? null
|
||||
: Origin.Parse(frontend.MatchingOrigin),
|
||||
MatchingPath = string.IsNullOrEmpty(frontend.MatchingPath) ? null : frontend.MatchingPath,
|
||||
},
|
||||
DataExtensions = extensions
|
||||
|
|
@ -161,7 +163,6 @@ internal class FrontendCollection : IDisposable, IFrontendCollection
|
|||
.Where(x => x.Name != frontend.Name)
|
||||
.Append(frontend)
|
||||
.ToArray());
|
||||
|
||||
}
|
||||
|
||||
// Notify subscribers that a frontend has changed.
|
||||
|
|
@ -192,9 +193,10 @@ internal class FrontendCollection : IDisposable, IFrontendCollection
|
|||
OnFrontendChanged(existing);
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentlySynchronizedField
|
||||
// The _frontends array is completely replaced on add/update, so we don't need to lock here.
|
||||
public IReadOnlyList<BffFrontend> GetAll() => _frontends.AsReadOnly();
|
||||
public int Count => _frontends.Length;
|
||||
|
||||
void IDisposable.Dispose() => _stopSubscription?.Dispose();
|
||||
public IEnumerator<BffFrontend> GetEnumerator() => ((IEnumerable<BffFrontend>)_frontends).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => _frontends.GetEnumerator();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,11 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace Duende.Bff.DynamicFrontends.Internal;
|
||||
|
||||
internal class FrontendSelector(FrontendCollection frontendCollection, ILogger<FrontendSelector> logger)
|
||||
internal class FrontendSelector(FrontendCollection frontends, ILogger<FrontendSelector> logger)
|
||||
{
|
||||
public bool TrySelectFrontend(HttpRequest request, [NotNullWhen(true)] out BffFrontend? selectedFrontend)
|
||||
{
|
||||
selectedFrontend = null;
|
||||
var frontends = frontendCollection.GetAll();
|
||||
|
||||
if (frontends.Count == 0)
|
||||
{
|
||||
|
|
@ -23,12 +22,14 @@ internal class FrontendSelector(FrontendCollection frontendCollection, ILogger<F
|
|||
|
||||
// Find frontends that match the request by origin (if specified)
|
||||
var matchingByOrigin = frontends
|
||||
.Where(x => x.SelectionCriteria.MatchingOrigin == null || x.SelectionCriteria.MatchingOrigin.Equals(request))
|
||||
.Where(x => x.SelectionCriteria.MatchingOrigin == null ||
|
||||
x.SelectionCriteria.MatchingOrigin.Equals(request))
|
||||
.ToList();
|
||||
|
||||
// First, look for a match by origin and path (if specified)
|
||||
selectedFrontend = matchingByOrigin
|
||||
.OrderByDescending(x => x.SelectionCriteria.MatchingOrigin != null) // Prefer frontends with a specific origin
|
||||
.OrderByDescending(x =>
|
||||
x.SelectionCriteria.MatchingOrigin != null) // Prefer frontends with a specific origin
|
||||
.ThenByDescending(x => PathOrder(x.SelectionCriteria.MatchingPath))
|
||||
.FirstOrDefault(x =>
|
||||
x.SelectionCriteria.MatchingPath == null ||
|
||||
|
|
@ -42,7 +43,8 @@ internal class FrontendSelector(FrontendCollection frontendCollection, ILogger<F
|
|||
StringComparison.Ordinal))
|
||||
{
|
||||
// There is a case difference in the path
|
||||
logger.FrontendSelectedWithPathCasingIssue(LogLevel.Warning, selectedFrontend.SelectionCriteria.MatchingPath, request.Path);
|
||||
logger.FrontendSelectedWithPathCasingIssue(LogLevel.Warning,
|
||||
selectedFrontend.SelectionCriteria.MatchingPath, request.Path);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -57,9 +59,9 @@ internal class FrontendSelector(FrontendCollection frontendCollection, ILogger<F
|
|||
return selectedFrontend != null;
|
||||
}
|
||||
|
||||
private bool PathMatches(HttpRequest request, PathString path) =>
|
||||
private static bool PathMatches(HttpRequest request, PathString path) =>
|
||||
request.Path.StartsWithSegments(path, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private int PathOrder(PathString? path) =>
|
||||
private static int PathOrder(PathString? path) =>
|
||||
path?.Value?.Length ?? 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ internal class IndexHtmlHttpClient : IIndexHtmlClient, IAsyncDisposable
|
|||
try
|
||||
{
|
||||
await cache.RemoveAsync(BuildCacheKey(changedFrontend), _stopping.Token);
|
||||
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
|
@ -50,12 +49,10 @@ internal class IndexHtmlHttpClient : IIndexHtmlClient, IAsyncDisposable
|
|||
throw;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public async Task<string?> GetIndexHtmlAsync(CT ct = default)
|
||||
{
|
||||
|
||||
if (!_selectedFrontend.TryGet(out var frontend))
|
||||
{
|
||||
// Todo: log
|
||||
|
|
@ -67,46 +64,50 @@ internal class IndexHtmlHttpClient : IIndexHtmlClient, IAsyncDisposable
|
|||
try
|
||||
{
|
||||
return await _cache.GetOrCreateAsync(cacheKey, async (ct1) =>
|
||||
{
|
||||
var client = _clientFactory.CreateClient(_options.Value.IndexHtmlClientName ?? Constants.HttpClientNames.IndexHtmlHttpClient);
|
||||
|
||||
var response = await client.GetAsync(frontend.IndexHtmlUrl, ct1);
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
var client = _clientFactory.CreateClient(_options.Value.IndexHtmlClientName ??
|
||||
Constants.HttpClientNames.IndexHtmlHttpClient);
|
||||
|
||||
var response = await client.GetAsync(frontend.IndexHtmlUrl, ct1);
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
// Todo: log
|
||||
throw new PreventCacheException();
|
||||
}
|
||||
|
||||
// Todo: log
|
||||
throw new PreventCacheException();
|
||||
}
|
||||
|
||||
// Todo: log
|
||||
var html = await response.Content.ReadAsStringAsync(ct1);
|
||||
|
||||
var html = await response.Content.ReadAsStringAsync(ct1);
|
||||
if (_transformer == null)
|
||||
{
|
||||
return html;
|
||||
}
|
||||
|
||||
if (_transformer == null)
|
||||
{
|
||||
return html;
|
||||
}
|
||||
|
||||
var transformed = await _transformer.Transform(html, ct1);
|
||||
return transformed;
|
||||
|
||||
},
|
||||
var transformed = await _transformer.Transform(html, ct1);
|
||||
return transformed;
|
||||
},
|
||||
options: new HybridCacheEntryOptions()
|
||||
{
|
||||
Expiration = TimeSpan.FromMinutes(5)
|
||||
},
|
||||
cancellationToken: ct);
|
||||
|
||||
}
|
||||
catch (PreventCacheException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static string BuildCacheKey(BffFrontend frontend) => "Duende.Bff.IndexHtml:" + frontend.Name;
|
||||
|
||||
private class PreventCacheException : Exception;
|
||||
#pragma warning disable CA1032 // Do not use a custom message for this exception, as it is used to prevent caching
|
||||
#pragma warning disable CA1064 // do not make this exception public as it's purely internal
|
||||
private class PreventCacheException : Exception
|
||||
#pragma warning restore CA1064
|
||||
#pragma warning restore CA1032
|
||||
{
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
|
|
@ -114,4 +115,3 @@ internal class IndexHtmlHttpClient : IIndexHtmlClient, IAsyncDisposable
|
|||
_stopping.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ internal class OpenIdConnectCallbackMiddleware(RequestDelegate next,
|
|||
var oidcOptionsFactory = context.RequestServices.GetRequiredService<IOptionsFactory<OpenIdConnectOptions>>();
|
||||
var options = oidcOptionsFactory.Create(frontend.OidcSchemeName);
|
||||
|
||||
if (context.Request.Path.StartsWithSegments(options.CallbackPath))
|
||||
if (context.Request.Path.StartsWithSegments(options.CallbackPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
|
||||
if (await handlers.GetHandlerAsync(context, frontend.OidcSchemeName) is IAuthenticationRequestHandler handler)
|
||||
|
|
@ -32,7 +32,7 @@ internal class OpenIdConnectCallbackMiddleware(RequestDelegate next,
|
|||
return;
|
||||
}
|
||||
}
|
||||
if (context.Request.Path.StartsWithSegments(options.SignedOutCallbackPath))
|
||||
if (context.Request.Path.StartsWithSegments(options.SignedOutCallbackPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
|
||||
if (await handlers.GetHandlerAsync(context, frontend.OidcSchemeName) is IAuthenticationRequestHandler handler)
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ using Microsoft.AspNetCore.Http;
|
|||
|
||||
namespace Duende.Bff.DynamicFrontends.Internal;
|
||||
|
||||
internal class PathMapper
|
||||
internal static class PathMapper
|
||||
{
|
||||
public void MapPath(HttpContext context, BffFrontend frontend)
|
||||
public static void MapPath(HttpContext context, BffFrontend frontend)
|
||||
{
|
||||
var path = frontend.SelectionCriteria.MatchingPath;
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ internal class PathMapper
|
|||
return;
|
||||
}
|
||||
|
||||
if (!context.Request.Path.StartsWithSegments(path))
|
||||
if (!context.Request.Path.StartsWithSegments(path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
context.Response.StatusCode = 404;
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ using Microsoft.AspNetCore.Http;
|
|||
|
||||
namespace Duende.Bff.DynamicFrontends.Internal;
|
||||
|
||||
internal class PathMappingMiddleware(RequestDelegate next, SelectedFrontend selectedFrontend, PathMapper pathMapper)
|
||||
internal class PathMappingMiddleware(RequestDelegate next, SelectedFrontend selectedFrontend)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (selectedFrontend.TryGet(out var frontend))
|
||||
{
|
||||
pathMapper.MapPath(context, frontend);
|
||||
PathMapper.MapPath(context, frontend);
|
||||
}
|
||||
|
||||
await next(context);
|
||||
|
|
|
|||
|
|
@ -20,12 +20,16 @@ public sealed record Origin : IEquatable<HttpRequest>
|
|||
return Parse(uri);
|
||||
}
|
||||
|
||||
public static Origin Parse(Uri uri) => new()
|
||||
public static Origin Parse(Uri uri)
|
||||
{
|
||||
Scheme = uri.Scheme,
|
||||
Host = uri.Host,
|
||||
Port = uri.Port
|
||||
};
|
||||
ArgumentNullException.ThrowIfNull(uri);
|
||||
return new()
|
||||
{
|
||||
Scheme = uri.Scheme,
|
||||
Host = uri.Host,
|
||||
Port = uri.Port
|
||||
};
|
||||
}
|
||||
|
||||
public required string Scheme { get; init; }
|
||||
|
||||
|
|
|
|||
|
|
@ -7,4 +7,6 @@ namespace Duende.Bff.Endpoints;
|
|||
/// Marks an endpoint as BFF API endpoint.
|
||||
/// By default, this provides anti-forgery protection and response handling.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1040
|
||||
public interface IBffApiMetadata;
|
||||
#pragma warning restore CA1040
|
||||
|
|
|
|||
|
|
@ -7,4 +7,6 @@ namespace Duende.Bff.Endpoints;
|
|||
/// <summary>
|
||||
/// Indicates that the BFF middleware will ignore the antiforgery header checks.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1040
|
||||
public interface IBffApiSkipAntiForgery;
|
||||
#pragma warning restore CA1040
|
||||
|
|
|
|||
|
|
@ -7,4 +7,6 @@ namespace Duende.Bff.Endpoints;
|
|||
/// <summary>
|
||||
/// Indicates that the BFF middleware will not override the HTTP response status code.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1040
|
||||
public interface IBffApiSkipResponseHandling;
|
||||
#pragma warning restore CA1040
|
||||
|
|
|
|||
|
|
@ -14,5 +14,5 @@ public interface IReturnUrlValidator
|
|||
/// </summary>
|
||||
/// <param name="returnUrl"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsValidAsync(string returnUrl);
|
||||
public bool IsValidAsync(Uri returnUrl);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ internal class BffAntiForgeryMiddleware(
|
|||
var isUiEndpoint = endpoint.Metadata.GetMetadata<IBffUIApiEndpoint>() != null;
|
||||
if (isUiEndpoint && context.IsAjaxRequest())
|
||||
{
|
||||
logger.ManagementEndpointAccessedViaAjax(context.Request.Path);
|
||||
logger.ManagementEndpointAccessedViaAjax(LogLevel.Debug, context.Request.Path.Sanitize());
|
||||
}
|
||||
|
||||
await next(context);
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ internal class BffAuthenticationService(Decorator<IAuthenticationService> decora
|
|||
return;
|
||||
}
|
||||
|
||||
// Todo: EV: not sure.
|
||||
logger.LogWarning("Authentiating scheme: {scheme}", scheme);
|
||||
logger.AuthenticatingScheme(LogLevel.Warning, scheme);
|
||||
await _inner.SignOutAsync(context, frontend.OidcSchemeName, properties);
|
||||
return;
|
||||
}
|
||||
|
|
@ -50,10 +49,7 @@ internal class BffAuthenticationService(Decorator<IAuthenticationService> decora
|
|||
return await _inner.AuthenticateAsync(context, scheme);
|
||||
}
|
||||
|
||||
// Todo: EV: not sure.
|
||||
// It looks like all schemes are authentiated, even if we only want the frontend scheme to be triggered.
|
||||
// Force the cookie scheme to be authentiated. LIkely this means it happens twice
|
||||
logger.LogWarning("Authentiating scheme: {scheme}", scheme);
|
||||
logger.AuthenticatingScheme(LogLevel.Warning, scheme);
|
||||
return await _inner.AuthenticateAsync(context, frontend.CookieSchemeName);
|
||||
}
|
||||
|
||||
|
|
@ -80,7 +76,7 @@ internal class BffAuthenticationService(Decorator<IAuthenticationService> decora
|
|||
var requireResponseHandling = endpoint?.Metadata.GetMetadata<IBffApiSkipResponseHandling>() == null;
|
||||
if (requireResponseHandling)
|
||||
{
|
||||
logger.ChallengeForBffApiEndpoint();
|
||||
logger.ChallengeForBffApiEndpoint(LogLevel.Debug);
|
||||
context.Response.StatusCode = 401;
|
||||
context.Response.Headers.Remove("Location");
|
||||
context.Response.Headers.Remove("Set-Cookie");
|
||||
|
|
@ -107,7 +103,7 @@ internal class BffAuthenticationService(Decorator<IAuthenticationService> decora
|
|||
var requireResponseHandling = endpoint?.Metadata.GetMetadata<IBffApiSkipResponseHandling>() == null;
|
||||
if (requireResponseHandling)
|
||||
{
|
||||
logger.ForbidForBffApiEndpoint();
|
||||
logger.ForbidForBffApiEndpoint(LogLevel.Debug);
|
||||
context.Response.StatusCode = 403;
|
||||
context.Response.Headers.Remove("Location");
|
||||
context.Response.Headers.Remove("Set-Cookie");
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.Bff.Configuration;
|
||||
using Duende.Bff.Otel;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
|
@ -35,12 +36,12 @@ internal class BffOpenIdConnectEvents(IOptions<BffOptions> options, ILogger<BffO
|
|||
var redirectPath = pathBase + options.Value.SilentLoginCallbackPath;
|
||||
|
||||
context.Properties.RedirectUri = redirectPath;
|
||||
logger.LogDebug("Setting OIDC ProtocolMessage.Prompt to 'none' for BFF silent login");
|
||||
logger.SettingOidcPromptNoneForSilentLogin(LogLevel.Debug);
|
||||
context.ProtocolMessage.Prompt = "none";
|
||||
}
|
||||
else if (context.Properties.TryGetPrompt(out var prompt) == true)
|
||||
{
|
||||
logger.LogDebug("Setting OIDC ProtocolMessage.Prompt to {prompt} for BFF silent login", prompt);
|
||||
logger.SettingOidcPromptForSilentLogin(LogLevel.Debug, prompt);
|
||||
context.ProtocolMessage.Prompt = prompt;
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +70,7 @@ internal class BffOpenIdConnectEvents(IOptions<BffOptions> options, ILogger<BffO
|
|||
|
||||
if (context.ProtocolMessage.Error != null)
|
||||
{
|
||||
logger.LogDebug("Handling error response from OIDC provider for BFF silent login.");
|
||||
logger.HandlingErrorResponseFromOidcProviderForSilentLogin(LogLevel.Debug);
|
||||
|
||||
context.HandleResponse();
|
||||
context.Response.Redirect(context.Properties.RedirectUri);
|
||||
|
|
@ -81,7 +82,7 @@ internal class BffOpenIdConnectEvents(IOptions<BffOptions> options, ILogger<BffO
|
|||
{
|
||||
if (context.ProtocolMessage.Error != null)
|
||||
{
|
||||
logger.LogDebug("Handling error response from OIDC provider for BFF silent login.");
|
||||
logger.HandlingErrorResponseFromOidcProviderForSilentLogin(LogLevel.Debug);
|
||||
|
||||
context.HandleResponse();
|
||||
context.Response.Redirect(context.Properties.RedirectUri);
|
||||
|
|
@ -111,7 +112,7 @@ internal class BffOpenIdConnectEvents(IOptions<BffOptions> options, ILogger<BffO
|
|||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
logger.LogDebug("Handling failed response from OIDC provider for BFF silent login.");
|
||||
logger.HandlingFailedResponseFromOidcProviderForSilentLogin(LogLevel.Debug);
|
||||
|
||||
context.HandleResponse();
|
||||
context.Response.Redirect(context.HttpContext.Items[SilentRedirectUrl]!.ToString()!);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@ namespace Duende.Bff.Endpoints.Internal;
|
|||
/// This implies that it is not intended for Ajax requests.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
internal class BffUiEndpointAttribute : Attribute, IBffUIApiEndpoint
|
||||
internal sealed class BffUiEndpointAttribute : Attribute, IBffUIApiEndpoint
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,49 +29,42 @@ internal class DefaultBackchannelLogoutEndpoint(
|
|||
/// <inheritdoc />
|
||||
public async Task ProcessRequestAsync(HttpContext context, CT ct = default)
|
||||
{
|
||||
logger.LogDebug("Processing back-channel logout request");
|
||||
logger.ProcessingBackChannelLogoutRequest(LogLevel.Debug);
|
||||
|
||||
context.Response.Headers.Append("Cache-Control", "no-cache, no-store");
|
||||
context.Response.Headers.Append("Pragma", "no-cache");
|
||||
|
||||
try
|
||||
if (context.Request.HasFormContentType)
|
||||
{
|
||||
if (context.Request.HasFormContentType)
|
||||
var logoutToken = context.Request.Form[OidcConstants.BackChannelLogoutRequest.LogoutToken].FirstOrDefault();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(logoutToken))
|
||||
{
|
||||
var logoutToken = context.Request.Form[OidcConstants.BackChannelLogoutRequest.LogoutToken].FirstOrDefault();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(logoutToken))
|
||||
var user = await ValidateLogoutTokenAsync(logoutToken);
|
||||
if (user != null)
|
||||
{
|
||||
var user = await ValidateLogoutTokenAsync(logoutToken);
|
||||
if (user != null)
|
||||
// these are the sub & sid to signout
|
||||
var sub = user.FindFirst("sub")?.Value;
|
||||
var sid = user.FindFirst("sid")?.Value;
|
||||
|
||||
logger.BackChannelLogout(LogLevel.Debug, sub ?? "missing", sid ?? "missing");
|
||||
|
||||
await userSession.RevokeSessionsAsync(new UserSessionsFilter
|
||||
{
|
||||
// these are the sub & sid to signout
|
||||
var sub = user.FindFirst("sub")?.Value;
|
||||
var sid = user.FindFirst("sid")?.Value;
|
||||
SubjectId = sub,
|
||||
SessionId = sid
|
||||
}, ct);
|
||||
|
||||
logger.BackChannelLogout(sub ?? "missing", sid ?? "missing");
|
||||
|
||||
await userSession.RevokeSessionsAsync(new UserSessionsFilter
|
||||
{
|
||||
SubjectId = sub,
|
||||
SessionId = sid
|
||||
}, ct);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.BackChannelLogoutError($"Failed to process backchannel logout request. 'Logout token is missing'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.BackChannelLogoutError($"Failed to process backchannel logout request. '{ex.Message}'");
|
||||
else
|
||||
{
|
||||
logger.FailedToProcessBackchannelLogoutRequestMissingToken(LogLevel.Information);
|
||||
}
|
||||
}
|
||||
|
||||
logger.BackChannelLogoutError($"Failed to process backchannel logout request.");
|
||||
logger.FailedToProcessBackchannelLogoutRequest(LogLevel.Information);
|
||||
context.Response.StatusCode = 400;
|
||||
}
|
||||
|
||||
|
|
@ -85,31 +78,31 @@ internal class DefaultBackchannelLogoutEndpoint(
|
|||
var claims = await ValidateJwt(logoutToken);
|
||||
if (claims == null)
|
||||
{
|
||||
logger.LogDebug("No claims in back-channel JWT");
|
||||
logger.NoClaimsInBackChannelJwt(LogLevel.Debug);
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogTrace("Claims found in back-channel JWT {claims}", claims.Claims);
|
||||
logger.ClaimsFoundInBackChannelJwt(LogLevel.Trace, string.Join(',', claims.Claims));
|
||||
}
|
||||
|
||||
if (claims.FindFirst("sub") == null && claims.FindFirst("sid") == null)
|
||||
{
|
||||
logger.BackChannelLogoutError("Logout token missing sub and sid claims.");
|
||||
logger.LogoutTokenMissingSubAndSidClaims(LogLevel.Information);
|
||||
return null;
|
||||
}
|
||||
|
||||
var nonce = claims.FindFirst("nonce")?.Value;
|
||||
if (!string.IsNullOrWhiteSpace(nonce))
|
||||
{
|
||||
logger.BackChannelLogoutError("Logout token should not contain nonce claim.");
|
||||
logger.LogoutTokenShouldNotContainNonceClaim(LogLevel.Information);
|
||||
return null;
|
||||
}
|
||||
|
||||
var eventsJson = claims.FindFirst("events")?.Value;
|
||||
if (string.IsNullOrWhiteSpace(eventsJson))
|
||||
{
|
||||
logger.BackChannelLogoutError("Logout token missing events claim.");
|
||||
logger.LogoutTokenMissingEventsClaim(LogLevel.Information);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -118,13 +111,13 @@ internal class DefaultBackchannelLogoutEndpoint(
|
|||
var events = JsonDocument.Parse(eventsJson);
|
||||
if (!events.RootElement.TryGetProperty("http://schemas.openid.net/event/backchannel-logout", out _))
|
||||
{
|
||||
logger.BackChannelLogoutError("Logout token contains missing http://schemas.openid.net/event/backchannel-logout value.");
|
||||
logger.LogoutTokenMissingBackchannelLogoutValue(LogLevel.Information);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (JsonException ex)
|
||||
{
|
||||
logger.BackChannelLogoutError($"Logout token contains invalid JSON in events claim value. '{ex.Message}'");
|
||||
logger.LogoutTokenContainsInvalidJsonInEventsClaim(LogLevel.Information, ex);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -144,11 +137,11 @@ internal class DefaultBackchannelLogoutEndpoint(
|
|||
var result = await handler.ValidateTokenAsync(jwt, parameters);
|
||||
if (result.IsValid)
|
||||
{
|
||||
logger.LogDebug("Back-channel JWT validation successful");
|
||||
logger.BackChannelJwtValidationSuccessful(LogLevel.Debug);
|
||||
return result.ClaimsIdentity;
|
||||
}
|
||||
|
||||
logger.BackChannelLogoutError($"Error validating logout token. '{result.Exception.ToString()}'");
|
||||
logger.ErrorValidatingLogoutToken(LogLevel.Information, result.Exception);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -162,13 +155,13 @@ internal class DefaultBackchannelLogoutEndpoint(
|
|||
var scheme = await authenticationSchemeProvider.GetDefaultChallengeSchemeAsync();
|
||||
if (scheme == null)
|
||||
{
|
||||
throw new Exception("Failed to obtain default challenge scheme");
|
||||
throw new InvalidOperationException("Failed to obtain default challenge scheme");
|
||||
}
|
||||
|
||||
var options = optionsMonitor.Get(scheme.Name);
|
||||
if (options == null)
|
||||
{
|
||||
throw new Exception("Failed to obtain OIDC options for default challenge scheme");
|
||||
throw new InvalidOperationException("Failed to obtain OIDC options for default challenge scheme");
|
||||
}
|
||||
|
||||
var config = options.Configuration;
|
||||
|
|
@ -179,7 +172,7 @@ internal class DefaultBackchannelLogoutEndpoint(
|
|||
|
||||
if (config == null)
|
||||
{
|
||||
throw new Exception("Failed to obtain OIDC configuration");
|
||||
throw new InvalidOperationException("Failed to obtain OIDC configuration");
|
||||
}
|
||||
|
||||
var parameters = new TokenValidationParameters
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ internal class DefaultLoginEndpoint(
|
|||
/// <inheritdoc />
|
||||
public async Task ProcessRequestAsync(HttpContext context, CT ct = default)
|
||||
{
|
||||
logger.LogDebug("Processing login request");
|
||||
logger.ProcessingLoginRequest(LogLevel.Debug);
|
||||
|
||||
context.CheckForBffMiddleware(bffOptions.Value);
|
||||
|
||||
|
|
@ -54,7 +54,8 @@ internal class DefaultLoginEndpoint(
|
|||
|
||||
if (!string.IsNullOrWhiteSpace(returnUrl))
|
||||
{
|
||||
if (!returnUrlValidator.IsValidAsync(returnUrl))
|
||||
if (!Uri.TryCreate(returnUrl, UriKind.RelativeOrAbsolute, out var returnUri)
|
||||
|| !returnUrlValidator.IsValidAsync(returnUri))
|
||||
{
|
||||
logger.InvalidReturnUrl(LogLevel.Information, returnUrl.Sanitize());
|
||||
context.ReturnHttpProblem("Invalid return url", (Constants.RequestParameters.ReturnUrl, [$"ReturnUrl '{returnUrl}' was invalid"]));
|
||||
|
|
@ -79,7 +80,7 @@ internal class DefaultLoginEndpoint(
|
|||
props.Items.Add(Constants.BffFlags.Prompt, prompt);
|
||||
}
|
||||
|
||||
logger.LogDebug("Login endpoint triggering Challenge with returnUrl {returnUrl}", returnUrl.Sanitize());
|
||||
logger.LoginEndpointTriggeringChallenge(LogLevel.Debug, returnUrl.Sanitize());
|
||||
|
||||
await context.ChallengeAsync(props);
|
||||
}
|
||||
|
|
@ -106,7 +107,7 @@ internal class DefaultLoginEndpoint(
|
|||
var openIdConnectOptions = openIdConnectOptionsMonitor.Get(scheme);
|
||||
if (openIdConnectOptions == null)
|
||||
{
|
||||
throw new Exception("Failed to obtain OIDC options for scheme: " + scheme);
|
||||
throw new InvalidOperationException("Failed to obtain OIDC options for scheme: " + scheme);
|
||||
}
|
||||
|
||||
var config = openIdConnectOptions.Configuration;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ internal class DefaultLogoutEndpoint(IOptions<BffOptions> options,
|
|||
/// <inheritdoc />
|
||||
public async Task ProcessRequestAsync(HttpContext context, CT ct = default)
|
||||
{
|
||||
logger.LogDebug("Processing logout request");
|
||||
logger.ProcessingLogoutRequest(LogLevel.Debug);
|
||||
|
||||
context.CheckForBffMiddleware(options.Value);
|
||||
|
||||
|
|
@ -50,7 +50,8 @@ internal class DefaultLogoutEndpoint(IOptions<BffOptions> options,
|
|||
var returnUrl = context.Request.Query[Constants.RequestParameters.ReturnUrl].FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(returnUrl))
|
||||
{
|
||||
if (!returnUrlValidator.IsValidAsync(returnUrl))
|
||||
if (!Uri.TryCreate(returnUrl, UriKind.RelativeOrAbsolute, out var returnUri) ||
|
||||
!returnUrlValidator.IsValidAsync(returnUri))
|
||||
{
|
||||
logger.InvalidReturnUrl(LogLevel.Information, returnUrl.Sanitize());
|
||||
context.ReturnHttpProblem("Invalid return url", (Constants.RequestParameters.ReturnUrl, [$"ReturnUrl '{returnUrl}' was invalid"]));
|
||||
|
|
@ -79,7 +80,7 @@ internal class DefaultLogoutEndpoint(IOptions<BffOptions> options,
|
|||
RedirectUri = returnUrl
|
||||
};
|
||||
|
||||
logger.LogDebug("Logout endpoint triggering SignOut with returnUrl {returnUrl}", returnUrl.Sanitize());
|
||||
logger.LogoutEndpointTriggeringSignOut(LogLevel.Debug, returnUrl.Sanitize());
|
||||
|
||||
// trigger idp logout
|
||||
await context.SignOutAsync(props);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ internal class DefaultSilentLoginCallbackEndpoint(
|
|||
/// <inheritdoc />
|
||||
public async Task ProcessRequestAsync(HttpContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
logger.LogDebug("Processing silent login callback request");
|
||||
logger.ProcessingSilentLoginCallbackRequest(LogLevel.Debug);
|
||||
|
||||
context.CheckForBffMiddleware(options.Value);
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ internal class DefaultSilentLoginCallbackEndpoint(
|
|||
context.Response.Headers["Cache-Control"] = "no-store, no-cache, max-age=0";
|
||||
context.Response.Headers["Pragma"] = "no-cache";
|
||||
|
||||
logger.LogDebug("Silent login endpoint rendering HTML with JS postMessage to origin {origin} with isLoggedIn {isLoggedIn}", origin.Sanitize(), result);
|
||||
logger.SilentLoginEndpointRenderingHtml(LogLevel.Debug, origin.Sanitize(), result);
|
||||
|
||||
await context.Response.WriteAsync(html, Encoding.UTF8, cancellationToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.Bff.Configuration;
|
||||
using Duende.Bff.Otel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -22,7 +23,7 @@ internal class DefaultSilentLoginEndpoint(IOptions<BffOptions> options, ILogger<
|
|||
/// <inheritdoc />
|
||||
public async Task ProcessRequestAsync(HttpContext context, CT ct = default)
|
||||
{
|
||||
logger.LogDebug("Processing silent login request");
|
||||
logger.ProcessingSilentLoginRequest(LogLevel.Debug);
|
||||
|
||||
context.CheckForBffMiddleware(_options);
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ internal class DefaultSilentLoginEndpoint(IOptions<BffOptions> options, ILogger<
|
|||
},
|
||||
};
|
||||
|
||||
logger.LogWarning("Using deprecated silentlogin endpoint. This endpoint will be removed in future versions. Consider calling the BFF Login endpoint with prompt=none.");
|
||||
logger.UsingDeprecatedSilentLoginEndpoint(LogLevel.Warning);
|
||||
|
||||
await context.ChallengeAsync(props);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Text;
|
|||
using System.Text.Json;
|
||||
using Duende.Bff.Configuration;
|
||||
using Duende.Bff.Internal;
|
||||
using Duende.Bff.Otel;
|
||||
using Duende.IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -24,11 +25,10 @@ internal class DefaultUserEndpoint(IOptions<BffOptions> options, ILogger<Default
|
|||
/// </summary>
|
||||
private readonly BffOptions _options = options.Value;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ProcessRequestAsync(HttpContext context, CT ct = default)
|
||||
{
|
||||
logger.LogDebug("Processing user request");
|
||||
logger.ProcessingUserRequest(LogLevel.Debug);
|
||||
|
||||
context.CheckForBffMiddleware(_options);
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ internal class DefaultUserEndpoint(IOptions<BffOptions> options, ILogger<Default
|
|||
context.Response.StatusCode = 401;
|
||||
}
|
||||
|
||||
logger.LogDebug("User endpoint indicates the user is not logged in, using status code {code}", context.Response.StatusCode);
|
||||
logger.UserEndpointNotLoggedIn(LogLevel.Debug, context.Response.StatusCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -61,7 +61,7 @@ internal class DefaultUserEndpoint(IOptions<BffOptions> options, ILogger<Default
|
|||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsync(json, Encoding.UTF8, ct);
|
||||
|
||||
logger.LogTrace("User endpoint indicates the user is logged in with claims {claims}", claims);
|
||||
logger.UserEndpointLoggedInWithClaims(LogLevel.Trace, string.Join(',', claims));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ internal class DefaultUserEndpoint(IOptions<BffOptions> options, ILogger<Default
|
|||
/// Collect user-centric claims
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private Task<IEnumerable<ClaimRecord>> GetUserClaimsAsync(AuthenticateResult authenticateResult, CT ct = default) =>
|
||||
private static Task<IEnumerable<ClaimRecord>> GetUserClaimsAsync(AuthenticateResult authenticateResult, CT ct = default) =>
|
||||
Task.FromResult(authenticateResult.Principal?.Claims.Select(x => new ClaimRecord(x.Type, x.Value)) ?? Enumerable.Empty<ClaimRecord>());
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -58,13 +58,13 @@ internal interface IStronglyTypedValue<TSelf> : IParsableType<TSelf>
|
|||
}
|
||||
}
|
||||
|
||||
if (!errors.Any())
|
||||
if (errors.Count == 0)
|
||||
{
|
||||
parsed = TSelf.Create(value);
|
||||
}
|
||||
foundErrors = errors.ToArray();
|
||||
|
||||
return !foundErrors.Any();
|
||||
return foundErrors.Length == 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,13 +118,13 @@ internal interface IStronglyTypedValue<TType, TSelf> : IParsableType<TSelf>
|
|||
}
|
||||
}
|
||||
|
||||
if (!errors.Any())
|
||||
if (errors.Count == 0)
|
||||
{
|
||||
parsed = TSelf.Create(converted);
|
||||
}
|
||||
|
||||
foundErrors = errors.ToArray();
|
||||
return !foundErrors.Any();
|
||||
return foundErrors.Length == 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ namespace Duende.Bff.Internal;
|
|||
internal class LocalUrlReturnUrlValidator : IReturnUrlValidator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public bool IsValidAsync(string returnUrl) => IsLocalUrl(returnUrl);
|
||||
#pragma warning disable CA1822 // Can't be marked as static, because it implements an interface method.
|
||||
public bool IsValidAsync(Uri returnUrl) => IsLocalUrl(returnUrl.ToString());
|
||||
#pragma warning restore CA1822
|
||||
|
||||
internal static bool IsLocalUrl(string url)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ internal class License
|
|||
var edition = claims.FindFirst("edition")?.Value;
|
||||
if (!Enum.TryParse<LicenseEdition>(edition, true, out var editionValue))
|
||||
{
|
||||
throw new Exception($"Invalid edition in license: '{edition}'");
|
||||
throw new InvalidOperationException($"Invalid edition in license: '{edition}'");
|
||||
}
|
||||
|
||||
Edition = editionValue;
|
||||
|
|
@ -43,12 +43,12 @@ internal class License
|
|||
|
||||
if (IsCommunityEdition && RedistributionFeature)
|
||||
{
|
||||
throw new Exception("Invalid License: Redistribution is not valid for community edition.");
|
||||
throw new InvalidOperationException("Invalid License: Redistribution is not valid for community edition.");
|
||||
}
|
||||
|
||||
if (IsBffEdition && RedistributionFeature)
|
||||
{
|
||||
throw new Exception("Invalid License: Redistribution is not valid for BFF edition.");
|
||||
throw new InvalidOperationException("Invalid License: Redistribution is not valid for BFF edition.");
|
||||
}
|
||||
|
||||
if (IsBffEdition)
|
||||
|
|
|
|||
|
|
@ -6,10 +6,15 @@
|
|||
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using Duende.Bff.Otel;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
// Logging APIs used by Duende license validation
|
||||
#pragma warning disable CA1848
|
||||
#pragma warning disable CA2254
|
||||
|
||||
namespace Duende.Bff.Licensing;
|
||||
|
||||
// shared APIs needed for Duende license validation
|
||||
|
|
@ -68,20 +73,15 @@ internal partial class LicenseValidator
|
|||
{
|
||||
if (Logger == null)
|
||||
{
|
||||
throw new Exception("LicenseValidator.Initalize has not yet been called.");
|
||||
throw new InvalidOperationException("LicenseValidator.Initalize has not yet been called.");
|
||||
}
|
||||
|
||||
var errors = new List<string>();
|
||||
|
||||
if (License == null)
|
||||
{
|
||||
var message = "You do not have a valid license key for the Duende software. " +
|
||||
"This is allowed for development and testing scenarios. " +
|
||||
"If you are running in production you are required to have a licensed version. " +
|
||||
"Please start a conversation with us: https://duendesoftware.com/contact";
|
||||
|
||||
// we're not using our _warningLog because we always want this emitted regardless of the context
|
||||
Logger.LogWarning(message);
|
||||
Logger.NoValidLicense(LogLevel.Warning);
|
||||
LicenseValidator.WarnForProductFeaturesWhenMissingLicense();
|
||||
return;
|
||||
}
|
||||
|
|
@ -149,7 +149,11 @@ internal partial class LicenseValidator
|
|||
ValidIssuer = "https://duendesoftware.com",
|
||||
ValidAudience = "IdentityServer",
|
||||
IssuerSigningKey = key,
|
||||
// CA5404: Do not set ValidateLifetime to false in production code
|
||||
// this is OK for the license key validation, as we do not want to enforce expiration on the license key itself
|
||||
#pragma warning disable CA5404
|
||||
ValidateLifetime = false
|
||||
#pragma warning restore CA5404
|
||||
};
|
||||
var validateResult = handler.ValidateTokenAsync(licenseKey, parms).Result;
|
||||
if (validateResult.IsValid)
|
||||
|
|
@ -158,13 +162,14 @@ internal partial class LicenseValidator
|
|||
}
|
||||
else
|
||||
{
|
||||
Logger.LogCritical(validateResult.Exception, "Error validating the Duende software license key");
|
||||
Logger.ErrorValidatingLicenseKey(LogLevel.Critical, validateResult.Exception);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private static void LogToTrace(string message, params object[] args)
|
||||
{
|
||||
if (Logger.IsEnabled(LogLevel.Trace))
|
||||
|
|
@ -204,4 +209,5 @@ internal partial class LicenseValidator
|
|||
Logger.LogError(message, args);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public readonly record struct LocalPath : IStronglyTypedValue<LocalPath>
|
|||
/// over the conversion process, please use <see cref="TryParse"/> or <see cref="Parse"/>.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static implicit operator LocalPath(PathString value) => Parse(value);
|
||||
public static implicit operator LocalPath(PathString value) => ToLocalPath(value);
|
||||
|
||||
/// <summary>
|
||||
/// Convenience method for converting a <see cref="LocalPath"/> into a string.
|
||||
|
|
@ -50,11 +50,14 @@ public readonly record struct LocalPath : IStronglyTypedValue<LocalPath>
|
|||
public static bool TryParse(string value, [NotNullWhen(true)] out LocalPath? parsed, out string[] errors) =>
|
||||
IStronglyTypedValue<LocalPath>.TryBuildValidatedObject(value, Validators, out parsed, out errors);
|
||||
|
||||
public static LocalPath ToLocalPath(PathString pathString) => Parse(pathString);
|
||||
|
||||
/// <summary>
|
||||
/// Parses a value to a <see cref="LocalPath"/>. This will throw an exception if the string is not valid.
|
||||
/// </summary>
|
||||
public static LocalPath Parse(string value) => StringParsers<LocalPath>.Parse(value);
|
||||
|
||||
static LocalPath IStronglyTypedValue<LocalPath>.Create(string result) => new(result);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,19 +5,20 @@ using System.Diagnostics.Metrics;
|
|||
|
||||
namespace Duende.Bff.Otel;
|
||||
|
||||
public sealed class BffMetrics
|
||||
public sealed class BffMetrics : IDisposable
|
||||
{
|
||||
public const string MeterName = "Duende.Bff";
|
||||
|
||||
private readonly Counter<int> _sessionStarted;
|
||||
private readonly Counter<int> _sessionEnded;
|
||||
private Meter _meter;
|
||||
|
||||
public BffMetrics(IMeterFactory meterFactory)
|
||||
{
|
||||
var meter = meterFactory.Create(MeterName);
|
||||
_meter = meterFactory.Create(MeterName);
|
||||
|
||||
_sessionStarted = meter.CreateCounter<int>("session.started", "count", "Number of sessions started");
|
||||
_sessionEnded = meter.CreateCounter<int>("session.ended", "count", "Number of sessions ended");
|
||||
_sessionStarted = _meter.CreateCounter<int>("session.started", "count", "Number of sessions started");
|
||||
_sessionEnded = _meter.CreateCounter<int>("session.ended", "count", "Number of sessions ended");
|
||||
}
|
||||
|
||||
public void SessionStarted() => _sessionStarted.Add(1);
|
||||
|
|
@ -25,4 +26,6 @@ public sealed class BffMetrics
|
|||
public void SessionEnded() => _sessionEnded.Add(1);
|
||||
|
||||
public void SessionsEnded(int count) => _sessionEnded.Add(count);
|
||||
|
||||
public void Dispose() => _meter.Dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,32 @@ namespace Duende.Bff.Otel;
|
|||
|
||||
internal static partial class LogMessages
|
||||
{
|
||||
[LoggerMessage(
|
||||
Message = $"Proxy response error. local path: '{{{OTelParameters.LocalPath}}}', error: '{{{OTelParameters.Error}}}'")]
|
||||
public static partial void ProxyResponseError(this ILogger logger, LogLevel level, string localPath, string error);
|
||||
|
||||
[LoggerMessage(
|
||||
message:
|
||||
$"Deserializing AuthenticationTicket envelope failed or found incorrect version for key {{{OTelParameters.Key}}}")]
|
||||
public static partial void AuthenticationTicketEnvelopeVersionInvalid(this ILogger logger, LogLevel logLevel,
|
||||
string key);
|
||||
|
||||
[LoggerMessage(
|
||||
message:
|
||||
$"Failed to unprotect AuthenticationTicket payload for key {{{OTelParameters.Key}}}")]
|
||||
public static partial void AuthenticationTicketPayloadInvalid(this ILogger logger, Exception? ex, LogLevel logLevel,
|
||||
string key);
|
||||
|
||||
[LoggerMessage(
|
||||
message:
|
||||
$"Failed to deserialize AuthenticationTicket payload for key {{{OTelParameters.Key}}}")]
|
||||
public static partial void AuthenticationTicketFailedToDeserialize(this ILogger logger, Exception? ex,
|
||||
LogLevel logLevel,
|
||||
string key);
|
||||
|
||||
[LoggerMessage(
|
||||
Message = "FrontendSelection: No frontends registered in the store.")]
|
||||
public static partial void NoFrontendsRegistered(ILogger logger, LogLevel logLevel);
|
||||
public static partial void NoFrontendsRegistered(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
$"Invalid prompt value {{{OTelParameters.Prompt}}}.")]
|
||||
|
|
@ -29,11 +52,13 @@ internal static partial class LogMessages
|
|||
|
||||
[LoggerMessage(
|
||||
$"Failed To clear IndexHtmlCache for BFF Frontend {{{OTelParameters.Frontend}}}")]
|
||||
public static partial void FailedToClearIndexHtmlCacheForFrontend(this ILogger logger, LogLevel logLevel, Exception ex, BffFrontendName frontend);
|
||||
public static partial void FailedToClearIndexHtmlCacheForFrontend(this ILogger logger, LogLevel logLevel,
|
||||
Exception ex, BffFrontendName frontend);
|
||||
|
||||
[LoggerMessage(
|
||||
$"No OpenID Configuration found for scheme {{{OTelParameters.Scheme}}}")]
|
||||
public static partial void NoOpenIdConfigurationFoundForScheme(this ILogger logger, LogLevel logLevel, Scheme scheme);
|
||||
public static partial void NoOpenIdConfigurationFoundForScheme(this ILogger logger, LogLevel logLevel,
|
||||
Scheme scheme);
|
||||
|
||||
[LoggerMessage(
|
||||
$"No frontend selected.")]
|
||||
|
|
@ -49,166 +74,325 @@ internal static partial class LogMessages
|
|||
public static partial void AntiForgeryValidationFailed(this ILogger logger, string localPath);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Back-channel logout. sub: '{{{OTelParameters.Sub}}}', sid: '{{{OTelParameters.Sid}}}'")]
|
||||
public static partial void BackChannelLogout(this ILogger logger, string sub, string sid);
|
||||
public static partial void BackChannelLogout(this ILogger logger, LogLevel logLevel, string sub, string sid);
|
||||
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Warning,
|
||||
message: $"Back-channel logout error. error: '{{{OTelParameters.Error}}}'")]
|
||||
public static partial void BackChannelLogoutError(this ILogger logger, string error);
|
||||
message:
|
||||
$"Access token is missing. token type: '{{{OTelParameters.TokenType}}}', local path: '{{{OTelParameters.LocalPath}}}', detail: '{{{OTelParameters.Detail}}}'")]
|
||||
public static partial void AccessTokenMissing(this ILogger logger, LogLevel logLevel, string tokenType,
|
||||
string localPath, string detail);
|
||||
|
||||
[LoggerMessage(
|
||||
message: $"Access token is missing. token type: '{{{OTelParameters.TokenType}}}', local path: '{{{OTelParameters.LocalPath}}}', detail: '{{{OTelParameters.Detail}}}'")]
|
||||
public static partial void AccessTokenMissing(this ILogger logger, LogLevel logLevel, string tokenType, string localPath, string detail);
|
||||
message:
|
||||
$"Invalid route configuration. Cannot combine a required access token (a call to WithAccessToken) and an optional access token (a call to WithOptionalUserAccessToken). clusterId: '{{{OTelParameters.ClusterId}}}', routeId: '{{{OTelParameters.RouteId}}}'")]
|
||||
public static partial void InvalidRouteConfiguration(this ILogger logger, LogLevel logLevel, string? clusterId, string routeId);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Warning,
|
||||
message: $"Invalid route configuration. Cannot combine a required access token (a call to WithAccessToken) and an optional access token (a call to WithOptionalUserAccessToken). clusterId: '{{{OTelParameters.ClusterId}}}', routeId: '{{{OTelParameters.RouteId}}}'")]
|
||||
public static partial void InvalidRouteConfiguration(this ILogger logger, string? clusterId, string routeId);
|
||||
message:
|
||||
$"Failed to request new User Access Token due to: {{{OTelParameters.Error}}}. This can mean that the refresh token is expired or revoked but the cookie session is still active. If the session was not revoked, ensure that the session cookie lifetime is smaller than the refresh token lifetime.")]
|
||||
public static partial void FailedToRequestNewUserAccessToken(this ILogger logger, LogLevel logLevel, string error);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Warning,
|
||||
message: $"Failed to request new User Access Token due to: {{{OTelParameters.Error}}}. This can mean that the refresh token is expired or revoked but the cookie session is still active. If the session was not revoked, ensure that the session cookie lifetime is smaller than the refresh token lifetime.")]
|
||||
public static partial void FailedToRequestNewUserAccessToken(this ILogger logger, string error);
|
||||
message:
|
||||
$"Failed to request new User Access Token due to: {{{OTelParameters.Error}}}. This likely means that the user's refresh token is expired or revoked. The user's session will be ended, which will force the user to log in.")]
|
||||
public static partial void UserSessionRevoked(this ILogger logger, LogLevel logLevel, string error);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Warning,
|
||||
message: $"Failed to request new User Access Token due to: {{{OTelParameters.Error}}}. This likely means that the user's refresh token is expired or revoked. The user's session will be ended, which will force the user to log in.")]
|
||||
public static partial void UserSessionRevoked(this ILogger logger, string error);
|
||||
message:
|
||||
$"BFF management endpoint {{endpoint}} is only intended for a browser window to request and load. It is not intended to be accessed with Ajax or fetch requests.")]
|
||||
public static partial void ManagementEndpointAccessedViaAjax(this ILogger logger, LogLevel logLevel, string endpoint);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"BFF management endpoint {{endpoint}} is only intended for a browser window to request and load. It is not intended to be accessed with Ajax or fetch requests.")]
|
||||
public static partial void ManagementEndpointAccessedViaAjax(this ILogger logger, string endpoint);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Challenge was called for a BFF API endpoint, BFF response handling changing status code to 401.")]
|
||||
public static partial void ChallengeForBffApiEndpoint(this ILogger logger);
|
||||
public static partial void ChallengeForBffApiEndpoint(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Forbid was called for a BFF API endpoint, BFF response handling changing status code to 403.")]
|
||||
public static partial void ForbidForBffApiEndpoint(this ILogger logger);
|
||||
public static partial void ForbidForBffApiEndpoint(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Creating user session record in store for sub {{{OTelParameters.Sub}}} sid {{{OTelParameters.Sid}}}")]
|
||||
public static partial void CreatingUserSession(this ILogger logger, string sub, string? sid);
|
||||
message:
|
||||
$"Creating user session record in store for sub {{{OTelParameters.Sub}}} sid {{{OTelParameters.Sid}}}")]
|
||||
public static partial void CreatingUserSession(this ILogger logger, LogLevel logLevel, string sub, string? sid);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Detected a duplicate insert of the same session. This can happen when multiple browser tabs are open and can safely be ignored.")]
|
||||
public static partial void DuplicateSessionInsertDetected(this ILogger logger, Exception ex);
|
||||
message:
|
||||
$"Detected a duplicate insert of the same session. This can happen when multiple browser tabs are open and can safely be ignored.")]
|
||||
public static partial void DuplicateSessionInsertDetected(this ILogger logger, LogLevel logLevel, Exception ex);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Warning,
|
||||
message: $"Exception creating new server-side session in database: {{{OTelParameters.Error}}}. If this is a duplicate key error, it's safe to ignore. This can happen (for example) when two identical tabs are open.")]
|
||||
public static partial void ExceptionCreatingSession(this ILogger logger, Exception ex, string error);
|
||||
message:
|
||||
$"Exception creating new server-side session in database: {{{OTelParameters.Error}}}. If this is a duplicate key error, it's safe to ignore. This can happen (for example) when two identical tabs are open.")]
|
||||
public static partial void ExceptionCreatingSession(this ILogger logger, LogLevel logLevel, Exception ex, string error);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"No record found in user session store when trying to delete user session for key {{{OTelParameters.Key}}}")]
|
||||
public static partial void NoRecordFoundForKey(this ILogger logger, string key);
|
||||
message:
|
||||
$"No record found in user session store when trying to delete user session for key {{{OTelParameters.Key}}}")]
|
||||
public static partial void NoRecordFoundForKey(this ILogger logger, LogLevel logLevel, string key);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Deleting user session record in store for sub {{{OTelParameters.Sub}}} sid {{{OTelParameters.Sid}}}")]
|
||||
public static partial void DeletingUserSession(this ILogger logger, string sub, string? sid);
|
||||
message:
|
||||
$"Deleting user session record in store for sub {{{OTelParameters.Sub}}} sid {{{OTelParameters.Sid}}}")]
|
||||
public static partial void DeletingUserSession(this ILogger logger, LogLevel logLevel, string sub, string? sid);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"DbUpdateConcurrencyException: {{{OTelParameters.Error}}}")]
|
||||
public static partial void DbUpdateConcurrencyException(this ILogger logger, string error);
|
||||
public static partial void DbUpdateConcurrencyException(this ILogger logger, LogLevel logLevel, string error);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Getting user session record from store for sub {{{OTelParameters.Sub}}} sid {{{OTelParameters.Sid}}}")]
|
||||
public static partial void GettingUserSession(this ILogger logger, string sub, string? sid);
|
||||
message:
|
||||
$"Getting user session record from store for sub {{{OTelParameters.Sub}}} sid {{{OTelParameters.Sid}}}")]
|
||||
public static partial void GettingUserSession(this ILogger logger, LogLevel logLevel, string sub, string? sid);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Getting {{{OTelParameters.Count}}} user session(s) from store for sub {{{OTelParameters.Sub}}} sid {{{OTelParameters.Sid}}}")]
|
||||
public static partial void GettingUserSessions(this ILogger logger, int count, string? sub, string? sid);
|
||||
message:
|
||||
$"Getting {{{OTelParameters.Count}}} user session(s) from store for sub {{{OTelParameters.Sub}}} sid {{{OTelParameters.Sid}}}")]
|
||||
public static partial void GettingUserSessions(this ILogger logger, LogLevel logLevel, int count, string? sub, string? sid);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Deleting {{{OTelParameters.Count}}} user session(s) from store for sub {{{OTelParameters.Sub}}} sid {{{OTelParameters.Sid}}}")]
|
||||
public static partial void DeletingUserSessions(this ILogger logger, int count, string? sub, string? sid);
|
||||
message:
|
||||
$"Deleting {{{OTelParameters.Count}}} user session(s) from store for sub {{{OTelParameters.Sub}}} sid {{{OTelParameters.Sid}}}")]
|
||||
public static partial void DeletingUserSessions(this ILogger logger, LogLevel logLevel, int count, string? sub, string? sid);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Updating user session record in store for sub {{{OTelParameters.Sub}}} sid {{{OTelParameters.Sid}}}")]
|
||||
public static partial void UpdatingUserSession(this ILogger logger, string? sub, string? sid);
|
||||
message:
|
||||
$"Updating user session record in store for sub {{{OTelParameters.Sub}}} sid {{{OTelParameters.Sid}}}")]
|
||||
public static partial void UpdatingUserSession(this ILogger logger, LogLevel logLevel, string? sub, string? sid);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Removing {{{OTelParameters.Count}}} server side sessions")]
|
||||
public static partial void RemovingServerSideSessions(this ILogger logger, int count);
|
||||
public static partial void RemovingServerSideSessions(this ILogger logger, LogLevel logLevel, int count);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Retrieving token for user {{{OTelParameters.User}}}")]
|
||||
public static partial void RetrievingTokenForUser(this ILogger logger, string? user);
|
||||
public static partial void RetrievingTokenForUser(this ILogger logger, LogLevel logLevel, string? user);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Retrieving session {{{OTelParameters.Sid}}} for sub {{{OTelParameters.Sub}}}")]
|
||||
public static partial void RetrievingSession(this ILogger logger, string sid, string sub);
|
||||
public static partial void RetrievingSession(this ILogger logger, LogLevel logLevel, string sid, string sub);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Storing token for user {{{OTelParameters.User}}}")]
|
||||
public static partial void StoringTokenForUser(this ILogger logger, string? user);
|
||||
public static partial void StoringTokenForUser(this ILogger logger, LogLevel logLevel, string? user);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Removing token for user {{{OTelParameters.User}}}")]
|
||||
public static partial void RemovingTokenForUser(this ILogger logger, string? user);
|
||||
public static partial void RemovingTokenForUser(this ILogger logger, LogLevel logLevel, string? user);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Failed to find a session to update, bailing out")]
|
||||
public static partial void FailedToFindSessionToUpdate(this ILogger logger);
|
||||
public static partial void FailedToFindSessionToUpdate(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Creating entry in store for AuthenticationTicket, key {{{OTelParameters.Key}}}, with expiration: {{{OTelParameters.Expiration}}}")]
|
||||
public static partial void CreatingAuthenticationTicketEntry(this ILogger logger, string key, DateTime? expiration);
|
||||
message:
|
||||
$"Creating entry in store for AuthenticationTicket, key {{{OTelParameters.Key}}}, with expiration: {{{OTelParameters.Expiration}}}")]
|
||||
public static partial void CreatingAuthenticationTicketEntry(this ILogger logger, LogLevel logLevel, string key, DateTime? expiration);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Retrieve AuthenticationTicket for key {{{OTelParameters.Key}}}")]
|
||||
public static partial void RetrieveAuthenticationTicket(this ILogger logger, string key);
|
||||
public static partial void RetrieveAuthenticationTicket(this ILogger logger, LogLevel logLevel, string key);
|
||||
|
||||
[LoggerMessage(
|
||||
message: $"Ticket loaded for key: {{{OTelParameters.Key}}}, with expiration: {{{OTelParameters.Expiration}}}")]
|
||||
public static partial void TicketLoaded(this ILogger logger, LogLevel logLevel, string key, DateTime? expiration);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"No AuthenticationTicket found in store for {{{OTelParameters.Key}}}")]
|
||||
public static partial void NoAuthenticationTicketFoundForKey(this ILogger logger, string key);
|
||||
public static partial void NoAuthenticationTicketFoundForKey(this ILogger logger, LogLevel logLevel, string key);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Warning,
|
||||
message: $"Failed to deserialize authentication ticket from store, deleting record for key {{{OTelParameters.Key}}}")]
|
||||
public static partial void FailedToDeserializeAuthenticationTicket(this ILogger logger, string key);
|
||||
message:
|
||||
$"Failed to deserialize authentication ticket from store, deleting record for key {{{OTelParameters.Key}}}")]
|
||||
public static partial void FailedToDeserializeAuthenticationTicket(this ILogger logger, LogLevel logLevel, string key);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Renewing AuthenticationTicket for key {{{OTelParameters.Key}}}, with expiration: {{{OTelParameters.Expiration}}}")]
|
||||
public static partial void RenewingAuthenticationTicket(this ILogger logger, string key, DateTime? expiration);
|
||||
message:
|
||||
$"Renewing AuthenticationTicket for key {{{OTelParameters.Key}}}, with expiration: {{{OTelParameters.Expiration}}}")]
|
||||
public static partial void RenewingAuthenticationTicket(this ILogger logger, LogLevel logLevel, string key, DateTime? expiration);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Removing AuthenticationTicket from store for key {{{OTelParameters.Key}}}")]
|
||||
public static partial void RemovingAuthenticationTicket(this ILogger logger, string key);
|
||||
public static partial void RemovingAuthenticationTicket(this ILogger logger, LogLevel logLevel, string key);
|
||||
|
||||
[LoggerMessage(
|
||||
level: LogLevel.Debug,
|
||||
message: $"Getting AuthenticationTickets from store for sub {{{OTelParameters.Sub}}} sid {{{OTelParameters.Sid}}}")]
|
||||
public static partial void GettingAuthenticationTickets(this ILogger logger, string? sub, string? sid);
|
||||
message:
|
||||
$"Getting AuthenticationTickets from store for sub {{{OTelParameters.Sub}}} sid {{{OTelParameters.Sid}}}")]
|
||||
public static partial void GettingAuthenticationTickets(this ILogger logger, LogLevel logLevel, string? sub, string? sid);
|
||||
|
||||
[LoggerMessage(
|
||||
message: $"Frontend selected via path mapping '{{{OTelParameters.PathMapping}}}', but request path '{{{OTelParameters.LocalPath}}}' has different case. Cookie path names are case sensitive, so the cookie likely doesn't work.")]
|
||||
public static partial void FrontendSelectedWithPathCasingIssue(this ILogger logger, LogLevel level, string pathMapping, LocalPath localPath);
|
||||
message:
|
||||
$"Frontend selected via path mapping '{{{OTelParameters.PathMapping}}}', but request path '{{{OTelParameters.LocalPath}}}' has different case. Cookie path names are case sensitive, so the cookie likely doesn't work.")]
|
||||
public static partial void FrontendSelectedWithPathCasingIssue(this ILogger logger, LogLevel logLevel,
|
||||
string pathMapping, LocalPath localPath);
|
||||
|
||||
[LoggerMessage(
|
||||
message:
|
||||
$"Already mapped {{{OTelParameters.Name}}} endpoint, so the call to MapBffManagementEndpoints will be ignored. If you're using BffOptions.AutomaticallyRegisterBffMiddleware, you don't need to call endpoints.MapBffManagementEndpoints()")]
|
||||
public static partial void AlreadyMappedManagementEndpoint(this ILogger logger, LogLevel logLevel, string name);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Authenticating scheme: {Scheme}")]
|
||||
public static partial void AuthenticatingScheme(this ILogger logger, LogLevel logLevel, string? scheme);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Setting OIDC ProtocolMessage.Prompt to 'none' for BFF silent login")]
|
||||
public static partial void SettingOidcPromptNoneForSilentLogin(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Setting OIDC ProtocolMessage.Prompt to {Prompt} for BFF silent login")]
|
||||
public static partial void SettingOidcPromptForSilentLogin(this ILogger logger, LogLevel logLevel, string prompt);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Handling error response from OIDC provider for BFF silent login.")]
|
||||
public static partial void HandlingErrorResponseFromOidcProviderForSilentLogin(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Handling failed response from OIDC provider for BFF silent login.")]
|
||||
public static partial void HandlingFailedResponseFromOidcProviderForSilentLogin(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Processing back-channel logout request")]
|
||||
public static partial void ProcessingBackChannelLogoutRequest(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "No claims in back-channel JWT")]
|
||||
public static partial void NoClaimsInBackChannelJwt(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Claims found in back-channel JWT {Claims}")]
|
||||
public static partial void ClaimsFoundInBackChannelJwt(this ILogger logger, LogLevel logLevel, string claims);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Back-channel JWT validation successful")]
|
||||
public static partial void BackChannelJwtValidationSuccessful(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Processing login request")]
|
||||
public static partial void ProcessingLoginRequest(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Login endpoint triggering Challenge with returnUrl {ReturnUrl}")]
|
||||
public static partial void LoginEndpointTriggeringChallenge(this ILogger logger, LogLevel logLevel, string returnUrl);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Processing logout request")]
|
||||
public static partial void ProcessingLogoutRequest(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Logout endpoint triggering SignOut with returnUrl {ReturnUrl}")]
|
||||
public static partial void LogoutEndpointTriggeringSignOut(this ILogger logger, LogLevel logLevel, string returnUrl);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Processing silent login callback request")]
|
||||
public static partial void ProcessingSilentLoginCallbackRequest(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Silent login endpoint rendering HTML with JS postMessage to origin {Origin} with isLoggedIn {IsLoggedIn}")]
|
||||
public static partial void SilentLoginEndpointRenderingHtml(this ILogger logger, LogLevel logLevel, string origin, string isLoggedIn);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Processing silent login request")]
|
||||
public static partial void ProcessingSilentLoginRequest(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Using deprecated silentlogin endpoint. This endpoint will be removed in future versions. Consider calling the BFF Login endpoint with prompt=none.")]
|
||||
public static partial void UsingDeprecatedSilentLoginEndpoint(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Processing user request")]
|
||||
public static partial void ProcessingUserRequest(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "User endpoint indicates the user is not logged in, using status code {StatusCode}")]
|
||||
public static partial void UserEndpointNotLoggedIn(this ILogger logger, LogLevel logLevel, int statusCode);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "User endpoint indicates the user is logged in with claims {Claims}")]
|
||||
public static partial void UserEndpointLoggedInWithClaims(this ILogger logger, LogLevel logLevel, string claims);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Nop implementation of session revocation for sub: {Sub}, and sid: {Sid}. Implement ISessionRevocationService to provide your own implementation.")]
|
||||
public static partial void NopSessionRevocation(this ILogger logger, LogLevel logLevel, string? sub, string? sid);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Revoking sessions for sub {Sub} and sid {Sid}")]
|
||||
public static partial void RevokingSessions(this ILogger logger, LogLevel logLevel, string? sub, string? sid);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Refresh token revoked for sub {Sub} and sid {Sid}")]
|
||||
public static partial void RefreshTokenRevoked(this ILogger logger, LogLevel logLevel, string sub, string? sid);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "BFF session cleanup is enabled, but no IUserSessionStoreCleanup is registered in DI. BFF session cleanup will not run.")]
|
||||
public static partial void SessionCleanupNotRegistered(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Failed to cleanup session")]
|
||||
public static partial void FailedToCleanupSession(this ILogger logger, LogLevel logLevel, Exception ex);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Failed to cleanup expired sessions")]
|
||||
public static partial void FailedToCleanupExpiredSessions(this ILogger logger, LogLevel logLevel, Exception ex);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Revoking user's refresh tokens in OnSigningOut for subject id: {Sub}")]
|
||||
public static partial void RevokingUserRefreshTokensOnSigningOut(this ILogger logger, LogLevel logLevel, string? sub);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Explicitly setting ShouldRenew=false in OnValidatePrincipal due to query param suppressing slide behavior.")]
|
||||
public static partial void SuppressingSlideBehaviorOnValidatePrincipal(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Explicitly setting ShouldRenew=false in OnCheckSlidingExpiration due to query param suppressing slide behavior.")]
|
||||
public static partial void SuppressingSlideBehaviorOnCheckSlidingExpiration(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Failed to process backchannel logout request. 'Logout token is missing'")]
|
||||
public static partial void FailedToProcessBackchannelLogoutRequestMissingToken(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Failed to process backchannel logout request.")]
|
||||
public static partial void FailedToProcessBackchannelLogoutRequest(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Logout token missing sub and sid claims.")]
|
||||
public static partial void LogoutTokenMissingSubAndSidClaims(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Logout token should not contain nonce claim.")]
|
||||
public static partial void LogoutTokenShouldNotContainNonceClaim(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Logout token missing events claim.")]
|
||||
public static partial void LogoutTokenMissingEventsClaim(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Logout token contains missing http://schemas.openid.net/event/backchannel-logout value.")]
|
||||
public static partial void LogoutTokenMissingBackchannelLogoutValue(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Logout token contains invalid JSON in events claim value.")]
|
||||
public static partial void LogoutTokenContainsInvalidJsonInEventsClaim(this ILogger logger, LogLevel logLevel, Exception ex);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Error validating logout token.")]
|
||||
public static partial void ErrorValidatingLogoutToken(this ILogger logger, LogLevel logLevel, Exception ex);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "You do not have a valid license key for the Duende software. " +
|
||||
"This is allowed for development and testing scenarios. " +
|
||||
"If you are running in production you are required to have a licensed version. " +
|
||||
"Please start a conversation with us: https://duendesoftware.com/contact")]
|
||||
public static partial void NoValidLicense(this ILogger logger, LogLevel logLevel);
|
||||
|
||||
[LoggerMessage(
|
||||
message: "Error validating the license key" +
|
||||
"If you are running in production you are required to have a licensed version. " +
|
||||
"Please start a conversation with us: https://duendesoftware.com/contact")]
|
||||
public static partial void ErrorValidatingLicenseKey(this ILogger logger, LogLevel logLevel, Exception ex);
|
||||
|
||||
public static string Sanitize(this string toSanitize) => toSanitize.ReplaceLineEndings(string.Empty);
|
||||
|
||||
|
|
|
|||
|
|
@ -23,4 +23,8 @@ internal class OTelParameters
|
|||
public const string Scheme = "Scheme";
|
||||
public const string Prompt = "Prompt";
|
||||
public const string PathMapping = "PathMapping";
|
||||
public const string Name = "Name";
|
||||
public const string Claims = "Claims";
|
||||
public const string Origin = "Origin";
|
||||
public const string IsLoggedIn = "IsLoggedIn";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using Duende.AccessTokenManagement.OpenIdConnect;
|
|||
using Duende.Bff.AccessTokenManagement;
|
||||
using Duende.Bff.Configuration;
|
||||
using Duende.Bff.Internal;
|
||||
using Duende.Bff.Otel;
|
||||
using Duende.IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -43,7 +44,7 @@ internal class PostConfigureApplicationCookieRevokeRefreshToken(
|
|||
{
|
||||
// Todo: Ev: logging with sourcegens
|
||||
// todo: ev: should we have userparameters here?
|
||||
logger.LogDebug("Revoking user's refresh tokens in OnSigningOut for subject id: {subjectId}", ctx.HttpContext.User.FindFirst(JwtClaimTypes.Subject)?.Value);
|
||||
logger.RevokingUserRefreshTokensOnSigningOut(LogLevel.Debug, ctx.HttpContext.User.FindFirst(JwtClaimTypes.Subject)?.Value);
|
||||
await ctx.HttpContext.RevokeRefreshTokenAsync(ct: ctx.HttpContext.RequestAborted);
|
||||
|
||||
await inner.Invoke(ctx);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using Duende.Bff.AccessTokenManagement;
|
||||
using Duende.Bff.Configuration;
|
||||
using Duende.Bff.Internal;
|
||||
using Duende.Bff.Otel;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
|
@ -44,7 +45,7 @@ internal class PostConfigureApplicationValidatePrincipal(
|
|||
var slide = ctx.Request.Query[Constants.RequestParameters.SlideCookie];
|
||||
if (slide == "false")
|
||||
{
|
||||
logger.LogDebug("Explicitly setting ShouldRenew=false in OnValidatePrincipal due to query param suppressing slide behavior.");
|
||||
logger.SuppressingSlideBehaviorOnValidatePrincipal(LogLevel.Debug);
|
||||
ctx.ShouldRenew = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
using Duende.Bff.AccessTokenManagement;
|
||||
using Duende.Bff.Configuration;
|
||||
using Duende.Bff.Internal;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Duende.Bff.Otel;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
|
@ -17,12 +17,10 @@ namespace Duende.Bff.SessionManagement.Configuration;
|
|||
internal class PostConfigureSlidingExpirationCheck(
|
||||
ActiveCookieAuthenticationScheme activeCookieScheme,
|
||||
IOptions<BffOptions> bffOptions,
|
||||
IOptions<AuthenticationOptions> authOptions,
|
||||
ILogger<PostConfigureSlidingExpirationCheck> logger)
|
||||
: IPostConfigureOptions<CookieAuthenticationOptions>
|
||||
{
|
||||
private readonly BffOptions _options = bffOptions.Value;
|
||||
private readonly string? _scheme = authOptions.Value.DefaultAuthenticateScheme ?? authOptions.Value.DefaultScheme;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PostConfigure(string? name, CookieAuthenticationOptions options)
|
||||
|
|
@ -47,7 +45,7 @@ internal class PostConfigureSlidingExpirationCheck(
|
|||
var slide = ctx.Request.Query[Constants.RequestParameters.SlideCookie];
|
||||
if (slide == "false")
|
||||
{
|
||||
logger.LogDebug("Explicitly setting ShouldRenew=false in OnCheckSlidingExpiration due to query param suppressing slide behavior.");
|
||||
logger.SuppressingSlideBehaviorOnCheckSlidingExpiration(LogLevel.Debug);
|
||||
ctx.ShouldRenew = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.Bff.Otel;
|
||||
using Duende.Bff.SessionManagement.SessionStore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ internal class NopSessionRevocationService(ILogger<NopSessionRevocationService>
|
|||
/// <inheritdoc />
|
||||
public Task RevokeSessionsAsync(UserSessionsFilter filter, CT ct = default)
|
||||
{
|
||||
logger.LogDebug("Nop implementation of session revocation for sub: {sub}, and sid: {sid}. Implement ISessionRevocationService to provide your own implementation.", filter.SubjectId, filter.SessionId);
|
||||
logger.NopSessionRevocation(LogLevel.Debug, filter.SubjectId, filter.SessionId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using Duende.AccessTokenManagement;
|
||||
using Duende.AccessTokenManagement.OpenIdConnect;
|
||||
using Duende.Bff.Configuration;
|
||||
using Duende.Bff.Otel;
|
||||
using Duende.Bff.SessionManagement.SessionStore;
|
||||
using Duende.Bff.SessionManagement.TicketStore;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
|
@ -32,7 +33,7 @@ internal class SessionRevocationService(
|
|||
filter.SessionId = null;
|
||||
}
|
||||
|
||||
logger.LogDebug("Revoking sessions for sub {sub} and sid {sid}", filter.SubjectId, filter.SessionId);
|
||||
logger.RevokingSessions(LogLevel.Debug, filter.SubjectId, filter.SessionId);
|
||||
|
||||
if (_options.RevokeRefreshTokenOnLogout)
|
||||
{
|
||||
|
|
@ -46,7 +47,7 @@ internal class SessionRevocationService(
|
|||
new UserRefreshToken(RefreshToken.Parse(refreshToken),
|
||||
options.Value.DPoPJsonWebKey), new UserTokenRequestParameters(), ct);
|
||||
|
||||
logger.LogDebug("Refresh token revoked for sub {sub} and sid {sid}", ticket.GetSubjectId(), ticket.GetSessionId());
|
||||
logger.RefreshTokenRevoked(LogLevel.Debug, ticket.GetSubjectId(), ticket.GetSessionId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ internal class InMemoryUserSessionStore : IUserSessionStore
|
|||
{
|
||||
if (!_store.TryAdd(session.Key, session.Clone()))
|
||||
{
|
||||
throw new Exception("Key already exists");
|
||||
throw new InvalidOperationException("Key already exists");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
|
|
|||
|
|
@ -17,66 +17,29 @@ internal class SessionCleanupHost(
|
|||
BffMetrics metrics,
|
||||
IServiceProvider serviceProvider,
|
||||
IOptions<BffOptions> options,
|
||||
ILogger<SessionCleanupHost> logger) : IHostedService
|
||||
ILogger<SessionCleanupHost> logger) : BackgroundService
|
||||
{
|
||||
private readonly BffOptions _options = options.Value;
|
||||
|
||||
private TimeSpan CleanupInterval => _options.SessionCleanupInterval;
|
||||
|
||||
private CancellationTokenSource? _source;
|
||||
|
||||
/// <summary>
|
||||
/// Starts the token cleanup polling.
|
||||
/// </summary>
|
||||
public Task StartAsync(CT ct)
|
||||
protected override async Task ExecuteAsync(CT ct)
|
||||
{
|
||||
if (_options.EnableSessionCleanup)
|
||||
if (!_options.EnableSessionCleanup)
|
||||
{
|
||||
if (_source != null)
|
||||
{
|
||||
throw new InvalidOperationException("Already started. Call Stop first.");
|
||||
}
|
||||
|
||||
if (IsIUserSessionStoreCleanupRegistered())
|
||||
{
|
||||
logger.LogDebug("Starting BFF session cleanup");
|
||||
|
||||
_source = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
|
||||
Task.Factory.StartNew(() => StartInternalAsync(_source.Token));
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning("BFF session cleanup is enabled, but no IUserSessionStoreCleanup is registered in DI. BFF session cleanup will not run.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the token cleanup polling.
|
||||
/// </summary>
|
||||
public Task StopAsync(CT ct)
|
||||
{
|
||||
if (_options.EnableSessionCleanup && _source != null)
|
||||
if (!IsIUserSessionStoreCleanupRegistered())
|
||||
{
|
||||
logger.LogDebug("Stopping BFF session cleanup");
|
||||
|
||||
_source.Cancel();
|
||||
_source = null;
|
||||
logger.SessionCleanupNotRegistered(LogLevel.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task StartInternalAsync(CT ct)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
logger.LogDebug("CancellationRequested. Exiting.");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -86,18 +49,20 @@ internal class SessionCleanupHost(
|
|||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
logger.LogDebug("TaskCanceledException. Exiting.");
|
||||
break;
|
||||
}
|
||||
|
||||
#pragma warning disable CA1031// Do not catch general exception types
|
||||
// Catching general exceptions here to prevent the host from crashing if an exception occurs during the delay.
|
||||
catch (Exception ex)
|
||||
#pragma warning restore CA1031
|
||||
{
|
||||
logger.LogError("Task.Delay exception: {0}. Exiting.", ex.Message);
|
||||
logger.FailedToCleanupSession(LogLevel.Error, ex);
|
||||
break;
|
||||
}
|
||||
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
logger.LogDebug("CancellationRequested. Exiting.");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -114,9 +79,12 @@ internal class SessionCleanupHost(
|
|||
var removed = await tokenCleanupService.DeleteExpiredSessionsAsync(ct);
|
||||
metrics.SessionsEnded(removed);
|
||||
}
|
||||
#pragma warning disable CA1031// Do not catch general exception types
|
||||
// Catching general exceptions here to prevent the host from crashing if an exception occurs during the cleanup.
|
||||
catch (Exception ex)
|
||||
#pragma warning restore CA1031
|
||||
{
|
||||
logger.LogError("Exception deleting expired sessions: {exception}", ex.Message);
|
||||
logger.FailedToCleanupExpiredSessions(LogLevel.Error, ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ public class UserSession : UserSessionUpdate
|
|||
/// <returns></returns>
|
||||
public void CopyTo(UserSession other)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
other.Key = Key;
|
||||
base.CopyTo(other);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ public class UserSessionUpdate
|
|||
/// <returns></returns>
|
||||
public void CopyTo(UserSessionUpdate other)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
other.SubjectId = SubjectId;
|
||||
other.SessionId = SessionId;
|
||||
other.Created = Created;
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ internal class ServerSideTicketStore(
|
|||
|
||||
private async Task CreateNewSessionAsync(string key, AuthenticationTicket ticket)
|
||||
{
|
||||
LogMessages.CreatingAuthenticationTicketEntry(logger, key, ticket.GetExpiration());
|
||||
logger.CreatingAuthenticationTicketEntry(LogLevel.Debug, key, ticket.GetExpiration());
|
||||
|
||||
var session = new UserSession
|
||||
{
|
||||
|
|
@ -68,24 +68,24 @@ internal class ServerSideTicketStore(
|
|||
/// <inheritdoc />
|
||||
public async Task<AuthenticationTicket?> RetrieveAsync(string key)
|
||||
{
|
||||
LogMessages.RetrieveAuthenticationTicket(logger, key);
|
||||
logger.RetrieveAuthenticationTicket(LogLevel.Debug, key);
|
||||
|
||||
var session = await store.GetUserSessionAsync(key);
|
||||
if (session == null)
|
||||
{
|
||||
LogMessages.NoAuthenticationTicketFoundForKey(logger, key);
|
||||
logger.NoAuthenticationTicketFoundForKey(LogLevel.Debug, key);
|
||||
return null;
|
||||
}
|
||||
|
||||
var ticket = session.Deserialize(_protector, logger);
|
||||
if (ticket != null)
|
||||
{
|
||||
logger.LogDebug("Ticket loaded for key: {key}, with expiration: {expiration}", key, ticket.GetExpiration());
|
||||
logger.TicketLoaded(LogLevel.Debug, key, ticket.GetExpiration());
|
||||
return ticket;
|
||||
}
|
||||
|
||||
// if we failed to get a ticket, then remove DB record
|
||||
LogMessages.FailedToDeserializeAuthenticationTicket(logger, key);
|
||||
logger.FailedToDeserializeAuthenticationTicket(LogLevel.Information, key);
|
||||
await RemoveAsync(key);
|
||||
return ticket;
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ internal class ServerSideTicketStore(
|
|||
return;
|
||||
}
|
||||
|
||||
LogMessages.RenewingAuthenticationTicket(logger, key, ticket.GetExpiration());
|
||||
logger.RenewingAuthenticationTicket(LogLevel.Debug, key, ticket.GetExpiration());
|
||||
|
||||
var sub = ticket.GetSubjectId();
|
||||
var sid = ticket.GetSessionId();
|
||||
|
|
@ -122,7 +122,7 @@ internal class ServerSideTicketStore(
|
|||
/// <inheritdoc />
|
||||
public Task RemoveAsync(string key)
|
||||
{
|
||||
LogMessages.RemovingAuthenticationTicket(logger, key);
|
||||
logger.RemovingAuthenticationTicket(LogLevel.Debug, key);
|
||||
metrics.SessionEnded();
|
||||
|
||||
return store.DeleteUserSessionAsync(key);
|
||||
|
|
@ -131,7 +131,7 @@ internal class ServerSideTicketStore(
|
|||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyCollection<AuthenticationTicket>> GetUserTicketsAsync(UserSessionsFilter filter, CT ct)
|
||||
{
|
||||
LogMessages.GettingAuthenticationTickets(logger, filter.SubjectId, filter.SessionId);
|
||||
logger.GettingAuthenticationTickets(LogLevel.Debug, filter.SubjectId, filter.SessionId);
|
||||
|
||||
var list = new List<AuthenticationTicket>();
|
||||
|
||||
|
|
@ -146,7 +146,7 @@ internal class ServerSideTicketStore(
|
|||
else
|
||||
{
|
||||
// if we failed to get a ticket, then remove DB record
|
||||
LogMessages.FailedToDeserializeAuthenticationTicket(logger, session.Key);
|
||||
logger.FailedToDeserializeAuthenticationTicket(LogLevel.Debug, session.Key);
|
||||
await RemoveAsync(session.Key);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<Import Project="../../src.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<AnalysisMode>All</AnalysisMode>
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageTags>OAuth 2.0;OpenID Connect;Security;BFF;IdentityServer;ASP.NET Core;SPA;Blazor</PackageTags>
|
||||
<Product>Duende BFF</Product>
|
||||
|
|
@ -14,4 +15,12 @@
|
|||
<IsBffProject>true</IsBffProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
CA1034: Nested Types are OK
|
||||
CA2007: ConfigureAwait() is not needed in library code
|
||||
-->
|
||||
<NoWarn>$(NoWarn);CA1034; CA2007;</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -14,18 +14,18 @@ public class BffFrontendSigninTests : BffTestBase
|
|||
{
|
||||
public BffFrontendSigninTests(ITestOutputHelper output) : base(output) =>
|
||||
Bff.OnConfigureEndpoints += endpoints =>
|
||||
{
|
||||
endpoints.MapGet("/secret", (HttpContext c) =>
|
||||
{
|
||||
endpoints.MapGet("/secret", (HttpContext c) =>
|
||||
if (!c.User.IsAuthenticated())
|
||||
{
|
||||
if (!c.User.IsAuthenticated())
|
||||
{
|
||||
c.Response.StatusCode = 401;
|
||||
return "";
|
||||
}
|
||||
|
||||
c.Response.StatusCode = 401;
|
||||
return "";
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return "";
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
[Fact]
|
||||
|
|
@ -121,14 +121,12 @@ public class BffFrontendSigninTests : BffTestBase
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task given_path_based_frontend_login_endpoint_should_challenge_and_redirect_to_root_with_custom_prefix()
|
||||
public async Task
|
||||
given_path_based_frontend_login_endpoint_should_challenge_and_redirect_to_root_with_custom_prefix()
|
||||
{
|
||||
Bff.OnConfigureServices += svcs =>
|
||||
{
|
||||
svcs.Configure<BffOptions>(options =>
|
||||
{
|
||||
options.ManagementBasePath = "/custom/bff";
|
||||
});
|
||||
svcs.Configure<BffOptions>(options => { options.ManagementBasePath = "/custom/bff"; });
|
||||
};
|
||||
await InitializeAsync();
|
||||
|
||||
|
|
@ -149,7 +147,6 @@ public class BffFrontendSigninTests : BffTestBase
|
|||
|
||||
var response = await Bff.BrowserClient.Login("/somepath/custom");
|
||||
response.RequestMessage!.RequestUri.ShouldBe(Bff.Url("/somepath"));
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -245,18 +242,15 @@ public class BffFrontendSigninTests : BffTestBase
|
|||
// it to a wrong value and see if it throws.
|
||||
AddOrUpdateFrontend(bffFrontend with
|
||||
{
|
||||
ConfigureCookieOptions = opt =>
|
||||
{
|
||||
opt.Cookie.Name = "my_custom_cookie_name";
|
||||
}
|
||||
ConfigureCookieOptions = opt => { opt.Cookie.Name = "my_custom_cookie_name"; }
|
||||
});
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
Bff.BrowserClient.Cookies.GetCookies(Bff.Url())
|
||||
.ShouldContain(c => c.Name == "my_custom_cookie_name" && c.HttpOnly && c.Secure && c.Path == "/");
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Default_settings_augment_frontend_settings()
|
||||
{
|
||||
|
|
@ -299,31 +293,29 @@ public class BffFrontendSigninTests : BffTestBase
|
|||
var onTokenValidatedInvoked = false;
|
||||
|
||||
Bff.EnableBackChannelHandler = false;
|
||||
Bff.SetBffOptions += options =>
|
||||
{
|
||||
options.ConfigureOpenIdConnectDefaults = (oidc =>
|
||||
{
|
||||
oidc.Authority = IdentityServer.Url().ToString();
|
||||
|
||||
oidc.ClientId = "some_frontend";
|
||||
oidc.ClientSecret = DefaultOidcClient.ClientSecret;
|
||||
oidc.ResponseType = DefaultOidcClient.ResponseType;
|
||||
oidc.ResponseMode = DefaultOidcClient.ResponseMode;
|
||||
|
||||
oidc.MapInboundClaims = false;
|
||||
oidc.GetClaimsFromUserInfoEndpoint = true;
|
||||
oidc.SaveTokens = true;
|
||||
oidc.BackchannelHttpHandler = Internet;
|
||||
oidc.Events.OnTokenValidated += c =>
|
||||
{
|
||||
onTokenValidatedInvoked = true;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
await InitializeAsync();
|
||||
|
||||
Bff.BffOptions.ConfigureOpenIdConnectDefaults = (oidc =>
|
||||
{
|
||||
oidc.Authority = IdentityServer.Url().ToString();
|
||||
|
||||
oidc.ClientId = "some_frontend";
|
||||
oidc.ClientSecret = DefaultOidcClient.ClientSecret;
|
||||
oidc.ResponseType = DefaultOidcClient.ResponseType;
|
||||
oidc.ResponseMode = DefaultOidcClient.ResponseMode;
|
||||
|
||||
oidc.MapInboundClaims = false;
|
||||
oidc.GetClaimsFromUserInfoEndpoint = true;
|
||||
oidc.SaveTokens = true;
|
||||
oidc.BackchannelHttpHandler = Internet;
|
||||
oidc.Events.OnTokenValidated += c =>
|
||||
{
|
||||
onTokenValidatedInvoked = true;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
});
|
||||
|
||||
AddOrUpdateFrontend(new BffFrontend()
|
||||
{
|
||||
Name = BffFrontendName.Parse("some_frontend")
|
||||
|
|
@ -392,16 +384,9 @@ public class BffFrontendSigninTests : BffTestBase
|
|||
AddOrUpdateFrontend(new BffFrontend()
|
||||
{
|
||||
Name = BffFrontendName.Parse("some_frontend"),
|
||||
ConfigureOpenIdConnectOptions = opt =>
|
||||
{
|
||||
|
||||
The.DefaultOpenIdConnectConfiguration(opt);
|
||||
}
|
||||
ConfigureOpenIdConnectOptions = opt => { The.DefaultOpenIdConnectConfiguration(opt); }
|
||||
});
|
||||
|
||||
onTokenValidatedInvoked.ShouldBeFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,23 +24,21 @@ public class BffWithoutExplicitFrontendTests(ITestOutputHelper output) : BffTest
|
|||
return "";
|
||||
});
|
||||
};
|
||||
Bff.SetBffOptions += options =>
|
||||
{
|
||||
options.ConfigureOpenIdConnectDefaults = oidc =>
|
||||
{
|
||||
oidc.Authority = IdentityServer.Url().ToString();
|
||||
oidc.ClientId = DefaultOidcClient.ClientId;
|
||||
oidc.ClientSecret = DefaultOidcClient.ClientSecret;
|
||||
oidc.ResponseType = DefaultOidcClient.ResponseType;
|
||||
oidc.ResponseMode = DefaultOidcClient.ResponseMode;
|
||||
oidc.MapInboundClaims = false;
|
||||
oidc.GetClaimsFromUserInfoEndpoint = true;
|
||||
oidc.SaveTokens = true;
|
||||
oidc.BackchannelHttpHandler = Internet;
|
||||
};
|
||||
};
|
||||
|
||||
await base.InitializeAsync();
|
||||
|
||||
Bff.BffOptions.ConfigureOpenIdConnectDefaults = oidc =>
|
||||
{
|
||||
oidc.Authority = IdentityServer.Url().ToString();
|
||||
oidc.ClientId = DefaultOidcClient.ClientId;
|
||||
oidc.ClientSecret = DefaultOidcClient.ClientSecret;
|
||||
oidc.ResponseType = DefaultOidcClient.ResponseType;
|
||||
oidc.ResponseMode = DefaultOidcClient.ResponseMode;
|
||||
oidc.MapInboundClaims = false;
|
||||
oidc.GetClaimsFromUserInfoEndpoint = true;
|
||||
oidc.SaveTokens = true;
|
||||
oidc.BackchannelHttpHandler = Internet;
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ namespace Bff.Tests.Blazor;
|
|||
|
||||
public class BffBlazorTests : BffTestBase
|
||||
{
|
||||
|
||||
public BffBlazorTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
|
||||
{
|
||||
Bff.MapGetForRoot = false;
|
||||
|
|
@ -44,8 +43,7 @@ public class BffBlazorTests : BffTestBase
|
|||
[Theory, MemberData(nameof(AllSetups))]
|
||||
public async Task Can_get_home(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
var response = await Bff.BrowserClient.GetAsync("/");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
}
|
||||
|
|
@ -53,8 +51,7 @@ public class BffBlazorTests : BffTestBase
|
|||
[Theory, MemberData(nameof(AllSetups))]
|
||||
public async Task Cannot_get_secure_without_loggin_in(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
Bff.BrowserClient.RedirectHandler.AutoFollowRedirects = false;
|
||||
var response = await Bff.BrowserClient.GetAsync("/secure");
|
||||
|
|
@ -64,8 +61,7 @@ public class BffBlazorTests : BffTestBase
|
|||
[Theory, MemberData(nameof(AllSetups))]
|
||||
public async Task Can_get_secure_when_logged_in(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
Bff.BrowserClient.RedirectHandler.AutoFollowRedirects = false;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public class BffBuilderTests
|
|||
.AddFrontends(frontend1, frontend2);
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>().GetAll();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>();
|
||||
frontends.Count.ShouldBe(2);
|
||||
frontends.ShouldContain(frontend1);
|
||||
frontends.ShouldContain(frontend2);
|
||||
|
|
@ -51,7 +51,7 @@ public class BffBuilderTests
|
|||
.AddFrontends(frontend2);
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>().GetAll();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>();
|
||||
frontends.Count.ShouldBe(2);
|
||||
frontends.ShouldContain(frontend1);
|
||||
frontends.ShouldContain(frontend2);
|
||||
|
|
@ -74,7 +74,7 @@ public class BffBuilderTests
|
|||
services.AddBff()
|
||||
.LoadConfiguration(configuration);
|
||||
var provider = services.BuildServiceProvider();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>().GetAll();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>();
|
||||
frontends.Count.ShouldBe(1);
|
||||
|
||||
var found = frontends.First(x => x.Name == The.FrontendName);
|
||||
|
|
@ -124,7 +124,7 @@ public class BffBuilderTests
|
|||
.AddRemoteApis()
|
||||
.LoadConfiguration(configuration);
|
||||
var provider = services.BuildServiceProvider();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>().GetAll();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>();
|
||||
frontends.Count.ShouldBe(1);
|
||||
|
||||
var found = frontends.First(x => x.Name == The.FrontendName);
|
||||
|
|
@ -145,7 +145,6 @@ public class BffBuilderTests
|
|||
Frontends = new Dictionary<string, BffFrontendConfiguration>()
|
||||
{
|
||||
[The.FrontendName] = new(),
|
||||
|
||||
}
|
||||
})
|
||||
.Build();
|
||||
|
|
@ -153,7 +152,7 @@ public class BffBuilderTests
|
|||
var services = new ServiceCollection();
|
||||
services.AddBff().LoadConfiguration(configuration);
|
||||
var provider = services.BuildServiceProvider();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>().GetAll();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>();
|
||||
frontends.Count.ShouldBe(1);
|
||||
|
||||
var found = frontends.First(x => x.Name == The.FrontendName);
|
||||
|
|
@ -181,9 +180,7 @@ public class BffBuilderTests
|
|||
Cookies = Some.CookieConfiguration()
|
||||
// ev: todo
|
||||
//CallbackPath = The.Path
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
})
|
||||
.Build();
|
||||
|
|
@ -191,10 +188,11 @@ public class BffBuilderTests
|
|||
|
||||
// Wire up the BFF
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IHttpContextAccessor, FakeHttpContextAccessor>(); // We need the http context to set the scope
|
||||
services
|
||||
.AddSingleton<IHttpContextAccessor, FakeHttpContextAccessor>(); // We need the http context to set the scope
|
||||
services.AddBff().LoadConfiguration(configuration);
|
||||
var provider = services.BuildServiceProvider();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>().GetAll();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>();
|
||||
frontends.Count.ShouldBe(1);
|
||||
|
||||
var found = frontends.First(x => x.Name == The.FrontendName);
|
||||
|
|
@ -232,7 +230,6 @@ public class BffBuilderTests
|
|||
[The.FrontendName] = new()
|
||||
{
|
||||
},
|
||||
|
||||
}
|
||||
})
|
||||
.Build();
|
||||
|
|
@ -240,10 +237,11 @@ public class BffBuilderTests
|
|||
|
||||
// Wire up the BFF
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IHttpContextAccessor, FakeHttpContextAccessor>(); // We need the http context to set the scope
|
||||
services
|
||||
.AddSingleton<IHttpContextAccessor, FakeHttpContextAccessor>(); // We need the http context to set the scope
|
||||
services.AddBff().LoadConfiguration(configuration);
|
||||
var provider = services.BuildServiceProvider();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>().GetAll();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>();
|
||||
frontends.Count.ShouldBe(1);
|
||||
|
||||
var found = frontends.First(x => x.Name == The.FrontendName);
|
||||
|
|
@ -277,7 +275,6 @@ public class BffBuilderTests
|
|||
IConfiguration configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>()
|
||||
{
|
||||
|
||||
["frontends:_FrontendName_:matchingPath"] = The.Path,
|
||||
["frontends:_FrontendName_:matchingOrigin"] = The.Origin.ToString(),
|
||||
["frontends:_FrontendName_:indexHtmlUrl"] = The.Url.ToString(),
|
||||
|
|
@ -294,7 +291,8 @@ public class BffBuilderTests
|
|||
["frontends:_FrontendName_:RemoteApis:0:localPath"] = The.Path,
|
||||
["frontends:_FrontendName_:RemoteApis:0:targetUri"] = The.Url.ToString(),
|
||||
["frontends:_FrontendName_:RemoteApis:0:requiredTokenType"] = The.RequiredTokenType.ToString(),
|
||||
["frontends:_FrontendName_:RemoteApis:0:tokenRetrieverTypeName"] = The.TokenRetrieverType.AssemblyQualifiedName,
|
||||
["frontends:_FrontendName_:RemoteApis:0:tokenRetrieverTypeName"] =
|
||||
The.TokenRetrieverType.AssemblyQualifiedName,
|
||||
["frontends:_FrontendName_:RemoteApis:0:userAccessTokenParameters:signinScheme"] = The.Scheme,
|
||||
["frontends:_FrontendName_:RemoteApis:0:userAccessTokenParameters:challengeScheme"] = The.Scheme,
|
||||
["frontends:_FrontendName_:RemoteApis:0:userAccessTokenParameters:forceRenewal"] = true.ToString(),
|
||||
|
|
@ -307,7 +305,7 @@ public class BffBuilderTests
|
|||
.LoadConfiguration(configuration)
|
||||
.AddRemoteApis();
|
||||
var provider = services.BuildServiceProvider();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>().GetAll();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>();
|
||||
frontends.Count.ShouldBe(1);
|
||||
|
||||
var found = frontends.First(x => x.Name == The.FrontendName);
|
||||
|
|
@ -347,7 +345,7 @@ public class BffBuilderTests
|
|||
var services = new ServiceCollection();
|
||||
services.AddBff().LoadConfiguration(configFile.Configuration);
|
||||
var provider = services.BuildServiceProvider();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>().GetAll();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>();
|
||||
frontends.Count.ShouldBe(2);
|
||||
|
||||
configFile.Save(new BffConfiguration()
|
||||
|
|
@ -359,7 +357,7 @@ public class BffBuilderTests
|
|||
}
|
||||
});
|
||||
|
||||
frontends = provider.GetRequiredService<FrontendCollection>().GetAll();
|
||||
frontends = provider.GetRequiredService<FrontendCollection>();
|
||||
frontends.Count.ShouldBe(2);
|
||||
|
||||
var found = frontends.ToArray();
|
||||
|
|
@ -398,7 +396,7 @@ public class BffBuilderTests
|
|||
optionsCache.TryAdd("to_be_removed", new OpenIdConnectOptions());
|
||||
optionsCache.TryAdd("to_be_updated", new OpenIdConnectOptions());
|
||||
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>().GetAll();
|
||||
var frontends = provider.GetRequiredService<FrontendCollection>();
|
||||
|
||||
configFile.Save(new BffConfiguration()
|
||||
{
|
||||
|
|
@ -409,11 +407,12 @@ public class BffBuilderTests
|
|||
}
|
||||
});
|
||||
|
||||
frontends = provider.GetRequiredService<FrontendCollection>().GetAll();
|
||||
frontends = provider.GetRequiredService<FrontendCollection>();
|
||||
frontends.Count.ShouldBe(2);
|
||||
|
||||
optionsCache.TryRemove("to_be_removed")
|
||||
.ShouldBeTrue("The frontend 'to_be_removed' is no longer in the config and should be removed from oidc config");
|
||||
.ShouldBeTrue(
|
||||
"The frontend 'to_be_removed' is no longer in the config and should be removed from oidc config");
|
||||
optionsCache.TryRemove("to_be_updated")
|
||||
.ShouldBeTrue("The frontend 'to_be_updated' is changed. We need to clear it from the oidc cache.");
|
||||
|
||||
|
|
@ -440,7 +439,6 @@ public class BffBuilderTests
|
|||
var options = provider.GetRequiredService<IOptions<OpenIdConnectOptions>>();
|
||||
|
||||
ValidateOpenIdConnectOptions(options.Value, The.CallbackPath.ToString());
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -491,8 +489,7 @@ public class BffBuilderTests
|
|||
ClientId = "not-null",
|
||||
ClientSecret = null
|
||||
}
|
||||
}
|
||||
,
|
||||
},
|
||||
}
|
||||
})
|
||||
.Build();
|
||||
|
|
@ -510,7 +507,7 @@ public class BffBuilderTests
|
|||
var services = new ServiceCollection();
|
||||
|
||||
Should.Throw<InvalidOperationException>(() => services.AddBff()
|
||||
.AddFrontends(Some.BffFrontend(), Some.BffFrontend()))
|
||||
.AddFrontends(Some.BffFrontend(), Some.BffFrontend()))
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
typeof(AddServerManagementClaimsTransform),
|
||||
typeof(BffServerAuthenticationStateProvider),
|
||||
typeof(BffClientAuthenticationStateProvider),
|
||||
typeof(SessionCleanupHost),
|
||||
typeof(SessionDbContext),
|
||||
typeof(SessionDbContext<>),
|
||||
typeof(ServerSideTokenStore), // This one needs to be removed after move to ATM 4.0
|
||||
|
|
|
|||
|
|
@ -11,20 +11,15 @@ using Xunit.Abstractions;
|
|||
|
||||
namespace Duende.Bff.Tests.Endpoints;
|
||||
|
||||
public class DPoPTestsWithManualAuthentication : BffTestBase, IAsyncLifetime
|
||||
public class DPoPTestsWithManualAuthentication(ITestOutputHelper output) : BffTestBase(output), IAsyncLifetime
|
||||
{
|
||||
public DPoPTestsWithManualAuthentication(ITestOutputHelper output) : base(output)
|
||||
public override async Task InitializeAsync()
|
||||
{
|
||||
Bff.EnableBackChannelHandler = false;
|
||||
var idSrvClient = IdentityServer.AddClient(The.ClientId, Bff.Url());
|
||||
|
||||
idSrvClient.RequireDPoP = true;
|
||||
|
||||
Bff.SetBffOptions += options =>
|
||||
{
|
||||
options.DPoPJsonWebKey = The.DPoPJsonWebKey;
|
||||
};
|
||||
|
||||
Bff.OnConfigureServices += services =>
|
||||
{
|
||||
services.AddAuthentication(opt =>
|
||||
|
|
@ -49,6 +44,8 @@ public class DPoPTestsWithManualAuthentication : BffTestBase, IAsyncLifetime
|
|||
;
|
||||
};
|
||||
|
||||
await base.InitializeAsync();
|
||||
Bff.BffOptions.DPoPJsonWebKey = The.DPoPJsonWebKey;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -60,7 +57,6 @@ public class DPoPTestsWithManualAuthentication : BffTestBase, IAsyncLifetime
|
|||
[Fact]
|
||||
public async Task When_calling_api_endpoint_with_dpop_enabled_then_dpop_headers_are_sent()
|
||||
{
|
||||
|
||||
ApiCallDetails callToApi = await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url(The.PathAndSubPath)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,24 +8,15 @@ using Duende.Bff.Yarp;
|
|||
using Xunit.Abstractions;
|
||||
|
||||
namespace Duende.Bff.Tests.Endpoints;
|
||||
public class DpopRemoteEndpointTests : BffTestBase, IAsyncLifetime
|
||||
|
||||
public class DpopRemoteEndpointTests(ITestOutputHelper output) : BffTestBase(output), IAsyncLifetime
|
||||
{
|
||||
public DpopRemoteEndpointTests(ITestOutputHelper output) : base(output)
|
||||
public override async Task InitializeAsync()
|
||||
{
|
||||
var idSrvClient = IdentityServer.AddClient(The.ClientId, Bff.Url());
|
||||
|
||||
idSrvClient.RequireDPoP = true;
|
||||
|
||||
Bff.SetBffOptions += options =>
|
||||
{
|
||||
options.DPoPJsonWebKey = The.DPoPJsonWebKey;
|
||||
options.ConfigureOpenIdConnectDefaults = opt =>
|
||||
{
|
||||
opt.BackchannelHttpHandler = Internet;
|
||||
The.DefaultOpenIdConnectConfiguration(opt);
|
||||
};
|
||||
};
|
||||
|
||||
Bff.OnConfigureBff += bff => bff.AddRemoteApis();
|
||||
Bff.OnConfigureEndpoints += endpoints =>
|
||||
{
|
||||
|
|
@ -34,16 +25,22 @@ public class DpopRemoteEndpointTests : BffTestBase, IAsyncLifetime
|
|||
;
|
||||
};
|
||||
|
||||
await base.InitializeAsync();
|
||||
Bff.BffOptions.DPoPJsonWebKey = The.DPoPJsonWebKey;
|
||||
Bff.BffOptions.ConfigureOpenIdConnectDefaults = opt =>
|
||||
{
|
||||
opt.BackchannelHttpHandler = Internet;
|
||||
The.DefaultOpenIdConnectConfiguration(opt);
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Can_login_with_dpop_enabled() => await Bff.BrowserClient.Login()
|
||||
.CheckHttpStatusCode();
|
||||
.CheckHttpStatusCode();
|
||||
|
||||
[Fact]
|
||||
public async Task When_calling_api_endpoint_with_dpop_enabled_then_dpop_headers_are_sent()
|
||||
{
|
||||
|
||||
ApiCallDetails callToApi = await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url(The.PathAndSubPath)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@ public class LocalEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
.AsBffApiEndpoint();
|
||||
};
|
||||
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
|
|
@ -49,8 +48,7 @@ public class LocalEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
.SkipAntiforgery()
|
||||
.AsBffApiEndpoint();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
|
|
@ -74,8 +72,7 @@ public class LocalEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
.RequireAuthorization()
|
||||
.AsBffApiEndpoint();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
|
||||
|
||||
|
|
@ -96,8 +93,7 @@ public class LocalEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
.AsBffApiEndpoint();
|
||||
};
|
||||
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
|
|
@ -117,8 +113,7 @@ public class LocalEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
.SkipAntiforgery()
|
||||
.AsBffApiEndpoint();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
|
||||
|
||||
|
|
@ -139,8 +134,7 @@ public class LocalEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
endpoints.Map(The.Path, c => ApiHost.ReturnApiCallDetails(c, () => LocalApiResponseStatus))
|
||||
.AsBffApiEndpoint();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
|
||||
|
||||
|
|
@ -160,8 +154,7 @@ public class LocalEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
endpoints.Map(The.Path, c => ApiHost.ReturnApiCallDetails(c, () => LocalApiResponseStatus))
|
||||
.AsBffApiEndpoint();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
|
||||
|
||||
|
|
@ -189,8 +182,8 @@ public class LocalEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
endpoints.Map(The.Path, c => ApiHost.ReturnApiCallDetails(c, () => LocalApiResponseStatus))
|
||||
.RequireAuthorization();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
|
||||
|
||||
Bff.BrowserClient.RedirectHandler.AutoFollowRedirects = false; // we want to see the redirect
|
||||
|
|
@ -216,8 +209,8 @@ public class LocalEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
.RequireAuthorization()
|
||||
.AsBffApiEndpoint();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
|
||||
|
||||
LocalApiResponseStatus = HttpStatusCode.Unauthorized;
|
||||
|
|
@ -239,9 +232,7 @@ public class LocalEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
.AsBffApiEndpoint();
|
||||
};
|
||||
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
LocalApiResponseStatus = HttpStatusCode.Forbidden;
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
|
@ -263,8 +254,8 @@ public class LocalEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
.AsBffApiEndpoint();
|
||||
};
|
||||
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
|
@ -287,8 +278,9 @@ public class LocalEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
.SkipResponseHandling();
|
||||
};
|
||||
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
Bff.BrowserClient.RedirectHandler.AutoFollowRedirects = false;
|
||||
|
|
@ -313,8 +305,7 @@ public class LocalEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
.Build();
|
||||
});
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
|
||||
var response = await Bff.BrowserClient.GetAsync(Bff.Url("/not-found"));
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ namespace Duende.Bff.Tests.Endpoints.Management;
|
|||
public class BackchannelLogoutEndpointTests : BffTestBase
|
||||
{
|
||||
public BackchannelLogoutEndpointTests(ITestOutputHelper output) : base(output) => Bff.OnConfigureBff += bff =>
|
||||
{
|
||||
bff.AddServerSideSessions();
|
||||
};
|
||||
{
|
||||
bff.AddServerSideSessions();
|
||||
};
|
||||
|
||||
public override async Task InitializeAsync()
|
||||
{
|
||||
|
|
@ -30,8 +30,6 @@ public class BackchannelLogoutEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task backchannel_logout_should_allow_anonymous(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
|
||||
Bff.OnConfigureServices += svcs =>
|
||||
{
|
||||
svcs.AddAuthorization(opts =>
|
||||
|
|
@ -42,7 +40,7 @@ public class BackchannelLogoutEndpointTests : BffTestBase
|
|||
.Build();
|
||||
});
|
||||
};
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
// if you call the endpoint without a token, it should return 400
|
||||
await Bff.BrowserClient.PostAsync(Bff.Url("/bff/backchannel"), null)
|
||||
|
|
@ -53,8 +51,7 @@ public class BackchannelLogoutEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task backchannel_logout_endpoint_should_signout(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
|
|
@ -67,8 +64,7 @@ public class BackchannelLogoutEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task backchannel_logout_endpoint_for_incorrect_sub_should_not_logout_user(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.CreateIdentityServerSessionCookieAsync(IdentityServer, The.Sub, The.Sid);
|
||||
|
||||
|
|
@ -85,8 +81,7 @@ public class BackchannelLogoutEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task backchannel_logout_endpoint_for_incorrect_sid_should_not_logout_user(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
|
|
@ -100,15 +95,11 @@ public class BackchannelLogoutEndpointTests : BffTestBase
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllSetups))]
|
||||
public async Task when_BackchannelLogoutAllUserSessions_is_false_backchannel_logout_should_only_logout_one_session(BffSetupType setup)
|
||||
public async Task when_BackchannelLogoutAllUserSessions_is_false_backchannel_logout_should_only_logout_one_session(
|
||||
BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
Bff.SetBffOptions += options =>
|
||||
{
|
||||
options.BackchannelLogoutAllUserSessions = false;
|
||||
};
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
Bff.BffOptions.BackchannelLogoutAllUserSessions = false;
|
||||
|
||||
await Bff.BrowserClient.CreateIdentityServerSessionCookieAsync(IdentityServer, The.Sub, The.Sid);
|
||||
|
||||
|
|
@ -139,15 +130,11 @@ public class BackchannelLogoutEndpointTests : BffTestBase
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllSetups))]
|
||||
public async Task when_BackchannelLogoutAllUserSessions_is_false_backchannel_logout_should_logout_all_sessions(BffSetupType setup)
|
||||
public async Task when_BackchannelLogoutAllUserSessions_is_false_backchannel_logout_should_logout_all_sessions(
|
||||
BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
Bff.SetBffOptions += options =>
|
||||
{
|
||||
options.BackchannelLogoutAllUserSessions = true;
|
||||
};
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
Bff.BffOptions.BackchannelLogoutAllUserSessions = true;
|
||||
|
||||
await Bff.BrowserClient.CreateIdentityServerSessionCookieAsync(IdentityServer, The.Sub, The.Sid);
|
||||
|
||||
|
|
@ -175,4 +162,3 @@ public class BackchannelLogoutEndpointTests : BffTestBase
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,21 +8,19 @@ using Xunit.Abstractions;
|
|||
|
||||
namespace Duende.Bff.Tests.Endpoints.Management;
|
||||
|
||||
public class LoginEndpointTests : BffTestBase
|
||||
public class LoginEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
||||
{
|
||||
public LoginEndpointTests(ITestOutputHelper output) : base(output) => Bff.SetBffOptions += options =>
|
||||
{
|
||||
options.ConfigureOpenIdConnectDefaults = opt =>
|
||||
{
|
||||
The.DefaultOpenIdConnectConfiguration(opt);
|
||||
};
|
||||
};
|
||||
public override async Task InitializeAsync()
|
||||
{
|
||||
await base.InitializeAsync();
|
||||
Bff.BffOptions.ConfigureOpenIdConnectDefaults = opt => { The.DefaultOpenIdConnectConfiguration(opt); };
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllSetups))]
|
||||
public async Task login_should_allow_anonymous(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await ConfigureBff(setup);
|
||||
|
||||
Bff.OnConfigureServices += svcs =>
|
||||
{
|
||||
|
|
@ -34,7 +32,6 @@ public class LoginEndpointTests : BffTestBase
|
|||
.Build();
|
||||
});
|
||||
};
|
||||
await InitializeAsync();
|
||||
|
||||
var response = await Bff.BrowserClient.Login();
|
||||
response.StatusCode.ShouldNotBe(HttpStatusCode.Unauthorized);
|
||||
|
|
@ -44,8 +41,7 @@ public class LoginEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task when_unauthenticated_silent_login_should_return_isLoggedIn_false(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
var response = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/silent-login?redirectUri=/"))
|
||||
.CheckHttpStatusCode();
|
||||
|
|
@ -60,8 +56,7 @@ public class LoginEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task silent_login_should_challenge_and_return_silent_login_html(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
|
|
@ -81,8 +76,7 @@ public class LoginEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task can_issue_silent_login_with_prompt_none(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
|
|
@ -102,8 +96,7 @@ public class LoginEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task login_with_unsupported_prompt_is_rejected(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
var response = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/login?prompt=not_supported_prompt"));
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
|
||||
|
|
@ -117,8 +110,7 @@ public class LoginEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task can_use_prompt_supported_by_IdentityServer(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
// Prompt=create is enabled in identity server configuration:
|
||||
// https://docs.duendesoftware.com/identityserver/reference/options#userinteraction
|
||||
|
|
@ -127,7 +119,8 @@ public class LoginEndpointTests : BffTestBase
|
|||
var response = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/login?prompt=create"))
|
||||
.CheckHttpStatusCode();
|
||||
|
||||
response.RequestMessage!.RequestUri!.ToString().ShouldStartWith(IdentityServer.Url("/account/create").ToString());
|
||||
response.RequestMessage!.RequestUri!.ToString()
|
||||
.ShouldStartWith(IdentityServer.Url("/account/create").ToString());
|
||||
response.RequestMessage!.RequestUri!.ToString().ShouldNotContain("error");
|
||||
}
|
||||
|
||||
|
|
@ -135,48 +128,41 @@ public class LoginEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task login_endpoint_should_authenticatre_and_redirect_to_root(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
var response = await Bff.BrowserClient.Login();
|
||||
response.RequestMessage!.RequestUri.ShouldBe(Bff.Url("/"));
|
||||
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllSetups))]
|
||||
public async Task login_endpoint_should_challenge_and_redirect_to_root_with_custom_prefix(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
Bff.OnConfigureServices += svcs =>
|
||||
{
|
||||
svcs.Configure<BffOptions>(options =>
|
||||
{
|
||||
options.ManagementBasePath = "/custom/bff";
|
||||
});
|
||||
svcs.Configure<BffOptions>(options => { options.ManagementBasePath = "/custom/bff"; });
|
||||
};
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
|
||||
await Bff.BrowserClient.Login(expectedStatusCode: HttpStatusCode.NotFound);
|
||||
|
||||
var response = await Bff.BrowserClient.Login("/custom");
|
||||
response.RequestMessage!.RequestUri.ShouldBe(Bff.Url("/"));
|
||||
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllSetups))]
|
||||
public async Task login_endpoint_should_challenge_and_redirect_to_root_with_custom_prefix_trailing_slash(BffSetupType setup)
|
||||
public async Task login_endpoint_should_challenge_and_redirect_to_root_with_custom_prefix_trailing_slash(
|
||||
BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
Bff.OnConfigureServices += svcs =>
|
||||
{
|
||||
svcs.Configure<BffOptions>(options =>
|
||||
{
|
||||
options.ManagementBasePath = "/custom/bff/";
|
||||
});
|
||||
svcs.Configure<BffOptions>(options => { options.ManagementBasePath = "/custom/bff/"; });
|
||||
};
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
|
||||
|
||||
await Bff.BrowserClient.Login(expectedStatusCode: HttpStatusCode.NotFound);
|
||||
|
||||
|
|
@ -188,29 +174,24 @@ public class LoginEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task login_endpoint_should_challenge_and_redirect_to_root_with_root_prefix(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
Bff.OnConfigureServices += svcs =>
|
||||
{
|
||||
svcs.Configure<BffOptions>(options =>
|
||||
{
|
||||
options.ManagementBasePath = "/";
|
||||
});
|
||||
svcs.Configure<BffOptions>(options => { options.ManagementBasePath = "/"; });
|
||||
};
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
|
||||
var response = await Bff.BrowserClient.GetAsync(Bff.Url("/login"))
|
||||
.CheckHttpStatusCode();
|
||||
|
||||
response.RequestMessage!.RequestUri.ShouldBe(Bff.Url("/"));
|
||||
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllSetups))]
|
||||
public async Task login_endpoint_with_existing_session_should_challenge(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
|
|
@ -225,10 +206,8 @@ public class LoginEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task login_endpoint_should_accept_returnUrl(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
Bff.OnConfigureEndpoints += endpoints => endpoints.MapGet("/foo", () => "foo'd you");
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
var response = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/login") + "?returnUrl=/foo")
|
||||
.CheckHttpStatusCode();
|
||||
|
|
@ -241,8 +220,7 @@ public class LoginEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task login_endpoint_should_not_accept_non_local_returnUrl(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
var problem = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/login") + "?returnUrl=https://foo")
|
||||
.ShouldBeProblem();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ public class LogoutEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task logout_endpoint_should_allow_anonymous(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
Bff.OnConfigureServices += svcs =>
|
||||
{
|
||||
svcs.AddAuthorization(opts =>
|
||||
|
|
@ -28,7 +27,7 @@ public class LogoutEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
});
|
||||
};
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
var response = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/logout"));
|
||||
response.StatusCode.ShouldNotBe(HttpStatusCode.Unauthorized);
|
||||
|
|
@ -38,8 +37,7 @@ public class LogoutEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task logout_endpoint_should_signout(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
|
|
@ -54,8 +52,7 @@ public class LogoutEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task logout_endpoint_for_authenticated_should_require_sid(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
var problem = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/logout"))
|
||||
|
|
@ -68,10 +65,10 @@ public class LogoutEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllSetups))]
|
||||
public async Task logout_endpoint_for_authenticated_when_require_option_is_false_should_not_require_sid(BffSetupType setup)
|
||||
public async Task logout_endpoint_for_authenticated_when_require_option_is_false_should_not_require_sid(
|
||||
BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
Bff.BffOptions.RequireLogoutSessionId = false;
|
||||
|
|
@ -79,28 +76,29 @@ public class LogoutEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
|
||||
var response = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/logout"));
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession
|
||||
response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServer.Url("/connect/endsession").ToString());
|
||||
response.Headers.Location!.ToString().ToLowerInvariant()
|
||||
.ShouldStartWith(IdentityServer.Url("/connect/endsession").ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllSetups))]
|
||||
public async Task logout_endpoint_for_authenticated_user_without_sid_should_succeed(BffSetupType setup)
|
||||
{
|
||||
|
||||
// Workaround to place a session cookie in the BFF without a session id claim.
|
||||
Bff.OnConfigureEndpoints += endpoints =>
|
||||
{
|
||||
endpoints.MapGet("/__signin", async ctx =>
|
||||
{
|
||||
var props = new AuthenticationProperties();
|
||||
await ctx.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, The.Sub)], "test", "name", "role")), props);
|
||||
await ctx.SignInAsync(
|
||||
new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.Subject, The.Sub)], "test", "name",
|
||||
"role")), props);
|
||||
|
||||
ctx.Response.StatusCode = 204;
|
||||
});
|
||||
};
|
||||
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
await Bff.BrowserClient.GetAsync("__signin");
|
||||
|
||||
// workaround for RevokeUserRefreshTokenAsync throwing when no RT in session
|
||||
|
|
@ -110,29 +108,29 @@ public class LogoutEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
|
||||
var response = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/logout"));
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession
|
||||
response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServer.Url("/connect/endsession").ToString());
|
||||
response.Headers.Location!.ToString().ToLowerInvariant()
|
||||
.ShouldStartWith(IdentityServer.Url("/connect/endsession").ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllSetups))]
|
||||
public async Task logout_endpoint_for_anonymous_user_without_sid_should_succeed(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
Bff.BrowserClient.RedirectHandler.AutoFollowRedirects = false;
|
||||
|
||||
var response = await Bff.BrowserClient.GetAsync(Bff.Url("/bff/logout"));
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession
|
||||
response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServer.Url("/connect/endsession").ToString());
|
||||
response.Headers.Location!.ToString().ToLowerInvariant()
|
||||
.ShouldStartWith(IdentityServer.Url("/connect/endsession").ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllSetups))]
|
||||
public async Task can_logout_twice(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
var sid = await Bff.BrowserClient.GetSid();
|
||||
|
|
@ -143,7 +141,8 @@ public class LogoutEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
var response = await Bff.BrowserClient.Logout(sid);
|
||||
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession
|
||||
response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServer.Url("/connect/endsession").ToString());
|
||||
response.Headers.Location!.ToString().ToLowerInvariant()
|
||||
.ShouldStartWith(IdentityServer.Url("/connect/endsession").ToString());
|
||||
|
||||
|
||||
(await Bff.BrowserClient.GetIsUserLoggedInAsync()).ShouldBeFalse();
|
||||
|
|
@ -156,23 +155,20 @@ public class LogoutEndpointTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
{
|
||||
Bff.OnConfigureEndpoints += endpoints => endpoints.MapGet("/foo", () => "foo'd you");
|
||||
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
var response = await Bff.BrowserClient.Logout(returnUrl: new Uri("/foo", UriKind.Relative))
|
||||
.CheckHttpStatusCode();
|
||||
|
||||
response.RequestMessage!.RequestUri.ShouldBe(Bff.Url("/foo"));
|
||||
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllSetups))]
|
||||
public async Task logout_endpoint_should_reject_non_local_returnUrl(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
var problem = await Bff.BrowserClient.Logout(returnUrl: new Uri("https://foo"))
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ public class ManagementBasePathTests(ITestOutputHelper output) : BffTestBase(out
|
|||
[InlineData(Constants.ManagementEndpoints.User, HttpStatusCode.Unauthorized)]
|
||||
public async Task custom_ManagementBasePath_should_affect_basepath(string path, HttpStatusCode expectedStatusCode)
|
||||
{
|
||||
ConfigureBff(BffSetupType.V4Bff);
|
||||
Bff.OnConfigureServices += svcs =>
|
||||
{
|
||||
svcs.Configure<BffOptions>(options =>
|
||||
|
|
@ -28,7 +27,7 @@ public class ManagementBasePathTests(ITestOutputHelper output) : BffTestBase(out
|
|||
options.ManagementBasePath = new PathString("/{path:regex(^[a-zA-Z\\d-]+$)}/bff");
|
||||
});
|
||||
};
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(BffSetupType.V4Bff);
|
||||
|
||||
// Don't follow the redirects, becuase otherwise we might folow a redirect flow that ends up in a 404
|
||||
Bff.BrowserClient.RedirectHandler.AutoFollowRedirects = false;
|
||||
|
|
@ -48,6 +47,5 @@ public class ManagementBasePathTests(ITestOutputHelper output) : BffTestBase(out
|
|||
|
||||
var response = await Bff.BrowserClient.SendAsync(req);
|
||||
response.StatusCode.ShouldBe(expectedStatusCode);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,7 @@ public class UserEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task user_endpoint_for_authenticated_user_should_return_claims(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
AddCustomUserClaims(new Claim("foo", "foo1"), new Claim("foo", "foo2"));
|
||||
await Bff.BrowserClient.Login();
|
||||
|
|
@ -56,8 +55,7 @@ public class UserEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task user_endpoint_for_authenticated_user_with_sid_should_return_claims_including_logout(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
UserToSignIn = new ClaimsPrincipal(new ClaimsIdentity([
|
||||
new Claim("sub", "alice"),
|
||||
new Claim("sid", "123"),
|
||||
|
|
@ -78,8 +76,7 @@ public class UserEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task user_endpoint_for_authenticated_user_without_csrf_header_should_fail(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
await Bff.BrowserClient.IssueSessionCookieAsync(new Claim("sub", "alice"), new Claim("foo", "foo1"), new Claim("foo", "foo2"));
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, Bff.Url("/bff/user"));
|
||||
|
|
@ -92,8 +89,7 @@ public class UserEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task user_endpoint_for_unauthenticated_user_should_fail(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, Bff.Url("/bff/user"));
|
||||
req.Headers.Add("x-csrf", "1");
|
||||
var response = await Bff.BrowserClient.SendAsync(req);
|
||||
|
|
@ -105,8 +101,7 @@ public class UserEndpointTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task when_configured_user_endpoint_for_unauthenticated_user_should_return_200_and_empty(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
var options = Bff.Resolve<IOptions<BffOptions>>();
|
||||
|
||||
options.Value.AnonymousSessionResponse = AnonymousSessionResponse.Response200;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ public class RemoteEndpointTests : BffTestBase
|
|||
{
|
||||
// Add a custom default transform that adds a header to the request
|
||||
services.AddSingleton<BffYarpTransformBuilder>(CustomDefaultBffTransformBuilder);
|
||||
|
||||
};
|
||||
|
||||
Bff.OnConfigureBff += bff => bff.AddRemoteApis();
|
||||
|
|
@ -51,8 +50,7 @@ public class RemoteEndpointTests : BffTestBase
|
|||
app.MapRemoteBffApiEndpoint(The.Path, Api.Url(The.Path))
|
||||
.WithAccessToken();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url(The.Path),
|
||||
|
|
@ -69,9 +67,8 @@ public class RemoteEndpointTests : BffTestBase
|
|||
app.MapRemoteBffApiEndpoint(The.Path, Api.Url(The.Path))
|
||||
.WithAccessToken();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await InitializeAsync();
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
var (response, apiResult) = await Bff.BrowserClient.CallBffHostApi(
|
||||
|
|
@ -111,7 +108,7 @@ public class RemoteEndpointTests : BffTestBase
|
|||
.WithAccessToken();
|
||||
};
|
||||
|
||||
ConfigureBff(setup, configureOpenIdConnect: options =>
|
||||
await ConfigureBff(setup, configureOpenIdConnect: options =>
|
||||
{
|
||||
options.Events.OnUserInformationReceived = context =>
|
||||
{
|
||||
|
|
@ -130,7 +127,6 @@ public class RemoteEndpointTests : BffTestBase
|
|||
};
|
||||
The.DefaultOpenIdConnectConfiguration(options);
|
||||
});
|
||||
await InitializeAsync();
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
var (response, apiResult) = await Bff.BrowserClient.CallBffHostApi(
|
||||
|
|
@ -167,7 +163,7 @@ public class RemoteEndpointTests : BffTestBase
|
|||
.WithAccessToken();
|
||||
};
|
||||
|
||||
ConfigureBff(setup, configureOpenIdConnect: options =>
|
||||
await ConfigureBff(setup, configureOpenIdConnect: options =>
|
||||
{
|
||||
options.Events.OnUserInformationReceived = context =>
|
||||
{
|
||||
|
|
@ -186,7 +182,6 @@ public class RemoteEndpointTests : BffTestBase
|
|||
};
|
||||
The.DefaultOpenIdConnectConfiguration(options);
|
||||
});
|
||||
await InitializeAsync();
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
var (response, apiResult) = await Bff.BrowserClient.CallBffHostApi(
|
||||
|
|
@ -203,9 +198,8 @@ public class RemoteEndpointTests : BffTestBase
|
|||
app.MapRemoteBffApiEndpoint(The.Path, Api.Url(The.Path))
|
||||
.WithAccessToken();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await InitializeAsync();
|
||||
await Bff.BrowserClient.Login();
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url(The.Path),
|
||||
|
|
@ -227,9 +221,9 @@ public class RemoteEndpointTests : BffTestBase
|
|||
app.MapRemoteBffApiEndpoint(The.Path, Api.Url(The.Path))
|
||||
.WithAccessToken();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url(The.Path),
|
||||
|
|
@ -251,9 +245,8 @@ public class RemoteEndpointTests : BffTestBase
|
|||
app.MapRemoteBffApiEndpoint(The.Path, Api.Url(The.Path))
|
||||
.WithAccessToken(RequiredTokenType.UserOrNone);
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
{
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url(The.Path)
|
||||
|
|
@ -288,9 +281,9 @@ public class RemoteEndpointTests : BffTestBase
|
|||
app.MapRemoteBffApiEndpoint(The.Path, Api.Url(The.Path))
|
||||
.WithAccessToken(RequiredTokenType.Client);
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url(The.Path)
|
||||
);
|
||||
|
|
@ -317,9 +310,9 @@ public class RemoteEndpointTests : BffTestBase
|
|||
.WithAccessTokenRetriever<FailureAccessTokenRetriever>()
|
||||
.WithAccessToken(RequiredTokenType.UserOrClient);
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url(The.Path),
|
||||
expectedStatusCode: HttpStatusCode.Unauthorized
|
||||
|
|
@ -339,8 +332,8 @@ public class RemoteEndpointTests : BffTestBase
|
|||
app.MapRemoteBffApiEndpoint(The.Path, Api.Url(The.Path))
|
||||
.WithAccessToken(RequiredTokenType.None);
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url(The.Path),
|
||||
|
|
@ -357,8 +350,8 @@ public class RemoteEndpointTests : BffTestBase
|
|||
app.MapRemoteBffApiEndpoint(The.Path, Api.Url(The.Path))
|
||||
.WithAccessToken(RequiredTokenType.None);
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url(The.Path),
|
||||
|
|
@ -382,8 +375,8 @@ public class RemoteEndpointTests : BffTestBase
|
|||
.WithAccessTokenRetriever<TestAccessTokenRetriever>()
|
||||
.WithAccessToken();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url(The.Path)
|
||||
|
|
@ -428,9 +421,8 @@ public class RemoteEndpointTests : BffTestBase
|
|||
app.MapRemoteBffApiEndpoint(The.Path, Api.Url(The.Path))
|
||||
.WithAccessToken(RequiredTokenType.UserOrClient);
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
{
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url(The.Path)
|
||||
|
|
@ -465,9 +457,8 @@ public class RemoteEndpointTests : BffTestBase
|
|||
app.MapRemoteBffApiEndpoint(The.Path, Api.Url(The.Path))
|
||||
.WithAccessToken(RequiredTokenType.None);
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
{
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
url: Bff.Url(The.Path)
|
||||
|
|
@ -506,9 +497,8 @@ public class RemoteEndpointTests : BffTestBase
|
|||
app.MapRemoteBffApiEndpoint("/none", Api.Url())
|
||||
.WithAccessToken(RequiredTokenType.None);
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
foreach (var client in IdentityServer.Clients)
|
||||
{
|
||||
client.Enabled = false;
|
||||
|
|
@ -537,9 +527,8 @@ public class RemoteEndpointTests : BffTestBase
|
|||
app.MapRemoteBffApiEndpoint(The.Path, Api.Url(The.Path))
|
||||
.WithAccessToken(RequiredTokenType.None);
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
Api.ApiStatusCodeToReturn = HttpStatusCode.Unauthorized;
|
||||
|
|
@ -558,9 +547,8 @@ public class RemoteEndpointTests : BffTestBase
|
|||
app.MapRemoteBffApiEndpoint(The.Path, Api.Url(The.Path))
|
||||
.WithAccessToken(RequiredTokenType.None);
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
Api.ApiStatusCodeToReturn = HttpStatusCode.Forbidden;
|
||||
|
|
@ -579,15 +567,13 @@ public class RemoteEndpointTests : BffTestBase
|
|||
app.MapRemoteBffApiEndpoint(The.Path, Api.Url(The.Path))
|
||||
.WithAccessToken(RequiredTokenType.None);
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, Bff.Url(The.Path));
|
||||
var response = await Bff.BrowserClient.SendAsync(req);
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized,
|
||||
"The endpoint requires CSRF protection, so it should return 403 Forbidden when no CSRF header is present.");
|
||||
|
||||
}
|
||||
|
||||
[Theory, MemberData(nameof(AllSetups))]
|
||||
|
|
@ -599,15 +585,13 @@ public class RemoteEndpointTests : BffTestBase
|
|||
.WithAccessToken(RequiredTokenType.UserOrClient)
|
||||
.SkipAntiforgery();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, Bff.Url(The.Path));
|
||||
var response = await Bff.BrowserClient.SendAsync(req);
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
}
|
||||
|
||||
[Theory, MemberData(nameof(AllSetups))]
|
||||
|
|
@ -623,18 +607,19 @@ public class RemoteEndpointTests : BffTestBase
|
|||
c.AddRequestTransform(async context =>
|
||||
{
|
||||
// One of our customers asked how to access the catch-all route value in subsequent transforms
|
||||
if (context.HttpContext.Request.RouteValues.TryGetValue("catch-all", out var value) && value is string s)
|
||||
if (context.HttpContext.Request.RouteValues.TryGetValue("catch-all", out var value) &&
|
||||
value is string s)
|
||||
{
|
||||
context.ProxyRequest.Headers.Add("catch-all", "/" + s);
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
})
|
||||
.WithAccessToken();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
ApiCallDetails result = await Bff.BrowserClient.CallBffHostApi(
|
||||
|
|
@ -665,6 +650,7 @@ public class RemoteEndpointTests : BffTestBase
|
|||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
await n();
|
||||
});
|
||||
};
|
||||
|
|
@ -686,8 +672,8 @@ public class RemoteEndpointTests : BffTestBase
|
|||
.WithAccessToken();
|
||||
};
|
||||
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
// first, ensure that the 'normal' process works, becuase delay's are turned off.
|
||||
|
|
@ -718,6 +704,7 @@ public class RemoteEndpointTests : BffTestBase
|
|||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
await n();
|
||||
});
|
||||
};
|
||||
|
|
@ -736,8 +723,8 @@ public class RemoteEndpointTests : BffTestBase
|
|||
.WithAccessToken();
|
||||
};
|
||||
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
// first, ensure that the 'normal' process works, becuase delay's are turned off.
|
||||
|
|
|
|||
|
|
@ -15,26 +15,22 @@ namespace Duende.Bff.Tests.Endpoints;
|
|||
public class YarpTests : BffTestBase
|
||||
{
|
||||
public YarpTests(ITestOutputHelper output) : base(output) => Bff.OnConfigureEndpoints += endpoints =>
|
||||
{
|
||||
endpoints.MapReverseProxy(proxyApp =>
|
||||
{
|
||||
proxyApp.UseAntiforgeryCheck();
|
||||
});
|
||||
};
|
||||
{
|
||||
endpoints.MapReverseProxy(proxyApp => { proxyApp.UseAntiforgeryCheck(); });
|
||||
};
|
||||
|
||||
private void ConfigureYarp(RouteConfig routeConfig) =>
|
||||
Bff.OnConfigureBff += bff =>
|
||||
{
|
||||
bff.AddYarpConfig([routeConfig], [Some.ClusterConfig(Api)]);
|
||||
};
|
||||
Bff.OnConfigureBff += bff => { bff.AddYarpConfig([routeConfig], [Some.ClusterConfig(Api)]); };
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllSetups))]
|
||||
public async Task anonymous_call_with_no_csrf_header_to_no_token_requirement_no_csrf_route_should_succeed(BffSetupType setup)
|
||||
public async Task anonymous_call_with_no_csrf_header_to_no_token_requirement_no_csrf_route_should_succeed(
|
||||
BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
ConfigureYarp(Some.RouteConfig());
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
|
||||
|
||||
await Bff.BrowserClient.CallBffHostApi(
|
||||
path: The.PathAndSubPath,
|
||||
|
|
@ -46,9 +42,8 @@ public class YarpTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task anonymous_call_with_no_csrf_header_to_csrf_route_should_fail(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
ConfigureYarp(Some.RouteConfig().WithAntiforgeryCheck());
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, The.PathAndSubPath);
|
||||
var response = await Bff.BrowserClient.SendAsync(req);
|
||||
|
|
@ -60,15 +55,11 @@ public class YarpTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task can_disable_anti_forgery_check(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
|
||||
Bff.SetBffOptions += options =>
|
||||
{
|
||||
options.DisableAntiForgeryCheck = (c) => true;
|
||||
};
|
||||
|
||||
ConfigureYarp(Some.RouteConfig().WithAntiforgeryCheck());
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
|
||||
Bff.BffOptions.DisableAntiForgeryCheck = (c) => true;
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, The.PathAndSubPath);
|
||||
var response = await Bff.BrowserClient.SendAsync(req);
|
||||
|
|
@ -80,9 +71,9 @@ public class YarpTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task anonymous_call_to_no_token_requirement_route_should_succeed(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
ConfigureYarp(Some.RouteConfig());
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.CallBffHostApi(
|
||||
path: The.PathAndSubPath,
|
||||
|
|
@ -94,9 +85,9 @@ public class YarpTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task anonymous_call_to_user_token_requirement_route_should_fail(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
ConfigureYarp(Some.RouteConfig().WithAccessToken(RequiredTokenType.User));
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.CallBffHostApi(
|
||||
path: The.PathAndSubPath,
|
||||
|
|
@ -108,9 +99,9 @@ public class YarpTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task anonymous_call_to_optional_user_token_route_should_succeed(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
ConfigureYarp(Some.RouteConfig().WithAccessToken(RequiredTokenType.UserOrNone));
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
path: The.PathAndSubPath,
|
||||
|
|
@ -127,13 +118,11 @@ public class YarpTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task old_anonymous_call_to_optional_user_token_route_should_succeed(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
ConfigureYarp(Some.RouteConfig().WithOptionalUserAccessToken());
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
path: The.PathAndSubPath,
|
||||
|
|
@ -150,9 +139,9 @@ public class YarpTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task authenticated_GET_should_forward_user_to_api_for_user(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
ConfigureYarp(Some.RouteConfig().WithAccessToken(RequiredTokenType.User));
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
|
|
@ -169,12 +158,12 @@ public class YarpTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task authenticated_PUT_should_forward_user_to_api_for_UserOrNone(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
|
||||
ConfigureYarp(Some.RouteConfig()
|
||||
.WithAccessToken(RequiredTokenType.UserOrNone)
|
||||
);
|
||||
await InitializeAsync();
|
||||
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
|
|
@ -192,9 +181,9 @@ public class YarpTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task authenticated_Post_should_forward_user_to_api_for_User(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
ConfigureYarp(Some.RouteConfig().WithAccessToken(RequiredTokenType.User));
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
|
|
@ -212,9 +201,9 @@ public class YarpTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task authenticated_Post_should_forward_user_to_api_for_UserOrNone(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
ConfigureYarp(Some.RouteConfig().WithAccessToken(RequiredTokenType.UserOrNone));
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
|
|
@ -232,9 +221,9 @@ public class YarpTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task call_to_client_token_route_should_forward_client_token_to_api(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
ConfigureYarp(Some.RouteConfig().WithAccessToken(RequiredTokenType.Client));
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
|
|
@ -251,9 +240,9 @@ public class YarpTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task call_to_user_or_client_token_route_should_forward_user_or_client_token_to_api(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
ConfigureYarp(Some.RouteConfig().WithAccessToken(RequiredTokenType.UserOrClient));
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
ApiCallDetails apiResult = await Bff.BrowserClient.CallBffHostApi(
|
||||
|
|
@ -301,10 +290,10 @@ public class YarpTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task response_status_401_from_remote_endpoint_should_return_401_from_bff(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
ConfigureYarp(Some.RouteConfig().WithAccessToken(RequiredTokenType.User));
|
||||
|
||||
await ConfigureBff(setup);
|
||||
Api.ApiStatusCodeToReturn = HttpStatusCode.Unauthorized;
|
||||
await InitializeAsync();
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
await Bff.BrowserClient.CallBffHostApi(
|
||||
|
|
@ -317,10 +306,10 @@ public class YarpTests : BffTestBase
|
|||
[MemberData(nameof(AllSetups))]
|
||||
public async Task response_status_403_from_remote_endpoint_should_return_403_from_bff(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
ConfigureYarp(Some.RouteConfig().WithAccessToken(RequiredTokenType.User));
|
||||
|
||||
await ConfigureBff(setup);
|
||||
Api.ApiStatusCodeToReturn = HttpStatusCode.Forbidden;
|
||||
await InitializeAsync();
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
await Bff.BrowserClient.CallBffHostApi(
|
||||
|
|
|
|||
|
|
@ -169,7 +169,8 @@ public class UserSessionStoreTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteUserSessionAsync_for_invalid_key_should_succeed() => await _subject.DeleteUserSessionAsync("invalid");
|
||||
public async Task DeleteUserSessionAsync_for_invalid_key_should_succeed() =>
|
||||
await _subject.DeleteUserSessionAsync("invalid");
|
||||
|
||||
[Fact]
|
||||
public async Task GetUserSessionsAsync_for_valid_sub_should_succeed()
|
||||
|
|
@ -421,7 +422,8 @@ public class UserSessionStoreTests
|
|||
SessionId = "sid3_1",
|
||||
});
|
||||
|
||||
var items = await _subject.GetUserSessionsAsync(new UserSessionsFilter { SubjectId = "sub2", SessionId = "sid2_2" });
|
||||
var items = await _subject.GetUserSessionsAsync(new UserSessionsFilter
|
||||
{ SubjectId = "sub2", SessionId = "sid2_2" });
|
||||
items.Count().ShouldBe(1);
|
||||
items.Select(x => x.SubjectId).ToArray().ShouldBeEquivalentTo(new[] { "sub2" });
|
||||
items.Select(x => x.SessionId).ToArray().ShouldBeEquivalentTo(new[] { "sid2_2" });
|
||||
|
|
@ -474,15 +476,18 @@ public class UserSessionStoreTests
|
|||
});
|
||||
|
||||
{
|
||||
var items = await _subject.GetUserSessionsAsync(new UserSessionsFilter { SubjectId = "invalid", SessionId = "invalid" });
|
||||
var items = await _subject.GetUserSessionsAsync(new UserSessionsFilter
|
||||
{ SubjectId = "invalid", SessionId = "invalid" });
|
||||
items.Count().ShouldBe(0);
|
||||
}
|
||||
{
|
||||
var items = await _subject.GetUserSessionsAsync(new UserSessionsFilter { SubjectId = "sub1", SessionId = "invalid" });
|
||||
var items = await _subject.GetUserSessionsAsync(new UserSessionsFilter
|
||||
{ SubjectId = "sub1", SessionId = "invalid" });
|
||||
items.Count().ShouldBe(0);
|
||||
}
|
||||
{
|
||||
var items = await _subject.GetUserSessionsAsync(new UserSessionsFilter { SubjectId = "invalid", SessionId = "sid1_1" });
|
||||
var items = await _subject.GetUserSessionsAsync(new UserSessionsFilter
|
||||
{ SubjectId = "invalid", SessionId = "sid1_1" });
|
||||
items.Count().ShouldBe(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -795,15 +800,18 @@ public class UserSessionStoreTests
|
|||
});
|
||||
|
||||
{
|
||||
await _subject.DeleteUserSessionsAsync(new UserSessionsFilter { SubjectId = "invalid", SessionId = "invalid" });
|
||||
await _subject.DeleteUserSessionsAsync(new UserSessionsFilter
|
||||
{ SubjectId = "invalid", SessionId = "invalid" });
|
||||
_database.UserSessions.Count().ShouldBe(6);
|
||||
}
|
||||
{
|
||||
await _subject.DeleteUserSessionsAsync(new UserSessionsFilter { SubjectId = "sub1", SessionId = "invalid" });
|
||||
await _subject.DeleteUserSessionsAsync(new UserSessionsFilter
|
||||
{ SubjectId = "sub1", SessionId = "invalid" });
|
||||
_database.UserSessions.Count().ShouldBe(6);
|
||||
}
|
||||
{
|
||||
await _subject.DeleteUserSessionsAsync(new UserSessionsFilter { SubjectId = "invalid", SessionId = "sid1_1" });
|
||||
await _subject.DeleteUserSessionsAsync(new UserSessionsFilter
|
||||
{ SubjectId = "invalid", SessionId = "sid1_1" });
|
||||
_database.UserSessions.Count().ShouldBe(6);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@ public class GeneralTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
endpoints.Map(The.Path, c => ApiHost.ReturnApiCallDetails(c))
|
||||
.RequireAuthorization();
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, Bff.Url(The.Path));
|
||||
req.Headers.Add("x-csrf", "1");
|
||||
|
|
@ -45,10 +44,8 @@ public class GeneralTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
{
|
||||
endpoints.MapRemoteBffApiEndpoint(The.Path, Api.Url())
|
||||
.WithAccessToken(RequiredTokenType.None);
|
||||
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, Bff.Url(The.Path));
|
||||
req.Headers.Add("x-csrf", "1");
|
||||
|
|
@ -71,10 +68,8 @@ public class GeneralTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
{
|
||||
endpoints.MapRemoteBffApiEndpoint(The.Path, Api.Url())
|
||||
.WithAccessToken(RequiredTokenType.None);
|
||||
|
||||
};
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, Bff.Url(The.Path));
|
||||
req.Headers.Add("x-csrf", "1");
|
||||
|
|
@ -94,8 +89,7 @@ public class GeneralTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
[Theory, MemberData(nameof(AllSetups))]
|
||||
public async Task Will_auto_register_login_endpoints(BffSetupType setup)
|
||||
{
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
Context.LogMessages.ToString().ShouldNotContain("Already mapped Login endpoint");
|
||||
|
||||
|
|
@ -107,14 +101,11 @@ public class GeneralTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
public async Task If_management_endpoints_are_mapped_manually_then_warning_is_written(BffSetupType setup)
|
||||
{
|
||||
Bff.OnConfigureEndpoints += endpoints => endpoints.MapBffManagementEndpoints();
|
||||
ConfigureBff(setup);
|
||||
await InitializeAsync();
|
||||
await ConfigureBff(setup);
|
||||
|
||||
// And we can log in, which means the login endpoint was registered
|
||||
await Bff.BrowserClient.Login();
|
||||
|
||||
Context.LogMessages.ToString().ShouldContain("Already mapped Login endpoint");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,19 +22,20 @@ public class FrontendCollectionTests
|
|||
[Fact]
|
||||
public void Can_load_frontends_from_constructor()
|
||||
{
|
||||
_frontendsConfiguredDuringStartup = [
|
||||
_frontendsConfiguredDuringStartup =
|
||||
[
|
||||
Some.BffFrontendWithSelectionCriteria(),
|
||||
Some.BffFrontendWithSelectionCriteria() with { Name = BffFrontendName.Parse("different") }
|
||||
];
|
||||
|
||||
var cache = BuildCache();
|
||||
var cache = BuildFrontendCollection();
|
||||
|
||||
var result = cache.GetAll();
|
||||
var result = cache;
|
||||
result.Count.ShouldBe(2);
|
||||
result.ShouldBeEquivalentTo(_frontendsConfiguredDuringStartup.AsReadOnly());
|
||||
result.ToArray().ShouldBeEquivalentTo(_frontendsConfiguredDuringStartup);
|
||||
}
|
||||
|
||||
private FrontendCollection BuildCache()
|
||||
private FrontendCollection BuildFrontendCollection()
|
||||
{
|
||||
// No longer inject OptionsCache
|
||||
var cache = new FrontendCollection(_bffConfigurationOptionsMonitor, [], _frontendsConfiguredDuringStartup);
|
||||
|
|
@ -53,8 +54,8 @@ public class FrontendCollectionTests
|
|||
}
|
||||
};
|
||||
|
||||
var cache = BuildCache();
|
||||
var result = cache.GetAll();
|
||||
var cache = BuildFrontendCollection();
|
||||
var result = cache;
|
||||
result.Count.ShouldBe(2);
|
||||
|
||||
result.ShouldBe(new[]
|
||||
|
|
@ -93,14 +94,14 @@ public class FrontendCollectionTests
|
|||
}
|
||||
};
|
||||
|
||||
var cache = BuildCache();
|
||||
var result = cache.GetAll();
|
||||
var cache = BuildFrontendCollection();
|
||||
var openIdConnectOptions = new OpenIdConnectOptions();
|
||||
result.First().ConfigureOpenIdConnectOptions!.Invoke(openIdConnectOptions);
|
||||
cache.First().ConfigureOpenIdConnectOptions!.Invoke(openIdConnectOptions);
|
||||
openIdConnectOptions.ClientId.ShouldBe("clientid from programmatic defaults");
|
||||
openIdConnectOptions.ClientSecret.ShouldBe("clientsecret from configured defaults");
|
||||
openIdConnectOptions.ResponseMode.ShouldBe("responsemode from frontend");
|
||||
}
|
||||
|
||||
[Fact(Skip = "find other way of testing this")]
|
||||
public void Cookie_Config_precedence_is_programmatic_defaults_then_configured_defaults_then_frontend_specific()
|
||||
{
|
||||
|
|
@ -130,10 +131,9 @@ public class FrontendCollectionTests
|
|||
}
|
||||
};
|
||||
|
||||
var cache = BuildCache();
|
||||
var result = cache.GetAll();
|
||||
var cache = BuildFrontendCollection();
|
||||
var cookieOptions = new CookieAuthenticationOptions();
|
||||
result.First().ConfigureCookieOptions!.Invoke(cookieOptions);
|
||||
cache.First().ConfigureCookieOptions!.Invoke(cookieOptions);
|
||||
cookieOptions.Cookie.Name.ShouldBe("Name from programmatic defaults");
|
||||
cookieOptions.Cookie.Path.ShouldBe("Path from configured defaults");
|
||||
cookieOptions.Cookie.Domain.ShouldBe("Domain from frontend");
|
||||
|
|
@ -143,13 +143,17 @@ public class FrontendCollectionTests
|
|||
[Fact]
|
||||
public void When_frontend_is_updated_then_event_is_raised()
|
||||
{
|
||||
var cache = BuildCache();
|
||||
var cache = BuildFrontendCollection();
|
||||
var bffFrontend = Some.BffFrontendWithSelectionCriteria();
|
||||
|
||||
// Track event invocations
|
||||
BffFrontend? eventArg = null;
|
||||
var eventCount = 0;
|
||||
cache.OnFrontendChanged += f => { eventArg = f; eventCount++; };
|
||||
cache.OnFrontendChanged += f =>
|
||||
{
|
||||
eventArg = f;
|
||||
eventCount++;
|
||||
};
|
||||
|
||||
// Add a new frontend (should not raise event, as it's an add)
|
||||
cache.AddOrUpdate(bffFrontend);
|
||||
|
|
@ -167,13 +171,17 @@ public class FrontendCollectionTests
|
|||
[Fact]
|
||||
public void When_frontend_is_removed_then_event_is_raised()
|
||||
{
|
||||
var cache = BuildCache();
|
||||
var cache = BuildFrontendCollection();
|
||||
var bffFrontend = Some.BffFrontendWithSelectionCriteria();
|
||||
|
||||
// Track event invocations
|
||||
BffFrontend? eventArg = null;
|
||||
var eventCount = 0;
|
||||
cache.OnFrontendChanged += f => { eventArg = f; eventCount++; };
|
||||
cache.OnFrontendChanged += f =>
|
||||
{
|
||||
eventArg = f;
|
||||
eventCount++;
|
||||
};
|
||||
|
||||
// Add a new frontend (should not raise event)
|
||||
cache.AddOrUpdate(bffFrontend);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ namespace Duende.Bff.Tests.MultiFrontend;
|
|||
|
||||
public class PathMapperTests
|
||||
{
|
||||
private readonly PathMapper _mapper = new();
|
||||
|
||||
[Fact]
|
||||
public void MapPath_WithNoMatchingPaths_DoesNotModifyPathBaseOrPath()
|
||||
|
|
@ -18,7 +17,7 @@ public class PathMapperTests
|
|||
var frontend = CreateFrontendWithPath();
|
||||
|
||||
// Act
|
||||
_mapper.MapPath(context, frontend);
|
||||
PathMapper.MapPath(context, frontend);
|
||||
|
||||
// Assert
|
||||
context.Request.PathBase.Value.ShouldBe("/app");
|
||||
|
|
@ -33,7 +32,7 @@ public class PathMapperTests
|
|||
var frontend = CreateFrontendWithPath("/api");
|
||||
|
||||
// Act
|
||||
_mapper.MapPath(context, frontend);
|
||||
PathMapper.MapPath(context, frontend);
|
||||
|
||||
// Assert
|
||||
context.Request.PathBase.Value.ShouldBe("/app/api");
|
||||
|
|
@ -48,7 +47,7 @@ public class PathMapperTests
|
|||
var frontend = CreateFrontendWithPath("/api");
|
||||
|
||||
// Act
|
||||
_mapper.MapPath(context, frontend);
|
||||
PathMapper.MapPath(context, frontend);
|
||||
|
||||
// Assert
|
||||
context.Request.PathBase.Value.ShouldBe("/app/api");
|
||||
|
|
@ -63,7 +62,7 @@ public class PathMapperTests
|
|||
var frontend = CreateFrontendWithPath("/api");
|
||||
|
||||
// Act
|
||||
_mapper.MapPath(context, frontend);
|
||||
PathMapper.MapPath(context, frontend);
|
||||
|
||||
// Assert
|
||||
context.Response.StatusCode.ShouldBe(404);
|
||||
|
|
@ -77,7 +76,7 @@ public class PathMapperTests
|
|||
var frontend = CreateFrontendWithPath("/api");
|
||||
|
||||
// Act
|
||||
_mapper.MapPath(context, frontend);
|
||||
PathMapper.MapPath(context, frontend);
|
||||
|
||||
// Assert
|
||||
context.Request.PathBase.Value.ShouldBe("/api");
|
||||
|
|
@ -92,7 +91,7 @@ public class PathMapperTests
|
|||
var frontend = CreateFrontendWithPath("/api");
|
||||
|
||||
// Act
|
||||
_mapper.MapPath(context, frontend);
|
||||
PathMapper.MapPath(context, frontend);
|
||||
|
||||
// Assert
|
||||
context.Request.PathBase.Value.ShouldBe("/app/api");
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue