istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/server/ca/authenticate/oidc.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package authenticate 16 17 import ( 18 "context" 19 "fmt" 20 "strings" 21 22 oidc "github.com/coreos/go-oidc/v3/oidc" 23 24 "istio.io/api/security/v1beta1" 25 "istio.io/istio/pkg/security" 26 "istio.io/istio/pkg/spiffe" 27 ) 28 29 const ( 30 IDTokenAuthenticatorType = "IDTokenAuthenticator" 31 ) 32 33 type JwtAuthenticator struct { 34 audiences []string 35 verifier *oidc.IDTokenVerifier 36 } 37 38 var _ security.Authenticator = &JwtAuthenticator{} 39 40 // newJwtAuthenticator is used when running istiod outside of a cluster, to validate the tokens using OIDC 41 // K8S is created with --service-account-issuer, service-account-signing-key-file and service-account-api-audiences 42 // which enable OIDC. 43 func NewJwtAuthenticator(jwtRule *v1beta1.JWTRule) (*JwtAuthenticator, error) { 44 issuer := jwtRule.GetIssuer() 45 jwksURL := jwtRule.GetJwksUri() 46 // The key of a JWT issuer may change, so the key may need to be updated. 47 // Based on https://pkg.go.dev/github.com/coreos/go-oidc/v3/oidc#NewRemoteKeySet 48 // the oidc library handles caching and cache invalidation. Thus, the verifier 49 // is only created once in the constructor. 50 var verifier *oidc.IDTokenVerifier 51 if len(jwksURL) == 0 { 52 // OIDC discovery is used if jwksURL is not set. 53 provider, err := oidc.NewProvider(context.Background(), issuer) 54 // OIDC discovery may fail, e.g. http request for the OIDC server may fail. 55 if err != nil { 56 return nil, fmt.Errorf("failed at creating an OIDC provider for %v: %v", issuer, err) 57 } 58 verifier = provider.Verifier(&oidc.Config{SkipClientIDCheck: true}) 59 } else { 60 keySet := oidc.NewRemoteKeySet(context.Background(), jwksURL) 61 verifier = oidc.NewVerifier(issuer, keySet, &oidc.Config{SkipClientIDCheck: true}) 62 } 63 return &JwtAuthenticator{ 64 verifier: verifier, 65 audiences: jwtRule.Audiences, 66 }, nil 67 } 68 69 // Authenticate - based on the old OIDC authenticator for mesh expansion. 70 func (j *JwtAuthenticator) Authenticate(authRequest security.AuthContext) (*security.Caller, error) { 71 if authRequest.GrpcContext != nil { 72 bearerToken, err := security.ExtractBearerToken(authRequest.GrpcContext) 73 if err != nil { 74 return nil, fmt.Errorf("ID token extraction error: %v", err) 75 } 76 return j.authenticate(authRequest.GrpcContext, bearerToken) 77 } 78 if authRequest.Request != nil { 79 bearerToken, err := security.ExtractRequestToken(authRequest.Request) 80 if err != nil { 81 return nil, fmt.Errorf("target JWT extraction error: %v", err) 82 } 83 return j.authenticate(authRequest.Request.Context(), bearerToken) 84 } 85 return nil, nil 86 } 87 88 func (j *JwtAuthenticator) authenticate(ctx context.Context, bearerToken string) (*security.Caller, error) { 89 idToken, err := j.verifier.Verify(ctx, bearerToken) 90 if err != nil { 91 return nil, fmt.Errorf("failed to verify the JWT token (error %v)", err) 92 } 93 94 sa := JwtPayload{} 95 // "aud" for trust domain, "sub" has "system:serviceaccount:$namespace:$serviceaccount". 96 // in future trust domain may use another field as a standard is defined. 97 if err := idToken.Claims(&sa); err != nil { 98 return nil, fmt.Errorf("failed to extract claims from ID token: %v", err) 99 } 100 if !strings.HasPrefix(sa.Sub, "system:serviceaccount") { 101 return nil, fmt.Errorf("invalid sub %v", sa.Sub) 102 } 103 parts := strings.Split(sa.Sub, ":") 104 ns := parts[2] 105 ksa := parts[3] 106 if !checkAudience(sa.Aud, j.audiences) { 107 return nil, fmt.Errorf("invalid audiences %v", sa.Aud) 108 } 109 return &security.Caller{ 110 AuthSource: security.AuthSourceIDToken, 111 Identities: []string{spiffe.MustGenSpiffeURI(ns, ksa)}, 112 }, nil 113 } 114 115 // checkAudience() returns true if the audiences to check are in 116 // the expected audiences. Otherwise, return false. 117 func checkAudience(audToCheck []string, audExpected []string) bool { 118 for _, a := range audToCheck { 119 for _, b := range audExpected { 120 if a == b { 121 return true 122 } 123 } 124 } 125 return false 126 } 127 128 type JwtPayload struct { 129 // Aud is the expected audience, defaults to istio-ca - but is based on istiod.yaml configuration. 130 // If set to a different value - use the value defined by istiod.yaml. Env variable can 131 // still override 132 Aud []string `json:"aud"` 133 134 // Exp is not currently used - we don't use the token for authn, just to determine k8s settings 135 Exp int `json:"exp"` 136 137 // Issuer - configured by K8S admin for projected tokens. Will be used to verify all tokens. 138 Iss string `json:"iss"` 139 140 Sub string `json:"sub"` 141 } 142 143 func (j JwtAuthenticator) AuthenticatorType() string { 144 return IDTokenAuthenticatorType 145 }