Skip to content

Commit eeebe58

Browse files
fix(cli): support GitHub federated tokens and avoid invalid auth metadatax
Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
1 parent e6f9f07 commit eeebe58

2 files changed

Lines changed: 78 additions & 6 deletions

File tree

app/cli/internal/token/token.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024-2025 The Chainloop Authors.
2+
// Copyright 2024-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.
@@ -36,6 +36,7 @@ const (
3636
//nolint:gosec
3737
apiTokenAudience = "api-token-auth.chainloop"
3838
federatedTokenAudience = "chainloop"
39+
githubFederatedIssuer = "https://token.actions.githubusercontent.com"
3940
)
4041

4142
// Parse the token and return the type of token. At the moment in Chainloop we have 3 types of tokens:
@@ -111,16 +112,29 @@ func Parse(token string) (*ParsedToken, error) {
111112
pToken.ID = userID
112113
}
113114
case federatedTokenAudience:
114-
if isGitLabFederatedToken(claims) {
115+
// Federated tokens require a known provider format.
116+
switch {
117+
case isGitLabFederatedToken(claims):
115118
pToken.TokenType = v1.Attestation_Auth_AUTH_TYPE_FEDERATED
116-
if issuer, ok := claims["iss"].(string); ok {
117-
pToken.ID = issuer
118-
}
119+
case isGitHubFederatedToken(claims):
120+
pToken.TokenType = v1.Attestation_Auth_AUTH_TYPE_FEDERATED
121+
default:
122+
return nil, nil
123+
}
124+
125+
if issuer, ok := claims["iss"].(string); ok {
126+
pToken.ID = issuer
119127
}
120128
default:
121129
return nil, nil
122130
}
123131

132+
// Avoid returning partially filled tokens that would create invalid auth metadata
133+
// in crafting state (e.g. type=UNSPECIFIED or empty ID).
134+
if pToken.TokenType == v1.Attestation_Auth_AUTH_TYPE_UNSPECIFIED || pToken.ID == "" {
135+
return nil, nil
136+
}
137+
124138
return pToken, nil
125139
}
126140

@@ -172,3 +186,14 @@ func isGitLabFederatedToken(claims jwt.MapClaims) bool {
172186

173187
return found >= requiredClaims
174188
}
189+
190+
// TODO: Evaluate adding claim-structure validation for GitHub tokens, similar to
191+
// the GitLab heuristic above, once we stabilize the expected claim set.
192+
func isGitHubFederatedToken(claims jwt.MapClaims) bool {
193+
iss, ok := claims["iss"].(string)
194+
if !ok {
195+
return false
196+
}
197+
198+
return iss == githubFederatedIssuer
199+
}

app/cli/internal/token/token_test.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 The Chainloop Authors.
2+
// Copyright 2024-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.
@@ -55,6 +55,18 @@ func TestParse(t *testing.T) {
5555
TokenType: v1.Attestation_Auth_AUTH_TYPE_FEDERATED,
5656
},
5757
},
58+
{
59+
name: "federated github token",
60+
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiY2hhaW5sb29wIiwicmVwb3NpdG9yeSI6Im1hdGlhc2luc2F1cnJhbGRlL3Byb2plY3QiLCJzdWIiOiJyZXBvOm1hdGlhc2luc2F1cnJhbGRlL3Byb2plY3Q6cmVmOnJlZnMvaGVhZHMvbWFpbiJ9.signature",
61+
want: &ParsedToken{
62+
ID: "https://token.actions.githubusercontent.com",
63+
TokenType: v1.Attestation_Auth_AUTH_TYPE_FEDERATED,
64+
},
65+
},
66+
{
67+
name: "federated token without issuer",
68+
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjaGFpbmxvb3AifQ.signature",
69+
},
5870
{
5971
name: "old api token (without orgID)",
6072
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjcC5jaGFpbmxvb3AiLCJhdWQiOlsiYXBpLXRva2VuLWF1dGguY2hhaW5sb29wIl0sImp0aSI6ImQ0ZTBlZTVlLTk3MTMtNDFkMi05ZmVhLTBiZGIxNDAzMzA4MSJ9.IOd3JIHPwfo9ihU20kvRwLIQJcQtTvp-ajlGqlCD4Es",
@@ -203,3 +215,38 @@ func TestIsGitLabFederatedToken(t *testing.T) {
203215
})
204216
}
205217
}
218+
219+
func TestIsGitHubFederatedToken(t *testing.T) {
220+
tests := []struct {
221+
name string
222+
claims jwt.MapClaims
223+
want bool
224+
}{
225+
{
226+
name: "github issuer",
227+
claims: jwt.MapClaims{
228+
"iss": "https://token.actions.githubusercontent.com",
229+
},
230+
want: true,
231+
},
232+
{
233+
name: "different issuer",
234+
claims: jwt.MapClaims{
235+
"iss": "https://gitlab.com",
236+
},
237+
want: false,
238+
},
239+
{
240+
name: "missing issuer",
241+
claims: jwt.MapClaims{},
242+
want: false,
243+
},
244+
}
245+
246+
for _, tt := range tests {
247+
t.Run(tt.name, func(t *testing.T) {
248+
got := isGitHubFederatedToken(tt.claims)
249+
assert.Equal(t, tt.want, got)
250+
})
251+
}
252+
}

0 commit comments

Comments
 (0)