Initial commit (history squashed)
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Build Test Deploy / authority-container (push) Has been cancelled
				
			
		
			
				
	
				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
				
			
		
			
				
	
				Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Build Test Deploy / authority-container (push) Has been cancelled
				
			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
				
			Docs CI / lint-and-preview (push) Has been cancelled
				
			This commit is contained in:
		| @@ -0,0 +1,208 @@ | ||||
| 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; | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user