346 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			346 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
| package main
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"crypto"
 | |
| 	"crypto/tls"
 | |
| 	"encoding/base64"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"regexp"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	oidc "github.com/coreos/go-oidc"
 | |
| 	"github.com/mbland/hmacauth"
 | |
| 	"github.com/pusher/oauth2_proxy/providers"
 | |
| )
 | |
| 
 | |
| // Options holds Configuration Options that can be set by Command Line Flag,
 | |
| // or Config File
 | |
| type Options struct {
 | |
| 	ProxyPrefix  string `flag:"proxy-prefix" cfg:"proxy-prefix"`
 | |
| 	HTTPAddress  string `flag:"http-address" cfg:"http_address"`
 | |
| 	HTTPSAddress string `flag:"https-address" cfg:"https_address"`
 | |
| 	RedirectURL  string `flag:"redirect-url" cfg:"redirect_url"`
 | |
| 	ClientID     string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"`
 | |
| 	ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"`
 | |
| 	TLSCertFile  string `flag:"tls-cert" cfg:"tls_cert_file"`
 | |
| 	TLSKeyFile   string `flag:"tls-key" cfg:"tls_key_file"`
 | |
| 
 | |
| 	AuthenticatedEmailsFile  string   `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
 | |
| 	AzureTenant              string   `flag:"azure-tenant" cfg:"azure_tenant"`
 | |
| 	EmailDomains             []string `flag:"email-domain" cfg:"email_domains"`
 | |
| 	GitHubOrg                string   `flag:"github-org" cfg:"github_org"`
 | |
| 	GitHubTeam               string   `flag:"github-team" cfg:"github_team"`
 | |
| 	GoogleGroups             []string `flag:"google-group" cfg:"google_group"`
 | |
| 	GoogleAdminEmail         string   `flag:"google-admin-email" cfg:"google_admin_email"`
 | |
| 	GoogleServiceAccountJSON string   `flag:"google-service-account-json" cfg:"google_service_account_json"`
 | |
| 	HtpasswdFile             string   `flag:"htpasswd-file" cfg:"htpasswd_file"`
 | |
| 	DisplayHtpasswdForm      bool     `flag:"display-htpasswd-form" cfg:"display_htpasswd_form"`
 | |
| 	CustomTemplatesDir       string   `flag:"custom-templates-dir" cfg:"custom_templates_dir"`
 | |
| 	Footer                   string   `flag:"footer" cfg:"footer"`
 | |
| 
 | |
| 	CookieName     string        `flag:"cookie-name" cfg:"cookie_name" env:"OAUTH2_PROXY_COOKIE_NAME"`
 | |
| 	CookieSecret   string        `flag:"cookie-secret" cfg:"cookie_secret" env:"OAUTH2_PROXY_COOKIE_SECRET"`
 | |
| 	CookieDomain   string        `flag:"cookie-domain" cfg:"cookie_domain" env:"OAUTH2_PROXY_COOKIE_DOMAIN"`
 | |
| 	CookieExpire   time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"OAUTH2_PROXY_COOKIE_EXPIRE"`
 | |
| 	CookieRefresh  time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"OAUTH2_PROXY_COOKIE_REFRESH"`
 | |
| 	CookieSecure   bool          `flag:"cookie-secure" cfg:"cookie_secure"`
 | |
| 	CookieHTTPOnly bool          `flag:"cookie-httponly" cfg:"cookie_httponly"`
 | |
| 
 | |
| 	Upstreams             []string `flag:"upstream" cfg:"upstreams"`
 | |
| 	SkipAuthRegex         []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
 | |
| 	PassBasicAuth         bool     `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
 | |
| 	BasicAuthPassword     string   `flag:"basic-auth-password" cfg:"basic_auth_password"`
 | |
| 	PassAccessToken       bool     `flag:"pass-access-token" cfg:"pass_access_token"`
 | |
| 	PassHostHeader        bool     `flag:"pass-host-header" cfg:"pass_host_header"`
 | |
| 	SkipProviderButton    bool     `flag:"skip-provider-button" cfg:"skip_provider_button"`
 | |
| 	PassUserHeaders       bool     `flag:"pass-user-headers" cfg:"pass_user_headers"`
 | |
| 	SSLInsecureSkipVerify bool     `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"`
 | |
| 	SetXAuthRequest       bool     `flag:"set-xauthrequest" cfg:"set_xauthrequest"`
 | |
| 	SetAuthorization      bool     `flag:"set-authorization-header" cfg:"set_authorization_header"`
 | |
| 	PassAuthorization     bool     `flag:"pass-authorization-header" cfg:"pass_authorization_header"`
 | |
| 	SkipAuthPreflight     bool     `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"`
 | |
| 
 | |
| 	// These options allow for other providers besides Google, with
 | |
| 	// potential overrides.
 | |
| 	Provider          string `flag:"provider" cfg:"provider"`
 | |
| 	OIDCIssuerURL     string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"`
 | |
| 	LoginURL          string `flag:"login-url" cfg:"login_url"`
 | |
| 	RedeemURL         string `flag:"redeem-url" cfg:"redeem_url"`
 | |
| 	ProfileURL        string `flag:"profile-url" cfg:"profile_url"`
 | |
| 	ProtectedResource string `flag:"resource" cfg:"resource"`
 | |
| 	ValidateURL       string `flag:"validate-url" cfg:"validate_url"`
 | |
| 	Scope             string `flag:"scope" cfg:"scope"`
 | |
| 	ApprovalPrompt    string `flag:"approval-prompt" cfg:"approval_prompt"`
 | |
| 
 | |
| 	RequestLogging       bool   `flag:"request-logging" cfg:"request_logging"`
 | |
| 	RequestLoggingFormat string `flag:"request-logging-format" cfg:"request_logging_format"`
 | |
| 
 | |
| 	SignatureKey string `flag:"signature-key" cfg:"signature_key" env:"OAUTH2_PROXY_SIGNATURE_KEY"`
 | |
| 
 | |
| 	// internal values that are set after config validation
 | |
| 	redirectURL   *url.URL
 | |
| 	proxyURLs     []*url.URL
 | |
| 	CompiledRegex []*regexp.Regexp
 | |
| 	provider      providers.Provider
 | |
| 	signatureData *SignatureData
 | |
| 	oidcVerifier  *oidc.IDTokenVerifier
 | |
| }
 | |
