Skip to content

Commit afa7c8c

Browse files
authored
feat: add custom audience support to user JWT builder (#2717)
Signed-off-by: Miguel Martinez <miguel@chainloop.dev>
1 parent 61d456c commit afa7c8c

File tree

3 files changed

+43
-4
lines changed

3 files changed

+43
-4
lines changed

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)