Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ public class AccessTokenHandlerTests

AccessTokenHandlerSubject _subject;

public AccessTokenHandlerTests()
public AccessTokenHandlerTests(ITestOutputHelper output)
{
_subject = new AccessTokenHandlerSubject(_testDPoPProofService, new TestDPoPNonceStore(), new TestLoggerProvider().CreateLogger("AccessTokenHandlerSubject"));
_subject = new AccessTokenHandlerSubject(_testDPoPProofService, new TestDPoPNonceStore(), new TestLoggerProvider(output.WriteLine, "AccessTokenHandler").CreateLogger("AccessTokenHandlerSubject"));
_subject.InnerHandler = _testHttpMessageHandler;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,26 @@

namespace Duende.AccessTokenManagement.Tests;

public class ClientTokenManagementApiTests : IntegrationTestBase
public class ClientTokenManagementApiTests(ITestOutputHelper output) : IntegrationTestBase(output), IAsyncLifetime
{
private static readonly string _jwkJson;
private static readonly string _jwkJson = CreateJWKJson();

private HttpClient _client;
private IClientCredentialsTokenManagementService _tokenService;
private IHttpClientFactory _clientFactory;
private ClientCredentialsClient _clientOptions;
private IClientCredentialsTokenManagementService _tokenService = null!;
private IHttpClientFactory _clientFactory = null!;
private ClientCredentialsClient _clientOptions = null!;

static ClientTokenManagementApiTests()
private static string CreateJWKJson()
{
var key = CryptoHelper.CreateRsaSecurityKey();
var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(key);
jwk.Alg = "RS256";
_jwkJson = JsonSerializer.Serialize(jwk);
var jwkJson = JsonSerializer.Serialize(jwk);
return jwkJson;
}

public ClientTokenManagementApiTests()
public override async ValueTask InitializeAsync()
{
await base.InitializeAsync();
var services = new ServiceCollection();

services.AddDistributedMemoryCache();
Expand All @@ -50,11 +51,11 @@ public ClientTokenManagementApiTests()
});

var provider = services.BuildServiceProvider();
_client = provider.GetRequiredService<IHttpClientFactory>().CreateClient("test");
_tokenService = provider.GetRequiredService<IClientCredentialsTokenManagementService>();
_clientFactory = provider.GetRequiredService<IHttpClientFactory>();
_clientOptions = provider.GetRequiredService<IOptionsMonitor<ClientCredentialsClient>>().Get("test");
}

