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  }