mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-10-30 21:30:16 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			337 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			337 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package oidc
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"encoding/base64"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io/ioutil"
 | |
| 	"net/http"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"golang.org/x/oauth2"
 | |
| 	jose "gopkg.in/square/go-jose.v2"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	issuerGoogleAccounts         = "https://accounts.google.com"
 | |
| 	issuerGoogleAccountsNoScheme = "accounts.google.com"
 | |
| )
 | |
| 
 | |
| // KeySet is a set of publc JSON Web Keys that can be used to validate the signature
 | |
| // of JSON web tokens. This is expected to be backed by a remote key set through
 | |
| // provider metadata discovery or an in-memory set of keys delivered out-of-band.
 | |
| type KeySet interface {
 | |
| 	// VerifySignature parses the JSON web token, verifies the signature, and returns
 | |
| 	// the raw payload. Header and claim fields are validated by other parts of the
 | |
| 	// package. For example, the KeySet does not need to check values such as signature
 | |
| 	// algorithm, issuer, and audience since the IDTokenVerifier validates these values
 | |
| 	// independently.
 | |
| 	//
 | |
| 	// If VerifySignature makes HTTP requests to verify the token, it's expected to
 | |
| 	// use any HTTP client associated with the context through ClientContext.
 | |
| 	VerifySignature(ctx context.Context, jwt string) (payload []byte, err error)
 | |
| }
 | |
| 
 | |
| // IDTokenVerifier provides verification for ID Tokens.
 | |
| type IDTokenVerifier struct {
 | |
| 	keySet KeySet
 | |
| 	config *Config
 | |
| 	issuer string
 | |
| }
 | |
| 
 | |
| // NewVerifier returns a verifier manually constructed from a key set and issuer URL.
 | |
| //
 | |
| // It's easier to use provider discovery to construct an IDTokenVerifier than creating
 | |
| // one directly. This method is intended to be used with provider that don't support
 | |
| // metadata discovery, or avoiding round trips when the key set URL is already known.
 | |
| //
 | |
| // This constructor can be used to create a verifier directly using the issuer URL and
 | |
| // JSON Web Key Set URL without using discovery:
 | |
| //
 | |
| //		keySet := oidc.NewRemoteKeySet(ctx, "https://www.googleapis.com/oauth2/v3/certs")
 | |
| //		verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config)
 | |
| //
 | |
| // Since KeySet is an interface, this constructor can also be used to supply custom
 | |
| // public key sources. For example, if a user wanted to supply public keys out-of-band
 | |
| // and hold them statically in-memory:
 | |
| //
 | |
| //		// Custom KeySet implementation.
 | |
| //		keySet := newStatisKeySet(publicKeys...)
 | |
| //
 | |
| //		// Verifier uses the custom KeySet implementation.
 | |
| //		verifier := oidc.NewVerifier("https://auth.example.com", keySet, config)
 | |
| //
 | |
| func NewVerifier(issuerURL string, keySet KeySet, config *Config) *IDTokenVerifier {
 | |
| 	return &IDTokenVerifier{keySet: keySet, config: config, issuer: issuerURL}
 | |
| }
 | |
| 
 | |
| // Config is the configuration for an IDTokenVerifier.
 | |
| type Config struct {
 | |
| 	// Expected audience of the token. For a majority of the cases this is expected to be
 | |
| 	// the ID of the client that initialized the login flow. It may occasionally differ if
 | |
| 	// the provider supports the authorizing party (azp) claim.
 | |
| 	//
 | |
| 	// If not provided, users must explicitly set SkipClientIDCheck.
 | |
| 	ClientID string
 | |
| 	// If specified, only this set of algorithms may be used to sign the JWT.
 | |
| 	//
 | |
| 	// If the IDTokenVerifier is created from a provider with (*Provider).Verifier, this
 | |
| 	// defaults to the set of algorithms the provider supports. Otherwise this values
 | |
| 	// defaults to RS256.
 | |
| 	SupportedSigningAlgs []string
 | |
| 
 | |
| 	// If true, no ClientID check performed. Must be true if ClientID field is empty.
 | |
| 	SkipClientIDCheck bool
 | |
| 	// If true, token expiry is not checked.
 | |
| 	SkipExpiryCheck bool
 | |
| 
 | |
| 	// SkipIssuerCheck is intended for specialized cases where the the caller wishes to
 | |
| 	// defer issuer validation. When enabled, callers MUST independently verify the Token's
 | |
| 	// Issuer is a known good value.
 | |
| 	//
 | |
| 	// Mismatched issuers often indicate client mis-configuration. If mismatches are
 | |
| 	// unexpected, evaluate if the provided issuer URL is incorrect instead of enabling
 | |
| 	// this option.
 | |
| 	SkipIssuerCheck bool
 | |
| 
 | |
| 	// Time function to check Token expiry. Defaults to time.Now
 | |
| 	Now func() time.Time
 | |
| }
 | |
