Skip to content

Commit 50f4b1e

Browse files
authored
Merge pull request kubernetes#123568 from enj/enj/i/jwt_username_required
jwt: fail on empty username via CEL expression
2 parents 26600b1 + 8345ad0 commit 50f4b1e

File tree

3 files changed

+131
-2
lines changed

3 files changed

+131
-2
lines changed

staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,13 @@ func (a *Authenticator) getUsername(ctx context.Context, c claims, claimsUnstruc
769769
return "", fmt.Errorf("oidc: error evaluating username claim expression: %w", fmt.Errorf("username claim expression must return a string"))
770770
}
771771

772-
return evalResult.EvalResult.Value().(string), nil
772+
username := evalResult.EvalResult.Value().(string)
773+
774+
if len(username) == 0 {
775+
return "", fmt.Errorf("oidc: empty username via CEL expression is not allowed")
776+
}
777+
778+
return username, nil
773779
}
774780

775781
var username string

staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2982,7 +2982,7 @@ func TestToken(t *testing.T) {
29822982
// test to ensure omitempty fields not included in user info
29832983
// are set and accessible for CEL evaluation.
29842984
{
2985-
name: "test user validation rule doesn't fail when user info is empty",
2985+
name: "test user validation rule doesn't fail when user info is empty except username",
29862986
options: Options{
29872987
JWTAuthenticator: apiserver.JWTAuthenticator{
29882988
Issuer: apiserver.Issuer{
@@ -2997,6 +2997,58 @@ func TestToken(t *testing.T) {
29972997
Expression: "claims.groups",
29982998
},
29992999
},
3000+
UserValidationRules: []apiserver.UserValidationRule{
3001+
{
3002+
Expression: `user.username == " "`,
3003+
Message: "username must be single space",
3004+
},
3005+
{
3006+
Expression: `user.uid == ""`,
3007+
Message: "uid must be empty string",
3008+
},
3009+
{
3010+
Expression: `!('bar' in user.groups)`,
3011+
Message: "groups must not contain bar",
3012+
},
3013+
{
3014+
Expression: `!('bar' in user.extra)`,
3015+
Message: "extra must not contain bar",
3016+
},
3017+
},
3018+
},
3019+
now: func() time.Time { return now },
3020+
},
3021+
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
3022+
pubKeys: []*jose.JSONWebKey{
3023+
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
3024+
},
3025+
claims: fmt.Sprintf(`{
3026+
"iss": "https://auth.example.com",
3027+
"aud": "my-client",
3028+
"username": " ",
3029+
"groups": null,
3030+
"exp": %d,
3031+
"baz": "qux"
3032+
}`, valid.Unix()),
3033+
want: &user.DefaultInfo{Name: " "},
3034+
},
3035+
{
3036+
name: "empty username is allowed via claim",
3037+
options: Options{
3038+
JWTAuthenticator: apiserver.JWTAuthenticator{
3039+
Issuer: apiserver.Issuer{
3040+
URL: "https://auth.example.com",
3041+
Audiences: []string{"my-client"},
3042+
},
3043+
ClaimMappings: apiserver.ClaimMappings{
3044+
Username: apiserver.PrefixedClaimOrExpression{
3045+
Claim: "username",
3046+
Prefix: pointer.String(""),
3047+
},
3048+
Groups: apiserver.PrefixedClaimOrExpression{
3049+
Expression: "claims.groups",
3050+
},
3051+
},
30003052
UserValidationRules: []apiserver.UserValidationRule{
30013053
{
30023054
Expression: `user.username == ""`,

test/integration/apiserver/oidc/oidc_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,77 @@ jwt:
325325
assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
326326
},
327327
},
328+
{
329+
name: "ID token is okay but username is empty",
330+
configureInfrastructure: func(t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey)) (
331+
oidcServer *utilsoidc.TestServer,
332+
apiServer *kubeapiserverapptesting.TestServer,
333+
signingPrivateKey *rsa.PrivateKey,
334+
caCertContent []byte,
335+
caFilePath string,
336+
) {
337+
caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
338+
339+
signingPrivateKey, _ = keyFunc(t)
340+
341+
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath, "")
342+
343+
if useAuthenticationConfig {
344+
authenticationConfig := fmt.Sprintf(`
345+
apiVersion: apiserver.config.k8s.io/v1alpha1
346+
kind: AuthenticationConfiguration
347+
jwt:
348+
- issuer:
349+
url: %s
350+
audiences:
351+
- %s
352+
certificateAuthority: |
353+
%s
354+
claimMappings:
355+
username:
356+
expression: claims.sub
357+
`, oidcServer.URL(), defaultOIDCClientID, indentCertificateAuthority(string(caCertContent)))
358+
apiServer = startTestAPIServerForOIDC(t, apiServerOIDCConfig{authenticationConfigYAML: authenticationConfig}, &signingPrivateKey.PublicKey)
359+
} else {
360+
apiServer = startTestAPIServerForOIDC(t, apiServerOIDCConfig{
361+
oidcURL: oidcServer.URL(), oidcClientID: defaultOIDCClientID, oidcCAFilePath: caFilePath, oidcUsernamePrefix: "-",
362+
},
363+
&signingPrivateKey.PublicKey)
364+
}
365+
366+
oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, &signingPrivateKey.PublicKey))
367+
368+
return oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath
369+
},
370+
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
371+
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
372+
t,
373+
signingPrivateKey,
374+
map[string]interface{}{
375+
"iss": oidcServer.URL(),
376+
"sub": "",
377+
"aud": defaultOIDCClientID,
378+
"exp": time.Now().Add(time.Second * 1200).Unix(),
379+
},
380+
defaultStubAccessToken,
381+
defaultStubRefreshToken,
382+
))
383+
},
384+
configureClient: configureClientFetchingOIDCCredentials,
385+
assertErrFn: func(t *testing.T, errorToCheck error) {
386+
if useAuthenticationConfig { // since the config uses a CEL expression
387+
assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
388+
} else {
389+
// the claim based approach is still allowed to use empty usernames
390+
_ = assert.True(t, apierrors.IsForbidden(errorToCheck), errorToCheck) &&
391+
assert.Equal(
392+
t,
393+
`pods is forbidden: User "" cannot list resource "pods" in API group "" in the namespace "default"`,
394+
errorToCheck.Error(),
395+
)
396+
}
397+
},
398+
},
328399
}
329400

330401
for _, tt := range tests {

0 commit comments

Comments
 (0)