github.com/argoproj/argo-cd/v2@v2.10.9/util/oidc/provider.go (about)

     1  package oidc
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"strings"
     9  
    10  	gooidc "github.com/coreos/go-oidc/v3/oidc"
    11  	log "github.com/sirupsen/logrus"
    12  	"golang.org/x/oauth2"
    13  
    14  	"github.com/argoproj/argo-cd/v2/util/security"
    15  	"github.com/argoproj/argo-cd/v2/util/settings"
    16  )
    17  
    18  // Provider is a wrapper around go-oidc provider to also provide the following features:
    19  // 1. lazy initialization/querying of the provider
    20  // 2. automatic detection of change in signing keys
    21  // 3. convenience function for verifying tokens
    22  // We have to initialize the provider lazily since Argo CD can be an OIDC client to itself (in the
    23  // case of dex reverse proxy), which presents a chicken-and-egg problem of (1) serving dex over
    24  // HTTP, and (2) querying the OIDC provider (ourself) to initialize the OIDC client.
    25  type Provider interface {
    26  	Endpoint() (*oauth2.Endpoint, error)
    27  
    28  	ParseConfig() (*OIDCConfiguration, error)
    29  
    30  	Verify(tokenString string, argoSettings *settings.ArgoCDSettings) (*gooidc.IDToken, error)
    31  }
    32  
    33  type providerImpl struct {
    34  	issuerURL      string
    35  	client         *http.Client
    36  	goOIDCProvider *gooidc.Provider
    37  }
    38  
    39  var _ Provider = &providerImpl{}
    40  
    41  // NewOIDCProvider initializes an OIDC provider
    42  func NewOIDCProvider(issuerURL string, client *http.Client) Provider {
    43  	return &providerImpl{
    44  		issuerURL: issuerURL,
    45  		client:    client,
    46  	}
    47  }
    48  
    49  // oidcProvider lazily initializes, memoizes, and returns the OIDC provider.
    50  func (p *providerImpl) provider() (*gooidc.Provider, error) {
    51  	if p.goOIDCProvider != nil {
    52  		return p.goOIDCProvider, nil
    53  	}
    54  	prov, err := p.newGoOIDCProvider()
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	p.goOIDCProvider = prov
    59  	return p.goOIDCProvider, nil
    60  }
    61  
    62  // newGoOIDCProvider creates a new instance of go-oidc.Provider querying the well known oidc
    63  // configuration path (http://example-argocd.com/api/dex/.well-known/openid-configuration)
    64  func (p *providerImpl) newGoOIDCProvider() (*gooidc.Provider, error) {
    65  	log.Infof("Initializing OIDC provider (issuer: %s)", p.issuerURL)
    66  	ctx := gooidc.ClientContext(context.Background(), p.client)
    67  	prov, err := gooidc.NewProvider(ctx, p.issuerURL)
    68  	if err != nil {
    69  		return nil, fmt.Errorf("Failed to query provider %q: %v", p.issuerURL, err)
    70  	}
    71  	s, _ := ParseConfig(prov)
    72  	log.Infof("OIDC supported scopes: %v", s.ScopesSupported)
    73  	return prov, nil
    74  }
    75  
    76  func (p *providerImpl) Verify(tokenString string, argoSettings *settings.ArgoCDSettings) (*gooidc.IDToken, error) {
    77  	// According to the JWT spec, the aud claim is optional. The spec also says (emphasis mine):
    78  	//
    79  	//   If the principal processing the claim does not identify itself with a value in the "aud" claim _when this
    80  	//   claim is present_, then the JWT MUST be rejected.
    81  	//
    82  	//     - https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3
    83  	//
    84  	// If the claim is not present, we can skip the audience claim check (called the "ClientID check" in go-oidc's
    85  	// terminology).
    86  	//
    87  	// The OIDC spec says that the aud claim is required (https://openid.net/specs/openid-connect-core-1_0.html#IDToken).
    88  	// But we cannot assume that all OIDC providers will follow the spec. For Argo CD <2.6.0, we will default to
    89  	// allowing the aud claim to be optional. In Argo CD >=2.6.0, we will default to requiring the aud claim to be
    90  	// present and give users the skipAudienceCheckWhenTokenHasNoAudience setting to revert the behavior if necessary.
    91  	//
    92  	// At this point, we have not verified that the token has not been altered. All code paths below MUST VERIFY
    93  	// THE TOKEN SIGNATURE to confirm that an attacker did not maliciously remove the "aud" claim.
    94  	unverifiedHasAudClaim, err := security.UnverifiedHasAudClaim(tokenString)
    95  	if err != nil {
    96  		return nil, fmt.Errorf("failed to determine whether the token has an aud claim: %w", err)
    97  	}
    98  
    99  	var idToken *gooidc.IDToken
   100  	if !unverifiedHasAudClaim {
   101  		idToken, err = p.verify("", tokenString, argoSettings.SkipAudienceCheckWhenTokenHasNoAudience())
   102  	} else {
   103  		allowedAudiences := argoSettings.OAuth2AllowedAudiences()
   104  		if len(allowedAudiences) == 0 {
   105  			return nil, errors.New("token has an audience claim, but no allowed audiences are configured")
   106  		}
   107  		// Token must be verified for at least one allowed audience
   108  		for _, aud := range allowedAudiences {
   109  			idToken, err = p.verify(aud, tokenString, false)
   110  			tokenExpiredError := &gooidc.TokenExpiredError{}
   111  			if errors.As(err, &tokenExpiredError) {
   112  				// If the token is expired, we won't bother checking other audiences. It's important to return a
   113  				// TokenExpiredError instead of an error related to an incorrect audience, because the caller may
   114  				// have specific behavior to handle expired tokens.
   115  				break
   116  			}
   117  			if err == nil {
   118  				break
   119  			}
   120  		}
   121  	}
   122  
   123  	if err != nil {
   124  		return nil, fmt.Errorf("failed to verify token: %w", err)
   125  	}
   126  
   127  	return idToken, nil
   128  }
   129  
   130  func (p *providerImpl) verify(clientID, tokenString string, skipClientIDCheck bool) (*gooidc.IDToken, error) {
   131  	ctx := context.Background()
   132  	prov, err := p.provider()
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	config := &gooidc.Config{ClientID: clientID, SkipClientIDCheck: skipClientIDCheck}
   137  	verifier := prov.Verifier(config)
   138  	idToken, err := verifier.Verify(ctx, tokenString)
   139  	if err != nil {
   140  		// HACK: if we failed token verification, it's possible the reason was because dex
   141  		// restarted and has new JWKS signing keys (we do not back dex with persistent storage
   142  		// so keys might be regenerated). Detect this by:
   143  		// 1. looking for the specific error message
   144  		// 2. re-initializing the OIDC provider
   145  		// 3. re-attempting token verification
   146  		// NOTE: the error message is sensitive to implementation of verifier.Verify()
   147  		if !strings.Contains(err.Error(), "failed to verify signature") {
   148  			return nil, err
   149  		}
   150  		newProvider, retryErr := p.newGoOIDCProvider()
   151  		if retryErr != nil {
   152  			// return original error if we fail to re-initialize OIDC
   153  			return nil, err
   154  		}
   155  		verifier = newProvider.Verifier(config)
   156  		idToken, err = verifier.Verify(ctx, tokenString)
   157  		if err != nil {
   158  			return nil, err
   159  		}
   160  		// If we get here, we successfully re-initialized OIDC and after re-initialization,
   161  		// the token is now valid.
   162  		log.Info("New OIDC settings detected")
   163  		p.goOIDCProvider = newProvider
   164  	}
   165  	return idToken, nil
   166  }
   167  
   168  func (p *providerImpl) Endpoint() (*oauth2.Endpoint, error) {
   169  	prov, err := p.provider()
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	endpoint := prov.Endpoint()
   174  	return &endpoint, nil
   175  }
   176  
   177  // ParseConfig parses the OIDC Config into the concrete datastructure
   178  func (p *providerImpl) ParseConfig() (*OIDCConfiguration, error) {
   179  	prov, err := p.provider()
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	return ParseConfig(prov)
   184  }
   185  
   186  // ParseConfig parses the OIDC Config into the concrete datastructure
   187  func ParseConfig(provider *gooidc.Provider) (*OIDCConfiguration, error) {
   188  	var conf OIDCConfiguration
   189  	err := provider.Claims(&conf)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  	return &conf, nil
   194  }