| 
 | |
| // Verifier returns an IDTokenVerifier that uses the provider's key set to verify JWTs.
 | |
| //
 | |
| // The returned IDTokenVerifier is tied to the Provider's context and its behavior is
 | |
| // undefined once the Provider's context is canceled.
 | |
| func (p *Provider) Verifier(config *Config) *IDTokenVerifier {
 | |
| 	if len(config.SupportedSigningAlgs) == 0 && len(p.algorithms) > 0 {
 | |
| 		// Make a copy so we don't modify the config values.
 | |
| 		cp := &Config{}
 | |
| 		*cp = *config
 | |
| 		cp.SupportedSigningAlgs = p.algorithms
 | |
| 		config = cp
 | |
| 	}
 | |
| 	return NewVerifier(p.issuer, p.remoteKeySet, config)
 | |
| }
 | |
| 
 | |
| func parseJWT(p string) ([]byte, error) {
 | |
| 	parts := strings.Split(p, ".")
 | |
| 	if len(parts) < 2 {
 | |
| 		return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts))
 | |
| 	}
 | |
| 	payload, err := base64.RawURLEncoding.DecodeString(parts[1])
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err)
 | |
| 	}
 | |
| 	return payload, nil
 | |
| }
 | |
| 
 | |
| func contains(sli []string, ele string) bool {
 | |
| 	for _, s := range sli {
 | |
| 		if s == ele {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // Returns the Claims from the distributed JWT token
 | |
| func resolveDistributedClaim(ctx context.Context, verifier *IDTokenVerifier, src claimSource) ([]byte, error) {
 | |
| 	req, err := http.NewRequest("GET", src.Endpoint, nil)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("malformed request: %v", err)
 | |
| 	}
 | |
| 	if src.AccessToken != "" {
 | |
| 		req.Header.Set("Authorization", "Bearer "+src.AccessToken)
 | |
| 	}
 | |
| 
 | |
| 	resp, err := doRequest(ctx, req)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("oidc: Request to endpoint failed: %v", err)
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 
 | |
| 	body, err := ioutil.ReadAll(resp.Body)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("unable to read response body: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	if resp.StatusCode != http.StatusOK {
 | |
| 		return nil, fmt.Errorf("oidc: request failed: %v", resp.StatusCode)
 | |
| 	}
 | |
| 
 | |
| 	token, err := verifier.Verify(ctx, string(body))
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("malformed response body: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	return token.claims, nil
 | |
| }
 | |
| 
 | |
| func parseClaim(raw []byte, name string, v interface{}) error {
 | |
| 	var parsed map[string]json.RawMessage
 | |
| 	if err := json.Unmarshal(raw, &parsed); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	val, ok := parsed[name]
 | |
| 	if !ok {
 | |
| 		return fmt.Errorf("claim doesn't exist: %s", name)
 | |
| 	}
 | |
| 
 | |
| 	return json.Unmarshal([]byte(val), v)
 | |
| }
 | |
| 
 | |
| // Verify parses a raw ID Token, verifies it's been signed by the provider, preforms
 | |
| // any additional checks depending on the Config, and returns the payload.
 | |
| //
 | |
| // Verify does NOT do nonce validation, which is the callers responsibility.
 | |
| //
 | |
| // See: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
 | |
| //
 | |
| //    oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
 | |
| //    if err != nil {
 | |
| //        // handle error
 | |
| //    }
 | |
| //
 | |
| //    // Extract the ID Token from oauth2 token.
 | |
| //    rawIDToken, ok := oauth2Token.Extra("id_token").(string)
 | |
| //    if !ok {
 | |
| //        // handle error
 | |
| //    }
 | |
| //
 | |
| //    token, err := verifier.Verify(ctx, rawIDToken)
 | |
| //
 | |
| func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDToken, error) {
 | |
| 	jws, err := jose.ParseSigned(rawIDToken)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Throw out tokens with invalid claims before trying to verify the token. This lets
 | |
| 	// us do cheap checks before possibly re-syncing keys.
 | |
| 	payload, err := parseJWT(rawIDToken)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
 | |
| 	}
 | |
| 	var token idToken
 | |
| 	if err := json.Unmarshal(payload, &token); err != nil {
 | |
| 		return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	distributedClaims := make(map[string]claimSource)
 | |
| 
 | |
| 	//step through the token to map claim names to claim sources"
 | |
| 	for cn, src := range token.ClaimNames {
 | |
| 		if src == "" {
 | |
| 			return nil, fmt.Errorf("oidc: failed to obtain source from claim name")
 | |
| 		}
 | |
| 		s, ok := token.ClaimSources[src]
 | |
| 		if !ok {
 | |
| 			return nil, fmt.Errorf("oidc: source does not exist")
 | |
| 		}
 | |
| 		distributedClaims[cn] = s
 | |
| 	}
 | |
| 
 | |
| 	t := &IDToken{
 | |
| 		Issuer:            token.Issuer,
 | |
| 		Subject:           token.Subject,
 | |
| 		Audience:          []string(token.Audience),
 | |
| 		Expiry:            time.Time(token.Expiry),
 | |
| 		IssuedAt:          time.Time(token.IssuedAt),
 | |
| 		Nonce:             token.Nonce,
 | |
| 		AccessTokenHash:   token.AtHash,
 | |
| 		claims:            payload,
 | |
| 		distributedClaims: distributedClaims,
 | |
| 	}
 | |
| 
 | |
| 	// Check issuer.
 | |
| 	if !v.config.SkipIssuerCheck && t.Issuer != v.issuer {
 | |
| 		// Google sometimes returns "accounts.google.com" as the issuer claim instead of
 | |
| 		// the required "https://accounts.google.com". Detect this case and allow it only
 | |
| 		// for Google.
 | |
| 		//
 | |
| 		// We will not add hooks to let other providers go off spec like this.
 | |
| 		if !(v.issuer == issuerGoogleAccounts && t.Issuer == issuerGoogleAccountsNoScheme) {
 | |
| 			return nil, fmt.Errorf("oidc: id token issued by a different provider, expected %q got %q", v.issuer, t.Issuer)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// If a client ID has been provided, make sure it's part of the audience. SkipClientIDCheck must be true if ClientID is empty.
 | |
| 	//
 | |
| 	// This check DOES NOT ensure that the ClientID is the party to which the ID Token was issued (i.e. Authorized party).
 | |
| 	if !v.config.SkipClientIDCheck {
 | |
| 		if v.config.ClientID != "" {
 | |
| 			if !contains(t.Audience, v.config.ClientID) {
 | |
| 				return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience)
 | |
| 			}
 | |
| 		} else {
 | |
| 			return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set")
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// If a SkipExpiryCheck is false, make sure token is not expired.
 | |
| 	if !v.config.SkipExpiryCheck {
 | |
| 		now := time.Now
 | |
| 		if v.config.Now != nil {
 | |
| 			now = v.config.Now
 | |
| 		}
 | |
| 		nowTime := now()
 | |
| 
 | |
| 		if t.Expiry.Before(nowTime) {
 | |
| 			return nil, fmt.Errorf("oidc: token is expired (Token Expiry: %v)", t.Expiry)
 | |
| 		}
 | |
| 
 | |
| 		// If nbf claim is provided in token, ensure that it is indeed in the past.
 | |
| 		if token.NotBefore != nil {
 | |
| 			nbfTime := time.Time(*token.NotBefore)
 | |
| 			leeway := 1 * time.Minute
 | |
| 
 | |
| 			if nowTime.Add(leeway).Before(nbfTime) {
 | |
| 				return nil, fmt.Errorf("oidc: current time %v before the nbf (not before) time: %v", nowTime, nbfTime)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	switch len(jws.Signatures) {
 | |
| 	case 0:
 | |
| 		return nil, fmt.Errorf("oidc: id token not signed")
 | |
| 	case 1:
 | |
| 	default:
 | |
| 		return nil, fmt.Errorf("oidc: multiple signatures on id token not supported")
 | |
| 	}
 | |
| 
 | |
| 	sig := jws.Signatures[0]
 | |
| 	supportedSigAlgs := v.config.SupportedSigningAlgs
 | |
| 	if len(supportedSigAlgs) == 0 {
 | |
| 		supportedSigAlgs = []string{RS256}
 | |
| 	}
 | |
| 
 | |
| 	if !contains(supportedSigAlgs, sig.Header.Algorithm) {
 | |
| 		return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm)
 | |
| 	}
 | |
| 
 | |
| 	t.sigAlgorithm = sig.Header.Algorithm
 | |
| 
 | |
| 	gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to verify signature: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Ensure that the payload returned by the square actually matches the payload parsed earlier.
 | |
| 	if !bytes.Equal(gotPayload, payload) {
 | |
| 		return nil, errors.New("oidc: internal error, payload parsed did not match previous payload")
 | |
| 	}
 | |
| 
 | |
| 	return t, nil
 | |
| }
 | |
| 
 | |
| // Nonce returns an auth code option which requires the ID Token created by the
 | |
| // OpenID Connect provider to contain the specified nonce.
 | |
| func Nonce(nonce string) oauth2.AuthCodeOption {
 | |
| 	return oauth2.SetAuthURLParam("nonce", nonce)
 | |
| }
 |