Add test for callback session persistence; extend authentication broker for refresh token hash initialization; enable config connection overrides
This commit is contained in:
parent
22bc531aa8
commit
1ccf5b7611
|
|
@ -59,6 +59,7 @@
|
|||
"EncryptionKey": "Q0hBTkdFX01FX1RPX0FfU0VDVVJFX1JBTkRPTV9LRVk="
|
||||
},
|
||||
"ExternalAuthentication": {
|
||||
"AllowConfigurationConnectionOverrides": true,
|
||||
"LocalLogin": {
|
||||
"IsEnabled": true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -214,7 +214,8 @@ public sealed class ExternalAuthenticationBroker(
|
|||
StartedAt = clock.UtcNow,
|
||||
LastRefreshedAt = clock.UtcNow,
|
||||
ExpiresAt = clock.UtcNow.Add(options.Value.Lifetimes.MaximumSessionAge),
|
||||
RefreshExpiresAt = clock.UtcNow.Add(options.Value.Lifetimes.MaximumSessionAge)
|
||||
RefreshExpiresAt = clock.UtcNow.Add(options.Value.Lifetimes.MaximumSessionAge),
|
||||
CurrentRefreshTokenHash = CreateUnissuedRefreshTokenHash()
|
||||
};
|
||||
await sessionStore.SaveAsync(session, cancellationToken);
|
||||
var code = CreateOpaqueValue();
|
||||
|
|
@ -511,6 +512,13 @@ public sealed class ExternalAuthenticationBroker(
|
|||
}
|
||||
private static bool VerifyPkce(string challenge, string? verifier) => !string.IsNullOrWhiteSpace(verifier) && string.Equals(challenge, Base64Url(SHA256.HashData(Encoding.ASCII.GetBytes(verifier))), StringComparison.Ordinal);
|
||||
private static string CreateOpaqueValue() => Base64Url(RandomNumberGenerator.GetBytes(32));
|
||||
|
||||
/// <summary>
|
||||
/// Sessions are persisted at callback completion, before the token issuer mints the first refresh token. The column is
|
||||
/// required and uniquely indexed, so a per-session placeholder is stored until issuance rotates the real hash in. The
|
||||
/// prefix keeps the value outside the hex-encoded hash space, so it can never be matched by a refresh-token lookup.
|
||||
/// </summary>
|
||||
private static string CreateUnissuedRefreshTokenHash() => $"unissued:{CreateOpaqueValue()}";
|
||||
private string Hash(string value) => handleHasher.Hash(value);
|
||||
private static string Base64Url(byte[] value) => Convert.ToBase64String(value).TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
||||
private static Uri AppendCallbackParameters(Uri uri, string code, string? clientState)
|
||||
|
|
|
|||
|
|
@ -241,6 +241,31 @@ public sealed class ExternalAuthenticationPersistenceTests : IAsyncLifetime
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CallbackCompletionPersistsTheSessionBeforeAnyRefreshTokenIsIssued()
|
||||
{
|
||||
var identityResolver = Substitute.For<IExternalIdentityResolver>();
|
||||
identityResolver.ResolveAsync(Arg.Any<ExternalIdentityResolutionContext>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ValueTask.FromResult(new ExternalIdentityResolution("user-a", false)));
|
||||
var permissionGrantResolver = Substitute.For<IPermissionGrantResolver>();
|
||||
permissionGrantResolver.ResolveAsync(Arg.Any<PermissionGrantResolutionContext>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ValueTask.FromResult(new PermissionGrantResult([], [])));
|
||||
var adapter = new Broker.BrokerSecurityTests.RecordingAdapter
|
||||
{
|
||||
AuthenticationResult = new ExternalAuthenticationResult(new ExternalIdentity("https://issuer.example", "subject-a", EmptyClaims), EmptyClaims, [])
|
||||
};
|
||||
var broker = Broker.BrokerSecurityTests.CreateBroker(adapter, identityResolver: identityResolver, permissionGrantResolver: permissionGrantResolver, sessionStore: new EFCoreExternalAuthenticationSessionStore(new ExternalAuthenticationDbContextFactory(_services.GetRequiredService<IServiceScopeFactory>()), _clock));
|
||||
await broker.InitiateExternalAsync(new BrokerAuthorizationRequest("studio", new Uri("https://studio.example/authentication/external/callback"), "code", "challenge", "S256", "/workflows", "contoso"), "tenant-a");
|
||||
|
||||
var result = await broker.CompleteCallbackAsync("contoso", adapter.CorrelationState!, new Dictionary<string, IReadOnlyCollection<string>> { ["state"] = [adapter.CorrelationState!] });
|
||||
|
||||
Assert.Null(result.Error);
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
var session = Assert.Single(await dbContext.ExternalAuthenticationSessions.ToListAsync());
|
||||
Assert.False(string.IsNullOrEmpty(session.CurrentRefreshTokenHash));
|
||||
Assert.Equal("user-a", session.UserId);
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, IReadOnlyCollection<string>> EmptyClaims { get; } = new Dictionary<string, IReadOnlyCollection<string>>();
|
||||
|
||||
private static IdentityProviderConnection CreateConnection(string id = "connection-a") => new()
|
||||
|
|
|
|||
Loading…
Reference in a new issue