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