using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using Microsoft.Extensions.Options; using StellaOps.Registry.TokenService; using StellaOps.Registry.TokenService.Observability; using StellaOps.TestKit; namespace StellaOps.Registry.TokenService.Tests; public sealed class RegistryTokenIssuerTests : IDisposable { private readonly List _tempFiles = new(); [Trait("Category", TestCategories.Unit)] [Fact] public void IssueToken_GeneratesJwtWithAccessClaim() { var pemPath = CreatePemKey(); var options = new RegistryTokenServiceOptions { Authority = new RegistryTokenServiceOptions.AuthorityOptions { Issuer = "https://authority.localhost", RequireHttpsMetadata = false, }, Signing = new RegistryTokenServiceOptions.SigningOptions { Issuer = "https://registry.localhost/token", KeyPath = pemPath, Lifetime = TimeSpan.FromMinutes(5) }, Registry = new RegistryTokenServiceOptions.RegistryOptions { Realm = "https://registry.localhost/v2/token" }, Plans = { new RegistryTokenServiceOptions.PlanRule { Name = "community", Repositories = { new RegistryTokenServiceOptions.RepositoryRule { Pattern = "stella-ops/public/*", Actions = new [] { "pull" } } } } } }; options.Validate(); var issuer = new RegistryTokenIssuer( Options.Create(options), new PlanRegistry(options), new RegistryTokenMetrics(), TimeProvider.System); var principal = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("sub", "client-1"), new Claim("stellaops:plan", "community") }, "test")); var accessRequests = new[] { new RegistryAccessRequest("repository", "stella-ops/public/base", new [] { "pull" }) }; var response = issuer.IssueToken(principal, "registry.localhost", accessRequests); Assert.NotEmpty(response.Token); var handler = new JwtSecurityTokenHandler(); var jwt = handler.ReadJwtToken(response.Token); Assert.Equal("https://registry.localhost/token", jwt.Issuer); Assert.True(jwt.Payload.TryGetValue("access", out var access)); Assert.NotNull(access); } private string CreatePemKey() { using var rsa = RSA.Create(2048); var builder = new StringWriter(); builder.WriteLine("-----BEGIN PRIVATE KEY-----"); builder.WriteLine(Convert.ToBase64String(rsa.ExportPkcs8PrivateKey(), Base64FormattingOptions.InsertLineBreaks)); builder.WriteLine("-----END PRIVATE KEY-----"); var path = Path.GetTempFileName(); File.WriteAllText(path, builder.ToString()); _tempFiles.Add(path); return path; } public void Dispose() { foreach (var file in _tempFiles) { try { File.Delete(file); } catch { // ignore } } } }