Skip to content

Commit e3900c3

Browse files
committed
feat: add custom audience support to user JWT builder
Add WithAudience functional option to allow generating JWT tokens with custom audience claims. This enables separate validation paths for different token types (e.g., MCP user tokens) while maintaining backward compatibility with the default audience. Signed-off-by: Miguel Martinez <miguel@chainloop.dev>
1 parent d2534c3 commit e3900c3

3 files changed

Lines changed: 43 additions & 4 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ Code reviews are required for all submissions via GitHub pull requests.
264264
- 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
265265
- after updating protos, make sure to run `buf format -w`
266266
- Please avoid sycophantic commentary like ‘You’re absolutely correct!’ or ‘Brilliant idea!’
267-
- 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.
267+
- 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.
268268
- if you add any new dependency to a constructor, remember to run wire ./...
269269
- 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.
270270
- 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

app/controlplane/pkg/jwt/user/user.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2023-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@ type Builder struct {
2828
issuer string
2929
hmacSecret string
3030
expiration time.Duration
31+
audience string
3132
}
3233

3334
type NewOpt func(b *Builder)
@@ -50,6 +51,12 @@ func WithExpiration(d time.Duration) NewOpt {
5051
}
5152
}
5253

54+
func WithAudience(aud string) NewOpt {
55+
return func(b *Builder) {
56+
b.audience = aud
57+
}
58+
}
59+
5360
var defaultExpiration = 24 * time.Hour
5461
var SigningMethod = jwt.SigningMethodHS256
5562

@@ -74,11 +81,16 @@ func NewBuilder(opts ...NewOpt) (*Builder, error) {
7481
}
7582

7683
func (ra *Builder) GenerateJWT(userID string) (string, error) {
84+
aud := Audience
85+
if ra.audience != "" {
86+
aud = ra.audience
87+
}
88+
7789
claims := CustomClaims{
7890
userID,
7991
jwt.RegisteredClaims{
8092
Issuer: ra.issuer,
81-
Audience: jwt.ClaimStrings{Audience},
93+
Audience: jwt.ClaimStrings{aud},
8294
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ra.expiration)),
8395
},
8496
}

app/controlplane/pkg/jwt/user/user_test.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2023-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -94,3 +94,30 @@ func TestGenerateJWT(t *testing.T) {
9494
assert.Contains(t, claims.Audience, Audience)
9595
assert.WithinDuration(t, time.Now(), claims.ExpiresAt.Time, 10*time.Second)
9696
}
97+
98+
func TestGenerateJWTWithCustomAudience(t *testing.T) {
99+
const hmacSecret = "my-secret"
100+
const customAudience = "mcp-user-auth.chainloop"
101+
102+
b, err := NewBuilder(
103+
WithIssuer("my-issuer"),
104+
WithKeySecret(hmacSecret),
105+
WithExpiration(10*time.Second),
106+
WithAudience(customAudience),
107+
)
108+
require.NoError(t, err)
109+
110+
token, err := b.GenerateJWT("user-id")
111+
require.NoError(t, err)
112+
assert.NotEmpty(t, token)
113+
114+
claims := &CustomClaims{}
115+
tokenInfo, err := jwt.ParseWithClaims(token, claims, func(_ *jwt.Token) (interface{}, error) {
116+
return []byte(hmacSecret), nil
117+
}, jwt.WithValidMethods([]string{SigningMethod.Alg()}))
118+
119+
require.NoError(t, err)
120+
assert.True(t, tokenInfo.Valid)
121+
assert.Equal(t, "user-id", claims.UserID)
122+
assert.Equal(t, jwt.ClaimStrings{customAudience}, claims.Audience)
123+
}

0 commit comments

Comments
 (0)