github.com/pusher/oauth2_proxy@v3.2.0+incompatible/options.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"crypto"
     6  	"crypto/tls"
     7  	"encoding/base64"
     8  	"fmt"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"regexp"
    13  	"strings"
    14  	"time"
    15  
    16  	oidc "github.com/coreos/go-oidc"
    17  	"github.com/dgrijalva/jwt-go"
    18  	"github.com/mbland/hmacauth"
    19  	"github.com/pusher/oauth2_proxy/providers"
    20  )
    21  
    22  // Options holds Configuration Options that can be set by Command Line Flag,
    23  // or Config File
    24  type Options struct {
    25  	ProxyPrefix     string `flag:"proxy-prefix" cfg:"proxy-prefix" env:"OAUTH2_PROXY_PROXY_PREFIX"`
    26  	ProxyWebSockets bool   `flag:"proxy-websockets" cfg:"proxy_websockets" env:"OAUTH2_PROXY_PROXY_WEBSOCKETS"`
    27  	HTTPAddress     string `flag:"http-address" cfg:"http_address" env:"OAUTH2_PROXY_HTTP_ADDRESS"`
    28  	HTTPSAddress    string `flag:"https-address" cfg:"https_address" env:"OAUTH2_PROXY_HTTPS_ADDRESS"`
    29  	RedirectURL     string `flag:"redirect-url" cfg:"redirect_url" env:"OAUTH2_PROXY_REDIRECT_URL"`
    30  	ClientID        string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"`
    31  	ClientSecret    string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"`
    32  	TLSCertFile     string `flag:"tls-cert" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"`
    33  	TLSKeyFile      string `flag:"tls-key" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"`
    34  
    35  	AuthenticatedEmailsFile  string   `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"`
    36  	AzureTenant              string   `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"`
    37  	EmailDomains             []string `flag:"email-domain" cfg:"email_domains" env:"OAUTH2_PROXY_EMAIL_DOMAINS"`
    38  	WhitelistDomains         []string `flag:"whitelist-domain" cfg:"whitelist_domains" env:"OAUTH2_PROXY_WHITELIST_DOMAINS"`
    39  	GitHubOrg                string   `flag:"github-org" cfg:"github_org" env:"OAUTH2_PROXY_GITHUB_ORG"`
    40  	GitHubTeam               string   `flag:"github-team" cfg:"github_team" env:"OAUTH2_PROXY_GITHUB_TEAM"`
    41  	GoogleGroups             []string `flag:"google-group" cfg:"google_group" env:"OAUTH2_PROXY_GOOGLE_GROUPS"`
    42  	GoogleAdminEmail         string   `flag:"google-admin-email" cfg:"google_admin_email" env:"OAUTH2_PROXY_GOOGLE_ADMIN_EMAIL"`
    43  	GoogleServiceAccountJSON string   `flag:"google-service-account-json" cfg:"google_service_account_json" env:"OAUTH2_PROXY_GOOGLE_SERVICE_ACCOUNT_JSON"`
    44  	HtpasswdFile             string   `flag:"htpasswd-file" cfg:"htpasswd_file" env:"OAUTH2_PROXY_HTPASSWD_FILE"`
    45  	DisplayHtpasswdForm      bool     `flag:"display-htpasswd-form" cfg:"display_htpasswd_form" env:"OAUTH2_PROXY_DISPLAY_HTPASSWD_FORM"`
    46  	CustomTemplatesDir       string   `flag:"custom-templates-dir" cfg:"custom_templates_dir" env:"OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR"`
    47  	Footer                   string   `flag:"footer" cfg:"footer" env:"OAUTH2_PROXY_FOOTER"`
    48  
    49  	CookieName     string        `flag:"cookie-name" cfg:"cookie_name" env:"OAUTH2_PROXY_COOKIE_NAME"`
    50  	CookieSecret   string        `flag:"cookie-secret" cfg:"cookie_secret" env:"OAUTH2_PROXY_COOKIE_SECRET"`
    51  	CookieDomain   string        `flag:"cookie-domain" cfg:"cookie_domain" env:"OAUTH2_PROXY_COOKIE_DOMAIN"`
    52  	CookiePath     string        `flag:"cookie-path" cfg:"cookie_path" env:"OAUTH2_PROXY_COOKIE_PATH"`
    53  	CookieExpire   time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"OAUTH2_PROXY_COOKIE_EXPIRE"`
    54  	CookieRefresh  time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"OAUTH2_PROXY_COOKIE_REFRESH"`
    55  	CookieSecure   bool          `flag:"cookie-secure" cfg:"cookie_secure" env:"OAUTH2_PROXY_COOKIE_SECURE"`
    56  	CookieHTTPOnly bool          `flag:"cookie-httponly" cfg:"cookie_httponly" env:"OAUTH2_PROXY_COOKIE_HTTPONLY"`
    57  
    58  	Upstreams             []string      `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"`
    59  	SkipAuthRegex         []string      `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"`
    60  	PassBasicAuth         bool          `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"`
    61  	BasicAuthPassword     string        `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"`
    62  	PassAccessToken       bool          `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"`
    63  	PassHostHeader        bool          `flag:"pass-host-header" cfg:"pass_host_header" env:"OAUTH2_PROXY_PASS_HOST_HEADER"`
    64  	SkipProviderButton    bool          `flag:"skip-provider-button" cfg:"skip_provider_button" env:"OAUTH2_PROXY_SKIP_PROVIDER_BUTTON"`
    65  	PassUserHeaders       bool          `flag:"pass-user-headers" cfg:"pass_user_headers" env:"OAUTH2_PROXY_PASS_USER_HEADERS"`
    66  	SSLInsecureSkipVerify bool          `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify" env:"OAUTH2_PROXY_SSL_INSECURE_SKIP_VERIFY"`
    67  	SetXAuthRequest       bool          `flag:"set-xauthrequest" cfg:"set_xauthrequest" env:"OAUTH2_PROXY_SET_XAUTHREQUEST"`
    68  	SetAuthorization      bool          `flag:"set-authorization-header" cfg:"set_authorization_header" env:"OAUTH2_PROXY_SET_AUTHORIZATION_HEADER"`
    69  	PassAuthorization     bool          `flag:"pass-authorization-header" cfg:"pass_authorization_header" env:"OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER"`
    70  	SkipAuthPreflight     bool          `flag:"skip-auth-preflight" cfg:"skip_auth_preflight" env:"OAUTH2_PROXY_SKIP_AUTH_PREFLIGHT"`
    71  	FlushInterval         time.Duration `flag:"flush-interval" cfg:"flush_interval" env:"OAUTH2_PROXY_FLUSH_INTERVAL"`
    72  
    73  	// These options allow for other providers besides Google, with
    74  	// potential overrides.
    75  	Provider          string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"`
    76  	OIDCIssuerURL     string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url" env:"OAUTH2_PROXY_OIDC_ISSUER_URL"`
    77  	SkipOIDCDiscovery bool   `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_SKIP_OIDC_DISCOVERY"`
    78  	OIDCJwksURL       string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_OIDC_JWKS_URL"`
    79  	LoginURL          string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"`
    80  	RedeemURL         string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"`
    81  	ProfileURL        string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"`
    82  	ProtectedResource string `flag:"resource" cfg:"resource" env:"OAUTH2_PROXY_RESOURCE"`
    83  	ValidateURL       string `flag:"validate-url" cfg:"validate_url" env:"OAUTH2_PROXY_VALIDATE_URL"`
    84  	Scope             string `flag:"scope" cfg:"scope" env:"OAUTH2_PROXY_SCOPE"`
    85  	ApprovalPrompt    string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"`
    86  
    87  	RequestLogging       bool   `flag:"request-logging" cfg:"request_logging" env:"OAUTH2_PROXY_REQUEST_LOGGING"`
    88  	RequestLoggingFormat string `flag:"request-logging-format" cfg:"request_logging_format" env:"OAUTH2_PROXY_REQUEST_LOGGING_FORMAT"`
    89  
    90  	SignatureKey    string `flag:"signature-key" cfg:"signature_key" env:"OAUTH2_PROXY_SIGNATURE_KEY"`
    91  	AcrValues       string `flag:"acr-values" cfg:"acr_values" env:"OAUTH2_PROXY_ACR_VALUES"`
    92  	JWTKey          string `flag:"jwt-key" cfg:"jwt_key" env:"OAUTH2_PROXY_JWT_KEY"`
    93  	PubJWKURL       string `flag:"pubjwk-url" cfg:"pubjwk_url" env:"OAUTH2_PROXY_PUBJWK_URL"`
    94  	GCPHealthChecks bool   `flag:"gcp-healthchecks" cfg:"gcp_healthchecks" env:"OAUTH2_PROXY_GCP_HEALTHCHECKS"`
    95  
    96  	// internal values that are set after config validation
    97  	redirectURL   *url.URL
    98  	proxyURLs     []*url.URL
    99  	CompiledRegex []*regexp.Regexp
   100  	provider      providers.Provider
   101  	signatureData *SignatureData
   102  	oidcVerifier  *oidc.IDTokenVerifier
   103  }
   104  
   105  // SignatureData holds hmacauth signature hash and key
   106  type SignatureData struct {
   107  	hash crypto.Hash
   108  	key  string
   109  }
   110  
   111  // NewOptions constructs a new Options with defaulted values
   112  func NewOptions() *Options {
   113  	return &Options{
   114  		ProxyPrefix:          "/oauth2",
   115  		ProxyWebSockets:      true,
   116  		HTTPAddress:          "127.0.0.1:4180",
   117  		HTTPSAddress:         ":443",
   118  		DisplayHtpasswdForm:  true,
   119  		CookieName:           "_oauth2_proxy",
   120  		CookieSecure:         true,
   121  		CookieHTTPOnly:       true,
   122  		CookieExpire:         time.Duration(168) * time.Hour,
   123  		CookieRefresh:        time.Duration(0),
   124  		SetXAuthRequest:      false,
   125  		SkipAuthPreflight:    false,
   126  		PassBasicAuth:        true,
   127  		PassUserHeaders:      true,
   128  		PassAccessToken:      false,
   129  		PassHostHeader:       true,
   130  		SetAuthorization:     false,
   131  		PassAuthorization:    false,
   132  		ApprovalPrompt:       "force",
   133  		RequestLogging:       true,
   134  		SkipOIDCDiscovery:    false,
   135  		RequestLoggingFormat: defaultRequestLoggingFormat,
   136  	}
   137  }
   138  
   139  func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) {
   140  	parsed, err := url.Parse(toParse)
   141  	if err != nil {
   142  		return nil, append(msgs, fmt.Sprintf(
   143  			"error parsing %s-url=%q %s", urltype, toParse, err))
   144  	}
   145  	return parsed, msgs
   146  }
   147  
   148  // Validate checks that required options are set and validates those that they
   149  // are of the correct format
   150  func (o *Options) Validate() error {
   151  	if o.SSLInsecureSkipVerify {
   152  		// TODO: Accept a certificate bundle.
   153  		insecureTransport := &http.Transport{
   154  			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
   155  		}
   156  		http.DefaultClient = &http.Client{Transport: insecureTransport}
   157  	}
   158  
   159  	msgs := make([]string, 0)
   160  	if o.CookieSecret == "" {
   161  		msgs = append(msgs, "missing setting: cookie-secret")
   162  	}
   163  	if o.ClientID == "" {
   164  		msgs = append(msgs, "missing setting: client-id")
   165  	}
   166  	// login.gov uses a signed JWT to authenticate, not a client-secret
   167  	if o.ClientSecret == "" && o.Provider != "login.gov" {
   168  		msgs = append(msgs, "missing setting: client-secret")
   169  	}
   170  	if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" {
   171  		msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required."+
   172  			"\n      use email-domain=* to authorize all email addresses")
   173  	}
   174  
   175  	if o.OIDCIssuerURL != "" {
   176  
   177  		ctx := context.Background()
   178  
   179  		// Construct a manual IDTokenVerifier from issuer URL & JWKS URI
   180  		// instead of metadata discovery if we enable -skip-oidc-discovery.
   181  		// In this case we need to make sure the required endpoints for
   182  		// the provider are configured.
   183  		if o.SkipOIDCDiscovery {
   184  			if o.LoginURL == "" {
   185  				msgs = append(msgs, "missing setting: login-url")
   186  			}
   187  			if o.RedeemURL == "" {
   188  				msgs = append(msgs, "missing setting: redeem-url")
   189  			}
   190  			if o.OIDCJwksURL == "" {
   191  				msgs = append(msgs, "missing setting: oidc-jwks-url")
   192  			}
   193  			keySet := oidc.NewRemoteKeySet(ctx, o.OIDCJwksURL)
   194  			o.oidcVerifier = oidc.NewVerifier(o.OIDCIssuerURL, keySet, &oidc.Config{
   195  				ClientID: o.ClientID,
   196  			})
   197  		} else {
   198  			// Configure discoverable provider data.
   199  			provider, err := oidc.NewProvider(ctx, o.OIDCIssuerURL)
   200  			if err != nil {
   201  				return err
   202  			}
   203  			o.oidcVerifier = provider.Verifier(&oidc.Config{
   204  				ClientID: o.ClientID,
   205  			})
   206  
   207  			o.LoginURL = provider.Endpoint().AuthURL
   208  			o.RedeemURL = provider.Endpoint().TokenURL
   209  		}
   210  		if o.Scope == "" {
   211  			o.Scope = "openid email profile"
   212  		}
   213  	}
   214  
   215  	o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
   216  
   217  	for _, u := range o.Upstreams {
   218  		upstreamURL, err := url.Parse(u)
   219  		if err != nil {
   220  			msgs = append(msgs, fmt.Sprintf("error parsing upstream: %s", err))
   221  		} else {
   222  			if upstreamURL.Path == "" {
   223  				upstreamURL.Path = "/"
   224  			}
   225  			o.proxyURLs = append(o.proxyURLs, upstreamURL)
   226  		}
   227  	}
   228  
   229  	for _, u := range o.SkipAuthRegex {
   230  		CompiledRegex, err := regexp.Compile(u)
   231  		if err != nil {
   232  			msgs = append(msgs, fmt.Sprintf("error compiling regex=%q %s", u, err))
   233  			continue
   234  		}
   235  		o.CompiledRegex = append(o.CompiledRegex, CompiledRegex)
   236  	}
   237  	msgs = parseProviderInfo(o, msgs)
   238  
   239  	if o.PassAccessToken || (o.CookieRefresh != time.Duration(0)) {
   240  		validCookieSecretSize := false
   241  		for _, i := range []int{16, 24, 32} {
   242  			if len(secretBytes(o.CookieSecret)) == i {
   243  				validCookieSecretSize = true
   244  			}
   245  		}
   246  		var decoded bool
   247  		if string(secretBytes(o.CookieSecret)) != o.CookieSecret {
   248  			decoded = true
   249  		}
   250  		if validCookieSecretSize == false {
   251  			var suffix string
   252  			if decoded {
   253  				suffix = fmt.Sprintf(" note: cookie secret was base64 decoded from %q", o.CookieSecret)
   254  			}
   255  			msgs = append(msgs, fmt.Sprintf(
   256  				"cookie_secret must be 16, 24, or 32 bytes "+
   257  					"to create an AES cipher when "+
   258  					"pass_access_token == true or "+
   259  					"cookie_refresh != 0, but is %d bytes.%s",
   260  				len(secretBytes(o.CookieSecret)), suffix))
   261  		}
   262  	}
   263  
   264  	if o.CookieRefresh >= o.CookieExpire {
   265  		msgs = append(msgs, fmt.Sprintf(
   266  			"cookie_refresh (%s) must be less than "+
   267  				"cookie_expire (%s)",
   268  			o.CookieRefresh.String(),
   269  			o.CookieExpire.String()))
   270  	}
   271  
   272  	if len(o.GoogleGroups) > 0 || o.GoogleAdminEmail != "" || o.GoogleServiceAccountJSON != "" {
   273  		if len(o.GoogleGroups) < 1 {
   274  			msgs = append(msgs, "missing setting: google-group")
   275  		}
   276  		if o.GoogleAdminEmail == "" {
   277  			msgs = append(msgs, "missing setting: google-admin-email")
   278  		}
   279  		if o.GoogleServiceAccountJSON == "" {
   280  			msgs = append(msgs, "missing setting: google-service-account-json")
   281  		}
   282  	}
   283  
   284  	msgs = parseSignatureKey(o, msgs)
   285  	msgs = validateCookieName(o, msgs)
   286  
   287  	if len(msgs) != 0 {
   288  		return fmt.Errorf("Invalid configuration:\n  %s",
   289  			strings.Join(msgs, "\n  "))
   290  	}
   291  	return nil
   292  }
   293  
   294  func parseProviderInfo(o *Options, msgs []string) []string {
   295  	p := &providers.ProviderData{
   296  		Scope:          o.Scope,
   297  		ClientID:       o.ClientID,
   298  		ClientSecret:   o.ClientSecret,
   299  		ApprovalPrompt: o.ApprovalPrompt,
   300  	}
   301  	p.LoginURL, msgs = parseURL(o.LoginURL, "login", msgs)
   302  	p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs)
   303  	p.ProfileURL, msgs = parseURL(o.ProfileURL, "profile", msgs)
   304  	p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
   305  	p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs)
   306  
   307  	o.provider = providers.New(o.Provider, p)
   308  	switch p := o.provider.(type) {
   309  	case *providers.AzureProvider:
   310  		p.Configure(o.AzureTenant)
   311  	case *providers.GitHubProvider:
   312  		p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
   313  	case *providers.GoogleProvider:
   314  		if o.GoogleServiceAccountJSON != "" {
   315  			file, err := os.Open(o.GoogleServiceAccountJSON)
   316  			if err != nil {
   317  				msgs = append(msgs, "invalid Google credentials file: "+o.GoogleServiceAccountJSON)
   318  			} else {
   319  				p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file)
   320  			}
   321  		}
   322  	case *providers.OIDCProvider:
   323  		if o.oidcVerifier == nil {
   324  			msgs = append(msgs, "oidc provider requires an oidc issuer URL")
   325  		} else {
   326  			p.Verifier = o.oidcVerifier
   327  		}
   328  	case *providers.LoginGovProvider:
   329  		p.AcrValues = o.AcrValues
   330  		p.PubJWKURL, msgs = parseURL(o.PubJWKURL, "pubjwk", msgs)
   331  		if o.JWTKey == "" {
   332  			msgs = append(msgs, "login.gov provider requires a private key for signing JWTs")
   333  		} else {
   334  			signKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(o.JWTKey))
   335  			if err != nil {
   336  				msgs = append(msgs, "could not parse RSA Private Key PEM")
   337  			} else {
   338  				p.JWTKey = signKey
   339  			}
   340  		}
   341  	}
   342  	return msgs
   343  }
   344  
   345  func parseSignatureKey(o *Options, msgs []string) []string {
   346  	if o.SignatureKey == "" {
   347  		return msgs
   348  	}
   349  
   350  	components := strings.Split(o.SignatureKey, ":")
   351  	if len(components) != 2 {
   352  		return append(msgs, "invalid signature hash:key spec: "+
   353  			o.SignatureKey)
   354  	}
   355  
   356  	algorithm, secretKey := components[0], components[1]
   357  	var hash crypto.Hash
   358  	var err error
   359  	if hash, err = hmacauth.DigestNameToCryptoHash(algorithm); err != nil {
   360  		return append(msgs, "unsupported signature hash algorithm: "+
   361  			o.SignatureKey)
   362  	}
   363  	o.signatureData = &SignatureData{hash, secretKey}
   364  	return msgs
   365  }
   366  
   367  func validateCookieName(o *Options, msgs []string) []string {
   368  	cookie := &http.Cookie{Name: o.CookieName}
   369  	if cookie.String() == "" {
   370  		return append(msgs, fmt.Sprintf("invalid cookie name: %q", o.CookieName))
   371  	}
   372  	return msgs
   373  }
   374  
   375  func addPadding(secret string) string {
   376  	padding := len(secret) % 4
   377  	switch padding {
   378  	case 1:
   379  		return secret + "==="
   380  	case 2:
   381  		return secret + "=="
   382  	case 3:
   383  		return secret + "="
   384  	default:
   385  		return secret
   386  	}
   387  }
   388  
   389  // secretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
   390  func secretBytes(secret string) []byte {
   391  	b, err := base64.URLEncoding.DecodeString(addPadding(secret))
   392  	if err == nil {
   393  		return []byte(addPadding(string(b)))
   394  	}
   395  	return []byte(secret)
   396  }