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 }