k8s.io/apiserver@v0.31.1/plugin/pkg/authenticator/token/oidc/oidc.go (about) 1 /* 2 Copyright 2015 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 /* 18 oidc implements the authenticator.Token interface using the OpenID Connect protocol. 19 20 config := oidc.Options{ 21 IssuerURL: "https://accounts.google.com", 22 ClientID: os.Getenv("GOOGLE_CLIENT_ID"), 23 UsernameClaim: "email", 24 } 25 tokenAuthenticator, err := oidc.New(config) 26 */ 27 package oidc 28 29 import ( 30 "context" 31 "crypto/tls" 32 "crypto/x509" 33 "encoding/base64" 34 "encoding/json" 35 "fmt" 36 "io/ioutil" 37 "net/http" 38 "net/url" 39 "reflect" 40 "strings" 41 "sync" 42 "sync/atomic" 43 "time" 44 45 "github.com/coreos/go-oidc" 46 celgo "github.com/google/cel-go/cel" 47 "github.com/google/cel-go/common/types/ref" 48 49 authenticationv1 "k8s.io/api/authentication/v1" 50 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 51 "k8s.io/apimachinery/pkg/runtime" 52 "k8s.io/apimachinery/pkg/util/net" 53 "k8s.io/apimachinery/pkg/util/sets" 54 "k8s.io/apimachinery/pkg/util/wait" 55 "k8s.io/apiserver/pkg/apis/apiserver" 56 apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation" 57 "k8s.io/apiserver/pkg/authentication/authenticator" 58 authenticationcel "k8s.io/apiserver/pkg/authentication/cel" 59 "k8s.io/apiserver/pkg/authentication/user" 60 certutil "k8s.io/client-go/util/cert" 61 "k8s.io/klog/v2" 62 ) 63 64 var ( 65 // synchronizeTokenIDVerifierForTest should be set to true to force a 66 // wait until the token ID verifiers are ready. 67 synchronizeTokenIDVerifierForTest = false 68 ) 69 70 const ( 71 wellKnownEndpointPath = "/.well-known/openid-configuration" 72 ) 73 74 type Options struct { 75 // JWTAuthenticator is the authenticator that will be used to verify the JWT. 76 JWTAuthenticator apiserver.JWTAuthenticator 77 78 // Optional KeySet to allow for synchronous initialization instead of fetching from the remote issuer. 79 // Mutually exclusive with JWTAuthenticator.Issuer.DiscoveryURL. 80 KeySet oidc.KeySet 81 82 // PEM encoded root certificate contents of the provider. Mutually exclusive with Client. 83 CAContentProvider CAContentProvider 84 85 // Optional http.Client used to make all requests to the remote issuer. Mutually exclusive with CAContentProvider. 86 Client *http.Client 87 88 // SupportedSigningAlgs sets the accepted set of JOSE signing algorithms that 89 // can be used by the provider to sign tokens. 90 // 91 // https://tools.ietf.org/html/rfc7518#section-3.1 92 // 93 // This value defaults to RS256, the value recommended by the OpenID Connect 94 // spec: 95 // 96 // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation 97 SupportedSigningAlgs []string 98 99 DisallowedIssuers []string 100 101 // now is used for testing. It defaults to time.Now. 102 now func() time.Time 103 } 104 105 // Subset of dynamiccertificates.CAContentProvider that can be used to dynamically load root CAs. 106 type CAContentProvider interface { 107 CurrentCABundleContent() []byte 108 } 109 110 // initVerifier creates a new ID token verifier for the given configuration and issuer URL. On success, calls setVerifier with the 111 // resulting verifier. 112 func initVerifier(ctx context.Context, config *oidc.Config, iss string, audiences sets.Set[string]) (*idTokenVerifier, error) { 113 provider, err := oidc.NewProvider(ctx, iss) 114 if err != nil { 115 return nil, fmt.Errorf("init verifier failed: %v", err) 116 } 117 return &idTokenVerifier{provider.Verifier(config), audiences}, nil 118 } 119 120 // asyncIDTokenVerifier is an ID token verifier that allows async initialization 121 // of the issuer check. Must be passed by reference as it wraps sync.Mutex. 122 type asyncIDTokenVerifier struct { 123 m sync.Mutex 124 125 // v is the ID token verifier initialized asynchronously. It remains nil 126 // up until it is eventually initialized. 127 // Guarded by m 128 v *idTokenVerifier 129 } 130 131 // newAsyncIDTokenVerifier creates a new asynchronous token verifier. The 132 // verifier is available immediately, but may remain uninitialized for some time 133 // after creation. 134 func newAsyncIDTokenVerifier(ctx context.Context, c *oidc.Config, iss string, audiences sets.Set[string]) *asyncIDTokenVerifier { 135 t := &asyncIDTokenVerifier{} 136 137 sync := make(chan struct{}) 138 // Polls indefinitely in an attempt to initialize the distributed claims 139 // verifier, or until context canceled. 140 initFn := func(ctx context.Context) (done bool, err error) { 141 klog.V(4).Infof("oidc authenticator: attempting init: iss=%v", iss) 142 v, err := initVerifier(ctx, c, iss, audiences) 143 if err != nil { 144 klog.Errorf("oidc authenticator: async token verifier for issuer: %q: %v", iss, err) 145 return false, nil 146 } 147 t.m.Lock() 148 defer t.m.Unlock() 149 t.v = v 150 close(sync) 151 return true, nil 152 } 153 154 go func() { 155 _ = wait.PollUntilContextCancel(ctx, 10*time.Second, true, initFn) 156 }() 157 158 if synchronizeTokenIDVerifierForTest { 159 select { 160 case <-sync: 161 case <-ctx.Done(): 162 } 163 } 164 165 return t 166 } 167 168 // verifier returns the underlying ID token verifier, or nil if one is not yet initialized. 169 func (a *asyncIDTokenVerifier) verifier() *idTokenVerifier { 170 a.m.Lock() 171 defer a.m.Unlock() 172 return a.v 173 } 174 175 type jwtAuthenticator struct { 176 jwtAuthenticator apiserver.JWTAuthenticator 177 178 // Contains an *oidc.IDTokenVerifier. Do not access directly use the 179 // idTokenVerifier method. 180 verifier atomic.Value 181 182 // resolver is used to resolve distributed claims. 183 resolver *claimResolver 184 185 // celMapper contains the compiled CEL expressions for 186 // username, groups, uid, extra, claimMapping and claimValidation 187 celMapper authenticationcel.CELMapper 188 189 // requiredClaims contains the list of claims that must be present in the token. 190 requiredClaims map[string]string 191 192 healthCheck atomic.Pointer[errorHolder] 193 } 194 195 // idTokenVerifier is a wrapper around oidc.IDTokenVerifier. It uses the oidc.IDTokenVerifier 196 // to verify the raw ID token and then performs audience validation locally. 197 type idTokenVerifier struct { 198 verifier *oidc.IDTokenVerifier 199 audiences sets.Set[string] 200 } 201 202 func (a *jwtAuthenticator) setVerifier(v *idTokenVerifier) { 203 a.verifier.Store(v) 204 if v != nil { 205 // this must be done after the verifier has been stored so that a nil error 206 // from HealthCheck always means that the authenticator is ready for use. 207 a.healthCheck.Store(&errorHolder{}) 208 } 209 } 210 211 func (a *jwtAuthenticator) idTokenVerifier() (*idTokenVerifier, bool) { 212 if v := a.verifier.Load(); v != nil { 213 return v.(*idTokenVerifier), true 214 } 215 return nil, false 216 } 217 218 func AllValidSigningAlgorithms() []string { 219 return sets.List(sets.KeySet(allowedSigningAlgs)) 220 } 221 222 // allowlist of signing algorithms to ensure users don't mistakenly pass something goofy. 223 var allowedSigningAlgs = map[string]bool{ 224 oidc.RS256: true, 225 oidc.RS384: true, 226 oidc.RS512: true, 227 oidc.ES256: true, 228 oidc.ES384: true, 229 oidc.ES512: true, 230 oidc.PS256: true, 231 oidc.PS384: true, 232 oidc.PS512: true, 233 } 234 235 type AuthenticatorTokenWithHealthCheck interface { 236 authenticator.Token 237 HealthCheck() error 238 } 239 240 // New returns an authenticator that is asynchronously initialized when opts.KeySet is not set. 241 // The input lifecycleCtx is used to: 242 // - terminate background goroutines that are needed for asynchronous initialization 243 // - as the base context for any requests that are made (i.e. for key fetching) 244 // Thus, once the lifecycleCtx is canceled, the authenticator must not be used. 245 // A caller may check if the authenticator is healthy by calling the HealthCheck method. 246 func New(lifecycleCtx context.Context, opts Options) (AuthenticatorTokenWithHealthCheck, error) { 247 celMapper, fieldErr := apiservervalidation.CompileAndValidateJWTAuthenticator(opts.JWTAuthenticator, opts.DisallowedIssuers) 248 if err := fieldErr.ToAggregate(); err != nil { 249 return nil, err 250 } 251 252 supportedSigningAlgs := opts.SupportedSigningAlgs 253 if len(supportedSigningAlgs) == 0 { 254 // RS256 is the default recommended by OpenID Connect and an 'alg' value 255 // providers are required to implement. 256 supportedSigningAlgs = []string{oidc.RS256} 257 } 258 for _, alg := range supportedSigningAlgs { 259 if !allowedSigningAlgs[alg] { 260 return nil, fmt.Errorf("oidc: unsupported signing alg: %q", alg) 261 } 262 } 263 264 if opts.Client != nil && opts.CAContentProvider != nil { 265 return nil, fmt.Errorf("oidc: Client and CAContentProvider are mutually exclusive") 266 } 267 268 client := opts.Client 269 270 if client == nil { 271 var roots *x509.CertPool 272 var err error 273 if opts.CAContentProvider != nil { 274 // TODO(enj): make this reload CA data dynamically 275 roots, err = certutil.NewPoolFromBytes(opts.CAContentProvider.CurrentCABundleContent()) 276 if err != nil { 277 return nil, fmt.Errorf("Failed to read the CA contents: %v", err) 278 } 279 } else { 280 klog.Info("OIDC: No x509 certificates provided, will use host's root CA set") 281 } 282 283 // Copied from http.DefaultTransport. 284 tr := net.SetTransportDefaults(&http.Transport{ 285 // According to golang's doc, if RootCAs is nil, 286 // TLS uses the host's root CA set. 287 TLSClientConfig: &tls.Config{RootCAs: roots}, 288 }) 289 290 client = &http.Client{Transport: tr, Timeout: 30 * time.Second} 291 } 292 293 // If the discovery URL is set in authentication configuration, we set up a 294 // roundTripper to rewrite the {url}/.well-known/openid-configuration to 295 // the discovery URL. This is useful for self-hosted providers, for example, 296 // providers that run on top of Kubernetes itself. 297 if len(opts.JWTAuthenticator.Issuer.DiscoveryURL) > 0 { 298 if opts.KeySet != nil { 299 return nil, fmt.Errorf("oidc: KeySet and DiscoveryURL are mutually exclusive") 300 } 301 302 discoveryURL, err := url.Parse(opts.JWTAuthenticator.Issuer.DiscoveryURL) 303 if err != nil { 304 return nil, fmt.Errorf("oidc: invalid discovery URL: %w", err) 305 } 306 307 clientWithDiscoveryURL := *client 308 baseTransport := clientWithDiscoveryURL.Transport 309 if baseTransport == nil { 310 baseTransport = http.DefaultTransport 311 } 312 // This matches the url construction in oidc.NewProvider as of go-oidc v2.2.1. 313 // xref: https://github.com/coreos/go-oidc/blob/40cd342c4a2076195294612a834d11df23c1b25a/oidc.go#L114 314 urlToRewrite := strings.TrimSuffix(opts.JWTAuthenticator.Issuer.URL, "/") + wellKnownEndpointPath 315 clientWithDiscoveryURL.Transport = &discoveryURLRoundTripper{baseTransport, discoveryURL, urlToRewrite} 316 client = &clientWithDiscoveryURL 317 } 318 319 lifecycleCtx = oidc.ClientContext(lifecycleCtx, client) 320 321 now := opts.now 322 if now == nil { 323 now = time.Now 324 } 325 326 audiences := sets.New[string](opts.JWTAuthenticator.Issuer.Audiences...) 327 verifierConfig := &oidc.Config{ 328 ClientID: opts.JWTAuthenticator.Issuer.Audiences[0], 329 SupportedSigningAlgs: supportedSigningAlgs, 330 Now: now, 331 } 332 if audiences.Len() > 1 { 333 verifierConfig.ClientID = "" 334 // SkipClientIDCheck is set to true because we want to support multiple audiences 335 // in the authentication configuration. 336 // The go oidc library does not support validating 337 // multiple audiences, so we have to skip the client ID check and do it ourselves. 338 // xref: https://github.com/coreos/go-oidc/issues/397 339 verifierConfig.SkipClientIDCheck = true 340 } 341 342 var resolver *claimResolver 343 groupsClaim := opts.JWTAuthenticator.ClaimMappings.Groups.Claim 344 if groupsClaim != "" { 345 resolver = newClaimResolver(lifecycleCtx, groupsClaim, client, verifierConfig, audiences) 346 } 347 348 requiredClaims := make(map[string]string) 349 for _, claimValidationRule := range opts.JWTAuthenticator.ClaimValidationRules { 350 if len(claimValidationRule.Claim) > 0 { 351 requiredClaims[claimValidationRule.Claim] = claimValidationRule.RequiredValue 352 } 353 } 354 355 authn := &jwtAuthenticator{ 356 jwtAuthenticator: opts.JWTAuthenticator, 357 resolver: resolver, 358 celMapper: celMapper, 359 requiredClaims: requiredClaims, 360 } 361 authn.healthCheck.Store(&errorHolder{ 362 err: fmt.Errorf("oidc: authenticator for issuer %q is not initialized", authn.jwtAuthenticator.Issuer.URL), 363 }) 364 365 issuerURL := opts.JWTAuthenticator.Issuer.URL 366 if opts.KeySet != nil { 367 // We already have a key set, synchronously initialize the verifier. 368 authn.setVerifier(&idTokenVerifier{ 369 oidc.NewVerifier(issuerURL, opts.KeySet, verifierConfig), 370 audiences, 371 }) 372 } else { 373 // Asynchronously attempt to initialize the authenticator. This enables 374 // self-hosted providers, providers that run on top of Kubernetes itself. 375 go func() { 376 // we ignore any errors from polling because they can only come from the context being canceled 377 _ = wait.PollUntilContextCancel(lifecycleCtx, 10*time.Second, true, func(_ context.Context) (done bool, err error) { 378 // this must always use lifecycleCtx because NewProvider uses that context for future key set fetching. 379 // this also means that there is no correct way to control the timeout of the discovery request made by NewProvider. 380 // the global timeout of the http.Client is still honored. 381 provider, err := oidc.NewProvider(lifecycleCtx, issuerURL) 382 if err != nil { 383 klog.Errorf("oidc authenticator: initializing plugin: %v", err) 384 authn.healthCheck.Store(&errorHolder{err: err}) 385 return false, nil 386 } 387 388 verifier := provider.Verifier(verifierConfig) 389 authn.setVerifier(&idTokenVerifier{verifier, audiences}) 390 return true, nil 391 }) 392 }() 393 } 394 395 return newInstrumentedAuthenticator(issuerURL, authn), nil 396 } 397 398 type errorHolder struct { 399 err error 400 } 401 402 // discoveryURLRoundTripper is a http.RoundTripper that rewrites the 403 // {url}/.well-known/openid-configuration to the discovery URL. 404 type discoveryURLRoundTripper struct { 405 base http.RoundTripper 406 // discoveryURL is the URL to use to fetch the openid configuration 407 discoveryURL *url.URL 408 // urlToRewrite is the URL to rewrite to the discovery URL 409 urlToRewrite string 410 } 411 412 func (t *discoveryURLRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 413 if req.Method == http.MethodGet && req.URL.String() == t.urlToRewrite { 414 clone := req.Clone(req.Context()) 415 clone.Host = "" 416 clone.URL = t.discoveryURL 417 return t.base.RoundTrip(clone) 418 } 419 return t.base.RoundTrip(req) 420 } 421 422 // untrustedIssuer extracts an untrusted "iss" claim from the given JWT token, 423 // or returns an error if the token can not be parsed. Since the JWT is not 424 // verified, the returned issuer should not be trusted. 425 func untrustedIssuer(token string) (string, error) { 426 if strings.HasPrefix(strings.TrimSpace(token), "{") { 427 return "", fmt.Errorf("token is not compact JWT") 428 } 429 parts := strings.Split(token, ".") 430 if len(parts) != 3 { 431 return "", fmt.Errorf("malformed token") 432 } 433 payload, err := base64.RawURLEncoding.DecodeString(parts[1]) 434 if err != nil { 435 return "", fmt.Errorf("error decoding token: %v", err) 436 } 437 claims := struct { 438 // WARNING: this JWT is not verified. Do not trust these claims. 439 Issuer string `json:"iss"` 440 }{} 441 if err := json.Unmarshal(payload, &claims); err != nil { 442 return "", fmt.Errorf("while unmarshaling token: %v", err) 443 } 444 // Coalesce the legacy GoogleIss with the new one. 445 // 446 // http://openid.net/specs/openid-connect-core-1_0.html#GoogleIss 447 if claims.Issuer == "accounts.google.com" { 448 return "https://accounts.google.com", nil 449 } 450 return claims.Issuer, nil 451 } 452 453 func hasCorrectIssuer(iss, tokenData string) bool { 454 uiss, err := untrustedIssuer(tokenData) 455 if err != nil { 456 return false 457 } 458 if uiss != iss { 459 return false 460 } 461 return true 462 } 463 464 // endpoint represents an OIDC distributed claims endpoint. 465 type endpoint struct { 466 // URL to use to request the distributed claim. This URL is expected to be 467 // prefixed by one of the known issuer URLs. 468 URL string `json:"endpoint,omitempty"` 469 // AccessToken is the bearer token to use for access. If empty, it is 470 // not used. Access token is optional per the OIDC distributed claims 471 // specification. 472 // See: http://openid.net/specs/openid-connect-core-1_0.html#DistributedExample 473 AccessToken string `json:"access_token,omitempty"` 474 // JWT is the container for aggregated claims. Not supported at the moment. 475 // See: http://openid.net/specs/openid-connect-core-1_0.html#AggregatedExample 476 JWT string `json:"JWT,omitempty"` 477 } 478 479 // claimResolver expands distributed claims by calling respective claim source 480 // endpoints. 481 type claimResolver struct { 482 ctx context.Context 483 484 // claim is the distributed claim that may be resolved. 485 claim string 486 487 // audiences is the set of acceptable audiences the JWT must be issued to. 488 // At least one of the entries must match the "aud" claim in presented JWTs. 489 audiences sets.Set[string] 490 491 // client is the to use for resolving distributed claims 492 client *http.Client 493 494 // config is the OIDC configuration used for resolving distributed claims. 495 config *oidc.Config 496 497 // verifierPerIssuer contains, for each issuer, the appropriate verifier to use 498 // for this claim. It is assumed that there will be very few entries in 499 // this map. 500 // Guarded by m. 501 verifierPerIssuer map[string]*asyncIDTokenVerifier 502 503 m sync.Mutex 504 } 505 506 // newClaimResolver creates a new resolver for distributed claims. 507 // the input ctx is retained and is used as the base context for background requests such as key fetching. 508 func newClaimResolver(ctx context.Context, claim string, client *http.Client, config *oidc.Config, audiences sets.Set[string]) *claimResolver { 509 return &claimResolver{ 510 ctx: ctx, 511 claim: claim, 512 audiences: audiences, 513 client: client, 514 config: config, 515 verifierPerIssuer: map[string]*asyncIDTokenVerifier{}, 516 } 517 } 518 519 // Verifier returns either the verifier for the specified issuer, or error. 520 func (r *claimResolver) Verifier(iss string) (*idTokenVerifier, error) { 521 r.m.Lock() 522 av := r.verifierPerIssuer[iss] 523 if av == nil { 524 // This lazy init should normally be very quick. 525 ctx := oidc.ClientContext(r.ctx, r.client) 526 av = newAsyncIDTokenVerifier(ctx, r.config, iss, r.audiences) 527 r.verifierPerIssuer[iss] = av 528 } 529 r.m.Unlock() 530 531 v := av.verifier() 532 if v == nil { 533 return nil, fmt.Errorf("verifier not initialized for issuer: %q", iss) 534 } 535 return v, nil 536 } 537 538 // expand extracts the distributed claims from claim names and claim sources. 539 // The extracted claim value is pulled up into the supplied claims. 540 // 541 // Distributed claims are of the form as seen below, and are defined in the 542 // OIDC Connect Core 1.0, section 5.6.2. 543 // See: https://openid.net/specs/openid-connect-core-1_0.html#AggregatedDistributedClaims 544 // 545 // { 546 // ... (other normal claims)... 547 // "_claim_names": { 548 // "groups": "src1" 549 // }, 550 // "_claim_sources": { 551 // "src1": { 552 // "endpoint": "https://www.example.com", 553 // "access_token": "f005ba11" 554 // }, 555 // }, 556 // } 557 func (r *claimResolver) expand(ctx context.Context, c claims) error { 558 const ( 559 // The claim containing a map of endpoint references per claim. 560 // OIDC Connect Core 1.0, section 5.6.2. 561 claimNamesKey = "_claim_names" 562 // The claim containing endpoint specifications. 563 // OIDC Connect Core 1.0, section 5.6.2. 564 claimSourcesKey = "_claim_sources" 565 ) 566 567 _, ok := c[r.claim] 568 if ok { 569 // There already is a normal claim, skip resolving. 570 return nil 571 } 572 names, ok := c[claimNamesKey] 573 if !ok { 574 // No _claim_names, no keys to look up. 575 return nil 576 } 577 578 claimToSource := map[string]string{} 579 if err := json.Unmarshal([]byte(names), &claimToSource); err != nil { 580 return fmt.Errorf("oidc: error parsing distributed claim names: %v", err) 581 } 582 583 rawSources, ok := c[claimSourcesKey] 584 if !ok { 585 // Having _claim_names claim, but no _claim_sources is not an expected 586 // state. 587 return fmt.Errorf("oidc: no claim sources") 588 } 589 590 var sources map[string]endpoint 591 if err := json.Unmarshal([]byte(rawSources), &sources); err != nil { 592 // The claims sources claim is malformed, this is not an expected state. 593 return fmt.Errorf("oidc: could not parse claim sources: %v", err) 594 } 595 596 src, ok := claimToSource[r.claim] 597 if !ok { 598 // No distributed claim present. 599 return nil 600 } 601 ep, ok := sources[src] 602 if !ok { 603 return fmt.Errorf("id token _claim_names contained a source %s missing in _claims_sources", src) 604 } 605 if ep.URL == "" { 606 // This is maybe an aggregated claim (ep.JWT != ""). 607 return nil 608 } 609 return r.resolve(ctx, ep, c) 610 } 611 612 // resolve requests distributed claims from all endpoints passed in, 613 // and inserts the lookup results into allClaims. 614 func (r *claimResolver) resolve(ctx context.Context, endpoint endpoint, allClaims claims) error { 615 // TODO: cache resolved claims. 616 jwt, err := getClaimJWT(ctx, r.client, endpoint.URL, endpoint.AccessToken) 617 if err != nil { 618 return fmt.Errorf("while getting distributed claim %q: %v", r.claim, err) 619 } 620 untrustedIss, err := untrustedIssuer(jwt) 621 if err != nil { 622 return fmt.Errorf("getting untrusted issuer from endpoint %v failed for claim %q: %v", endpoint.URL, r.claim, err) 623 } 624 v, err := r.Verifier(untrustedIss) 625 if err != nil { 626 return fmt.Errorf("verifying untrusted issuer %v failed: %v", untrustedIss, err) 627 } 628 t, err := v.Verify(ctx, jwt) 629 if err != nil { 630 return fmt.Errorf("verify distributed claim token: %v", err) 631 } 632 var distClaims claims 633 if err := t.Claims(&distClaims); err != nil { 634 return fmt.Errorf("could not parse distributed claims for claim %v: %v", r.claim, err) 635 } 636 value, ok := distClaims[r.claim] 637 if !ok { 638 return fmt.Errorf("jwt returned by distributed claim endpoint %q did not contain claim: %v", endpoint.URL, r.claim) 639 } 640 allClaims[r.claim] = value 641 return nil 642 } 643 644 func (v *idTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*oidc.IDToken, error) { 645 t, err := v.verifier.Verify(ctx, rawIDToken) 646 if err != nil { 647 return nil, err 648 } 649 if err := v.verifyAudience(t); err != nil { 650 return nil, err 651 } 652 return t, nil 653 } 654 655 // verifyAudience verifies the audience field in the ID token matches the expected audience. 656 // This is added based on https://github.com/coreos/go-oidc/blob/b203e58c24394ddf5e816706a7645f01280245c7/oidc/verify.go#L275-L281 657 // with the difference that we allow multiple audiences. 658 // 659 // AuthenticationConfiguration has a audienceMatchPolicy field, but the only supported value now is "MatchAny". 660 // So, The default match behavior is to match at least one of the audiences in the ID token. 661 func (v *idTokenVerifier) verifyAudience(t *oidc.IDToken) error { 662 // We validate audience field is not empty in the authentication configuration. 663 // This check ensures callers of "Verify" using idTokenVerifier are not passing 664 // an empty audience. 665 if v.audiences.Len() == 0 { 666 return fmt.Errorf("oidc: invalid configuration, audiences cannot be empty") 667 } 668 if v.audiences.HasAny(t.Audience...) { 669 return nil 670 } 671 672 return fmt.Errorf("oidc: expected audience in %q got %q", sets.List(v.audiences), t.Audience) 673 } 674 675 func (a *jwtAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) { 676 if !hasCorrectIssuer(a.jwtAuthenticator.Issuer.URL, token) { 677 return nil, false, nil 678 } 679 680 verifier, ok := a.idTokenVerifier() 681 if !ok { 682 return nil, false, fmt.Errorf("oidc: authenticator not initialized") 683 } 684 685 idToken, err := verifier.Verify(ctx, token) 686 if err != nil { 687 return nil, false, fmt.Errorf("oidc: verify token: %v", err) 688 } 689 690 var c claims 691 if err := idToken.Claims(&c); err != nil { 692 return nil, false, fmt.Errorf("oidc: parse claims: %v", err) 693 } 694 if a.resolver != nil { 695 if err := a.resolver.expand(ctx, c); err != nil { 696 return nil, false, fmt.Errorf("oidc: could not expand distributed claims: %v", err) 697 } 698 } 699 700 var claimsUnstructured *unstructured.Unstructured 701 // Convert the claims to unstructured so that we can evaluate the CEL expressions 702 // against the claims. This is done once here so that we don't have to convert 703 // the claims to unstructured multiple times in the CEL mapper for each mapping. 704 // Only perform this conversion if any of the mapping or validation rules contain 705 // CEL expressions. 706 // TODO(aramase): In the future when we look into making distributed claims work, 707 // we should see if we can skip this function and use a dynamic type resolver for 708 // both json.RawMessage and the distributed claim fetching. 709 if a.celMapper.Username != nil || a.celMapper.Groups != nil || a.celMapper.UID != nil || a.celMapper.Extra != nil || a.celMapper.ClaimValidationRules != nil { 710 if claimsUnstructured, err = convertObjectToUnstructured(&c); err != nil { 711 return nil, false, fmt.Errorf("oidc: could not convert claims to unstructured: %w", err) 712 } 713 } 714 715 var username string 716 if username, err = a.getUsername(ctx, c, claimsUnstructured); err != nil { 717 return nil, false, err 718 } 719 720 info := &user.DefaultInfo{Name: username} 721 if info.Groups, err = a.getGroups(ctx, c, claimsUnstructured); err != nil { 722 return nil, false, err 723 } 724 725 if info.UID, err = a.getUID(ctx, c, claimsUnstructured); err != nil { 726 return nil, false, err 727 } 728 729 extra, err := a.getExtra(ctx, claimsUnstructured) 730 if err != nil { 731 return nil, false, err 732 } 733 if len(extra) > 0 { 734 info.Extra = extra 735 } 736 737 // check to ensure all required claims are present in the ID token and have matching values. 738 for claim, value := range a.requiredClaims { 739 if !c.hasClaim(claim) { 740 return nil, false, fmt.Errorf("oidc: required claim %s not present in ID token", claim) 741 } 742 743 // NOTE: Only string values are supported as valid required claim values. 744 var claimValue string 745 if err := c.unmarshalClaim(claim, &claimValue); err != nil { 746 return nil, false, fmt.Errorf("oidc: parse claim %s: %w", claim, err) 747 } 748 if claimValue != value { 749 return nil, false, fmt.Errorf("oidc: required claim %s value does not match. Got = %s, want = %s", claim, claimValue, value) 750 } 751 } 752 753 if a.celMapper.ClaimValidationRules != nil { 754 evalResult, err := a.celMapper.ClaimValidationRules.EvalClaimMappings(ctx, claimsUnstructured) 755 if err != nil { 756 return nil, false, fmt.Errorf("oidc: error evaluating claim validation expression: %w", err) 757 } 758 if err := checkValidationRulesEvaluation(evalResult, func(a authenticationcel.ExpressionAccessor) (string, error) { 759 claimValidationCondition, ok := a.(*authenticationcel.ClaimValidationCondition) 760 if !ok { 761 return "", fmt.Errorf("invalid type conversion, expected ClaimValidationCondition") 762 } 763 return claimValidationCondition.Message, nil 764 }); err != nil { 765 return nil, false, fmt.Errorf("oidc: error evaluating claim validation expression: %w", err) 766 } 767 } 768 769 if a.celMapper.UserValidationRules != nil { 770 // Convert the user info to unstructured so that we can evaluate the CEL expressions 771 // against the user info. This is done once here so that we don't have to convert 772 // the user info to unstructured multiple times in the CEL mapper for each mapping. 773 userInfoUnstructured, err := convertUserInfoToUnstructured(info) 774 if err != nil { 775 return nil, false, fmt.Errorf("oidc: could not convert user info to unstructured: %w", err) 776 } 777 778 evalResult, err := a.celMapper.UserValidationRules.EvalUser(ctx, userInfoUnstructured) 779 if err != nil { 780 return nil, false, fmt.Errorf("oidc: error evaluating user info validation rule: %w", err) 781 } 782 if err := checkValidationRulesEvaluation(evalResult, func(a authenticationcel.ExpressionAccessor) (string, error) { 783 userValidationCondition, ok := a.(*authenticationcel.UserValidationCondition) 784 if !ok { 785 return "", fmt.Errorf("invalid type conversion, expected UserValidationCondition") 786 } 787 return userValidationCondition.Message, nil 788 }); err != nil { 789 return nil, false, fmt.Errorf("oidc: error evaluating user info validation rule: %w", err) 790 } 791 } 792 793 return &authenticator.Response{User: info}, true, nil 794 } 795 796 func (a *jwtAuthenticator) HealthCheck() error { 797 if holder := *a.healthCheck.Load(); holder.err != nil { 798 return fmt.Errorf("oidc: authenticator for issuer %q is not healthy: %w", a.jwtAuthenticator.Issuer.URL, holder.err) 799 } 800 801 return nil 802 } 803 804 func (a *jwtAuthenticator) getUsername(ctx context.Context, c claims, claimsUnstructured *unstructured.Unstructured) (string, error) { 805 if a.celMapper.Username != nil { 806 evalResult, err := a.celMapper.Username.EvalClaimMapping(ctx, claimsUnstructured) 807 if err != nil { 808 return "", fmt.Errorf("oidc: error evaluating username claim expression: %w", err) 809 } 810 if evalResult.EvalResult.Type() != celgo.StringType { 811 return "", fmt.Errorf("oidc: error evaluating username claim expression: %w", fmt.Errorf("username claim expression must return a string")) 812 } 813 814 username := evalResult.EvalResult.Value().(string) 815 816 if len(username) == 0 { 817 return "", fmt.Errorf("oidc: empty username via CEL expression is not allowed") 818 } 819 820 return username, nil 821 } 822 823 var username string 824 usernameClaim := a.jwtAuthenticator.ClaimMappings.Username.Claim 825 if err := c.unmarshalClaim(usernameClaim, &username); err != nil { 826 return "", fmt.Errorf("oidc: parse username claims %q: %v", usernameClaim, err) 827 } 828 829 if usernameClaim == "email" { 830 // If the email_verified claim is present, ensure the email is valid. 831 // https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims 832 if hasEmailVerified := c.hasClaim("email_verified"); hasEmailVerified { 833 var emailVerified bool 834 if err := c.unmarshalClaim("email_verified", &emailVerified); err != nil { 835 return "", fmt.Errorf("oidc: parse 'email_verified' claim: %v", err) 836 } 837 838 // If the email_verified claim is present we have to verify it is set to `true`. 839 if !emailVerified { 840 return "", fmt.Errorf("oidc: email not verified") 841 } 842 } 843 } 844 845 userNamePrefix := a.jwtAuthenticator.ClaimMappings.Username.Prefix 846 if userNamePrefix != nil && *userNamePrefix != "" { 847 return *userNamePrefix + username, nil 848 } 849 return username, nil 850 } 851 852 func (a *jwtAuthenticator) getGroups(ctx context.Context, c claims, claimsUnstructured *unstructured.Unstructured) ([]string, error) { 853 groupsClaim := a.jwtAuthenticator.ClaimMappings.Groups.Claim 854 if len(groupsClaim) > 0 { 855 if _, ok := c[groupsClaim]; ok { 856 // Some admins want to use string claims like "role" as the group value. 857 // Allow the group claim to be a single string instead of an array. 858 // 859 // See: https://github.com/kubernetes/kubernetes/issues/33290 860 var groups stringOrArray 861 if err := c.unmarshalClaim(groupsClaim, &groups); err != nil { 862 return nil, fmt.Errorf("oidc: parse groups claim %q: %w", groupsClaim, err) 863 } 864 865 prefix := a.jwtAuthenticator.ClaimMappings.Groups.Prefix 866 if prefix != nil && *prefix != "" { 867 for i, group := range groups { 868 groups[i] = *prefix + group 869 } 870 } 871 872 return []string(groups), nil 873 } 874 } 875 876 if a.celMapper.Groups == nil { 877 return nil, nil 878 } 879 880 evalResult, err := a.celMapper.Groups.EvalClaimMapping(ctx, claimsUnstructured) 881 if err != nil { 882 return nil, fmt.Errorf("oidc: error evaluating group claim expression: %w", err) 883 } 884 885 groups, err := convertCELValueToStringList(evalResult.EvalResult) 886 if err != nil { 887 return nil, fmt.Errorf("oidc: error evaluating group claim expression: %w", err) 888 } 889 return groups, nil 890 } 891 892 func (a *jwtAuthenticator) getUID(ctx context.Context, c claims, claimsUnstructured *unstructured.Unstructured) (string, error) { 893 uidClaim := a.jwtAuthenticator.ClaimMappings.UID.Claim 894 if len(uidClaim) > 0 { 895 var uid string 896 if err := c.unmarshalClaim(uidClaim, &uid); err != nil { 897 return "", fmt.Errorf("oidc: parse uid claim %q: %w", uidClaim, err) 898 } 899 return uid, nil 900 } 901 902 if a.celMapper.UID == nil { 903 return "", nil 904 } 905 906 evalResult, err := a.celMapper.UID.EvalClaimMapping(ctx, claimsUnstructured) 907 if err != nil { 908 return "", fmt.Errorf("oidc: error evaluating uid claim expression: %w", err) 909 } 910 if evalResult.EvalResult.Type() != celgo.StringType { 911 return "", fmt.Errorf("oidc: error evaluating uid claim expression: %w", fmt.Errorf("uid claim expression must return a string")) 912 } 913 914 return evalResult.EvalResult.Value().(string), nil 915 } 916 917 func (a *jwtAuthenticator) getExtra(ctx context.Context, claimsUnstructured *unstructured.Unstructured) (map[string][]string, error) { 918 if a.celMapper.Extra == nil { 919 return nil, nil 920 } 921 922 evalResult, err := a.celMapper.Extra.EvalClaimMappings(ctx, claimsUnstructured) 923 if err != nil { 924 return nil, err 925 } 926 927 extra := make(map[string][]string, len(evalResult)) 928 for _, result := range evalResult { 929 extraMapping, ok := result.ExpressionAccessor.(*authenticationcel.ExtraMappingExpression) 930 if !ok { 931 return nil, fmt.Errorf("oidc: error evaluating extra claim expression: %w", fmt.Errorf("invalid type conversion, expected ExtraMappingCondition")) 932 } 933 934 extraValues, err := convertCELValueToStringList(result.EvalResult) 935 if err != nil { 936 return nil, fmt.Errorf("oidc: error evaluating extra claim expression: %s: %w", extraMapping.Expression, err) 937 } 938 939 if len(extraValues) == 0 { 940 continue 941 } 942 943 extra[extraMapping.Key] = extraValues 944 } 945 946 return extra, nil 947 } 948 949 // getClaimJWT gets a distributed claim JWT from url, using the supplied access 950 // token as bearer token. If the access token is "", the authorization header 951 // will not be set. 952 // TODO: Allow passing in JSON hints to the IDP. 953 func getClaimJWT(ctx context.Context, client *http.Client, url, accessToken string) (string, error) { 954 // TODO: Allow passing request body with configurable information. 955 req, err := http.NewRequest("GET", url, nil) 956 if err != nil { 957 return "", fmt.Errorf("while calling %v: %v", url, err) 958 } 959 if accessToken != "" { 960 req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", accessToken)) 961 } 962 req = req.WithContext(ctx) 963 response, err := client.Do(req) 964 if err != nil { 965 return "", err 966 } 967 defer response.Body.Close() 968 // Report non-OK status code as an error. 969 if response.StatusCode < http.StatusOK || response.StatusCode > http.StatusIMUsed { 970 return "", fmt.Errorf("error while getting distributed claim JWT: %v", response.Status) 971 } 972 responseBytes, err := ioutil.ReadAll(response.Body) 973 if err != nil { 974 return "", fmt.Errorf("could not decode distributed claim response") 975 } 976 return string(responseBytes), nil 977 } 978 979 type stringOrArray []string 980 981 func (s *stringOrArray) UnmarshalJSON(b []byte) error { 982 var a []string 983 if err := json.Unmarshal(b, &a); err == nil { 984 *s = a 985 return nil 986 } 987 var str string 988 if err := json.Unmarshal(b, &str); err != nil { 989 return err 990 } 991 *s = []string{str} 992 return nil 993 } 994 995 type claims map[string]json.RawMessage 996 997 func (c claims) unmarshalClaim(name string, v interface{}) error { 998 val, ok := c[name] 999 if !ok { 1000 return fmt.Errorf("claim not present") 1001 } 1002 return json.Unmarshal([]byte(val), v) 1003 } 1004 1005 func (c claims) hasClaim(name string) bool { 1006 if _, ok := c[name]; !ok { 1007 return false 1008 } 1009 return true 1010 } 1011 1012 // convertCELValueToStringList converts the CEL value to a string list. 1013 // The CEL value needs to be either a string or a list of strings. 1014 // "", [] are treated as not being present and will return nil. 1015 // Empty string in a list of strings is treated as not being present and will be filtered out. 1016 func convertCELValueToStringList(val ref.Val) ([]string, error) { 1017 switch val.Type().TypeName() { 1018 case celgo.StringType.TypeName(): 1019 out := val.Value().(string) 1020 if len(out) == 0 { 1021 return nil, nil 1022 } 1023 return []string{out}, nil 1024 1025 case celgo.ListType(nil).TypeName(): 1026 var result []string 1027 switch val.Value().(type) { 1028 case []interface{}: 1029 for _, v := range val.Value().([]interface{}) { 1030 out, ok := v.(string) 1031 if !ok { 1032 return nil, fmt.Errorf("expression must return a string or a list of strings") 1033 } 1034 if len(out) == 0 { 1035 continue 1036 } 1037 result = append(result, out) 1038 } 1039 case []ref.Val: 1040 for _, v := range val.Value().([]ref.Val) { 1041 out, ok := v.Value().(string) 1042 if !ok { 1043 return nil, fmt.Errorf("expression must return a string or a list of strings") 1044 } 1045 if len(out) == 0 { 1046 continue 1047 } 1048 result = append(result, out) 1049 } 1050 default: 1051 return nil, fmt.Errorf("expression must return a string or a list of strings") 1052 } 1053 1054 if len(result) == 0 { 1055 return nil, nil 1056 } 1057 1058 return result, nil 1059 case celgo.NullType.TypeName(): 1060 return nil, nil 1061 default: 1062 return nil, fmt.Errorf("expression must return a string or a list of strings") 1063 } 1064 } 1065 1066 // messageFunc is a function that returns a message for a validation rule. 1067 type messageFunc func(authenticationcel.ExpressionAccessor) (string, error) 1068 1069 // checkValidationRulesEvaluation checks if the validation rules evaluation results 1070 // are valid. If the validation rules evaluation results are not valid, it returns 1071 // an error with an optional message that was set in the validation rule. 1072 func checkValidationRulesEvaluation(results []authenticationcel.EvaluationResult, messageFn messageFunc) error { 1073 for _, result := range results { 1074 if result.EvalResult.Type() != celgo.BoolType { 1075 return fmt.Errorf("validation expression must return a boolean") 1076 } 1077 if !result.EvalResult.Value().(bool) { 1078 expression := result.ExpressionAccessor.GetExpression() 1079 1080 message, err := messageFn(result.ExpressionAccessor) 1081 if err != nil { 1082 return err 1083 } 1084 1085 return fmt.Errorf("validation expression '%s' failed: %s", expression, message) 1086 } 1087 } 1088 1089 return nil 1090 } 1091 1092 func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) { 1093 if obj == nil || reflect.ValueOf(obj).IsNil() { 1094 return &unstructured.Unstructured{Object: nil}, nil 1095 } 1096 ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) 1097 if err != nil { 1098 return nil, err 1099 } 1100 return &unstructured.Unstructured{Object: ret}, nil 1101 } 1102 1103 func convertUserInfoToUnstructured(info user.Info) (*unstructured.Unstructured, error) { 1104 userInfo := &authenticationv1.UserInfo{ 1105 Extra: make(map[string]authenticationv1.ExtraValue), 1106 Groups: info.GetGroups(), 1107 UID: info.GetUID(), 1108 Username: info.GetName(), 1109 } 1110 // Convert the extra information in the user object 1111 for key, val := range info.GetExtra() { 1112 userInfo.Extra[key] = authenticationv1.ExtraValue(val) 1113 } 1114 1115 // Convert the user info to unstructured so that we can evaluate the CEL expressions 1116 // against the user info. This is done once here so that we don't have to convert 1117 // the user info to unstructured multiple times in the CEL mapper for each mapping. 1118 userInfoUnstructured, err := convertObjectToUnstructured(userInfo) 1119 if err != nil { 1120 return nil, err 1121 } 1122 1123 // check if the user info contains the required fields. If not, set them to empty values. 1124 // This is done because the CEL expressions expect these fields to be present. 1125 if userInfoUnstructured.Object["username"] == nil { 1126 userInfoUnstructured.Object["username"] = "" 1127 } 1128 if userInfoUnstructured.Object["uid"] == nil { 1129 userInfoUnstructured.Object["uid"] = "" 1130 } 1131 if userInfoUnstructured.Object["groups"] == nil { 1132 userInfoUnstructured.Object["groups"] = []string{} 1133 } 1134 if userInfoUnstructured.Object["extra"] == nil { 1135 userInfoUnstructured.Object["extra"] = map[string]authenticationv1.ExtraValue{} 1136 } 1137 return userInfoUnstructured, nil 1138 }