Skip to content

Commit 8345ad0

Browse files
committed
jwt: fail on empty username via CEL expression
Signed-off-by: Monis Khan <[email protected]>
1 parent 55d1518 commit 8345ad0

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
@@ -767,7 +767,13 @@ func (a *Authenticator) getUsername(ctx context.Context, c claims, claimsUnstruc
767767
return "", fmt.Errorf("oidc: error evaluating username claim expression: %w", fmt.Errorf("username claim expression must return a string"))
768768
}
769769

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

773779
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
@@ -2930,7 +2930,7 @@ func TestToken(t *testing.T) {
29302930
// test to ensure omitempty fields not included in user info
29312931
// are set and accessible for CEL evaluation.
29322932
{
2933-
name: "test user validation rule doesn't fail when user info is empty",
2933+
name: "test user validation rule doesn't fail when user info is empty except username",
29342934
options: Options{
29352935
JWTAuthenticator: apiserver.JWTAuthenticator{
29362936
Issuer: apiserver.Issuer{
@@ -2945,6 +2945,58 @@ func TestToken(t *testing.T) {
29452945
Expression: "claims.groups",
29462946
},
29472947
},
2948+
UserValidationRules: []apiserver.UserValidationRule{
2949+
{
2950+
Expression: `user.username == " "`,
2951+
Message: "username must be single space",
2952+
},
2953+
{
2954+
Expression: `user.uid == ""`,
2955+
Message: "uid must be empty string",
2956+
},
2957+
{
2958+
Expression: `!('bar' in user.groups)`,
2959+
Message: "groups must not contain bar",
2960+
},
2961+
{
2962+
Expression: `!('bar' in user.extra)`,
2963+
Message: "extra must not contain bar",
2964+
},
2965+
},
2966+
},
2967+
now: func() time.Time { return now },
2968+
},
2969+
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
2970+
pubKeys: []*jose.JSONWebKey{
2971+
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
2972+
},
2973+
claims: fmt.Sprintf(`{
2974+
"iss": "https://auth.example.com",
2975+
"aud": "my-client",
2976+
"username": " ",
2977+
"groups": null,
2978+
"exp": %d,
2979+
"baz": "qux"
2980+
}`, valid.Unix()),
2981+
want: &user.DefaultInfo{Name: " "},
2982+
},
2983+
{
2984+
name: "empty username is allowed via claim",
2985+
options: Options{
2986+
JWTAuthenticator: apiserver.JWTAuthenticator{
2987+
Issuer: apiserver.Issuer{
2988+
URL: "https://auth.example.com",
2989+
Audiences: []string{"my-client"},
2990+
},
2991+
ClaimMappings: apiserver.ClaimMappings{
2992+
Username: apiserver.PrefixedClaimOrExpression{
2993+
Claim: "username",
2994+
Prefix: pointer.String(""),
2995+
},
2996+
Groups: apiserver.PrefixedClaimOrExpression{
2997+
Expression: "claims.groups",
2998+
},
2999+
},
29483000
UserValidationRules: []apiserver.UserValidationRule{
29493001
{
29503002
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)