github.com/argoproj/argo-cd/v3@v3.2.1/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/v3/util/security" 15 "github.com/argoproj/argo-cd/v3/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: %w", p.issuerURL, err) 70 } 71 s, _ := ParseConfig(prov) 72 log.Infof("OIDC supported scopes: %v", s.ScopesSupported) 73 return prov, nil 74 } 75 76 type tokenVerificationError struct { 77 errorsByAudience map[string]error 78 } 79 80 func (t tokenVerificationError) Error() string { 81 var errorStrings []string 82 for aud, err := range t.errorsByAudience { 83 errorStrings = append(errorStrings, fmt.Sprintf("error for aud %q: %v", aud, err)) 84 } 85 return "token verification failed for all audiences: " + strings.Join(errorStrings, ", ") 86 } 87 88 func (p *providerImpl) Verify(tokenString string, argoSettings *settings.ArgoCDSettings) (*gooidc.IDToken, error) { 89 // According to the JWT spec, the aud claim is optional. The spec also says (emphasis mine): 90 // 91 // If the principal processing the claim does not identify itself with a value in the "aud" claim _when this 92 // claim is present_, then the JWT MUST be rejected. 93 // 94 // - https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3 95 // 96 // If the claim is not present, we can skip the audience claim check (called the "ClientID check" in go-oidc's 97 // terminology). 98 // 99 // The OIDC spec says that the aud claim is required (https://openid.net/specs/openid-connect-core-1_0.html#IDToken). 100 // But we cannot assume that all OIDC providers will follow the spec. For Argo CD <2.6.0, we will default to 101 // allowing the aud claim to be optional. In Argo CD >=2.6.0, we will default to requiring the aud claim to be 102 // present and give users the skipAudienceCheckWhenTokenHasNoAudience setting to revert the behavior if necessary. 103 // 104 // At this point, we have not verified that the token has not been altered. All code paths below MUST VERIFY 105 // THE TOKEN SIGNATURE to confirm that an attacker did not maliciously remove the "aud" claim. 106 unverifiedHasAudClaim, err := security.UnverifiedHasAudClaim(tokenString) 107 if err != nil { 108 return nil, fmt.Errorf("failed to determine whether the token has an aud claim: %w", err) 109 } 110 111 var idToken *gooidc.IDToken 112 if !unverifiedHasAudClaim { 113 idToken, err = p.verify("", tokenString, argoSettings.SkipAudienceCheckWhenTokenHasNoAudience()) 114 } else { 115 allowedAudiences := argoSettings.OAuth2AllowedAudiences() 116 if len(allowedAudiences) == 0 { 117 return nil, errors.New("token has an audience claim, but no allowed audiences are configured") 118 } 119 tokenVerificationErrors := make(map[string]error) 120 // Token must be verified for at least one allowed audience 121 for _, aud := range allowedAudiences { 122 idToken, err = p.verify(aud, tokenString, false) 123 tokenExpiredError := &gooidc.TokenExpiredError{} 124 if errors.As(err, &tokenExpiredError) { 125 // If the token is expired, we won't bother checking other audiences. It's important to return a 126 // TokenExpiredError instead of an error related to an incorrect audience, because the caller may 127 // have specific behavior to handle expired tokens. 128 break 129 } 130 if err == nil { 131 break 132 } 133 // We store the error for each audience so that we can return a more detailed error message to the user. 134 // If this gets merged, we'll be able to detect failures unrelated to audiences and short-circuit this loop 135 // to avoid logging irrelevant warnings: https://github.com/coreos/go-oidc/pull/406 136 tokenVerificationErrors[aud] = err 137 } 138 // If the most recent attempt encountered an error, and if we have collected multiple errors, switch to the 139 // other error type to gather more context. 140 if err != nil && len(tokenVerificationErrors) > 0 { 141 err = tokenVerificationError{errorsByAudience: tokenVerificationErrors} 142 } 143 } 144 145 if err != nil { 146 return nil, fmt.Errorf("failed to verify token: %w", err) 147 } 148 149 return idToken, nil 150 } 151 152 func (p *providerImpl) verify(clientID, tokenString string, skipClientIDCheck bool) (*gooidc.IDToken, error) { 153 ctx := context.Background() 154 prov, err := p.provider() 155 if err != nil { 156 return nil, err 157 } 158 config := &gooidc.Config{ClientID: clientID, SkipClientIDCheck: skipClientIDCheck} 159 verifier := prov.Verifier(config) 160 idToken, err := verifier.Verify(ctx, tokenString) 161 if err != nil { 162 // HACK: if we failed token verification, it's possible the reason was because dex 163 // restarted and has new JWKS signing keys (we do not back dex with persistent storage 164 // so keys might be regenerated). Detect this by: 165 // 1. looking for the specific error message 166 // 2. re-initializing the OIDC provider 167 // 3. re-attempting token verification 168 // NOTE: the error message is sensitive to implementation of verifier.Verify() 169 if !strings.Contains(err.Error(), "failed to verify signature") { 170 return nil, err 171 } 172 newProvider, retryErr := p.newGoOIDCProvider() 173 if retryErr != nil { 174 // return original error if we fail to re-initialize OIDC 175 return nil, err 176 } 177 verifier = newProvider.Verifier(config) 178 idToken, err = verifier.Verify(ctx, tokenString) 179 if err != nil { 180 return nil, err 181 } 182 // If we get here, we successfully re-initialized OIDC and after re-initialization, 183 // the token is now valid. 184 log.Info("New OIDC settings detected") 185 p.goOIDCProvider = newProvider 186 } 187 return idToken, nil 188 } 189 190 func (p *providerImpl) Endpoint() (*oauth2.Endpoint, error) { 191 prov, err := p.provider() 192 if err != nil { 193 return nil, err 194 } 195 endpoint := prov.Endpoint() 196 return &endpoint, nil 197 } 198 199 // ParseConfig parses the OIDC Config into the concrete datastructure 200 func (p *providerImpl) ParseConfig() (*OIDCConfiguration, error) { 201 prov, err := p.provider() 202 if err != nil { 203 return nil, err 204 } 205 return ParseConfig(prov) 206 } 207 208 // ParseConfig parses the OIDC Config into the concrete datastructure 209 func ParseConfig(provider *gooidc.Provider) (*OIDCConfiguration, error) { 210 var conf OIDCConfiguration 211 err := provider.Claims(&conf) 212 if err != nil { 213 return nil, err 214 } 215 return &conf, nil 216 }