diff --git a/CLAUDE.md b/CLAUDE.md index 1464243ea..8bfeef386 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -264,7 +264,7 @@ Code reviews are required for all submissions via GitHub pull requests. - when adding new inedexes, make sure to update the generated sql migraiton files and make them CREATE INDEX CONCURRENTLY and set -- atlas:txmode none at the top - after updating protos, make sure to run `buf format -w` - Please avoid sycophantic commentary like ‘You’re absolutely correct!’ or ‘Brilliant idea!’ -- For each file you modify, update the license header. If it says 2024, change it to 2024-2025. If there's no license header, create one with the current year. +- For each file you modify, update the license header to include the current year (2026). For example, if it says 2024, change it to 2024-2026. If there's no license header, create one with the current year. - if you add any new dependency to a constructor, remember to run wire ./... - when creating PR message, keep it high-level, what functionality was added, don't add info about testing, no icons, no info about how the message was generated. - app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts is a special case that we don't want to upgrade, so if it upgrades, put it back to main diff --git a/app/controlplane/pkg/jwt/user/user.go b/app/controlplane/pkg/jwt/user/user.go index 5414ab432..3c6ca0e7b 100644 --- a/app/controlplane/pkg/jwt/user/user.go +++ b/app/controlplane/pkg/jwt/user/user.go @@ -1,5 +1,5 @@ // -// Copyright 2023 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ type Builder struct { issuer string hmacSecret string expiration time.Duration + audience string } type NewOpt func(b *Builder) @@ -50,6 +51,12 @@ func WithExpiration(d time.Duration) NewOpt { } } +func WithAudience(aud string) NewOpt { + return func(b *Builder) { + b.audience = aud + } +} + var defaultExpiration = 24 * time.Hour var SigningMethod = jwt.SigningMethodHS256 @@ -74,11 +81,16 @@ func NewBuilder(opts ...NewOpt) (*Builder, error) { } func (ra *Builder) GenerateJWT(userID string) (string, error) { + aud := Audience + if ra.audience != "" { + aud = ra.audience + } + claims := CustomClaims{ userID, jwt.RegisteredClaims{ Issuer: ra.issuer, - Audience: jwt.ClaimStrings{Audience}, + Audience: jwt.ClaimStrings{aud}, ExpiresAt: jwt.NewNumericDate(time.Now().Add(ra.expiration)), }, } diff --git a/app/controlplane/pkg/jwt/user/user_test.go b/app/controlplane/pkg/jwt/user/user_test.go index 05a3e8772..4edb8a166 100644 --- a/app/controlplane/pkg/jwt/user/user_test.go +++ b/app/controlplane/pkg/jwt/user/user_test.go @@ -1,5 +1,5 @@ // -// Copyright 2023 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -94,3 +94,30 @@ func TestGenerateJWT(t *testing.T) { assert.Contains(t, claims.Audience, Audience) assert.WithinDuration(t, time.Now(), claims.ExpiresAt.Time, 10*time.Second) } + +func TestGenerateJWTWithCustomAudience(t *testing.T) { + const hmacSecret = "my-secret" + const customAudience = "mcp-user-auth.chainloop" + + b, err := NewBuilder( + WithIssuer("my-issuer"), + WithKeySecret(hmacSecret), + WithExpiration(10*time.Second), + WithAudience(customAudience), + ) + require.NoError(t, err) + + token, err := b.GenerateJWT("user-id") + require.NoError(t, err) + assert.NotEmpty(t, token) + + claims := &CustomClaims{} + tokenInfo, err := jwt.ParseWithClaims(token, claims, func(_ *jwt.Token) (interface{}, error) { + return []byte(hmacSecret), nil + }, jwt.WithValidMethods([]string{SigningMethod.Alg()})) + + require.NoError(t, err) + assert.True(t, tokenInfo.Valid) + assert.Equal(t, "user-id", claims.UserID) + assert.Equal(t, jwt.ClaimStrings{customAudience}, claims.Audience) +}