git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge commit '2bee25b'
commit9c54946129741dfc1b79e1c735f35504d86cc721 authorTero Pihlaja <tero.pihlaja@turckvilant.com> dateWed 27 May 2020, 09:52 (6 years ago) parent11c8a982bee25b
3 files changed+924
oauthproxy.go+13 options.go+627 options_test.go+284
oauthproxy.go+13
@@ -147,7 +147,20 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
147147// NewReverseProxy creates a new reverse proxy for proxying requests to upstream
148148// servers
149149func NewReverseProxy(target *url.URL, opts *options.Options) (proxy *httputil.ReverseProxy) {
150+
150151 proxy = httputil.NewSingleHostReverseProxy(target)
152+ proxy.Transport = &http.Transport{
153+ Proxy: http.ProxyFromEnvironment,
154+ DialContext: (&net.Dialer{
155+ Timeout: time.Duration(opts.ProxyTimeOut) * time.Second,
156+ KeepAlive: 30 * time.Second,
157+ DualStack: true,
158+ }).DialContext,
159+ MaxIdleConns: 100,
160+ IdleConnTimeout: 90 * time.Second,
161+ TLSHandshakeTimeout: 10 * time.Second,
162+ ExpectContinueTimeout: 1 * time.Second,
163+ }
151164 proxy.FlushInterval = opts.FlushInterval
152165 if opts.SSLUpstreamInsecureSkipVerify {
153166 proxy.Transport = &http.Transport{
options.gonew file+627
@@ -0,0 +1,627 @@
1+package main
2+
3+import (
4+ "context"
5+ "crypto"
6+ "crypto/tls"
7+ "encoding/base64"
8+ "fmt"
9+ "io/ioutil"
10+ "net/http"
11+ "net/url"
12+ "os"
13+ "regexp"
14+ "strings"
15+ "time"
16+
17+ oidc "github.com/coreos/go-oidc"
18+ "github.com/dgrijalva/jwt-go"
19+ "github.com/mbland/hmacauth"
20+ "github.com/pusher/oauth2_proxy/pkg/apis/options"
21+ sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
22+ "github.com/pusher/oauth2_proxy/pkg/encryption"
23+ "github.com/pusher/oauth2_proxy/pkg/logger"
24+ "github.com/pusher/oauth2_proxy/pkg/sessions"
25+ "github.com/pusher/oauth2_proxy/providers"
26+ "gopkg.in/natefinch/lumberjack.v2"
27+)
28+
29+// Options holds Configuration Options that can be set by Command Line Flag,
30+// or Config File
31+type Options struct {
32+ ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix" env:"OAUTH2_PROXY_PROXY_PREFIX"`
33+ PingPath string `flag:"ping-path" cfg:"ping_path" env:"OAUTH2_PROXY_PING_PATH"`
34+ ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets" env:"OAUTH2_PROXY_PROXY_WEBSOCKETS"`
35+ HTTPAddress string `flag:"http-address" cfg:"http_address" env:"OAUTH2_PROXY_HTTP_ADDRESS"`
36+ HTTPSAddress string `flag:"https-address" cfg:"https_address" env:"OAUTH2_PROXY_HTTPS_ADDRESS"`
37+ RedirectURL string `flag:"redirect-url" cfg:"redirect_url" env:"OAUTH2_PROXY_REDIRECT_URL"`
38+ ClientID string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"`
39+ ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"`
40+ TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"`
41+ TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"`
42+ ProxyTimeOut time.Duration `flag:"proxy-time-out" cfg:"proxy_time_out" env:"OAUTH2_PROXY_PROXY_TIME_OUT"`
43+
44+ AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"`
45+ KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group" env:"OAUTH2_PROXY_KEYCLOAK_GROUP"`
46+ AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"`
47+ BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team" env:"OAUTH2_PROXY_BITBUCKET_TEAM"`
48+ BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository" env:"OAUTH2_PROXY_BITBUCKET_REPOSITORY"`
49+ EmailDomains []string `flag:"email-domain" cfg:"email_domains" env:"OAUTH2_PROXY_EMAIL_DOMAINS"`
50+ WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains" env:"OAUTH2_PROXY_WHITELIST_DOMAINS"`
51+ GitHubOrg string `flag:"github-org" cfg:"github_org" env:"OAUTH2_PROXY_GITHUB_ORG"`
52+ GitHubTeam string `flag:"github-team" cfg:"github_team" env:"OAUTH2_PROXY_GITHUB_TEAM"`
53+ GitLabGroup string `flag:"gitlab-group" cfg:"gitlab_group" env:"OAUTH2_PROXY_GITLAB_GROUP"`
54+ GoogleGroups []string `flag:"google-group" cfg:"google_group" env:"OAUTH2_PROXY_GOOGLE_GROUPS"`
55+ GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email" env:"OAUTH2_PROXY_GOOGLE_ADMIN_EMAIL"`
56+ GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json" env:"OAUTH2_PROXY_GOOGLE_SERVICE_ACCOUNT_JSON"`
57+ HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file" env:"OAUTH2_PROXY_HTPASSWD_FILE"`
58+ DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form" env:"OAUTH2_PROXY_DISPLAY_HTPASSWD_FORM"`
59+ CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir" env:"OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR"`
60+ Banner string `flag:"banner" cfg:"banner" env:"OAUTH2_PROXY_BANNER"`
61+ Footer string `flag:"footer" cfg:"footer" env:"OAUTH2_PROXY_FOOTER"`
62+
63+ // Embed CookieOptions
64+ options.CookieOptions
65+
66+ // Embed SessionOptions
67+ options.SessionOptions
68+
69+ Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"`
70+ SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"`
71+ SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens" env:"OAUTH2_PROXY_SKIP_JWT_BEARER_TOKENS"`
72+ ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers" env:"OAUTH2_PROXY_EXTRA_JWT_ISSUERS"`
73+ PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"`
74+ BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"`
75+ PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"`
76+ PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header" env:"OAUTH2_PROXY_PASS_HOST_HEADER"`
77+ SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button" env:"OAUTH2_PROXY_SKIP_PROVIDER_BUTTON"`
78+ PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers" env:"OAUTH2_PROXY_PASS_USER_HEADERS"`
79+ SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify" env:"OAUTH2_PROXY_SSL_INSECURE_SKIP_VERIFY"`
80+ SSLUpstreamInsecureSkipVerify bool `flag:"ssl-upstream-insecure-skip-verify" cfg:"ssl_upstream_insecure_skip_verify" env:"OAUTH2_PROXY_SSL_UPSTREAM_INSECURE_SKIP_VERIFY"`
81+ SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest" env:"OAUTH2_PROXY_SET_XAUTHREQUEST"`
82+ SetAuthorization bool `flag:"set-authorization-header" cfg:"set_authorization_header" env:"OAUTH2_PROXY_SET_AUTHORIZATION_HEADER"`
83+ PassAuthorization bool `flag:"pass-authorization-header" cfg:"pass_authorization_header" env:"OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER"`
84+ SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight" env:"OAUTH2_PROXY_SKIP_AUTH_PREFLIGHT"`
85+ FlushInterval time.Duration `flag:"flush-interval" cfg:"flush_interval" env:"OAUTH2_PROXY_FLUSH_INTERVAL"`
86+
87+ // These options allow for other providers besides Google, with
88+ // potential overrides.
89+ Provider string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"`
90+ OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url" env:"OAUTH2_PROXY_OIDC_ISSUER_URL"`
91+ InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email" env:"OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL"`
92+ SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_PROXY_SKIP_OIDC_DISCOVERY"`
93+ OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_PROXY_OIDC_JWKS_URL"`
94+ LoginURL string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"`
95+ RedeemURL string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"`
96+ ProfileURL string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"`
97+ ProtectedResource string `flag:"resource" cfg:"resource" env:"OAUTH2_PROXY_RESOURCE"`
98+ ValidateURL string `flag:"validate-url" cfg:"validate_url" env:"OAUTH2_PROXY_VALIDATE_URL"`
99+ Scope string `flag:"scope" cfg:"scope" env:"OAUTH2_PROXY_SCOPE"`
100+ ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"`
101+
102+ // Configuration values for logging
103+ LoggingFilename string `flag:"logging-filename" cfg:"logging_filename" env:"OAUTH2_PROXY_LOGGING_FILENAME"`
104+ LoggingMaxSize int `flag:"logging-max-size" cfg:"logging_max_size" env:"OAUTH2_PROXY_LOGGING_MAX_SIZE"`
105+ LoggingMaxAge int `flag:"logging-max-age" cfg:"logging_max_age" env:"OAUTH2_PROXY_LOGGING_MAX_AGE"`
106+ LoggingMaxBackups int `flag:"logging-max-backups" cfg:"logging_max_backups" env:"OAUTH2_PROXY_LOGGING_MAX_BACKUPS"`
107+ LoggingLocalTime bool `flag:"logging-local-time" cfg:"logging_local_time" env:"OAUTH2_PROXY_LOGGING_LOCAL_TIME"`
108+ LoggingCompress bool `flag:"logging-compress" cfg:"logging_compress" env:"OAUTH2_PROXY_LOGGING_COMPRESS"`
109+ StandardLogging bool `flag:"standard-logging" cfg:"standard_logging" env:"OAUTH2_PROXY_STANDARD_LOGGING"`
110+ StandardLoggingFormat string `flag:"standard-logging-format" cfg:"standard_logging_format" env:"OAUTH2_PROXY_STANDARD_LOGGING_FORMAT"`
111+ RequestLogging bool `flag:"request-logging" cfg:"request_logging" env:"OAUTH2_PROXY_REQUEST_LOGGING"`
112+ RequestLoggingFormat string `flag:"request-logging-format" cfg:"request_logging_format" env:"OAUTH2_PROXY_REQUEST_LOGGING_FORMAT"`
113+ ExcludeLoggingPaths string `flag:"exclude-logging-paths" cfg:"exclude_logging_paths" env:"OAUTH2_PROXY_EXCLUDE_LOGGING_PATHS"`
114+ SilencePingLogging bool `flag:"silence-ping-logging" cfg:"silence_ping_logging" env:"OAUTH2_PROXY_SILENCE_PING_LOGGING"`
115+ AuthLogging bool `flag:"auth-logging" cfg:"auth_logging" env:"OAUTH2_PROXY_LOGGING_AUTH_LOGGING"`
116+ AuthLoggingFormat string `flag:"auth-logging-format" cfg:"auth_logging_format" env:"OAUTH2_PROXY_AUTH_LOGGING_FORMAT"`
117+ SignatureKey string `flag:"signature-key" cfg:"signature_key" env:"OAUTH2_PROXY_SIGNATURE_KEY"`
118+ AcrValues string `flag:"acr-values" cfg:"acr_values" env:"OAUTH2_PROXY_ACR_VALUES"`
119+ JWTKey string `flag:"jwt-key" cfg:"jwt_key" env:"OAUTH2_PROXY_JWT_KEY"`
120+ JWTKeyFile string `flag:"jwt-key-file" cfg:"jwt_key_file" env:"OAUTH2_PROXY_JWT_KEY_FILE"`
121+ PubJWKURL string `flag:"pubjwk-url" cfg:"pubjwk_url" env:"OAUTH2_PROXY_PUBJWK_URL"`
122+ GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks" env:"OAUTH2_PROXY_GCP_HEALTHCHECKS"`
123+
124+ // internal values that are set after config validation
125+ redirectURL *url.URL
126+ proxyURLs []*url.URL
127+ CompiledRegex []*regexp.Regexp
128+ provider providers.Provider
129+ sessionStore sessionsapi.SessionStore
130+ signatureData *SignatureData
131+ oidcVerifier *oidc.IDTokenVerifier
132+ jwtBearerVerifiers []*oidc.IDTokenVerifier
133+}
134+
135+// SignatureData holds hmacauth signature hash and key
136+type SignatureData struct {
137+ hash crypto.Hash
138+ key string
139+}
140+
141+// NewOptions constructs a new Options with defaulted values
142+func NewOptions() *Options {
143+ return &Options{
144+ ProxyPrefix: "/oauth2",
145+ PingPath: "/ping",
146+ ProxyWebSockets: true,
147+ HTTPAddress: "127.0.0.1:4180",
148+ HTTPSAddress: ":443",
149+ ProxyTimeOut: 30, //TIME_OUT in seconds
150+ DisplayHtpasswdForm: true,
151+ CookieOptions: options.CookieOptions{
152+ CookieName: "_oauth2_proxy",
153+ CookieSecure: true,
154+ CookieHTTPOnly: true,
155+ CookieExpire: time.Duration(168) * time.Hour,
156+ CookieRefresh: time.Duration(0),
157+ },
158+ SessionOptions: options.SessionOptions{
159+ Type: "cookie",
160+ },
161+ SetXAuthRequest: false,
162+ SkipAuthPreflight: false,
163+ PassBasicAuth: true,
164+ PassUserHeaders: true,
165+ PassAccessToken: false,
166+ PassHostHeader: true,
167+ SetAuthorization: false,
168+ PassAuthorization: false,
169+ ApprovalPrompt: "force",
170+ InsecureOIDCAllowUnverifiedEmail: false,
171+ SkipOIDCDiscovery: false,
172+ LoggingFilename: "",
173+ LoggingMaxSize: 100,
174+ LoggingMaxAge: 7,
175+ LoggingMaxBackups: 0,
176+ LoggingLocalTime: true,
177+ LoggingCompress: false,
178+ ExcludeLoggingPaths: "",
179+ SilencePingLogging: false,
180+ StandardLogging: true,
181+ StandardLoggingFormat: logger.DefaultStandardLoggingFormat,
182+ RequestLogging: true,
183+ RequestLoggingFormat: logger.DefaultRequestLoggingFormat,
184+ AuthLogging: true,
185+ AuthLoggingFormat: logger.DefaultAuthLoggingFormat,
186+ }
187+}
188+
189+// jwtIssuer hold parsed JWT issuer info that's used to construct a verifier.
190+type jwtIssuer struct {
191+ issuerURI string
192+ audience string
193+}
194+
195+func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) {
196+ parsed, err := url.Parse(toParse)
197+ if err != nil {
198+ return nil, append(msgs, fmt.Sprintf(
199+ "error parsing %s-url=%q %s", urltype, toParse, err))
200+ }
201+ return parsed, msgs
202+}
203+
204+// Validate checks that required options are set and validates those that they
205+// are of the correct format
206+func (o *Options) Validate() error {
207+ if o.SSLInsecureSkipVerify {
208+ // TODO: Accept a certificate bundle.
209+ insecureTransport := &http.Transport{
210+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
211+ }
212+ http.DefaultClient = &http.Client{Transport: insecureTransport}
213+ }
214+
215+ msgs := make([]string, 0)
216+ if o.CookieSecret == "" {
217+ msgs = append(msgs, "missing setting: cookie-secret")
218+ }
219+ if o.ClientID == "" {
220+ msgs = append(msgs, "missing setting: client-id")
221+ }
222+ // login.gov uses a signed JWT to authenticate, not a client-secret
223+ if o.ClientSecret == "" && o.Provider != "login.gov" {
224+ msgs = append(msgs, "missing setting: client-secret")
225+ }
226+ if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" {
227+ msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required."+
228+ "\n use email-domain=* to authorize all email addresses")
229+ }
230+
231+ if o.OIDCIssuerURL != "" {
232+
233+ ctx := context.Background()
234+
235+ // Construct a manual IDTokenVerifier from issuer URL & JWKS URI
236+ // instead of metadata discovery if we enable -skip-oidc-discovery.
237+ // In this case we need to make sure the required endpoints for
238+ // the provider are configured.
239+ if o.SkipOIDCDiscovery {
240+ if o.LoginURL == "" {
241+ msgs = append(msgs, "missing setting: login-url")
242+ }
243+ if o.RedeemURL == "" {
244+ msgs = append(msgs, "missing setting: redeem-url")
245+ }
246+ if o.OIDCJwksURL == "" {
247+ msgs = append(msgs, "missing setting: oidc-jwks-url")
248+ }
249+ keySet := oidc.NewRemoteKeySet(ctx, o.OIDCJwksURL)
250+ o.oidcVerifier = oidc.NewVerifier(o.OIDCIssuerURL, keySet, &oidc.Config{
251+ ClientID: o.ClientID,
252+ })
253+ } else {
254+ // Configure discoverable provider data.
255+ provider, err := oidc.NewProvider(ctx, o.OIDCIssuerURL)
256+ if err != nil {
257+ return err
258+ }
259+ o.oidcVerifier = provider.Verifier(&oidc.Config{
260+ ClientID: o.ClientID,
261+ })
262+
263+ o.LoginURL = provider.Endpoint().AuthURL
264+ o.RedeemURL = provider.Endpoint().TokenURL
265+ }
266+ if o.Scope == "" {
267+ o.Scope = "openid email profile"
268+ }
269+ }
270+
271+ if o.SkipJwtBearerTokens {
272+ // If we are using an oidc provider, go ahead and add that provider to the list
273+ if o.oidcVerifier != nil {
274+ o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, o.oidcVerifier)
275+ }
276+ // Configure extra issuers
277+ if len(o.ExtraJwtIssuers) > 0 {
278+ var jwtIssuers []jwtIssuer
279+ jwtIssuers, msgs = parseJwtIssuers(o.ExtraJwtIssuers, msgs)
280+ for _, jwtIssuer := range jwtIssuers {
281+ verifier, err := newVerifierFromJwtIssuer(jwtIssuer)
282+ if err != nil {
283+ msgs = append(msgs, fmt.Sprintf("error building verifiers: %s", err))
284+ }
285+ o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, verifier)
286+ }
287+ }
288+ }
289+
290+ o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
291+
292+ for _, u := range o.Upstreams {
293+ upstreamURL, err := url.Parse(u)
294+ if err != nil {
295+ msgs = append(msgs, fmt.Sprintf("error parsing upstream: %s", err))
296+ } else {
297+ if upstreamURL.Path == "" {
298+ upstreamURL.Path = "/"
299+ }
300+ o.proxyURLs = append(o.proxyURLs, upstreamURL)
301+ }
302+ }
303+
304+ for _, u := range o.SkipAuthRegex {
305+ CompiledRegex, err := regexp.Compile(u)
306+ if err != nil {
307+ msgs = append(msgs, fmt.Sprintf("error compiling regex=%q %s", u, err))
308+ continue
309+ }
310+ o.CompiledRegex = append(o.CompiledRegex, CompiledRegex)
311+ }
312+ msgs = parseProviderInfo(o, msgs)
313+
314+ var cipher *encryption.Cipher
315+ if o.PassAccessToken || o.SetAuthorization || o.PassAuthorization || (o.CookieRefresh != time.Duration(0)) {
316+ validCookieSecretSize := false
317+ for _, i := range []int{16, 24, 32} {
318+ if len(secretBytes(o.CookieSecret)) == i {
319+ validCookieSecretSize = true
320+ }
321+ }
322+ var decoded bool
323+ if string(secretBytes(o.CookieSecret)) != o.CookieSecret {
324+ decoded = true
325+ }
326+ if validCookieSecretSize == false {
327+ var suffix string
328+ if decoded {
329+ suffix = fmt.Sprintf(" note: cookie secret was base64 decoded from %q", o.CookieSecret)
330+ }
331+ msgs = append(msgs, fmt.Sprintf(
332+ "cookie_secret must be 16, 24, or 32 bytes "+
333+ "to create an AES cipher when "+
334+ "pass_access_token == true or "+
335+ "cookie_refresh != 0, but is %d bytes.%s",
336+ len(secretBytes(o.CookieSecret)), suffix))
337+ } else {
338+ var err error
339+ cipher, err = encryption.NewCipher(secretBytes(o.CookieSecret))
340+ if err != nil {
341+ msgs = append(msgs, fmt.Sprintf("cookie-secret error: %v", err))
342+ }
343+ }
344+ }
345+
346+ o.SessionOptions.Cipher = cipher
347+ sessionStore, err := sessions.NewSessionStore(&o.SessionOptions, &o.CookieOptions)
348+ if err != nil {
349+ msgs = append(msgs, fmt.Sprintf("error initialising session storage: %v", err))
350+ } else {
351+ o.sessionStore = sessionStore
352+ }
353+
354+ if o.CookieRefresh >= o.CookieExpire {
355+ msgs = append(msgs, fmt.Sprintf(
356+ "cookie_refresh (%s) must be less than "+
357+ "cookie_expire (%s)",
358+ o.CookieRefresh.String(),
359+ o.CookieExpire.String()))
360+ }
361+
362+ if len(o.GoogleGroups) > 0 || o.GoogleAdminEmail != "" || o.GoogleServiceAccountJSON != "" {
363+ if len(o.GoogleGroups) < 1 {
364+ msgs = append(msgs, "missing setting: google-group")
365+ }
366+ if o.GoogleAdminEmail == "" {
367+ msgs = append(msgs, "missing setting: google-admin-email")
368+ }
369+ if o.GoogleServiceAccountJSON == "" {
370+ msgs = append(msgs, "missing setting: google-service-account-json")
371+ }
372+ }
373+
374+ msgs = parseSignatureKey(o, msgs)
375+ msgs = validateCookieName(o, msgs)
376+ msgs = setupLogger(o, msgs)
377+
378+ if len(msgs) != 0 {
379+ return fmt.Errorf("Invalid configuration:\n %s",
380+ strings.Join(msgs, "\n "))
381+ }
382+ return nil
383+}
384+
385+func parseProviderInfo(o *Options, msgs []string) []string {
386+ p := &providers.ProviderData{
387+ Scope: o.Scope,
388+ ClientID: o.ClientID,
389+ ClientSecret: o.ClientSecret,
390+ ApprovalPrompt: o.ApprovalPrompt,
391+ }
392+ p.LoginURL, msgs = parseURL(o.LoginURL, "login", msgs)
393+ p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs)
394+ p.ProfileURL, msgs = parseURL(o.ProfileURL, "profile", msgs)
395+ p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
396+ p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs)
397+
398+ o.provider = providers.New(o.Provider, p)
399+ switch p := o.provider.(type) {
400+ case *providers.AzureProvider:
401+ p.Configure(o.AzureTenant)
402+ case *providers.GitHubProvider:
403+ p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
404+ case *providers.KeycloakProvider:
405+ p.SetGroup(o.KeycloakGroup)
406+ case *providers.GoogleProvider:
407+ if o.GoogleServiceAccountJSON != "" {
408+ file, err := os.Open(o.GoogleServiceAccountJSON)
409+ if err != nil {
410+ msgs = append(msgs, "invalid Google credentials file: "+o.GoogleServiceAccountJSON)
411+ } else {
412+ p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file)
413+ }
414+ }
415+ case *providers.BitbucketProvider:
416+ p.SetTeam(o.BitbucketTeam)
417+ p.SetRepository(o.BitbucketRepository)
418+ case *providers.OIDCProvider:
419+ p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
420+ if o.oidcVerifier == nil {
421+ msgs = append(msgs, "oidc provider requires an oidc issuer URL")
422+ } else {
423+ p.Verifier = o.oidcVerifier
424+ }
425+ case *providers.GitLabProvider:
426+ p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
427+ p.Group = o.GitLabGroup
428+ p.EmailDomains = o.EmailDomains
429+
430+ if o.oidcVerifier != nil {
431+ p.Verifier = o.oidcVerifier
432+ } else {
433+ // Initialize with default verifier for gitlab.com
434+ ctx := context.Background()
435+
436+ provider, err := oidc.NewProvider(ctx, "https://gitlab.com")
437+ if err != nil {
438+ msgs = append(msgs, "failed to initialize oidc provider for gitlab.com")
439+ } else {
440+ p.Verifier = provider.Verifier(&oidc.Config{
441+ ClientID: o.ClientID,
442+ })
443+
444+ p.LoginURL, msgs = parseURL(provider.Endpoint().AuthURL, "login", msgs)
445+ p.RedeemURL, msgs = parseURL(provider.Endpoint().TokenURL, "redeem", msgs)
446+ }
447+ }
448+ case *providers.LoginGovProvider:
449+ p.AcrValues = o.AcrValues
450+ p.PubJWKURL, msgs = parseURL(o.PubJWKURL, "pubjwk", msgs)
451+
452+ // JWT key can be supplied via env variable or file in the filesystem, but not both.
453+ switch {
454+ case o.JWTKey != "" && o.JWTKeyFile != "":
455+ msgs = append(msgs, "cannot set both jwt-key and jwt-key-file options")
456+ case o.JWTKey == "" && o.JWTKeyFile == "":
457+ msgs = append(msgs, "login.gov provider requires a private key for signing JWTs")
458+ case o.JWTKey != "":
459+ // The JWT Key is in the commandline argument
460+ signKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(o.JWTKey))
461+ if err != nil {
462+ msgs = append(msgs, "could not parse RSA Private Key PEM")
463+ } else {
464+ p.JWTKey = signKey
465+ }
466+ case o.JWTKeyFile != "":
467+ // The JWT key is in the filesystem
468+ keyData, err := ioutil.ReadFile(o.JWTKeyFile)
469+ if err != nil {
470+ msgs = append(msgs, "could not read key file: "+o.JWTKeyFile)
471+ }
472+ signKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyData)
473+ if err != nil {
474+ msgs = append(msgs, "could not parse private key from PEM file:"+o.JWTKeyFile)
475+ } else {
476+ p.JWTKey = signKey
477+ }
478+ }
479+ }
480+ return msgs
481+}
482+
483+func parseSignatureKey(o *Options, msgs []string) []string {
484+ if o.SignatureKey == "" {
485+ return msgs
486+ }
487+
488+ components := strings.Split(o.SignatureKey, ":")
489+ if len(components) != 2 {
490+ return append(msgs, "invalid signature hash:key spec: "+
491+ o.SignatureKey)
492+ }
493+
494+ algorithm, secretKey := components[0], components[1]
495+ var hash crypto.Hash
496+ var err error
497+ if hash, err = hmacauth.DigestNameToCryptoHash(algorithm); err != nil {
498+ return append(msgs, "unsupported signature hash algorithm: "+
499+ o.SignatureKey)
500+ }
501+ o.signatureData = &SignatureData{hash: hash, key: secretKey}
502+ return msgs
503+}
504+
505+// parseJwtIssuers takes in an array of strings in the form of issuer=audience
506+// and parses to an array of jwtIssuer structs.
507+func parseJwtIssuers(issuers []string, msgs []string) ([]jwtIssuer, []string) {
508+ var parsedIssuers []jwtIssuer
509+ for _, jwtVerifier := range issuers {
510+ components := strings.Split(jwtVerifier, "=")
511+ if len(components) < 2 {
512+ msgs = append(msgs, fmt.Sprintf("invalid jwt verifier uri=audience spec: %s", jwtVerifier))
513+ continue
514+ }
515+ uri, audience := components[0], strings.Join(components[1:], "=")
516+ parsedIssuers = append(parsedIssuers, jwtIssuer{issuerURI: uri, audience: audience})
517+ }
518+ return parsedIssuers, msgs
519+}
520+
521+// newVerifierFromJwtIssuer takes in issuer information in jwtIssuer info and returns
522+// a verifier for that issuer.
523+func newVerifierFromJwtIssuer(jwtIssuer jwtIssuer) (*oidc.IDTokenVerifier, error) {
524+ config := &oidc.Config{
525+ ClientID: jwtIssuer.audience,
526+ }
527+ // Try as an OpenID Connect Provider first
528+ var verifier *oidc.IDTokenVerifier
529+ provider, err := oidc.NewProvider(context.Background(), jwtIssuer.issuerURI)
530+ if err != nil {
531+ // Try as JWKS URI
532+ jwksURI := strings.TrimSuffix(jwtIssuer.issuerURI, "/") + "/.well-known/jwks.json"
533+ _, err := http.NewRequest("GET", jwksURI, nil)
534+ if err != nil {
535+ return nil, err
536+ }
537+ verifier = oidc.NewVerifier(jwtIssuer.issuerURI, oidc.NewRemoteKeySet(context.Background(), jwksURI), config)
538+ } else {
539+ verifier = provider.Verifier(config)
540+ }
541+ return verifier, nil
542+}
543+
544+func validateCookieName(o *Options, msgs []string) []string {
545+ cookie := &http.Cookie{Name: o.CookieName}
546+ if cookie.String() == "" {
547+ return append(msgs, fmt.Sprintf("invalid cookie name: %q", o.CookieName))
548+ }
549+ return msgs
550+}
551+
552+func addPadding(secret string) string {
553+ padding := len(secret) % 4
554+ switch padding {
555+ case 1:
556+ return secret + "==="
557+ case 2:
558+ return secret + "=="
559+ case 3:
560+ return secret + "="
561+ default:
562+ return secret
563+ }
564+}
565+
566+// secretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
567+func secretBytes(secret string) []byte {
568+ b, err := base64.URLEncoding.DecodeString(addPadding(secret))
569+ if err == nil {
570+ return []byte(addPadding(string(b)))
571+ }
572+ return []byte(secret)
573+}
574+
575+func setupLogger(o *Options, msgs []string) []string {
576+ // Setup the log file
577+ if len(o.LoggingFilename) > 0 {
578+ // Validate that the file/dir can be written
579+ file, err := os.OpenFile(o.LoggingFilename, os.O_WRONLY|os.O_CREATE, 0666)
580+ if err != nil {
581+ if os.IsPermission(err) {
582+ return append(msgs, "unable to write to log file: "+o.LoggingFilename)
583+ }
584+ }
585+ file.Close()
586+
587+ logger.Printf("Redirecting logging to file: %s", o.LoggingFilename)
588+
589+ logWriter := &lumberjack.Logger{
590+ Filename: o.LoggingFilename,
591+ MaxSize: o.LoggingMaxSize, // megabytes
592+ MaxAge: o.LoggingMaxAge, // days
593+ MaxBackups: o.LoggingMaxBackups,
594+ LocalTime: o.LoggingLocalTime,
595+ Compress: o.LoggingCompress,
596+ }
597+
598+ logger.SetOutput(logWriter)
599+ }
600+
601+ // Supply a sanity warning to the logger if all logging is disabled
602+ if !o.StandardLogging && !o.AuthLogging && !o.RequestLogging {
603+ logger.Print("Warning: Logging disabled. No further logs will be shown.")
604+ }
605+
606+ // Pass configuration values to the standard logger
607+ logger.SetStandardEnabled(o.StandardLogging)
608+ logger.SetAuthEnabled(o.AuthLogging)
609+ logger.SetReqEnabled(o.RequestLogging)
610+ logger.SetStandardTemplate(o.StandardLoggingFormat)
611+ logger.SetAuthTemplate(o.AuthLoggingFormat)
612+ logger.SetReqTemplate(o.RequestLoggingFormat)
613+
614+ excludePaths := make([]string, 0)
615+ excludePaths = append(excludePaths, strings.Split(o.ExcludeLoggingPaths, ",")...)
616+ if o.SilencePingLogging {
617+ excludePaths = append(excludePaths, o.PingPath)
618+ }
619+
620+ logger.SetExcludePaths(excludePaths)
621+
622+ if !o.LoggingLocalTime {
623+ logger.SetFlags(logger.Flags() | logger.LUTC)
624+ }
625+
626+ return msgs
627+}
options_test.gonew file+284
@@ -0,0 +1,284 @@
1+package main
2+
3+import (
4+ "crypto"
5+ "fmt"
6+ "net/url"
7+ "strings"
8+ "testing"
9+ "time"
10+
11+ "github.com/stretchr/testify/assert"
12+)
13+
14+func testOptions() *Options {
15+ o := NewOptions()
16+ o.Upstreams = append(o.Upstreams, "http://127.0.0.1:8080/")
17+ o.CookieSecret = "foobar"
18+ o.ClientID = "bazquux"
19+ o.ClientSecret = "xyzzyplugh"
20+ o.EmailDomains = []string{"*"}
21+ return o
22+}
23+
24+func errorMsg(msgs []string) string {
25+ result := make([]string, 0)
26+ result = append(result, "Invalid configuration:")
27+ result = append(result, msgs...)
28+ return strings.Join(result, "\n ")
29+}
30+
31+func TestNewOptions(t *testing.T) {
32+ o := NewOptions()
33+ o.EmailDomains = []string{"*"}
34+ err := o.Validate()
35+ assert.NotEqual(t, nil, err)
36+
37+ expected := errorMsg([]string{
38+ "missing setting: cookie-secret",
39+ "missing setting: client-id",
40+ "missing setting: client-secret"})
41+ assert.Equal(t, expected, err.Error())
42+}
43+
44+func TestGoogleGroupOptions(t *testing.T) {
45+ o := testOptions()
46+ o.GoogleGroups = []string{"googlegroup"}
47+ err := o.Validate()
48+ assert.NotEqual(t, nil, err)
49+
50+ expected := errorMsg([]string{
51+ "missing setting: google-admin-email",
52+ "missing setting: google-service-account-json"})
53+ assert.Equal(t, expected, err.Error())
54+}
55+
56+func TestGoogleGroupInvalidFile(t *testing.T) {
57+ o := testOptions()
58+ o.GoogleGroups = []string{"test_group"}
59+ o.GoogleAdminEmail = "admin@example.com"
60+ o.GoogleServiceAccountJSON = "file_doesnt_exist.json"
61+ err := o.Validate()
62+ assert.NotEqual(t, nil, err)
63+
64+ expected := errorMsg([]string{
65+ "invalid Google credentials file: file_doesnt_exist.json",
66+ })
67+ assert.Equal(t, expected, err.Error())
68+}
69+
70+func TestInitializedOptions(t *testing.T) {
71+ o := testOptions()
72+ assert.Equal(t, nil, o.Validate())
73+}
74+
75+// Note that it's not worth testing nonparseable URLs, since url.Parse()
76+// seems to parse damn near anything.
77+func TestRedirectURL(t *testing.T) {
78+ o := testOptions()
79+ o.RedirectURL = "https://myhost.com/oauth2/callback"
80+ assert.Equal(t, nil, o.Validate())
81+ expected := &url.URL{
82+ Scheme: "https", Host: "myhost.com", Path: "/oauth2/callback"}
83+ assert.Equal(t, expected, o.redirectURL)
84+}
85+
86+func TestProxyTimeOut(t *testing.T) {
87+ o := testOptions()
88+ o.ProxyTimeOut = 40
89+ assert.Equal(t, nil, o.Validate())
90+ expected := 40
91+ assert.Equal(t, expected, o.ProxyTimeOut)
92+}
93+
94+func TestProxyURLs(t *testing.T) {
95+ o := testOptions()
96+ o.Upstreams = append(o.Upstreams, "http://127.0.0.1:8081")
97+ assert.Equal(t, nil, o.Validate())
98+ expected := []*url.URL{
99+ {Scheme: "http", Host: "127.0.0.1:8080", Path: "/"},
100+ // note the '/' was added
101+ {Scheme: "http", Host: "127.0.0.1:8081", Path: "/"},
102+ }
103+ assert.Equal(t, expected, o.proxyURLs)
104+}
105+
106+func TestProxyURLsError(t *testing.T) {
107+ o := testOptions()
108+ o.Upstreams = append(o.Upstreams, "127.0.0.1:8081")
109+ err := o.Validate()
110+ assert.NotEqual(t, nil, err)
111+
112+ expected := errorMsg([]string{
113+ "error parsing upstream: parse 127.0.0.1:8081: " +
114+ "first path segment in URL cannot contain colon"})
115+ assert.Equal(t, expected, err.Error())
116+}
117+
118+func TestCompiledRegex(t *testing.T) {
119+ o := testOptions()
120+ regexps := []string{"/foo/.*", "/ba[rz]/quux"}
121+ o.SkipAuthRegex = regexps
122+ assert.Equal(t, nil, o.Validate())
123+ actual := make([]string, 0)
124+ for _, regex := range o.CompiledRegex {
125+ actual = append(actual, regex.String())
126+ }
127+ assert.Equal(t, regexps, actual)
128+}
129+
130+func TestCompiledRegexError(t *testing.T) {
131+ o := testOptions()
132+ o.SkipAuthRegex = []string{"(foobaz", "barquux)"}
133+ err := o.Validate()
134+ assert.NotEqual(t, nil, err)
135+
136+ expected := errorMsg([]string{
137+ "error compiling regex=\"(foobaz\" error parsing regexp: " +
138+ "missing closing ): `(foobaz`",
139+ "error compiling regex=\"barquux)\" error parsing regexp: " +
140+ "unexpected ): `barquux)`"})
141+ assert.Equal(t, expected, err.Error())
142+
143+ o.SkipAuthRegex = []string{"foobaz", "barquux)"}
144+ err = o.Validate()
145+ assert.NotEqual(t, nil, err)
146+
147+ expected = errorMsg([]string{
148+ "error compiling regex=\"barquux)\" error parsing regexp: " +
149+ "unexpected ): `barquux)`"})
150+ assert.Equal(t, expected, err.Error())
151+}
152+
153+func TestDefaultProviderApiSettings(t *testing.T) {
154+ o := testOptions()
155+ assert.Equal(t, nil, o.Validate())
156+ p := o.provider.Data()
157+ assert.Equal(t, "https://accounts.google.com/o/oauth2/auth?access_type=offline",
158+ p.LoginURL.String())
159+ assert.Equal(t, "https://www.googleapis.com/oauth2/v3/token",
160+ p.RedeemURL.String())
161+ assert.Equal(t, "", p.ProfileURL.String())
162+ assert.Equal(t, "profile email", p.Scope)
163+}
164+
165+func TestPassAccessTokenRequiresSpecificCookieSecretLengths(t *testing.T) {
166+ o := testOptions()
167+ assert.Equal(t, nil, o.Validate())
168+
169+ assert.Equal(t, false, o.PassAccessToken)
170+ o.PassAccessToken = true
171+ o.CookieSecret = "cookie of invalid length-"
172+ assert.NotEqual(t, nil, o.Validate())
173+
174+ o.PassAccessToken = false
175+ o.CookieRefresh = time.Duration(24) * time.Hour
176+ assert.NotEqual(t, nil, o.Validate())
177+
178+ o.CookieSecret = "16 bytes AES-128"
179+ assert.Equal(t, nil, o.Validate())
180+
181+ o.CookieSecret = "24 byte secret AES-192--"
182+ assert.Equal(t, nil, o.Validate())
183+
184+ o.CookieSecret = "32 byte secret for AES-256------"
185+ assert.Equal(t, nil, o.Validate())
186+}
187+
188+func TestCookieRefreshMustBeLessThanCookieExpire(t *testing.T) {
189+ o := testOptions()
190+ assert.Equal(t, nil, o.Validate())
191+
192+ o.CookieSecret = "0123456789abcdefabcd"
193+ o.CookieRefresh = o.CookieExpire
194+ assert.NotEqual(t, nil, o.Validate())
195+
196+ o.CookieRefresh -= time.Duration(1)
197+ assert.Equal(t, nil, o.Validate())
198+}
199+
200+func TestBase64CookieSecret(t *testing.T) {
201+ o := testOptions()
202+ assert.Equal(t, nil, o.Validate())
203+
204+ // 32 byte, base64 (urlsafe) encoded key
205+ o.CookieSecret = "yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ="
206+ assert.Equal(t, nil, o.Validate())
207+
208+ // 32 byte, base64 (urlsafe) encoded key, w/o padding
209+ o.CookieSecret = "yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ"
210+ assert.Equal(t, nil, o.Validate())
211+
212+ // 24 byte, base64 (urlsafe) encoded key
213+ o.CookieSecret = "Kp33Gj-GQmYtz4zZUyUDdqQKx5_Hgkv3"
214+ assert.Equal(t, nil, o.Validate())
215+
216+ // 16 byte, base64 (urlsafe) encoded key
217+ o.CookieSecret = "LFEqZYvYUwKwzn0tEuTpLA=="
218+ assert.Equal(t, nil, o.Validate())
219+
220+ // 16 byte, base64 (urlsafe) encoded key, w/o padding
221+ o.CookieSecret = "LFEqZYvYUwKwzn0tEuTpLA"
222+ assert.Equal(t, nil, o.Validate())
223+}
224+
225+func TestValidateSignatureKey(t *testing.T) {
226+ o := testOptions()
227+ o.SignatureKey = "sha1:secret"
228+ assert.Equal(t, nil, o.Validate())
229+ assert.Equal(t, o.signatureData.hash, crypto.SHA1)
230+ assert.Equal(t, o.signatureData.key, "secret")
231+}
232+
233+func TestValidateSignatureKeyInvalidSpec(t *testing.T) {
234+ o := testOptions()
235+ o.SignatureKey = "invalid spec"
236+ err := o.Validate()
237+ assert.Equal(t, err.Error(), "Invalid configuration:\n"+
238+ " invalid signature hash:key spec: "+o.SignatureKey)
239+}
240+
241+func TestValidateSignatureKeyUnsupportedAlgorithm(t *testing.T) {
242+ o := testOptions()
243+ o.SignatureKey = "unsupported:default secret"
244+ err := o.Validate()
245+ assert.Equal(t, err.Error(), "Invalid configuration:\n"+
246+ " unsupported signature hash algorithm: "+o.SignatureKey)
247+}
248+
249+func TestValidateCookie(t *testing.T) {
250+ o := testOptions()
251+ o.CookieName = "_valid_cookie_name"
252+ assert.Equal(t, nil, o.Validate())
253+}
254+
255+func TestValidateCookieBadName(t *testing.T) {
256+ o := testOptions()
257+ o.CookieName = "_bad_cookie_name{}"
258+ err := o.Validate()
259+ assert.Equal(t, err.Error(), "Invalid configuration:\n"+
260+ fmt.Sprintf(" invalid cookie name: %q", o.CookieName))
261+}
262+
263+func TestSkipOIDCDiscovery(t *testing.T) {
264+ o := testOptions()
265+ o.Provider = "oidc"
266+ o.OIDCIssuerURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/v2.0/"
267+ o.SkipOIDCDiscovery = true
268+
269+ err := o.Validate()
270+ assert.Equal(t, "Invalid configuration:\n"+
271+ fmt.Sprintf(" missing setting: login-url\n missing setting: redeem-url\n missing setting: oidc-jwks-url"), err.Error())
272+
273+ o.LoginURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1_sign_in"
274+ o.RedeemURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_sign_in"
275+ o.OIDCJwksURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/discovery/v2.0/keys"
276+
277+ assert.Equal(t, nil, o.Validate())
278+}
279+
280+func TestGCPHealthcheck(t *testing.T) {
281+ o := testOptions()
282+ o.GCPHealthChecks = true
283+ assert.Equal(t, nil, o.Validate())
284+}