@@ -2,8 +2,18 @@ package providers
22
33import (
44 "context"
5+ "errors"
6+ "fmt"
7+ "net/url"
8+ "strings"
59
10+ "github.com/coreos/go-oidc/v3/oidc"
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
612 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
13+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
14+ internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
15+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
16+ k8serrors "k8s.io/apimachinery/pkg/util/errors"
717)
818
919// Provider represents an upstream identity provider implementation
@@ -20,38 +30,247 @@ type Provider interface {
2030 CreateSessionFromToken(ctx context.Context, token string) (*sessions.SessionState, error)
2131}
2232
23−// New provides a new Provider based on the configured provider string
24−func New(provider string, p *ProviderData) Provider {
25− switch provider {
26− case "linkedin":
27− return NewLinkedInProvider(p)
28− case "facebook":
29− return NewFacebookProvider(p)
30− case "github":
31− return NewGitHubProvider(p)
32− case "keycloak":
33− return NewKeycloakProvider(p)
34− case "keycloak-oidc":
35− return NewKeycloakOIDCProvider(p)
36− case "azure":
37− return NewAzureProvider(p)
38− case "adfs":
39− return NewADFSProvider(p)
40− case "gitlab":
41− return NewGitLabProvider(p)
42− case "oidc":
43− return NewOIDCProvider(p)
44− case "login.gov":
45− return NewLoginGovProvider(p)
46− case "bitbucket":
47− return NewBitbucketProvider(p)
48− case "nextcloud":
49− return NewNextcloudProvider(p)
50− case "digitalocean":
51− return NewDigitalOceanProvider(p)
52− case "google":
53− return NewGoogleProvider(p)
33+func NewProvider(providerConfig options.Provider) (Provider, error) {
34+ providerData, err := newProviderDataFromConfig(providerConfig)
35+ if err != nil {
36+ return nil, fmt.Errorf("could not create provider data: %v", err)
37+ }
38+ switch providerConfig.Type {
39+ case options.ADFSProvider:
40+ return NewADFSProvider(providerData, providerConfig.ADFSConfig), nil
41+ case options.AzureProvider:
42+ return NewAzureProvider(providerData, providerConfig.AzureConfig), nil
43+ case options.BitbucketProvider:
44+ return NewBitbucketProvider(providerData, providerConfig.BitbucketConfig), nil
45+ case options.DigitalOceanProvider:
46+ return NewDigitalOceanProvider(providerData), nil
47+ case options.FacebookProvider:
48+ return NewFacebookProvider(providerData), nil
49+ case options.GitHubProvider:
50+ return NewGitHubProvider(providerData, providerConfig.GitHubConfig), nil
51+ case options.GitLabProvider:
52+ return NewGitLabProvider(providerData, providerConfig.GitLabConfig)
53+ case options.GoogleProvider:
54+ return NewGoogleProvider(providerData, providerConfig.GoogleConfig)
55+ case options.KeycloakProvider:
56+ return NewKeycloakProvider(providerData, providerConfig.KeycloakConfig), nil
57+ case options.KeycloakOIDCProvider:
58+ return NewKeycloakOIDCProvider(providerData, providerConfig.KeycloakConfig), nil
59+ case options.LinkedInProvider:
60+ return NewLinkedInProvider(providerData), nil
61+ case options.LoginGovProvider:
62+ return NewLoginGovProvider(providerData, providerConfig.LoginGovConfig)
63+ case options.NextCloudProvider:
64+ return NewNextcloudProvider(providerData), nil
65+ case options.OIDCProvider:
66+ return NewOIDCProvider(providerData, providerConfig.OIDCConfig), nil
67+ default:
68+ return nil, fmt.Errorf("unknown provider type %q", providerConfig.Type)
69+ }
70+}
71+
72+func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData, error) {
73+ p := &ProviderData{
74+ Scope: providerConfig.Scope,
75+ ClientID: providerConfig.ClientID,
76+ ClientSecret: providerConfig.ClientSecret,
77+ ClientSecretFile: providerConfig.ClientSecretFile,
78+ Prompt: providerConfig.Prompt,
79+ ApprovalPrompt: providerConfig.ApprovalPrompt,
80+ AcrValues: providerConfig.AcrValues,
81+ }
82+
83+ needsVerifier, err := providerRequiresOIDCProviderVerifier(providerConfig.Type)
84+ if err != nil {
85+ return nil, err
86+ }
87+
88+ if needsVerifier {
89+ oidcProvider, verifier, err := newOIDCProviderVerifier(providerConfig)
90+ if err != nil {
91+ return nil, fmt.Errorf("error setting OIDC configuration: %v", err)
92+ }
93+
94+ p.Verifier = verifier
95+ if oidcProvider != nil {
96+ // Use the discovered values rather than any specified values
97+ providerConfig.LoginURL = oidcProvider.Endpoint().AuthURL
98+ providerConfig.RedeemURL = oidcProvider.Endpoint().TokenURL
99+ }
100+ }
101+
102+ errs := []error{}
103+ for name, u := range map[string]struct {
104+ dst *url.URL
105+ raw string
106+ }{
107+ "login": {dst: p.LoginURL, raw: providerConfig.LoginURL},
108+ "redeem": {dst: p.RedeemURL, raw: providerConfig.RedeemURL},
109+ "profile": {dst: p.ProfileURL, raw: providerConfig.ProfileURL},
110+ "validate": {dst: p.ValidateURL, raw: providerConfig.ValidateURL},
111+ "resource": {dst: p.ProtectedResource, raw: providerConfig.ProtectedResource},
112+ } {
113+ var err error
114+ u.dst, err = url.Parse(u.raw)
115+ if err != nil {
116+ errs = append(errs, fmt.Errorf("could not parse %s URL: %v", name, err))
117+ }
118+ }
119+ if len(errs) > 0 {
120+ return nil, k8serrors.NewAggregate(errs)
121+ }
122+
123+ // Make the OIDC options available to all providers that support it
124+ p.AllowUnverifiedEmail = providerConfig.OIDCConfig.InsecureAllowUnverifiedEmail
125+ p.EmailClaim = providerConfig.OIDCConfig.EmailClaim
126+ p.GroupsClaim = providerConfig.OIDCConfig.GroupsClaim
127+
128+ // TODO (@NickMeves) - Remove This
129+ // Backwards Compatibility for Deprecated UserIDClaim option
130+ if providerConfig.OIDCConfig.EmailClaim == options.OIDCEmailClaim &&
131+ providerConfig.OIDCConfig.UserIDClaim != options.OIDCEmailClaim {
132+ p.EmailClaim = providerConfig.OIDCConfig.UserIDClaim
133+ }
134+
135+ if providerConfig.Scope == "" {
136+ providerConfig.Scope = "openid email profile"
137+
138+ if len(providerConfig.AllowedGroups) > 0 {
139+ providerConfig.Scope += " groups"
140+ }
141+ }
142+ if providerConfig.OIDCConfig.UserIDClaim == "" {
143+ providerConfig.OIDCConfig.UserIDClaim = "email"
144+ }
145+
146+ p.setAllowedGroups(providerConfig.AllowedGroups)
147+
148+ return p, nil
149+}
150+
151+func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bool, error) {
152+ switch providerType {
153+ case options.BitbucketProvider, options.DigitalOceanProvider, options.FacebookProvider, options.GitHubProvider,
154+ options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider, options.NextCloudProvider:
155+ return false, nil
156+ case options.ADFSProvider, options.AzureProvider, options.GitLabProvider, options.KeycloakOIDCProvider, options.OIDCProvider:
157+ return true, nil
54158 default:
55− return nil
159+ return false, fmt.Errorf("unknown provider type: %s", providerType)
160+ }
161+}
162+
163+func newOIDCProviderVerifier(providerConfig options.Provider) (*oidc.Provider, *internaloidc.IDTokenVerifier, error) {
164+ // If the issuer isn't set, default it for platforms where it makes sense
165+ if providerConfig.OIDCConfig.IssuerURL == "" {
166+ switch providerConfig.Type {
167+ case "gitlab":
168+ providerConfig.OIDCConfig.IssuerURL = "https://gitlab.com"
169+ case "oidc":
170+ return nil, nil, errors.New("missing required setting: OIDC Issuer URL cannot be empty")
171+ }
172+ }
173+
174+ switch {
175+ case providerConfig.OIDCConfig.InsecureSkipIssuerVerification && !providerConfig.OIDCConfig.SkipDiscovery:
176+ verifier, err := newInsecureSkipIssuerVerificationOIDCVerifier(providerConfig)
177+ return nil, verifier, err
178+ case providerConfig.OIDCConfig.SkipDiscovery:
179+ verifier, err := newSkipDiscoveryOIDCVerifier(providerConfig)
180+ return nil, verifier, err
181+ default:
182+ return newDiscoveryOIDCProviderVerifier(providerConfig)
183+ }
184+}
185+
186+func newDiscoveryOIDCProviderVerifier(providerConfig options.Provider) (*oidc.Provider, *internaloidc.IDTokenVerifier, error) {
187+ // Configure discoverable provider data.
188+ provider, err := oidc.NewProvider(context.TODO(), providerConfig.OIDCConfig.IssuerURL)
189+ if err != nil {
190+ return nil, nil, err
191+ }
192+ verificationOptions := &internaloidc.IDTokenVerificationOptions{
193+ AudienceClaims: providerConfig.OIDCConfig.AudienceClaims,
194+ ClientID: providerConfig.ClientID,
195+ ExtraAudiences: providerConfig.OIDCConfig.ExtraAudiences,
196+ }
197+ verifier := internaloidc.NewVerifier(provider.Verifier(&oidc.Config{
198+ ClientID: providerConfig.ClientID,
199+ SkipIssuerCheck: providerConfig.OIDCConfig.InsecureSkipIssuerVerification,
200+ SkipClientIDCheck: true, // client id check is done within oauth2-proxy: IDTokenVerifier.Verify
201+ }), verificationOptions)
202+
203+ return provider, verifier, nil
204+}
205+
206+func newInsecureSkipIssuerVerificationOIDCVerifier(providerConfig options.Provider) (*internaloidc.IDTokenVerifier, error) {
207+ // go-oidc doesn't let us pass bypass the issuer check this in the oidc.NewProvider call
208+ // (which uses discovery to get the URLs), so we'll do a quick check ourselves and if
209+ // we get the URLs, we'll just use the non-discovery path.
210+
211+ logger.Printf("Performing OIDC Discovery...")
212+
213+ requestURL := strings.TrimSuffix(providerConfig.OIDCConfig.IssuerURL, "/") + "/.well-known/openid-configuration"
214+ body, err := requests.New(requestURL).
215+ Do().
216+ UnmarshalJSON()
217+ if err != nil {
218+ return nil, fmt.Errorf("failed to discover OIDC configuration: %v", err)
219+ }
220+
221+ // Prefer manually configured URLs. It's a bit unclear
222+ // why you'd be doing discovery and also providing the URLs
223+ // explicitly though...
224+ if providerConfig.LoginURL == "" {
225+ providerConfig.LoginURL = body.Get("authorization_endpoint").MustString()
226+ }
227+
228+ if providerConfig.RedeemURL == "" {
229+ providerConfig.RedeemURL = body.Get("token_endpoint").MustString()
230+ }
231+
232+ if providerConfig.OIDCConfig.JwksURL == "" {
233+ providerConfig.OIDCConfig.JwksURL = body.Get("jwks_uri").MustString()
234+ }
235+
236+ if providerConfig.ProfileURL == "" {
237+ providerConfig.ProfileURL = body.Get("userinfo_endpoint").MustString()
238+ }
239+
240+ // Now we have performed the discovery, construct the verifier manually
241+ return newSkipDiscoveryOIDCVerifier(providerConfig)
242+}
243+
244+func newSkipDiscoveryOIDCVerifier(providerConfig options.Provider) (*internaloidc.IDTokenVerifier, error) {
245+ var errs []error
246+
247+ // Construct a manual IDTokenVerifier from issuer URL & JWKS URI
248+ // instead of metadata discovery if we enable -skip-oidc-discovery.
249+ // In this case we need to make sure the required endpoints for
250+ // the provider are configured.
251+ if providerConfig.LoginURL == "" {
252+ errs = append(errs, errors.New("missing required setting: login-url"))
253+ }
254+ if providerConfig.RedeemURL == "" {
255+ errs = append(errs, errors.New("missing required setting: redeem-url"))
256+ }
257+ if providerConfig.OIDCConfig.JwksURL == "" {
258+ errs = append(errs, errors.New("missing required setting: oidc-jwks-url"))
259+ }
260+ if len(errs) > 0 {
261+ return nil, k8serrors.NewAggregate(errs)
262+ }
263+
264+ keySet := oidc.NewRemoteKeySet(context.TODO(), providerConfig.OIDCConfig.JwksURL)
265+ verificationOptions := &internaloidc.IDTokenVerificationOptions{
266+ AudienceClaims: providerConfig.OIDCConfig.AudienceClaims,
267+ ClientID: providerConfig.ClientID,
268+ ExtraAudiences: providerConfig.OIDCConfig.ExtraAudiences,
56269 }
270+ verifier := internaloidc.NewVerifier(oidc.NewVerifier(providerConfig.OIDCConfig.IssuerURL, keySet, &oidc.Config{
271+ ClientID: providerConfig.ClientID,
272+ SkipIssuerCheck: providerConfig.OIDCConfig.InsecureSkipIssuerVerification,
273+ SkipClientIDCheck: true, // client id check is done within oauth2-proxy: IDTokenVerifier.Verify
274+ }), verificationOptions)
275+ return verifier, nil
57276}