219 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			219 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
| package providers
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io/ioutil"
 | |
| 	"net/http"
 | |
| 	"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/providers/util"
 | |
| 	"golang.org/x/oauth2"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	// This is not exported as it's not currently user configurable
 | |
| 	oidcUserClaim = "sub"
 | |
| )
 | |
| 
 | |
| // ProviderData contains information required to configure all implementations
 | |
| // of OAuth2 providers
 | |
| type ProviderData struct {
 | |
| 	ProviderName      string
 | |
| 	LoginURL          *url.URL
 | |
| 	RedeemURL         *url.URL
 | |
| 	ProfileURL        *url.URL
 | |
| 	ProtectedResource *url.URL
 | |
| 	ValidateURL       *url.URL
 | |
| 	// Auth request params & related, see
 | |
| 	//https://openid.net/specs/openid-connect-basic-1_0.html#rfc.section.2.1.1.1
 | |
| 	AcrValues        string
 | |
| 	ApprovalPrompt   string // NOTE: Renamed to "prompt" in OAuth2
 | |
| 	ClientID         string
 | |
| 	ClientSecret     string
 | |
| 	ClientSecretFile string
 | |
| 	Scope            string
 | |
| 	Prompt           string
 | |
| 
 | |
| 	// Common OIDC options for any OIDC-based providers to consume
 | |
| 	AllowUnverifiedEmail bool
 | |
| 	UserClaim            string
 | |
| 	EmailClaim           string
 | |
| 	GroupsClaim          string
 | |
| 	Verifier             *internaloidc.IDTokenVerifier
 | |
| 
 | |
| 	// Universal Group authorization data structure
 | |
| 	// any provider can set to consume
 | |
| 	AllowedGroups map[string]struct{}
 | |
| 
 | |
| 	getAuthorizationHeaderFunc func(string) http.Header
 | |
| }
 | |
| 
 | |
| // Data returns the ProviderData
 | |
| func (p *ProviderData) Data() *ProviderData { return p }
 | |
| 
 | |
| func (p *ProviderData) GetClientSecret() (clientSecret string, err error) {
 | |
| 	if p.ClientSecret != "" || p.ClientSecretFile == "" {
 | |
| 		return p.ClientSecret, nil
 | |
| 	}
 | |
| 
 | |
| 	// Getting ClientSecret can fail in runtime so we need to report it without returning the file name to the user
 | |
| 	fileClientSecret, err := ioutil.ReadFile(p.ClientSecretFile)
 | |
| 	if err != nil {
 | |
| 		logger.Errorf("error reading client secret file %s: %s", p.ClientSecretFile, err)
 | |
| 		return "", errors.New("could not read client secret file")
 | |
| 	}
 | |
| 	return string(fileClientSecret), nil
 | |
| }
 | |
| 
 | |
| // setAllowedGroups organizes a group list into the AllowedGroups map
 | |
| // to be consumed by Authorize implementations
 | |
