Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions app/controlplane/pkg/jwt/user/user.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -28,6 +28,7 @@ type Builder struct {
issuer string
hmacSecret string
expiration time.Duration
audience string
}

type NewOpt func(b *Builder)
Expand All @@ -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

Expand All @@ -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)),
},
}
Expand Down
29 changes: 28 additions & 1 deletion app/controlplane/pkg/jwt/user/user_test.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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)
}
Loading