| 
 | |
| // SignatureData holds hmacauth signature hash and key
 | |
| type SignatureData struct {
 | |
| 	hash crypto.Hash
 | |
| 	key  string
 | |
| }
 | |
| 
 | |
| // NewOptions constructs a new Options with defaulted values
 | |
| func NewOptions() *Options {
 | |
| 	return &Options{
 | |
| 		ProxyPrefix:          "/oauth2",
 | |
| 		HTTPAddress:          "127.0.0.1:4180",
 | |
| 		HTTPSAddress:         ":443",
 | |
| 		DisplayHtpasswdForm:  true,
 | |
| 		CookieName:           "_oauth2_proxy",
 | |
| 		CookieSecure:         true,
 | |
| 		CookieHTTPOnly:       true,
 | |
| 		CookieExpire:         time.Duration(168) * time.Hour,
 | |
| 		CookieRefresh:        time.Duration(0),
 | |
| 		SetXAuthRequest:      false,
 | |
| 		SkipAuthPreflight:    false,
 | |
| 		PassBasicAuth:        true,
 | |
| 		PassUserHeaders:      true,
 | |
| 		PassAccessToken:      false,
 | |
| 		PassHostHeader:       true,
 | |
| 		SetAuthorization:     false,
 | |
| 		PassAuthorization:    false,
 | |
| 		ApprovalPrompt:       "force",
 | |
| 		RequestLogging:       true,
 | |
| 		RequestLoggingFormat: defaultRequestLoggingFormat,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) {
 | |
| 	parsed, err := url.Parse(toParse)
 | |
| 	if err != nil {
 | |
| 		return nil, append(msgs, fmt.Sprintf(
 | |
| 			"error parsing %s-url=%q %s", urltype, toParse, err))
 | |
| 	}
 | |
| 	return parsed, msgs
 | |
| }
 | |
| 
 | |
| // Validate checks that required options are set and validates those that they
 | |
| // are of the correct format
 | |
| func (o *Options) Validate() error {
 | |
| 	if o.SSLInsecureSkipVerify {
 | |
| 		// TODO: Accept a certificate bundle.
 | |
| 		insecureTransport := &http.Transport{
 | |
| 			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
 | |
| 		}
 | |
| 		http.DefaultClient = &http.Client{Transport: insecureTransport}
 | |
| 	}
 | |
| 
 | |
| 	msgs := make([]string, 0)
 | |
| 	if o.CookieSecret == "" {
 | |
| 		msgs = append(msgs, "missing setting: cookie-secret")
 | |
| 	}
 | |
| 	if o.ClientID == "" {
 | |
| 		msgs = append(msgs, "missing setting: client-id")
 | |
| 	}
 | |
| 	if o.ClientSecret == "" {
 | |
| 		msgs = append(msgs, "missing setting: client-secret")
 | |
| 	}
 | |
| 	if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" {
 | |
| 		msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required."+
 | |
| 			"\n      use email-domain=* to authorize all email addresses")
 | |
| 	}
 | |
