Skip to content

Commit

Permalink
chore: add organizationID to API token (#795)
Browse files Browse the repository at this point in the history
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
  • Loading branch information
migmartri committed May 17, 2024
1 parent 78995a9 commit bfe0422
Show file tree
Hide file tree
Showing 5 changed files with 29 additions and 22 deletions.
2 changes: 1 addition & 1 deletion app/controlplane/internal/biz/apitoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func (uc *APITokenUseCase) Create(ctx context.Context, name string, description
}

// generate the JWT
token.JWT, err = uc.jwtBuilder.GenerateJWT(token.ID.String(), expiresAt)
token.JWT, err = uc.jwtBuilder.GenerateJWT(token.OrganizationID.String(), token.ID.String(), expiresAt)
if err != nil {
return nil, fmt.Errorf("generating jwt: %w", err)
}
Expand Down
22 changes: 15 additions & 7 deletions app/controlplane/internal/jwt/apitoken/apitoken.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// Copyright 2024 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 @@ -67,12 +67,15 @@ func NewBuilder(opts ...NewOpt) (*Builder, error) {
}

// it can both expire and being revoked, revocation is performed by checking the keyID against our revocation list
func (ra *Builder) GenerateJWT(keyID string, expiresAt *time.Time) (string, error) {
claims := jwt.RegisteredClaims{
// Key identifier so we can check it's revocation status
ID: keyID,
Issuer: ra.issuer,
Audience: jwt.ClaimStrings{Audience},
func (ra *Builder) GenerateJWT(orgID, keyID string, expiresAt *time.Time) (string, error) {
claims := CustomClaims{
orgID,
jwt.RegisteredClaims{
// Key identifier so we can check it's revocation status
ID: keyID,
Issuer: ra.issuer,
Audience: jwt.ClaimStrings{Audience},
},
}

// optional expiration value, i.e 30 days
Expand All @@ -83,3 +86,8 @@ func (ra *Builder) GenerateJWT(keyID string, expiresAt *time.Time) (string, erro
resultToken := jwt.NewWithClaims(SigningMethod, claims)
return resultToken.SignedString([]byte(ra.hmacSecret))
}

type CustomClaims struct {
OrgID string `json:"org_id"`
jwt.RegisteredClaims
}
12 changes: 5 additions & 7 deletions app/controlplane/internal/jwt/apitoken/apitoken_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// Copyright 2024 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 @@ -70,24 +70,22 @@ func TestNewBuilder(t *testing.T) {
func TestGenerateJWT(t *testing.T) {
const hmacSecret = "my-secret"

b, err := NewBuilder(
WithIssuer("my-issuer"),
WithKeySecret(hmacSecret),
)
b, err := NewBuilder(WithIssuer("my-issuer"), WithKeySecret(hmacSecret))
require.NoError(t, err)

token, err := b.GenerateJWT("key-id", toPtrTime(time.Now().Add(1*time.Hour)))
token, err := b.GenerateJWT("org-id", "key-id", toPtrTime(time.Now().Add(1*time.Hour)))
assert.NoError(t, err)
assert.NotEmpty(t, token)

// Verify signature and check claims
claims := &jwt.RegisteredClaims{}
claims := &CustomClaims{}
tokenInfo, err := jwt.ParseWithClaims(token, claims, func(_ *jwt.Token) (interface{}, error) {
return []byte(hmacSecret), nil
})

require.NoError(t, err)
assert.True(t, tokenInfo.Valid)
assert.Equal(t, "org-id", claims.OrgID)
assert.Equal(t, "key-id", claims.ID)
assert.Equal(t, "my-issuer", claims.Issuer)
assert.Contains(t, claims.Audience, Audience)
Expand Down
11 changes: 5 additions & 6 deletions app/controlplane/internal/usercontext/apitoken_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,23 +118,22 @@ func WithAttestationContextFromAPIToken(apiTokenUC *biz.APITokenUseCase, logger
return handler(ctx, req)
}

genericClaims, ok := authInfo.Claims.(jwt.MapClaims)
claims, ok := authInfo.Claims.(*apitoken.CustomClaims)
if !ok {
return nil, errors.New("error mapping the claims")
}

// We've received an API-token, double check its audience
if !genericClaims.VerifyAudience(apitoken.Audience, true) {
if !claims.VerifyAudience(apitoken.Audience, true) {
return nil, errors.New("unexpected token, invalid audience")
}

var err error
tokenID, ok := genericClaims["jti"].(string)
if !ok || tokenID == "" {
tokenID := claims.ID
if tokenID == "" {
return nil, errors.New("error mapping the API-token claims")
}

ctx, err = setRobotAccountFromAPIToken(ctx, apiTokenUC, tokenID)
ctx, err := setRobotAccountFromAPIToken(ctx, apiTokenUC, tokenID)
if err != nil {
return nil, errors.New("error extracting organization from APIToken")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,13 @@ func NewRobotAccountProvider(signingSecret string) JWTOption {
func NewAPITokenProvider(signingSecret string) JWTOption {
return withTokenProvider(
APITokenProviderKey,
WithClaims(func() jwt.Claims { return &apitoken.CustomClaims{} }),
WithVerifyAudienceFunc(func(token *jwt.Token) bool {
claims, ok := token.Claims.(jwt.MapClaims)
claims, ok := token.Claims.(*apitoken.CustomClaims)
if !ok {
return false
}

return claims.VerifyAudience(apitoken.Audience, true)
}),
WithSigningMethod(user.SigningMethod),
Expand Down

0 comments on commit bfe0422

Please sign in to comment.