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

     1  package oidc
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"strings"
     8  
     9  	gooidc "github.com/coreos/go-oidc"
    10  	log "github.com/sirupsen/logrus"
    11  	"golang.org/x/oauth2"
    12  )
    13  
    14  // Provider is a wrapper around go-oidc provider to also provide the following features:
    15  // 1. lazy initialization/querying of the provider
    16  // 2. automatic detection of change in signing keys
    17  // 3. convenience function for verifying tokens
    18  // We have to initialize the provider lazily since Argo CD can be an OIDC client to itself (in the
    19  // case of dex reverse proxy), which presents a chicken-and-egg problem of (1) serving dex over
    20  // HTTP, and (2) querying the OIDC provider (ourself) to initialize the OIDC client.
    21  type Provider interface {
    22  	Endpoint() (*oauth2.Endpoint, error)
    23  
    24  	ParseConfig() (*OIDCConfiguration, error)
    25  
    26  	Verify(clientID, tokenString string) (*gooidc.IDToken, error)
    27  }
    28  
    29  type providerImpl struct {
    30  	issuerURL      string
    31  	client         *http.Client
    32  	goOIDCProvider *gooidc.Provider
    33  }
    34  
    35  // NewOIDCProvider initializes an OIDC provider
    36  func NewOIDCProvider(issuerURL string, client *http.Client) Provider {
    37  	return &providerImpl{
    38  		issuerURL: issuerURL,
    39  		client:    client,
    40  	}
    41  }
    42  
    43  // oidcProvider lazily initializes, memoizes, and returns the OIDC provider.
    44  func (p *providerImpl) provider() (*gooidc.Provider, error) {
    45  	if p.goOIDCProvider != nil {
    46  		return p.goOIDCProvider, nil
    47  	}
    48  	prov, err := p.newGoOIDCProvider()
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	p.goOIDCProvider = prov
    53  	return p.goOIDCProvider, nil
    54  }
    55  
    56  // newGoOIDCProvider creates a new instance of go-oidc.Provider querying the well known oidc
    57  // configuration path (http://example-argocd.com/api/dex/.well-known/openid-configuration)
    58  func (p *providerImpl) newGoOIDCProvider() (*gooidc.Provider, error) {
    59  	log.Infof("Initializing OIDC provider (issuer: %s)", p.issuerURL)
    60  	ctx := gooidc.ClientContext(context.Background(), p.client)
    61  	prov, err := gooidc.NewProvider(ctx, p.issuerURL)
    62  	if err != nil {
    63  		return nil, fmt.Errorf("Failed to query provider %q: %v", p.issuerURL, err)
    64  	}
    65  	s, _ := ParseConfig(prov)
    66  	log.Infof("OIDC supported scopes: %v", s.ScopesSupported)
    67  	return prov, nil
    68  }
    69  
    70  func (p *providerImpl) Verify(clientID, tokenString string) (*gooidc.IDToken, error) {
    71  	ctx := context.Background()
    72  	prov, err := p.provider()
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	verifier := prov.Verifier(&gooidc.Config{ClientID: clientID})
    77  	idToken, err := verifier.Verify(ctx, tokenString)
    78  	if err != nil {
    79  		// HACK: if we failed token verification, it's possible the reason was because dex
    80  		// restarted and has new JWKS signing keys (we do not back dex with persistent storage
    81  		// so keys might be regenerated). Detect this by:
    82  		// 1. looking for the specific error message
    83  		// 2. re-initializing the OIDC provider
    84  		// 3. re-attempting token verification
    85  		// NOTE: the error message is sensitive to implementation of verifier.Verify()
    86  		if !strings.Contains(err.Error(), "failed to verify signature") {
    87  			return nil, err
    88  		}
    89  		newProvider, retryErr := p.newGoOIDCProvider()
    90  		if retryErr != nil {
    91  			// return original error if we fail to re-initialize OIDC
    92  			return nil, err
    93  		}
    94  		verifier = newProvider.Verifier(&gooidc.Config{ClientID: clientID})
    95  		idToken, err = verifier.Verify(ctx, tokenString)
    96  		if err != nil {
    97  			return nil, err
    98  		}
    99  		// If we get here, we successfully re-initialized OIDC and after re-initialization,
   100  		// the token is now valid.
   101  		log.Info("New OIDC settings detected")
   102  		p.goOIDCProvider = newProvider
   103  	}
   104  	return idToken, nil
   105  }
   106  
   107  func (p *providerImpl) Endpoint() (*oauth2.Endpoint, error) {
   108  	prov, err := p.provider()
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  	endpoint := prov.Endpoint()
   113  	return &endpoint, nil
   114  }
   115  
   116  // ParseConfig parses the OIDC Config into the concrete datastructure
   117  func (p *providerImpl) ParseConfig() (*OIDCConfiguration, error) {
   118  	prov, err := p.provider()
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  	return ParseConfig(prov)
   123  }
   124  
   125  // ParseConfig parses the OIDC Config into the concrete datastructure
   126  func ParseConfig(provider *gooidc.Provider) (*OIDCConfiguration, error) {
   127  	var conf OIDCConfiguration
   128  	err := provider.Claims(&conf)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	return &conf, nil
   133  }