Skip to content

Commit 0f85898

Browse files
fix(cli): fall back to federated auth when stored user token is expired
Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
1 parent d427768 commit 0f85898

3 files changed

Lines changed: 34 additions & 5 deletions

File tree

app/cli/cmd/root.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,11 @@ func NewRootCmd(l zerolog.Logger) *cobra.Command {
121121
return err
122122
}
123123

124-
// If the auth token is not set and the command supports federated auth, we try to discover the runner and use the federated token for the runner if available
125-
if authToken == "" && cmdSupportsFederatedAuth(cmd) {
124+
// If the auth token is not set (or it's an expired stored user token) and the command supports federated
125+
// auth, we try to discover the runner and use the federated token if available.
126+
// The isUserToken guard ensures we only fall through for implicitly loaded config tokens, not for
127+
// explicit API tokens provided via flag or CHAINLOOP_TOKEN env var.
128+
if (authToken == "" || (isUserToken && token.IsExpired(authToken))) && cmdSupportsFederatedAuth(cmd) {
126129
r := crafter.DiscoverRunner(authToken, logger)
127130
if r.IsAuthenticated() && r.FederatedToken() != "" {
128131
logger.Debug().Str("runner", r.ID().String()).Msg("using federated auth token")

app/cli/internal/token/token.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package token
1717

1818
import (
19+
"time"
20+
1921
v1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
2022
"github.com/golang-jwt/jwt/v4"
2123
)
@@ -129,3 +131,26 @@ func Parse(token string) (*ParsedToken, error) {
129131

130132
return pToken, nil
131133
}
134+
135+
// IsExpired returns true if the token has an exp claim that is in the past.
136+
// Returns false for tokens with no exp claim or tokens that cannot be parsed.
137+
func IsExpired(tokenStr string) bool {
138+
if tokenStr == "" {
139+
return false
140+
}
141+
142+
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
143+
t, _, err := parser.ParseUnverified(tokenStr, jwt.MapClaims{})
144+
if err != nil {
145+
return false
146+
}
147+
148+
claims, ok := t.Claims.(jwt.MapClaims)
149+
if !ok {
150+
return false
151+
}
152+
153+
// VerifyExpiresAt returns false when the token is expired.
154+
// With req=false, tokens without an exp claim are considered not expired.
155+
return !claims.VerifyExpiresAt(time.Now().Unix(), false)
156+
}

pkg/attestation/crafter/runners/oidc/github.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2025 The Chainloop Authors.
2+
// Copyright 2025-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.
@@ -169,7 +169,7 @@ func (c *GitHubOIDCClient) Token(ctx context.Context) (any, error) {
169169
return nil, fmt.Errorf("error verifying token: %w", err)
170170
}
171171

172-
token, err := c.decodeToken(t)
172+
token, err := c.decodeToken(t, tokenPayload)
173173
if err != nil {
174174
return nil, fmt.Errorf("error decoding token: %w", err)
175175
}
@@ -262,13 +262,14 @@ func (c *GitHubOIDCClient) verifyToken(ctx context.Context, audience []string, a
262262
return idToken, nil
263263
}
264264

265-
func (c *GitHubOIDCClient) decodeToken(token *oidc.IDToken) (*Token, error) {
265+
func (c *GitHubOIDCClient) decodeToken(token *oidc.IDToken, rawToken string) (*Token, error) {
266266
c.logger.Debug().Msgf("decoding token: %s", token)
267267
var t Token
268268
if err := token.Claims(&t); err != nil {
269269
return nil, fmt.Errorf("%w: getting claims: %w", errToken, err)
270270
}
271271

272+
t.RawToken = rawToken
272273
return &t, nil
273274
}
274275

0 commit comments

Comments
 (0)