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
				
			
		
			
				
	
	
		
			209 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			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;
 | |
|     }
 | |
| }
 |