277 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			277 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
| package providers
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"net/url"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/coreos/go-oidc/v3/oidc"
 | |
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
 | |
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
 | |
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
 | |
| 	internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
 | |
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
 | |
| 	k8serrors "k8s.io/apimachinery/pkg/util/errors"
 | |
| )
 | |
| 
 | |
| // Provider represents an upstream identity provider implementation
 | |
| type Provider interface {
 | |
| 	Data() *ProviderData
 | |
| 	GetLoginURL(redirectURI, finalRedirect string, nonce string) string
 | |
| 	Redeem(ctx context.Context, redirectURI, code string) (*sessions.SessionState, error)
 | |
| 	// Deprecated: Migrate to EnrichSession
 | |
| 	GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error)
 | |
| 	EnrichSession(ctx context.Context, s *sessions.SessionState) error
 | |
| 	Authorize(ctx context.Context, s *sessions.SessionState) (bool, error)
 | |
| 	ValidateSession(ctx context.Context, s *sessions.SessionState) bool
 | |
| 	RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error)
 | |
| 	CreateSessionFromToken(ctx context.Context, token string) (*sessions.SessionState, error)
 | |
| }
 | |
| 
 | |
| func NewProvider(providerConfig options.Provider) (Provider, error) {
 | |
| 	providerData, err := newProviderDataFromConfig(providerConfig)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("could not create provider data: %v", err)
 | |
| 	}
 | |