public class ApiHandler : DelegatingHandler
{
private HttpMessageHandler? _innerHandler;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ public class ApiHost : GenericHost
private readonly IdentityServerHost _identityServerHost;
public event Action<Microsoft.AspNetCore.Http.HttpContext> ApiInvoked = ctx => { };

public ApiHost(IdentityServerHost identityServerHost, string scope, string baseAddress = "https://api", string resource = "urn:api")
: base(baseAddress)
public ApiHost(
WriteTestOutput writeTestOutput,
IdentityServerHost identityServerHost,
string scope,
string baseAddress = "https://api",
string resource = "urn:api")
: base(writeTestOutput, baseAddress)
{
_identityServerHost = identityServerHost;
_identityServerHost.ApiScopes.Add(new ApiScope(scope));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,65 @@
using Duende.AccessTokenManagement.OpenIdConnect;
using RichardSzalay.MockHttp;
using System.Net.Http.Json;
using System.Security.Claims;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;

namespace Duende.AccessTokenManagement.Tests;

public class EagerTokenRefresher(
IStoreTokensInAuthenticationProperties tokensInProps,
IOptions<UserTokenManagementOptions> options,
IUserTokenRequestSynchronization sync,
IUserTokenEndpointService tokenEndpointService,
IUserTokenStore userAccessTokenStore,

Check warning on line 25 in access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs

View workflow job for this annotation

GitHub Actions / Build

Parameter 'userAccessTokenStore' is unread.

Check warning on line 25 in access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs

View workflow job for this annotation

GitHub Actions / Build

Parameter 'userAccessTokenStore' is unread.

Check warning on line 25 in access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs

View workflow job for this annotation

GitHub Actions / Build

Parameter 'userAccessTokenStore' is unread.

Check warning on line 25 in access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs

View workflow job for this annotation

GitHub Actions / Build

Parameter 'userAccessTokenStore' is unread.
TimeProvider clock,
ILogger<UserAccessAccessTokenManagementService> logger)

{
public async Task RefreshTokenIfNeeded(ClaimsPrincipal? user, AuthenticationProperties contextProperties,
CancellationToken cancellationToken)
{
var userToken = tokensInProps.GetUserToken(contextProperties);
var dtRefresh = userToken.Expiration.Subtract(options.Value.RefreshBeforeExpiration);
var utcNow = clock.GetUtcNow();

if (userToken.AccessToken == null || userToken.RefreshToken == null)
return;

var parameters = new UserTokenRequestParameters();

if (dtRefresh < utcNow)
{
await sync.SynchronizeAsync(userToken.RefreshToken!, async () =>
{
try
{
var refreshedToken =
await tokenEndpointService.RefreshAccessTokenAsync(userToken, parameters, cancellationToken).ConfigureAwait(false);
if (refreshedToken.IsError)
{
logger.LogError("Error refreshing access token. Error = {error}", refreshedToken.Error);
}
else
{
tokensInProps.SetUserToken(refreshedToken, contextProperties, parameters);
}

}
catch (Exception ex)
{

Console.WriteLine("Exception: " + ex.ToString());
}
return null;

Check warning on line 65 in access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference return.

Check warning on line 65 in access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference return.

Check warning on line 65 in access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference return.

Check warning on line 65 in access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference return.
}).ConfigureAwait(false);
}

}

}

public class AppHost : GenericHost
{
public string ClientId;
Expand All @@ -22,13 +78,16 @@
private readonly ApiHost _apiHost;
private readonly Action<UserTokenManagementOptions>? _configureUserTokenManagementOptions;

public bool AutoRefreshToken { get; set; } = false;

public AppHost(
WriteTestOutput writeTestOutput,
IdentityServerHost identityServerHost,
ApiHost apiHost,
string clientId,
string baseAddress = "https://app",
Action<UserTokenManagementOptions>? configureUserTokenManagementOptions = default)
: base(baseAddress)
: base(writeTestOutput, baseAddress)
{
_identityServerHost = identityServerHost;
_apiHost = apiHost;
Expand All @@ -44,11 +103,22 @@
{
services.AddRouting();
services.AddAuthorization();
services.AddTransient<EagerTokenRefresher>();

services.AddAuthentication("cookie")
.AddCookie("cookie", options =>
{
options.Cookie.Name = "bff";

options.Events.OnValidatePrincipal += async context =>
{
if (AutoRefreshToken)
{
var refresher = context.HttpContext.RequestServices.GetRequiredService<EagerTokenRefresher>();

await refresher.RefreshTokenIfNeeded(context.Principal, context.Properties, context.HttpContext.RequestAborted);
}
};
});

services.AddAuthentication(options =>
Expand Down Expand Up @@ -93,11 +163,11 @@
IdentityServerHttpHandler.When("/.well-known/*")
.Respond(identityServerHandler);

options.BackchannelHttpHandler = IdentityServerHttpHandler;
options.BackchannelHttpHandler = new LoggingHttpHandler(IdentityServerHttpHandler);
}
else
{
options.BackchannelHttpHandler = identityServerHandler;
options.BackchannelHttpHandler = new LoggingHttpHandler(identityServerHandler);
}

options.ProtocolValidator.RequireNonce = false;
Expand All @@ -117,7 +187,7 @@
services.AddUserAccessTokenHttpClient("callApi", configureClient: client => {
client.BaseAddress = new Uri(_apiHost.Url());
})
.ConfigurePrimaryHttpMessageHandler(() => _apiHost.HttpMessageHandler);
.ConfigurePrimaryHttpMessageHandler(() => new LoggingHttpHandler(_apiHost.HttpMessageHandler));
}

private void Configure(IApplicationBuilder app)
Expand Down Expand Up @@ -221,4 +291,25 @@
response = await BrowserClient.GetAsync(Url(response.Headers.Location!.ToString()));
return response;
}
}

public class LoggingHttpHandler(HttpMessageHandler inner) : DelegatingHandler(inner)
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
try
{
Console.WriteLine("--> " + request.RequestUri!.ToString());

var response = await base.SendAsync(request, cancellationToken);

Console.WriteLine("<-- " + response.RequestMessage!.RequestUri!.ToString() + " - " + response.StatusCode);
return response;
}
catch (Exception ex)
{
Console.WriteLine("Exception: " + ex.ToString());
throw;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,13 @@

namespace Duende.AccessTokenManagement.Tests;

public class GenericHost
public class GenericHost(WriteTestOutput writeOutput, string baseAddress = "https://server") : IAsyncDisposable
{
public GenericHost(string baseAddress = "https://server")
{
if (baseAddress.EndsWith("/")) baseAddress = baseAddress.Substring(0, baseAddress.Length - 1);
_baseAddress = baseAddress;
}

protected readonly string _baseAddress;
protected readonly string BaseAddress = baseAddress.EndsWith("/")
? baseAddress.Substring(0, baseAddress.Length - 1)
: baseAddress;

IServiceProvider _appServices = default!;

public Assembly HostAssembly { get; set; } = default!;
Expand All @@ -33,7 +31,8 @@ public GenericHost(string baseAddress = "https://server")
public HttpClient HttpClient { get; set; } = default!;
public HttpMessageHandler HttpMessageHandler { get; set; } = default!;

public TestLoggerProvider Logger { get; set; } = new TestLoggerProvider();
private TestLoggerProvider Logger { get; } = new(writeOutput, baseAddress + " - ");



public T Resolve<T>()
Expand All @@ -47,11 +46,13 @@ public string Url(string? path = null)
{
path = path ?? String.Empty;
if (!path.StartsWith("/")) path = "/" + path;
return _baseAddress + path;
return BaseAddress + path;
}

public async Task InitializeAsync()
{
if (Server != null) throw new InvalidOperationException("Already initialized");

var hostBuilder = new HostBuilder()
.ConfigureWebHost(builder =>
{
Expand Down Expand Up @@ -178,4 +179,23 @@ public Task IssueSessionCookieAsync(string sub, params Claim[] claims)
{
return IssueSessionCookieAsync(claims.Append(new Claim("sub", sub)).ToArray());
}

public async ValueTask DisposeAsync()
{
await CastAndDispose(Server);
await CastAndDispose(BrowserClient);
await CastAndDispose(HttpClient);
await CastAndDispose(HttpMessageHandler);
await CastAndDispose(Logger);

return;

static async ValueTask CastAndDispose(IDisposable resource)
{
if (resource is IAsyncDisposable resourceAsyncDisposable)
await resourceAsyncDisposable.DisposeAsync();
else
resource?.Dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ namespace Duende.AccessTokenManagement.Tests;

public class IdentityServerHost : GenericHost
{
public IdentityServerHost(string baseAddress = "https://identityserver")
: base(baseAddress)
public IdentityServerHost(WriteTestOutput writeTestOutput, string baseAddress = "https://identityserver")
: base(writeTestOutput, baseAddress)
{
OnConfigureServices += ConfigureServices;
OnConfigure += Configure;
Expand Down Expand Up @@ -108,7 +108,7 @@ public string CreateIdToken(string sub, string clientId)
{
var descriptor = new SecurityTokenDescriptor
{
Issuer = _baseAddress,
Issuer = BaseAddress,
Audience = clientId,
Claims = new Dictionary<string, object>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@

namespace Duende.AccessTokenManagement.Tests;

public class IntegrationTestBase
public class IntegrationTestBase : IAsyncDisposable
{
protected readonly IdentityServerHost IdentityServerHost;
protected ApiHost ApiHost;
protected AppHost AppHost;

public IntegrationTestBase(string clientId = "web", Action<UserTokenManagementOptions>? configureUserTokenManagementOptions = null)
public IntegrationTestBase(ITestOutputHelper output, string clientId = "web", Action<UserTokenManagementOptions>? configureUserTokenManagementOptions = null)
{
IdentityServerHost = new IdentityServerHost();
IdentityServerHost = new IdentityServerHost(output.WriteLine);

IdentityServerHost.Clients.Add(new Client
{
Expand Down Expand Up @@ -67,17 +67,27 @@ public IntegrationTestBase(string clientId = "web", Action<UserTokenManagementOp
AccessTokenLifetime = 10
});

IdentityServerHost.InitializeAsync().Wait();
ApiHost = new ApiHost(output.WriteLine, IdentityServerHost, "scope1");

ApiHost = new ApiHost(IdentityServerHost, "scope1");
ApiHost.InitializeAsync().Wait();

AppHost = new AppHost(IdentityServerHost, ApiHost, clientId, configureUserTokenManagementOptions: configureUserTokenManagementOptions);
AppHost.InitializeAsync().Wait();
AppHost = new AppHost(output.WriteLine, IdentityServerHost, ApiHost, clientId, configureUserTokenManagementOptions: configureUserTokenManagementOptions);
}

public async Task Login(string sub)
{
await IdentityServerHost.IssueSessionCookieAsync(new Claim("sub", sub));
}

public virtual async ValueTask DisposeAsync()
{
await IdentityServerHost.DisposeAsync();
await ApiHost.DisposeAsync();
await AppHost.DisposeAsync();
}

public virtual async ValueTask InitializeAsync()
{
await ApiHost.InitializeAsync();
await AppHost.InitializeAsync();
await IdentityServerHost.InitializeAsync();
}
}
Loading