| 
 | |
| 	if o.OIDCIssuerURL != "" {
 | |
| 		// Configure discoverable provider data.
 | |
| 		provider, err := oidc.NewProvider(context.Background(), o.OIDCIssuerURL)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		o.oidcVerifier = provider.Verifier(&oidc.Config{
 | |
| 			ClientID: o.ClientID,
 | |
| 		})
 | |
| 		o.LoginURL = provider.Endpoint().AuthURL
 | |
| 		o.RedeemURL = provider.Endpoint().TokenURL
 | |
| 		if o.Scope == "" {
 | |
| 			o.Scope = "openid email profile"
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
 | |
| 
 | |
| 	for _, u := range o.Upstreams {
 | |
| 		upstreamURL, err := url.Parse(u)
 | |
| 		if err != nil {
 | |
| 			msgs = append(msgs, fmt.Sprintf("error parsing upstream: %s", err))
 | |
| 		} else {
 | |
| 			if upstreamURL.Path == "" {
 | |
| 				upstreamURL.Path = "/"
 | |
| 			}
 | |
| 			o.proxyURLs = append(o.proxyURLs, upstreamURL)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for _, u := range o.SkipAuthRegex {
 | |
| 		CompiledRegex, err := regexp.Compile(u)
 | |
| 		if err != nil {
 | |
| 			msgs = append(msgs, fmt.Sprintf("error compiling regex=%q %s", u, err))
 | |
| 			continue
 | |
| 		}
 | |
| 		o.CompiledRegex = append(o.CompiledRegex, CompiledRegex)
 | |
| 	}
 | |
| 	msgs = parseProviderInfo(o, msgs)
 | |
| 
 | |
| 	if o.PassAccessToken || (o.CookieRefresh != time.Duration(0)) {
 | |
| 		validCookieSecretSize := false
 | |
| 		for _, i := range []int{16, 24, 32} {
 | |
| 			if len(secretBytes(o.CookieSecret)) == i {
 | |
| 				validCookieSecretSize = true
 | |
| 			}
 | |
| 		}
 | |
| 		var decoded bool
 | |
| 		if string(secretBytes(o.CookieSecret)) != o.CookieSecret {
 | |
| 			decoded = true
 | |
| 		}
 | |
| 		if validCookieSecretSize == false {
 | |
| 			var suffix string
 | |
| 			if decoded {
 | |
| 				suffix = fmt.Sprintf(" note: cookie secret was base64 decoded from %q", o.CookieSecret)
 | |
| 			}
 | |
| 			msgs = append(msgs, fmt.Sprintf(
 | |
| 				"cookie_secret must be 16, 24, or 32 bytes "+
 | |
| 					"to create an AES cipher when "+
 | |
| 					"pass_access_token == true or "+
 | |
| 					"cookie_refresh != 0, but is %d bytes.%s",
 | |
| 				len(secretBytes(o.CookieSecret)), suffix))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if o.CookieRefresh >= o.CookieExpire {
 | |
| 		msgs = append(msgs, fmt.Sprintf(
 | |
| 			"cookie_refresh (%s) must be less than "+
 | |
| 				"cookie_expire (%s)",
 | |
| 			o.CookieRefresh.String(),
 | |
| 			o.CookieExpire.String()))
 | |
| 	}
 | |
| 
 | |
| 	if len(o.GoogleGroups) > 0 || o.GoogleAdminEmail != "" || o.GoogleServiceAccountJSON != "" {
 | |
| 		if len(o.GoogleGroups) < 1 {
 | |
| 			msgs = append(msgs, "missing setting: google-group")
 | |
| 		}
 | |
| 		if o.GoogleAdminEmail == "" {
 | |
| 			msgs = append(msgs, "missing setting: google-admin-email")
 | |
| 		}
 | |
| 		if o.GoogleServiceAccountJSON == "" {
 | |
| 			msgs = append(msgs, "missing setting: google-service-account-json")
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	msgs = parseSignatureKey(o, msgs)
 | |
| 	msgs = validateCookieName(o, msgs)
 | |
| 
 | |
| 	if len(msgs) != 0 {
 | |
| 		return fmt.Errorf("Invalid configuration:\n  %s",
 | |
| 			strings.Join(msgs, "\n  "))
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func parseProviderInfo(o *Options, msgs []string) []string {
 | |
| 	p := &providers.ProviderData{
 | |
| 		Scope:          o.Scope,
 | |
| 		ClientID:       o.ClientID,
 | |
| 		ClientSecret:   o.ClientSecret,
 | |
| 		ApprovalPrompt: o.ApprovalPrompt,
 | |
| 	}
 | |
| 	p.LoginURL, msgs = parseURL(o.LoginURL, "login", msgs)
 | |
| 	p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs)
 | |
| 	p.ProfileURL, msgs = parseURL(o.ProfileURL, "profile", msgs)
 | |
| 	p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
 | |
| 	p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs)
 | |
| 
 | |
| 	o.provider = providers.New(o.Provider, p)
 | |
| 	switch p := o.provider.(type) {
 | |
| 	case *providers.AzureProvider:
 | |
| 		p.Configure(o.AzureTenant)
 | |
| 	case *providers.GitHubProvider:
 | |
| 		p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
 | |
| 	case *providers.GoogleProvider:
 | |
| 		if o.GoogleServiceAccountJSON != "" {
 | |
| 			file, err := os.Open(o.GoogleServiceAccountJSON)
 | |
| 			if err != nil {
 | |
| 				msgs = append(msgs, "invalid Google credentials file: "+o.GoogleServiceAccountJSON)
 | |
| 			} else {
 | |
| 				p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file)
 | |
| 			}
 | |
| 		}
 | |
| 	case *providers.OIDCProvider:
 | |
| 		if o.oidcVerifier == nil {
 | |
| 			msgs = append(msgs, "oidc provider requires an oidc issuer URL")
 | |
| 		} else {
 | |
| 			p.Verifier = o.oidcVerifier
 | |
| 		}
 | |
| 	}
 | |
| 	return msgs
 | |
| }
 | |
| 
 | |
| func parseSignatureKey(o *Options, msgs []string) []string {
 | |
| 	if o.SignatureKey == "" {
 | |
| 		return msgs
 | |
| 	}
 | |
| 
 | |
| 	components := strings.Split(o.SignatureKey, ":")
 | |
| 	if len(components) != 2 {
 | |
| 		return append(msgs, "invalid signature hash:key spec: "+
 | |
| 			o.SignatureKey)
 | |
| 	}
 | |
| 
 | |
| 	algorithm, secretKey := components[0], components[1]
 | |
| 	var hash crypto.Hash
 | |
| 	var err error
 | |
| 	if hash, err = hmacauth.DigestNameToCryptoHash(algorithm); err != nil {
 | |
| 		return append(msgs, "unsupported signature hash algorithm: "+
 | |
| 			o.SignatureKey)
 | |
| 	}
 | |
| 	o.signatureData = &SignatureData{hash, secretKey}
 | |
| 	return msgs
 | |
| }
 | |
| 
 | |
| func validateCookieName(o *Options, msgs []string) []string {
 | |
| 	cookie := &http.Cookie{Name: o.CookieName}
 | |
| 	if cookie.String() == "" {
 | |
| 		return append(msgs, fmt.Sprintf("invalid cookie name: %q", o.CookieName))
 | |
| 	}
 | |
| 	return msgs
 | |
| }
 | |
| 
 | |
| func addPadding(secret string) string {
 | |
| 	padding := len(secret) % 4
 | |
| 	switch padding {
 | |
| 	case 1:
 | |
| 		return secret + "==="
 | |
| 	case 2:
 | |
| 		return secret + "=="
 | |
| 	case 3:
 | |
| 		return secret + "="
 | |
| 	default:
 | |
| 		return secret
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // secretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
 | |
| func secretBytes(secret string) []byte {
 | |
| 	b, err := base64.URLEncoding.DecodeString(addPadding(secret))
 | |
| 	if err == nil {
 | |
| 		return []byte(addPadding(string(b)))
 | |
| 	}
 | |
| 	return []byte(secret)
 | |
| }
 |