| func (p *ProviderData) setAllowedGroups(groups []string) {
 | |
| 	p.AllowedGroups = make(map[string]struct{}, len(groups))
 | |
| 	for _, group := range groups {
 | |
| 		p.AllowedGroups[group] = struct{}{}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type providerDefaults struct {
 | |
| 	name        string
 | |
| 	loginURL    *url.URL
 | |
| 	redeemURL   *url.URL
 | |
| 	profileURL  *url.URL
 | |
| 	validateURL *url.URL
 | |
| 	scope       string
 | |
| }
 | |
| 
 | |
| func (p *ProviderData) setProviderDefaults(defaults providerDefaults) {
 | |
| 	p.ProviderName = defaults.name
 | |
| 	p.LoginURL = defaultURL(p.LoginURL, defaults.loginURL)
 | |
| 	p.RedeemURL = defaultURL(p.RedeemURL, defaults.redeemURL)
 | |
| 	p.ProfileURL = defaultURL(p.ProfileURL, defaults.profileURL)
 | |
| 	p.ValidateURL = defaultURL(p.ValidateURL, defaults.validateURL)
 | |
| 
 | |
| 	if p.Scope == "" {
 | |
| 		p.Scope = defaults.scope
 | |
| 	}
 | |
| 
 | |
| 	if p.UserClaim == "" {
 | |
| 		p.UserClaim = oidcUserClaim
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // defaultURL will set return a default value if the given value is not set.
 | |
| func defaultURL(u *url.URL, d *url.URL) *url.URL {
 | |
| 	if u != nil && u.String() != "" {
 | |
| 		// The value is already set
 | |
| 		return u
 | |
| 	}
 | |
| 
 | |
| 	// If the default is given, return that
 | |
| 	if d != nil {
 | |
| 		return d
 | |
| 	}
 | |
| 	return &url.URL{}
 | |
| }
 | |
| 
 | |
| // ****************************************************************************
 | |
| // These private OIDC helper methods are available to any providers that are
 | |
| // OIDC compliant
 | |
| // ****************************************************************************
 | |
| 
 | |
| func (p *ProviderData) verifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
 | |
| 	rawIDToken := getIDToken(token)
 | |
| 	if strings.TrimSpace(rawIDToken) == "" {
 | |
| 		return nil, ErrMissingIDToken
 | |
| 	}
 | |
| 	if p.Verifier == nil {
 | |
| 		return nil, ErrMissingOIDCVerifier
 | |
| 	}
 | |
| 	return p.Verifier.Verify(ctx, rawIDToken)
 | |
| }
 | |
| 
 | |
| // buildSessionFromClaims uses IDToken claims to populate a fresh SessionState
 | |
| // with non-Token related fields.
 | |
| func (p *ProviderData) buildSessionFromClaims(rawIDToken, accessToken string) (*sessions.SessionState, error) {
 | |
| 	ss := &sessions.SessionState{}
 | |
| 
 | |
| 	if rawIDToken == "" {
 | |
| 		return ss, nil
 | |
| 	}
 | |
| 
 | |
| 	extractor, err := p.getClaimExtractor(rawIDToken, accessToken)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Use a slice of a struct (vs map) here in case the same claim is used twice
 | |
| 	for _, c := range []struct {
 | |
| 		claim string
 | |
| 		dst   interface{}
 | |
| 	}{
 | |
| 		{p.UserClaim, &ss.User},
 | |
| 		{p.EmailClaim, &ss.Email},
 | |
| 		{p.GroupsClaim, &ss.Groups},
 | |
| 		// TODO (@NickMeves) Deprecate for dynamic claim to session mapping
 | |
| 		{"preferred_username", &ss.PreferredUsername},
 | |
| 	} {
 | |
| 		if _, err := extractor.GetClaimInto(c.claim, c.dst); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// `email_verified` must be present and explicitly set to `false` to be
 | |
| 	// considered unverified.
 | |
| 	verifyEmail := (p.EmailClaim == options.OIDCEmailClaim) && !p.AllowUnverifiedEmail
 | |
| 
 | |
| 	var verified bool
 | |
| 	exists, err := extractor.GetClaimInto("email_verified", &verified)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	if verifyEmail && exists && !verified {
 | |
| 		return nil, fmt.Errorf("email in id_token (%s) isn't verified", ss.Email)
 | |
| 	}
 | |
| 
 | |
| 	return ss, nil
 | |
| }
 | |
| 
 | |
| func (p *ProviderData) getClaimExtractor(rawIDToken, accessToken string) (util.ClaimExtractor, error) {
 | |
| 	extractor, err := util.NewClaimExtractor(context.TODO(), rawIDToken, p.ProfileURL, p.getAuthorizationHeader(accessToken))
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("could not initialise claim extractor: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	return extractor, nil
 | |
| }
 | |
| 
 | |
| // checkNonce compares the session's nonce with the IDToken's nonce claim
 | |
| func (p *ProviderData) checkNonce(s *sessions.SessionState) error {
 | |
| 	extractor, err := p.getClaimExtractor(s.IDToken, "")
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("id_token claims extraction failed: %v", err)
 | |
| 	}
 | |
| 	var nonce string
 | |
| 	if _, err := extractor.GetClaimInto("nonce", &nonce); err != nil {
 | |
| 		return fmt.Errorf("could not extract nonce from ID Token: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	if !s.CheckNonce(nonce) {
 | |
| 		return errors.New("id_token nonce claim does not match the session nonce")
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (p *ProviderData) getAuthorizationHeader(accessToken string) http.Header {
 | |
| 	if p.getAuthorizationHeaderFunc != nil && accessToken != "" {
 | |
| 		return p.getAuthorizationHeaderFunc(accessToken)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 |