| 	switch providerConfig.Type {
 | |
| 	case options.ADFSProvider:
 | |
| 		return NewADFSProvider(providerData, providerConfig.ADFSConfig), nil
 | |
| 	case options.AzureProvider:
 | |
| 		return NewAzureProvider(providerData, providerConfig.AzureConfig), nil
 | |
| 	case options.BitbucketProvider:
 | |
| 		return NewBitbucketProvider(providerData, providerConfig.BitbucketConfig), nil
 | |
| 	case options.DigitalOceanProvider:
 | |
| 		return NewDigitalOceanProvider(providerData), nil
 | |
| 	case options.FacebookProvider:
 | |
| 		return NewFacebookProvider(providerData), nil
 | |
| 	case options.GitHubProvider:
 | |
| 		return NewGitHubProvider(providerData, providerConfig.GitHubConfig), nil
 | |
| 	case options.GitLabProvider:
 | |
| 		return NewGitLabProvider(providerData, providerConfig.GitLabConfig)
 | |
| 	case options.GoogleProvider:
 | |
| 		return NewGoogleProvider(providerData, providerConfig.GoogleConfig)
 | |
| 	case options.KeycloakProvider:
 | |
| 		return NewKeycloakProvider(providerData, providerConfig.KeycloakConfig), nil
 | |
| 	case options.KeycloakOIDCProvider:
 | |
| 		return NewKeycloakOIDCProvider(providerData, providerConfig.KeycloakConfig), nil
 | |
| 	case options.LinkedInProvider:
 | |
| 		return NewLinkedInProvider(providerData), nil
 | |
| 	case options.LoginGovProvider:
 | |
| 		return NewLoginGovProvider(providerData, providerConfig.LoginGovConfig)
 | |
| 	case options.NextCloudProvider:
 | |
| 		return NewNextcloudProvider(providerData), nil
 | |
| 	case options.OIDCProvider:
 | |
| 		return NewOIDCProvider(providerData, providerConfig.OIDCConfig), nil
 | |
| 	default:
 | |
| 		return nil, fmt.Errorf("unknown provider type %q", providerConfig.Type)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData, error) {
 | |
| 	p := &ProviderData{
 | |
| 		Scope:            providerConfig.Scope,
 | |
| 		ClientID:         providerConfig.ClientID,
 | |
| 		ClientSecret:     providerConfig.ClientSecret,
 | |
| 		ClientSecretFile: providerConfig.ClientSecretFile,
 | |
| 		Prompt:           providerConfig.Prompt,
 | |
| 		ApprovalPrompt:   providerConfig.ApprovalPrompt,
 | |
| 		AcrValues:        providerConfig.AcrValues,
 | |
| 	}
 | |
| 
 | |
| 	needsVerifier, err := providerRequiresOIDCProviderVerifier(providerConfig.Type)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	if needsVerifier {
 | |
| 		oidcProvider, verifier, err := newOIDCProviderVerifier(providerConfig)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("error setting OIDC configuration: %v", err)
 | |
| 		}
 | |
| 
 | |
| 		p.Verifier = verifier
 | |
| 		if oidcProvider != nil {
 | |
| 			// Use the discovered values rather than any specified values
 | |
| 			providerConfig.LoginURL = oidcProvider.Endpoint().AuthURL
 | |
| 			providerConfig.RedeemURL = oidcProvider.Endpoint().TokenURL
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	errs := []error{}
 | |
| 	for name, u := range map[string]struct {
 | |
| 		dst *url.URL
 | |
| 		raw string
 | |
| 	}{
 | |
| 		"login":    {dst: p.LoginURL, raw: providerConfig.LoginURL},
 | |
| 		"redeem":   {dst: p.RedeemURL, raw: providerConfig.RedeemURL},
 | |
| 		"profile":  {dst: p.ProfileURL, raw: providerConfig.ProfileURL},
 | |
| 		"validate": {dst: p.ValidateURL, raw: providerConfig.ValidateURL},
 | |
| 		"resource": {dst: p.ProtectedResource, raw: providerConfig.ProtectedResource},
 | |
| 	} {
 | |
| 		var err error
 | |
| 		u.dst, err = url.Parse(u.raw)
 | |
| 		if err != nil {
 | |
| 			errs = append(errs, fmt.Errorf("could not parse %s URL: %v", name, err))
 | |
| 		}
 | |
| 	}
 | |
| 	if len(errs) > 0 {
 | |
| 		return nil, k8serrors.NewAggregate(errs)
 | |
| 	}
 | |
| 
 | |
| 	// Make the OIDC options available to all providers that support it
 | |
| 	p.AllowUnverifiedEmail = providerConfig.OIDCConfig.InsecureAllowUnverifiedEmail
 | |
| 	p.EmailClaim = providerConfig.OIDCConfig.EmailClaim
 | |
| 	p.GroupsClaim = providerConfig.OIDCConfig.GroupsClaim
 | |
| 
 | |
| 	// TODO (@NickMeves) - Remove This
 | |
| 	// Backwards Compatibility for Deprecated UserIDClaim option
 | |
| 	if providerConfig.OIDCConfig.EmailClaim == options.OIDCEmailClaim &&
 | |
| 		providerConfig.OIDCConfig.UserIDClaim != options.OIDCEmailClaim {
 | |
| 		p.EmailClaim = providerConfig.OIDCConfig.UserIDClaim
 | |
| 	}
 | |
| 
 | |
| 	if providerConfig.Scope == "" {
 | |
| 		providerConfig.Scope = "openid email profile"
 | |
| 
 | |
| 		if len(providerConfig.AllowedGroups) > 0 {
 | |
| 			providerConfig.Scope += " groups"
 | |
| 		}
 | |
| 	}
 | |
| 	if providerConfig.OIDCConfig.UserIDClaim == "" {
 | |
| 		providerConfig.OIDCConfig.UserIDClaim = "email"
 | |
| 	}
 | |
| 
 | |
| 	p.setAllowedGroups(providerConfig.AllowedGroups)
 | |
| 
 | |
| 	return p, nil
 | |
| }
 | |
| 
 | |
| func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bool, error) {
 | |
| 	switch providerType {
 | |
| 	case options.BitbucketProvider, options.DigitalOceanProvider, options.FacebookProvider, options.GitHubProvider,
 | |
| 		options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider, options.NextCloudProvider:
 | |
| 		return false, nil
 | |
| 	case options.ADFSProvider, options.AzureProvider, options.GitLabProvider, options.KeycloakOIDCProvider, options.OIDCProvider:
 | |
| 		return true, nil
 | |
| 	default:
 | |
| 		return false, fmt.Errorf("unknown provider type: %s", providerType)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func newOIDCProviderVerifier(providerConfig options.Provider) (*oidc.Provider, *internaloidc.IDTokenVerifier, error) {
 | |
| 	// If the issuer isn't set, default it for platforms where it makes sense
 | |
| 	if providerConfig.OIDCConfig.IssuerURL == "" {
 | |
| 		switch providerConfig.Type {
 | |
| 		case "gitlab":
 | |
| 			providerConfig.OIDCConfig.IssuerURL = "https://gitlab.com"
 | |
| 		case "oidc":
 | |
| 			return nil, nil, errors.New("missing required setting: OIDC Issuer URL cannot be empty")
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	switch {
 | |
| 	case providerConfig.OIDCConfig.InsecureSkipIssuerVerification && !providerConfig.OIDCConfig.SkipDiscovery:
 | |
| 		verifier, err := newInsecureSkipIssuerVerificationOIDCVerifier(providerConfig)
 | |
| 		return nil, verifier, err
 | |
| 	case providerConfig.OIDCConfig.SkipDiscovery:
 | |
| 		verifier, err := newSkipDiscoveryOIDCVerifier(providerConfig)
 | |
| 		return nil, verifier, err
 | |
| 	default:
 | |
| 		return newDiscoveryOIDCProviderVerifier(providerConfig)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func newDiscoveryOIDCProviderVerifier(providerConfig options.Provider) (*oidc.Provider, *internaloidc.IDTokenVerifier, error) {
 | |
| 	// Configure discoverable provider data.
 | |
| 	provider, err := oidc.NewProvider(context.TODO(), providerConfig.OIDCConfig.IssuerURL)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, err
 | |
| 	}
 | |
| 	verificationOptions := &internaloidc.IDTokenVerificationOptions{
 | |
| 		AudienceClaims: providerConfig.OIDCConfig.AudienceClaims,
 | |
| 		ClientID:       providerConfig.ClientID,
 | |
| 		ExtraAudiences: providerConfig.OIDCConfig.ExtraAudiences,
 | |
| 	}
 | |
| 	verifier := internaloidc.NewVerifier(provider.Verifier(&oidc.Config{
 | |
| 		ClientID:          providerConfig.ClientID,
 | |
| 		SkipIssuerCheck:   providerConfig.OIDCConfig.InsecureSkipIssuerVerification,
 | |
| 		SkipClientIDCheck: true, // client id check is done within oauth2-proxy: IDTokenVerifier.Verify
 | |
| 	}), verificationOptions)
 | |
| 
 | |
| 	return provider, verifier, nil
 | |
| }
 | |
| 
 | |
| func newInsecureSkipIssuerVerificationOIDCVerifier(providerConfig options.Provider) (*internaloidc.IDTokenVerifier, error) {
 | |
| 	// go-oidc doesn't let us pass bypass the issuer check this in the oidc.NewProvider call
 | |
| 	// (which uses discovery to get the URLs), so we'll do a quick check ourselves and if
 | |
| 	// we get the URLs, we'll just use the non-discovery path.
 | |
| 
 | |
| 	logger.Printf("Performing OIDC Discovery...")
 | |
| 
 | |
| 	requestURL := strings.TrimSuffix(providerConfig.OIDCConfig.IssuerURL, "/") + "/.well-known/openid-configuration"
 | |
| 	body, err := requests.New(requestURL).
 | |
| 		Do().
 | |
| 		UnmarshalJSON()
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to discover OIDC configuration: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Prefer manually configured URLs. It's a bit unclear
 | |
| 	// why you'd be doing discovery and also providing the URLs
 | |
| 	// explicitly though...
 | |
| 	if providerConfig.LoginURL == "" {
 | |
| 		providerConfig.LoginURL = body.Get("authorization_endpoint").MustString()
 | |
| 	}
 | |
| 
 | |
| 	if providerConfig.RedeemURL == "" {
 | |
| 		providerConfig.RedeemURL = body.Get("token_endpoint").MustString()
 | |
| 	}
 | |
| 
 | |
| 	if providerConfig.OIDCConfig.JwksURL == "" {
 | |
| 		providerConfig.OIDCConfig.JwksURL = body.Get("jwks_uri").MustString()
 | |
| 	}
 | |
| 
 | |
| 	if providerConfig.ProfileURL == "" {
 | |
| 		providerConfig.ProfileURL = body.Get("userinfo_endpoint").MustString()
 | |
| 	}
 | |
| 
 | |
| 	// Now we have performed the discovery, construct the verifier manually
 | |
| 	return newSkipDiscoveryOIDCVerifier(providerConfig)
 | |
| }
 | |
| 
 | |
| func newSkipDiscoveryOIDCVerifier(providerConfig options.Provider) (*internaloidc.IDTokenVerifier, error) {
 | |
| 	var errs []error
 | |
| 
 | |
| 	// Construct a manual IDTokenVerifier from issuer URL & JWKS URI
 | |
| 	// instead of metadata discovery if we enable -skip-oidc-discovery.
 | |
| 	// In this case we need to make sure the required endpoints for
 | |
| 	// the provider are configured.
 | |
| 	if providerConfig.LoginURL == "" {
 | |
| 		errs = append(errs, errors.New("missing required setting: login-url"))
 | |
| 	}
 | |
| 	if providerConfig.RedeemURL == "" {
 | |
| 		errs = append(errs, errors.New("missing required setting: redeem-url"))
 | |
| 	}
 | |
| 	if providerConfig.OIDCConfig.JwksURL == "" {
 | |
| 		errs = append(errs, errors.New("missing required setting: oidc-jwks-url"))
 | |
| 	}
 | |
| 	if len(errs) > 0 {
 | |
| 		return nil, k8serrors.NewAggregate(errs)
 | |
| 	}
 | |
| 
 | |
| 	keySet := oidc.NewRemoteKeySet(context.TODO(), providerConfig.OIDCConfig.JwksURL)
 | |
| 	verificationOptions := &internaloidc.IDTokenVerificationOptions{
 | |
| 		AudienceClaims: providerConfig.OIDCConfig.AudienceClaims,
 | |
| 		ClientID:       providerConfig.ClientID,
 | |
| 		ExtraAudiences: providerConfig.OIDCConfig.ExtraAudiences,
 | |
| 	}
 | |
| 	verifier := internaloidc.NewVerifier(oidc.NewVerifier(providerConfig.OIDCConfig.IssuerURL, keySet, &oidc.Config{
 | |
| 		ClientID:          providerConfig.ClientID,
 | |
| 		SkipIssuerCheck:   providerConfig.OIDCConfig.InsecureSkipIssuerVerification,
 | |
| 		SkipClientIDCheck: true, // client id check is done within oauth2-proxy: IDTokenVerifier.Verify
 | |
| 	}), verificationOptions)
 | |
| 	return verifier, nil
 | |
| }
 |