Files
git.stella-ops.org/src/StellaOps.Authority/StellaOps.Authority.Tests/OpenIddict/TokenPersistenceIntegrationTests.cs
Vladimir Moushkov 3083c77a9e
Some checks failed
Build Test Deploy / docs (push) Has been cancelled
Build Test Deploy / deploy (push) Has been cancelled
Build Test Deploy / build-test (push) Has been cancelled
Build Test Deploy / authority-container (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
up
2025-10-10 18:33:10 +03:00

209 lines
8.5 KiB
C#

using System.Collections.Generic;
using System.Diagnostics;
using System.Security.Claims;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using MongoDB.Driver;
using OpenIddict.Abstractions;
using OpenIddict.Server;
using StellaOps.Authority;
using StellaOps.Authority.OpenIddict;
using StellaOps.Authority.OpenIddict.Handlers;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.Storage.Mongo;
using StellaOps.Authority.Storage.Mongo.Documents;
using StellaOps.Authority.Storage.Mongo.Extensions;
using StellaOps.Authority.Storage.Mongo.Initialization;
using StellaOps.Authority.Storage.Mongo.Stores;
using StellaOps.Feedser.Testing;
using Xunit;
namespace StellaOps.Authority.Tests.OpenIddict;
[Collection("mongo-fixture")]
public sealed class TokenPersistenceIntegrationTests
{
private static readonly ActivitySource TestActivitySource = new("StellaOps.Authority.Tests.Persistence");
private readonly MongoIntegrationFixture fixture;
public TokenPersistenceIntegrationTests(MongoIntegrationFixture fixture)
=> this.fixture = fixture ?? throw new ArgumentNullException(nameof(fixture));
[Fact]
public async Task HandleClientCredentials_PersistsTokenInMongo()
{
await ResetCollectionsAsync();
var issuedAt = new DateTimeOffset(2025, 10, 10, 12, 0, 0, TimeSpan.Zero);
var clock = new FakeTimeProvider(issuedAt);
await using var provider = await BuildMongoProviderAsync(clock);
var clientStore = provider.GetRequiredService<IAuthorityClientStore>();
var tokenStore = provider.GetRequiredService<IAuthorityTokenStore>();
var clientDocument = TestHelpers.CreateClient(
secret: "s3cr3t!",
allowedGrantTypes: "client_credentials",
allowedScopes: "jobs:trigger jobs:read");
await clientStore.UpsertAsync(clientDocument, CancellationToken.None);
var registry = TestHelpers.CreateRegistry(
withClientProvisioning: true,
clientDescriptor: TestHelpers.CreateDescriptor(clientDocument));
var validateHandler = new ValidateClientCredentialsHandler(clientStore, registry, TestActivitySource, NullLogger<ValidateClientCredentialsHandler>.Instance);
var handleHandler = new HandleClientCredentialsHandler(registry, tokenStore, clock, TestActivitySource, NullLogger<HandleClientCredentialsHandler>.Instance);
var transaction = TestHelpers.CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:trigger");
transaction.Options.AccessTokenLifetime = TimeSpan.FromMinutes(15);
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await validateHandler.HandleAsync(validateContext);
Assert.False(validateContext.IsRejected);
var handleContext = new OpenIddictServerEvents.HandleTokenRequestContext(transaction);
await handleHandler.HandleAsync(handleContext);
Assert.True(handleContext.IsRequestHandled);
var principal = Assert.IsType<ClaimsPrincipal>(handleContext.Principal);
var tokenId = principal.GetClaim(OpenIddictConstants.Claims.JwtId);
Assert.False(string.IsNullOrWhiteSpace(tokenId));
var stored = await tokenStore.FindByTokenIdAsync(tokenId!, CancellationToken.None);
Assert.NotNull(stored);
Assert.Equal(clientDocument.ClientId, stored!.ClientId);
Assert.Equal(OpenIddictConstants.TokenTypeHints.AccessToken, stored.Type);
Assert.Equal("valid", stored.Status);
Assert.Equal(issuedAt, stored.CreatedAt);
Assert.Equal(issuedAt.AddMinutes(15), stored.ExpiresAt);
Assert.Equal(new[] { "jobs:trigger" }, stored.Scope);
}
[Fact]
public async Task ValidateAccessTokenHandler_RejectsRevokedRefreshTokenPersistedInMongo()
{
await ResetCollectionsAsync();
var now = new DateTimeOffset(2025, 10, 10, 14, 0, 0, TimeSpan.Zero);
var clock = new FakeTimeProvider(now);
await using var provider = await BuildMongoProviderAsync(clock);
var clientStore = provider.GetRequiredService<IAuthorityClientStore>();
var tokenStore = provider.GetRequiredService<IAuthorityTokenStore>();
var clientDocument = TestHelpers.CreateClient(
secret: null,
clientType: "public",
allowedGrantTypes: "password refresh_token",
allowedScopes: "openid profile jobs:read");
await clientStore.UpsertAsync(clientDocument, CancellationToken.None);
var descriptor = TestHelpers.CreateDescriptor(clientDocument);
var userDescriptor = new AuthorityUserDescriptor("subject-1", "alice", displayName: "Alice", requiresPasswordReset: false);
var plugin = TestHelpers.CreatePlugin(
name: clientDocument.Plugin ?? "standard",
supportsClientProvisioning: true,
descriptor,
userDescriptor);
var registry = new AuthorityIdentityProviderRegistry(
new[] { plugin },
NullLogger<AuthorityIdentityProviderRegistry>.Instance);
const string revokedTokenId = "refresh-token-1";
var refreshToken = new AuthorityTokenDocument
{
TokenId = revokedTokenId,
Type = OpenIddictConstants.TokenTypeHints.RefreshToken,
SubjectId = userDescriptor.SubjectId,
ClientId = clientDocument.ClientId,
Scope = new List<string> { "openid", "profile" },
Status = "valid",
CreatedAt = now.AddMinutes(-5),
ExpiresAt = now.AddHours(4),
ReferenceId = "refresh-reference-1"
};
await tokenStore.InsertAsync(refreshToken, CancellationToken.None);
var revokedAt = now.AddMinutes(1);
await tokenStore.UpdateStatusAsync(revokedTokenId, "revoked", revokedAt, CancellationToken.None);
var handler = new ValidateAccessTokenHandler(
tokenStore,
clientStore,
registry,
clock,
TestActivitySource,
NullLogger<ValidateAccessTokenHandler>.Instance);
var transaction = new OpenIddictServerTransaction
{
EndpointType = OpenIddictServerEndpointType.Token,
Options = new OpenIddictServerOptions(),
Request = new OpenIddictRequest
{
GrantType = OpenIddictConstants.GrantTypes.RefreshToken
}
};
var principal = TestHelpers.CreatePrincipal(
clientDocument.ClientId,
revokedTokenId,
plugin.Name,
userDescriptor.SubjectId);
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
{
Principal = principal,
TokenId = revokedTokenId
};
await handler.HandleAsync(context);
Assert.True(context.IsRejected);
Assert.Equal(OpenIddictConstants.Errors.InvalidToken, context.Error);
var stored = await tokenStore.FindByTokenIdAsync(revokedTokenId, CancellationToken.None);
Assert.NotNull(stored);
Assert.Equal("revoked", stored!.Status);
Assert.Equal(revokedAt, stored.RevokedAt);
}
private async Task ResetCollectionsAsync()
{
var tokens = fixture.Database.GetCollection<AuthorityTokenDocument>(AuthorityMongoDefaults.Collections.Tokens);
await tokens.DeleteManyAsync(Builders<AuthorityTokenDocument>.Filter.Empty);
var clients = fixture.Database.GetCollection<AuthorityClientDocument>(AuthorityMongoDefaults.Collections.Clients);
await clients.DeleteManyAsync(Builders<AuthorityClientDocument>.Filter.Empty);
}
private async Task<ServiceProvider> BuildMongoProviderAsync(FakeTimeProvider clock)
{
var services = new ServiceCollection();
services.AddSingleton<TimeProvider>(clock);
services.AddLogging();
services.AddAuthorityMongoStorage(options =>
{
options.ConnectionString = fixture.Runner.ConnectionString;
options.DatabaseName = fixture.Database.DatabaseNamespace.DatabaseName;
options.CommandTimeout = TimeSpan.FromSeconds(5);
});
var provider = services.BuildServiceProvider();
var initializer = provider.GetRequiredService<AuthorityMongoInitializer>();
var database = provider.GetRequiredService<IMongoDatabase>();
await initializer.InitialiseAsync(database, CancellationToken.None);
return provider;
}
}