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 }