diff --git a/main.go b/main.go index 13d18886..c978be99 100644 --- a/main.go +++ b/main.go @@ -67,7 +67,7 @@ func main() { logger.Fatalf("%s", err) } - validator := NewValidator(opts.EmailDomains, opts.AuthenticatedEmailsFile) + validator := NewValidator(opts.ProxyOptions.EmailDomains, opts.ProxyOptions.AuthenticatedEmailsFile) oauthproxy, err := NewOAuthProxy(opts, validator) if err != nil { logger.Fatalf("ERROR: Failed to initialise OAuth2 Proxy: %v", err) diff --git a/main_test.go b/main_test.go index 654cd0ac..741ff93a 100644 --- a/main_test.go +++ b/main_test.go @@ -34,9 +34,15 @@ google_target_principal="principal" cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" cookie_secure="false" + +email_domains="example.com" +redirect_url="http://localhost:4180/oauth2/callback" ` const testAlphaConfig = ` +proxyOptions: + emailDomains: ["example.com"] + redirectUrl: http://localhost:4180/oauth2/callback upstreamConfig: upstreams: - id: / @@ -116,19 +122,21 @@ providers: - force ` - const testCoreConfig = ` -email_domains="example.com" -redirect_url="http://localhost:4180/oauth2/callback" -` + const testCoreConfig = `` testExpectedOptions := func() *options.Options { opts, err := options.NewLegacyOptions().ToOptions() Expect(err).ToNot(HaveOccurred()) opts.Cookie.Secret = &options.SecretSource{Value: []byte("OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=")} - opts.EmailDomains = []string{"example.com"} opts.Cookie.Insecure = ptr.To(true) - opts.RawRedirectURL = "http://localhost:4180/oauth2/callback" + + opts.ProxyOptions = options.ProxyOptions{ + ProxyPrefix: "/oauth2", + RealClientIPHeader: "X-Real-IP", + EmailDomains: []string{"example.com"}, + RedirectURL: "http://localhost:4180/oauth2/callback", + } opts.UpstreamServers = options.UpstreamConfig{ ProxyRawPath: ptr.To(false), diff --git a/oauthproxy.go b/oauthproxy.go index b2fc3c6d..184efed9 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -83,28 +83,22 @@ type apiRoute struct { // OAuthProxy is the main authentication proxy type OAuthProxy struct { + ProxyOptions *options.ProxyOptions CookieOptions *options.Cookie - Validator func(string) bool + + Validator func(string) bool SignInPath string - allowedRoutes []allowedRoute - apiRoutes []apiRoute - redirectURL *url.URL // the url to receive requests at - relativeRedirectURL bool - whitelistDomains []string - provider providers.Provider - sessionStore sessionsapi.SessionStore - ProxyPrefix string - basicAuthValidator basic.Validator - basicAuthGroups []string - SkipProviderButton bool - skipAuthPreflight bool - skipJwtBearerTokens bool - forceJSONErrors bool - allowQuerySemicolons bool - realClientIPParser ipapi.RealClientIPParser - trustedIPs *ip.NetSet + allowedRoutes []allowedRoute + apiRoutes []apiRoute + redirectURL *url.URL // the url to receive requests at + provider providers.Provider + sessionStore sessionsapi.SessionStore + basicAuthValidator basic.Validator + basicAuthGroups []string + realClientIPParser ipapi.RealClientIPParser + trustedIPs *ip.NetSet sessionChain alice.Chain headersChain alice.Chain @@ -115,8 +109,6 @@ type OAuthProxy struct { serveMux *mux.Router redirectValidator redirect.Validator appDirector redirect.AppDirector - - encodeState bool } // NewOAuthProxy creates a new instance of OAuthProxy from the options provided @@ -127,10 +119,10 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr } var basicAuthValidator basic.Validator - if opts.HtpasswdFile != "" { - logger.Printf("using htpasswd file: %s", opts.HtpasswdFile) + if opts.ProxyOptions.HtpasswdFile != "" { + logger.Printf("using htpasswd file: %s", opts.ProxyOptions.HtpasswdFile) var err error - basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile) + basicAuthValidator, err = basic.NewHTPasswdValidator(opts.ProxyOptions.HtpasswdFile) if err != nil { return nil, fmt.Errorf("could not validate htpasswd: %v", err) } @@ -144,7 +136,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr pageWriter, err := pagewriter.NewWriter(pagewriter.Opts{ TemplatesPath: opts.Templates.Path, CustomLogo: opts.Templates.CustomLogo, - ProxyPrefix: opts.ProxyPrefix, + ProxyPrefix: opts.ProxyOptions.ProxyPrefix, Footer: opts.Templates.Footer, Version: version.VERSION, Debug: opts.Templates.Debug, @@ -161,19 +153,19 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr return nil, fmt.Errorf("error initialising upstream proxy: %v", err) } - if opts.SkipJwtBearerTokens { + if opts.ProxyOptions.SkipJwtBearerTokens { logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.Providers[0].OIDCConfig.IssuerURL) - for _, issuer := range opts.ExtraJwtIssuers { + for _, issuer := range opts.ProxyOptions.ExtraJwtIssuers { logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer) } - if !opts.BearerTokenLoginFallback { + if !opts.ProxyOptions.BearerTokenLoginFallback { logger.Println("Denying requests with invalid JWT tokens") } } redirectURL := opts.GetRedirectURL() if redirectURL.Path == "" { - redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix) + redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyOptions.ProxyPrefix) } logger.Printf("OAuthProxy configured for %s Client ID: %s", provider.Data().ProviderName, opts.Providers[0].ClientID) @@ -185,7 +177,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr logger.Printf("Cookie settings: name:%s insecure(http):%v scriptaccess:%v expiry:%s domains:%s path:%s samesite:%s refresh:%s", opts.Cookie.Name, *opts.Cookie.Insecure, opts.Cookie.ScriptAccess, opts.Cookie.Expire, strings.Join(opts.Cookie.Domains, ","), opts.Cookie.Path, opts.Cookie.SameSite, refresh) trustedIPs := ip.NewNetSet() - for _, ipStr := range opts.TrustedIPs { + for _, ipStr := range opts.ProxyOptions.TrustedIPs { if ipNet := ip.ParseIPNet(ipStr); ipNet != nil { trustedIPs.AddIPNet(*ipNet) } else { @@ -213,36 +205,29 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr return nil, fmt.Errorf("could not build headers chain: %v", err) } - redirectValidator := redirect.NewValidator(opts.WhitelistDomains) + redirectValidator := redirect.NewValidator(opts.ProxyOptions.WhitelistDomains) appDirector := redirect.NewAppDirector(redirect.AppDirectorOpts{ - ProxyPrefix: opts.ProxyPrefix, + ProxyPrefix: opts.ProxyOptions.ProxyPrefix, Validator: redirectValidator, }) p := &OAuthProxy{ + ProxyOptions: &opts.ProxyOptions, CookieOptions: &opts.Cookie, Validator: validator, - SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix), + SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyOptions.ProxyPrefix), - ProxyPrefix: opts.ProxyPrefix, - provider: provider, - sessionStore: sessionStore, - redirectURL: redirectURL, - relativeRedirectURL: opts.RelativeRedirectURL, - apiRoutes: apiRoutes, - allowedRoutes: allowedRoutes, - whitelistDomains: opts.WhitelistDomains, - skipAuthPreflight: opts.SkipAuthPreflight, - skipJwtBearerTokens: opts.SkipJwtBearerTokens, - realClientIPParser: opts.GetRealClientIPParser(), - SkipProviderButton: opts.SkipProviderButton, - forceJSONErrors: opts.ForceJSONErrors, - allowQuerySemicolons: opts.AllowQuerySemicolons, - trustedIPs: trustedIPs, + provider: provider, + sessionStore: sessionStore, + redirectURL: redirectURL, + apiRoutes: apiRoutes, + allowedRoutes: allowedRoutes, + realClientIPParser: opts.GetRealClientIPParser(), + trustedIPs: trustedIPs, basicAuthValidator: basicAuthValidator, - basicAuthGroups: opts.HtpasswdUserGroups, + basicAuthGroups: opts.ProxyOptions.HtpasswdUserGroups, sessionChain: sessionChain, headersChain: headersChain, preAuthChain: preAuthChain, @@ -250,9 +235,8 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr upstreamProxy: upstreamProxy, redirectValidator: redirectValidator, appDirector: appDirector, - encodeState: opts.EncodeState, } - p.buildServeMux(opts.ProxyPrefix) + p.buildServeMux(opts.ProxyOptions.ProxyPrefix) if err := p.setupServer(opts); err != nil { return nil, fmt.Errorf("error setting up server: %v", err) @@ -290,7 +274,7 @@ func (p *OAuthProxy) setupServer(opts *options.Options) error { } // Option: AllowQuerySemicolons - if opts.AllowQuerySemicolons { + if opts.ProxyOptions.AllowQuerySemicolons { serverOpts.Handler = http.AllowQuerySemicolons(serverOpts.Handler) } @@ -346,7 +330,7 @@ func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) { s.Path(oauthCallbackPath).HandlerFunc(p.OAuthCallback) // Static file paths - s.PathPrefix(staticPathPrefix).Handler(http.StripPrefix(p.ProxyPrefix, http.FileServer(http.FS(staticFiles)))) + s.PathPrefix(staticPathPrefix).Handler(http.StripPrefix(p.ProxyOptions.ProxyPrefix, http.FileServer(http.FS(staticFiles)))) // The userinfo and logout endpoints needs to load sessions before handling the request s.Path(userInfoPath).Handler(p.sessionChain.ThenFunc(p.UserInfo)) @@ -357,9 +341,9 @@ func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) { // the OAuth2 Proxy authentication logic kicks in. // For example forcing HTTPS or health checks. func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore) (alice.Chain, error) { - chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader)) + chain := alice.New(middleware.NewScope(opts.ProxyOptions.ReverseProxy, opts.Logging.RequestIDHeader)) - if opts.ForceHTTPS { + if opts.ProxyOptions.ForceHTTPS { _, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress) if err != nil { return alice.Chain{}, fmt.Errorf("invalid HTTPS address %q: %v", opts.Server.SecureBindAddress, err) @@ -399,7 +383,7 @@ func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionSt func buildSessionChain(opts *options.Options, provider providers.Provider, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain { chain := alice.New() - if opts.SkipJwtBearerTokens { + if opts.ProxyOptions.SkipJwtBearerTokens { verifiers := opts.GetJWTBearerVerifiers() sessionLoaders := make([]middlewareapi.TokenToSessionFunc, 0, len(verifiers)+1) @@ -410,11 +394,11 @@ func buildSessionChain(opts *options.Options, provider providers.Provider, sessi middlewareapi.CreateTokenToSessionFunc(verifier.Verify)) } - chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders, opts.BearerTokenLoginFallback)) + chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders, opts.ProxyOptions.BearerTokenLoginFallback)) } if validator != nil { - chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator, opts.HtpasswdUserGroups, opts.LegacyPreferEmailToUser)) + chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator, opts.ProxyOptions.HtpasswdUserGroups, opts.LegacyPreferEmailToUser)) } chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{ @@ -449,11 +433,11 @@ func buildSignInMessage(opts *options.Options) string { } else { msg = opts.Templates.Banner } - } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" { - if len(opts.EmailDomains) > 1 { - msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", ")) - } else if opts.EmailDomains[0] != "*" { - msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0]) + } else if len(opts.ProxyOptions.EmailDomains) != 0 && opts.ProxyOptions.AuthenticatedEmailsFile == "" { + if len(opts.ProxyOptions.EmailDomains) > 1 { + msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.ProxyOptions.EmailDomains, ", ")) + } else if opts.ProxyOptions.EmailDomains[0] != "*" { + msg = fmt.Sprintf("Authenticate using %v", opts.ProxyOptions.EmailDomains[0]) } } return msg @@ -470,9 +454,9 @@ func buildProviderName(p providers.Provider, override string) string { // SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option // (method=path support) func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) { - routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes)) + routes := make([]allowedRoute, 0, len(opts.ProxyOptions.SkipAuthRegex)+len(opts.ProxyOptions.SkipAuthRoutes)) - for _, path := range opts.SkipAuthRegex { + for _, path := range opts.ProxyOptions.SkipAuthRegex { compiledRegex, err := regexp.Compile(path) if err != nil { return nil, err @@ -484,7 +468,7 @@ func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) { }) } - for _, methodPath := range opts.SkipAuthRoutes { + for _, methodPath := range opts.ProxyOptions.SkipAuthRoutes { var ( method string path string @@ -517,9 +501,9 @@ func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) { // buildAPIRoutes builds an []apiRoute from ApiRoutes option func buildAPIRoutes(opts *options.Options) ([]apiRoute, error) { - routes := make([]apiRoute, 0, len(opts.APIRoutes)) + routes := make([]apiRoute, 0, len(opts.ProxyOptions.APIRoutes)) - for _, path := range opts.APIRoutes { + for _, path := range opts.ProxyOptions.APIRoutes { compiledRegex, err := regexp.Compile(path) if err != nil { return nil, err @@ -575,7 +559,7 @@ func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code i // IsAllowedRequest is used to check if auth should be skipped for this request func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool { - isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS" + isPreflightRequestAllowed := p.ProxyOptions.SkipAuthPreflight && req.Method == "OPTIONS" return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req) } @@ -694,7 +678,7 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) { } http.Redirect(rw, req, redirect, http.StatusFound) } else { - if p.SkipProviderButton { + if p.ProxyOptions.SkipProviderButton { p.OAuthStart(rw, req) } else { // TODO - should we pass on /oauth2/sign_in query params to /oauth2/start? @@ -855,7 +839,7 @@ func (p *OAuthProxy) doOAuthStart(rw http.ResponseWriter, req *http.Request, ove callbackRedirect := p.getOAuthRedirectURI(req) loginURL := p.provider.GetLoginURL( callbackRedirect, - encodeState(csrf.HashOAuthState(), appRedirect, p.encodeState), + encodeState(csrf.HashOAuthState(), appRedirect, p.ProxyOptions.EncodeState), csrf.HashOIDCNonce(), extraParams, ) @@ -891,7 +875,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { return } - nonce, appRedirect, err := decodeState(req.Form.Get("state"), p.encodeState) + nonce, appRedirect, err := decodeState(req.Form.Get("state"), p.ProxyOptions.EncodeState) if err != nil { logger.Errorf("Error while parsing OAuth2 state: %v", err) p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) @@ -1042,7 +1026,7 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { p.headersChain.Then(p.upstreamProxy).ServeHTTP(rw, req) case ErrNeedsLogin: // we need to send the user to a login screen - if p.forceJSONErrors || isAjax(req) || p.isAPIPath(req) { + if p.ProxyOptions.ForceJSONErrors || isAjax(req) || p.isAPIPath(req) { logger.Printf("No valid authentication in request. Access Denied.") // no point redirecting an AJAX request p.errorJSON(rw, http.StatusUnauthorized) @@ -1050,7 +1034,7 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { } logger.Printf("No valid authentication in request. Initiating login.") - if p.SkipProviderButton { + if p.ProxyOptions.SkipProviderButton { // start OAuth flow, but only with the default login URL params - do not // consider this request's query params as potential overrides, since // the user did not explicitly start the login flow @@ -1060,7 +1044,7 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { } case ErrAccessDenied: - if p.forceJSONErrors { + if p.ProxyOptions.ForceJSONErrors { p.errorJSON(rw, http.StatusForbidden) } else { p.ErrorPage(rw, req, http.StatusForbidden, "The session failed authorization checks") @@ -1100,7 +1084,7 @@ func prepareNoCacheMiddleware(next http.Handler) http.Handler { // This is usually the OAuthProxy callback URL. func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string { // if `p.redirectURL` already has a host, return it - if p.relativeRedirectURL || p.redirectURL.Host != "" { + if p.ProxyOptions.RelativeRedirectURL || p.redirectURL.Host != "" { return p.redirectURL.String() } diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 760d89a4..3c59abaf 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -591,7 +591,7 @@ func NewSignInPageTest(skipProvider bool) (*SignInPageTest, error) { var sipTest SignInPageTest sipTest.opts = baseTestOptions() - sipTest.opts.SkipProviderButton = skipProvider + sipTest.opts.ProxyOptions.SkipProviderButton = skipProvider err := validation.Validate(sipTest.opts) if err != nil { return nil, err @@ -627,7 +627,7 @@ func TestManualSignInStoresUserGroupsInTheSession(t *testing.T) { userGroups := []string{"somegroup", "someothergroup"} opts := baseTestOptions() - opts.HtpasswdUserGroups = userGroups + opts.ProxyOptions.HtpasswdUserGroups = userGroups err := validation.Validate(opts) if err != nil { t.Fatal(err) @@ -987,7 +987,7 @@ func NewUserInfoEndpointTest() (*ProcessCookieTest, error) { return nil, err } pcTest.req, _ = http.NewRequest("GET", - pcTest.opts.ProxyPrefix+"/userinfo", nil) + pcTest.opts.ProxyOptions.ProxyPrefix+"/userinfo", nil) return pcTest, nil } @@ -1095,7 +1095,7 @@ func NewAuthOnlyEndpointTest(querystring string, modifiers ...OptionsModifier) ( } pcTest.req, _ = http.NewRequest( "GET", - fmt.Sprintf("%s/auth%s", pcTest.opts.ProxyPrefix, querystring), + fmt.Sprintf("%s/auth%s", pcTest.opts.ProxyOptions.ProxyPrefix, querystring), nil) return pcTest, nil } @@ -1234,7 +1234,7 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) { pcTest.rw = httptest.NewRecorder() pcTest.req, _ = http.NewRequest("GET", - pcTest.opts.ProxyPrefix+authOnlyPath, nil) + pcTest.opts.ProxyOptions.ProxyPrefix+authOnlyPath, nil) created := time.Now() startSession := &sessions.SessionState{ @@ -1327,7 +1327,7 @@ func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) { pcTest.rw = httptest.NewRecorder() pcTest.req, _ = http.NewRequest("GET", - pcTest.opts.ProxyPrefix+authOnlyPath, nil) + pcTest.opts.ProxyOptions.ProxyPrefix+authOnlyPath, nil) created := time.Now() startSession := &sessions.SessionState{ @@ -1407,7 +1407,7 @@ func TestAuthOnlyEndpointSetBasicAuthFalseRequestHeaders(t *testing.T) { pcTest.rw = httptest.NewRecorder() pcTest.req, _ = http.NewRequest("GET", - pcTest.opts.ProxyPrefix+authOnlyPath, nil) + pcTest.opts.ProxyOptions.ProxyPrefix+authOnlyPath, nil) created := time.Now() startSession := &sessions.SessionState{ @@ -1442,7 +1442,7 @@ func TestAuthSkippedForPreflightRequests(t *testing.T) { }, }, } - opts.SkipAuthPreflight = true + opts.ProxyOptions.SkipAuthPreflight = true err := validation.Validate(opts) assert.NoError(t, err) @@ -1502,7 +1502,7 @@ type SignatureTest struct { func NewSignatureTest() (*SignatureTest, error) { opts := baseTestOptions() - opts.EmailDomains = []string{"acm.org"} + opts.ProxyOptions.EmailDomains = []string{"acm.org"} authenticator := &SignatureAuthenticator{} upstreamServer := httptest.NewServer( @@ -1637,7 +1637,7 @@ func TestRequestSignature(t *testing.T) { } t.Cleanup(st.Close) if tc.key != "" { - st.opts.SignatureKey = fmt.Sprintf("sha1:%s", tc.key) + st.opts.ProxyOptions.LegacySignatureKey = fmt.Sprintf("sha1:%s", tc.key) } err = st.MakeRequestWithExpectedKey(tc.method, tc.body, tc.key) assert.NoError(t, err) @@ -1655,7 +1655,7 @@ type ajaxRequestTest struct { func newAjaxRequestTest(forceJSONErrors bool) (*ajaxRequestTest, error) { test := &ajaxRequestTest{} test.opts = baseTestOptions() - test.opts.ForceJSONErrors = forceJSONErrors + test.opts.ProxyOptions.ForceJSONErrors = forceJSONErrors err := validation.Validate(test.opts) if err != nil { return nil, err @@ -1907,7 +1907,7 @@ func TestGetJwtSession(t *testing.T) { }, }, } - opts.SkipJwtBearerTokens = true + opts.ProxyOptions.SkipJwtBearerTokens = true opts.SetJWTBearerVerifiers(append(opts.GetJWTBearerVerifiers(), internalVerifier)) }) if err != nil { @@ -1974,7 +1974,7 @@ func Test_noCacheHeaders(t *testing.T) { }, }, } - opts.SkipAuthRegex = []string{".*"} + opts.ProxyOptions.SkipAuthRegex = []string{".*"} err := validation.Validate(opts) assert.NoError(t, err) proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true }) @@ -2090,7 +2090,7 @@ func baseTestOptions() *options.Options { opts.Providers[0].ID = "providerID" opts.Providers[0].ClientID = clientID opts.Providers[0].ClientSecret = clientSecret - opts.EmailDomains = []string{"*"} + opts.ProxyOptions.EmailDomains = []string{"*"} // Default injected headers for legacy configuration opts.InjectRequestHeaders = []options.Header{ @@ -2312,9 +2312,9 @@ func TestTrustedIPs(t *testing.T) { }, }, } - opts.TrustedIPs = tt.trustedIPs - opts.ReverseProxy = tt.reverseProxy - opts.RealClientIPHeader = tt.realClientIPHeader + opts.ProxyOptions.TrustedIPs = tt.trustedIPs + opts.ProxyOptions.ReverseProxy = tt.reverseProxy + opts.ProxyOptions.RealClientIPHeader = tt.realClientIPHeader err := validation.Validate(opts) assert.NoError(t, err) @@ -2488,8 +2488,10 @@ func Test_buildRoutesAllowlist(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { opts := &options.Options{ - SkipAuthRegex: tc.skipAuthRegex, - SkipAuthRoutes: tc.skipAuthRoutes, + ProxyOptions: options.ProxyOptions{ + SkipAuthRegex: tc.skipAuthRegex, + SkipAuthRoutes: tc.skipAuthRoutes, + }, } routes, err := buildRoutesAllowlist(opts) if tc.shouldError { @@ -2557,10 +2559,10 @@ func TestApiRoutes(t *testing.T) { }, }, } - opts.APIRoutes = []string{ + opts.ProxyOptions.APIRoutes = []string{ "^/api", } - opts.SkipProviderButton = true + opts.ProxyOptions.SkipProviderButton = true err := validation.Validate(opts) assert.NoError(t, err) proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true }) @@ -2638,10 +2640,10 @@ func TestAllowedRequest(t *testing.T) { }, }, } - opts.SkipAuthRegex = []string{ + opts.ProxyOptions.SkipAuthRegex = []string{ "^/skip/auth/regex$", } - opts.SkipAuthRoutes = []string{ + opts.ProxyOptions.SkipAuthRoutes = []string{ "GET=^/skip/auth/routes/get", } err := validation.Validate(opts) @@ -2727,7 +2729,7 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) { t.Cleanup(upstreamServer.Close) opts := baseTestOptions() - opts.ReverseProxy = true + opts.ProxyOptions.ReverseProxy = true opts.UpstreamServers = options.UpstreamConfig{ Upstreams: []options.Upstream{ { @@ -2737,10 +2739,10 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) { }, }, } - opts.SkipAuthRegex = []string{ + opts.ProxyOptions.SkipAuthRegex = []string{ "^/skip/auth/regex$", } - opts.SkipAuthRoutes = []string{ + opts.ProxyOptions.SkipAuthRoutes = []string{ "GET=^/skip/auth/routes/get", } err := validation.Validate(opts) @@ -2802,7 +2804,7 @@ func TestAllowedRequestWithForwardedUriHeader(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - req, err := http.NewRequest(tc.method, opts.ProxyPrefix+authOnlyPath, nil) + req, err := http.NewRequest(tc.method, opts.ProxyOptions.ProxyPrefix+authOnlyPath, nil) req.Header.Set("X-Forwarded-Uri", tc.url) assert.NoError(t, err) @@ -2838,7 +2840,7 @@ func TestAllowedRequestNegateWithoutMethod(t *testing.T) { }, }, } - opts.SkipAuthRoutes = []string{ + opts.ProxyOptions.SkipAuthRoutes = []string{ "!=^/api", // any non-api routes "POST=^/api/public-entity/?$", } @@ -2938,7 +2940,7 @@ func TestAllowedRequestNegateWithMethod(t *testing.T) { }, }, } - opts.SkipAuthRoutes = []string{ + opts.ProxyOptions.SkipAuthRoutes = []string{ "GET!=^/api", // any non-api routes "POST=^/api/public-entity/?$", } @@ -3319,8 +3321,8 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { test, err := NewAuthOnlyEndpointTest("?allowed_groups=a,b", func(opts *options.Options) { - opts.SkipAuthPreflight = true - opts.TrustedIPs = []string{"1.2.3.4"} + opts.ProxyOptions.SkipAuthPreflight = true + opts.ProxyOptions.TrustedIPs = []string{"1.2.3.4"} }) if err != nil { t.Fatal(err) @@ -3566,7 +3568,7 @@ func TestGetOAuthRedirectURI(t *testing.T) { { name: "relative redirect url", setupOpts: func(baseOpts *options.Options) *options.Options { - baseOpts.RelativeRedirectURL = true + baseOpts.ProxyOptions.RelativeRedirectURL = true return baseOpts }, req: &http.Request{}, @@ -3575,7 +3577,7 @@ func TestGetOAuthRedirectURI(t *testing.T) { { name: "proxy prefix", setupOpts: func(baseOpts *options.Options) *options.Options { - baseOpts.ProxyPrefix = "/prefix" + baseOpts.ProxyOptions.ProxyPrefix = "/prefix" return baseOpts }, req: &http.Request{ @@ -3589,8 +3591,8 @@ func TestGetOAuthRedirectURI(t *testing.T) { { name: "proxy prefix with relative redirect", setupOpts: func(baseOpts *options.Options) *options.Options { - baseOpts.ProxyPrefix = "/prefix" - baseOpts.RelativeRedirectURL = true + baseOpts.ProxyOptions.ProxyPrefix = "/prefix" + baseOpts.ProxyOptions.RelativeRedirectURL = true return baseOpts }, req: &http.Request{ @@ -3618,7 +3620,7 @@ func TestGetOAuthRedirectURI(t *testing.T) { func TestIdTokenPlaceholderInSignOut(t *testing.T) { opts := baseTestOptions() - opts.WhitelistDomains = []string{"my-oidc-provider.example.com"} + opts.ProxyOptions.WhitelistDomains = []string{"my-oidc-provider.example.com"} err := validation.Validate(opts) assert.NoError(t, err) diff --git a/pkg/apis/options/alpha_options.go b/pkg/apis/options/alpha_options.go index c75347a9..ffd2067c 100644 --- a/pkg/apis/options/alpha_options.go +++ b/pkg/apis/options/alpha_options.go @@ -9,6 +9,9 @@ package options // They may change between releases without notice. // ::: type AlphaOptions struct { + // ProxyOptions + ProxyOptions ProxyOptions `yaml:"proxyOptions,omitempty"` + // UpstreamConfig is used to configure upstream servers. // Once a user is authenticated, requests to the server will be proxied to // these upstream servers based on the path mappings defined in this list. @@ -65,6 +68,7 @@ func NewAlphaOptions(opts *Options) *AlphaOptions { // ExtractFrom populates the fields in the AlphaOptions with the values from // the Options func (a *AlphaOptions) ExtractFrom(opts *Options) { + a.ProxyOptions = opts.ProxyOptions a.UpstreamConfig = opts.UpstreamServers a.InjectRequestHeaders = opts.InjectRequestHeaders a.InjectResponseHeaders = opts.InjectResponseHeaders @@ -78,6 +82,7 @@ func (a *AlphaOptions) ExtractFrom(opts *Options) { // MergeOptionsWithDefaults replaces alpha options in the Options struct // with the values from the AlphaOptions and ensures the defaults func (a *AlphaOptions) MergeOptionsWithDefaults(opts *Options) { + opts.ProxyOptions = a.ProxyOptions opts.UpstreamServers = a.UpstreamConfig opts.InjectRequestHeaders = a.InjectRequestHeaders opts.InjectResponseHeaders = a.InjectResponseHeaders diff --git a/pkg/apis/options/legacy_options.go b/pkg/apis/options/legacy_options.go index b8888232..c1e3d77a 100644 --- a/pkg/apis/options/legacy_options.go +++ b/pkg/apis/options/legacy_options.go @@ -8,6 +8,9 @@ import ( ) type LegacyOptions struct { + // Legacy options for the overall proxy behaviour + LegacyProxyOptions LegacyProxyOptions `cfg:",squash"` + // Legacy options related to upstream servers LegacyUpstreams LegacyUpstreams `cfg:",squash"` @@ -31,6 +34,13 @@ type LegacyOptions struct { func NewLegacyOptions() *LegacyOptions { return &LegacyOptions{ + LegacyProxyOptions: LegacyProxyOptions{ + ProxyPrefix: "/oauth2", + RealClientIPHeader: "X-Real-IP", + ForceHTTPS: false, + SkipAuthPreflight: false, + }, + LegacyUpstreams: LegacyUpstreams{ PassHostHeader: true, ProxyWebSockets: true, @@ -92,6 +102,7 @@ func NewLegacyOptions() *LegacyOptions { func NewLegacyFlagSet() *pflag.FlagSet { flagSet := NewFlagSet() + flagSet.AddFlagSet(legacyProxyOptionsFlagSet()) flagSet.AddFlagSet(legacyUpstreamsFlagSet()) flagSet.AddFlagSet(legacyHeadersFlagSet()) flagSet.AddFlagSet(legacyServerFlagset()) @@ -104,6 +115,8 @@ func NewLegacyFlagSet() *pflag.FlagSet { } func (l *LegacyOptions) ToOptions() (*Options, error) { + l.Options.ProxyOptions = l.LegacyProxyOptions.convert() + upstreams, err := l.LegacyUpstreams.convert() if err != nil { return nil, fmt.Errorf("error converting upstreams: %v", err) diff --git a/pkg/apis/options/legacy_proxy.go b/pkg/apis/options/legacy_proxy.go new file mode 100644 index 00000000..1f317f74 --- /dev/null +++ b/pkg/apis/options/legacy_proxy.go @@ -0,0 +1,99 @@ +package options + +import ( + "github.com/spf13/pflag" +) + +type LegacyProxyOptions struct { + AllowQuerySemicolons bool `flag:"allow-query-semicolons" cfg:"allow_query_semicolons"` + ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix"` + RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"` + ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"` + TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"` + ForceHTTPS bool `flag:"force-https" cfg:"force_https"` + SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"` + ForceJSONErrors bool `flag:"force-json-errors" cfg:"force_json_errors"` + SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"` + SkipAuthRoutes []string `flag:"skip-auth-route" cfg:"skip_auth_routes"` + AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"` + EmailDomains []string `flag:"email-domain" cfg:"email_domains"` + WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains"` + HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"` + HtpasswdUserGroups []string `flag:"htpasswd-user-group" cfg:"htpasswd_user_groups"` + RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"` + RelativeRedirectURL bool `flag:"relative-redirect-url" cfg:"relative_redirect_url"` + APIRoutes []string `flag:"api-route" cfg:"api_routes"` + SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"` + BearerTokenLoginFallback bool `flag:"bearer-token-login-fallback" cfg:"bearer_token_login_fallback"` + ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers"` + SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button"` + SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"` + SignatureKey string `flag:"signature-key" cfg:"signature_key"` + EncodeState bool `flag:"encode-state" cfg:"encode_state"` +} + +func legacyProxyOptionsFlagSet() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("proxy", pflag.ExitOnError) + flagSet.Bool("reverse-proxy", false, "are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted") + flagSet.String("real-client-ip-header", "X-Real-IP", "Header used to determine the real IP of the client (one of: X-Forwarded-For, X-Real-IP, or X-ProxyUser-IP)") + flagSet.StringSlice("trusted-ip", []string{}, "list of IPs or CIDR ranges to allow to bypass authentication. WARNING: trusting by IP has inherent security flaws, read the configuration documentation for more information.") + flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests") + flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"") + flagSet.Bool("relative-redirect-url", false, "allow relative OAuth Redirect URL.") + flagSet.StringSlice("skip-auth-regex", []string{}, "(DEPRECATED for --skip-auth-route) bypass authentication for requests path's that match (may be given multiple times)") + flagSet.StringSlice("skip-auth-route", []string{}, "bypass authentication for requests that match the method & path. Format: method=path_regex OR method!=path_regex. For all methods: path_regex OR !=path_regex") + flagSet.StringSlice("api-route", []string{}, "return HTTP 401 instead of redirecting to authentication server if token is not valid. Format: path_regex") + flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start") + flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests") + flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS providers") + flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens (default false)") + flagSet.Bool("bearer-token-login-fallback", true, "if skip-jwt-bearer-tokens is set, fall back to normal login redirect with an invalid JWT. If false, 403 instead") + flagSet.Bool("force-json-errors", false, "will force JSON errors instead of HTTP error pages or redirects") + flagSet.Bool("encode-state", false, "will encode oauth state with base64") + flagSet.Bool("allow-query-semicolons", false, "allow the use of semicolons in query args") + flagSet.StringSlice("extra-jwt-issuers", []string{}, "if skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)") + flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email") + flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . or a *. to allow subdomains (eg .example.com, *.example.com)") + flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)") + flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -B\" for bcrypt encryption") + flagSet.StringSlice("htpasswd-user-group", []string{}, "the groups to be set on sessions for htpasswd users (may be given multiple times)") + flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. //sign_in)") + flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)") + return flagSet +} + +func (l *LegacyProxyOptions) convert() ProxyOptions { + return ProxyOptions{ + // security + AllowQuerySemicolons: l.AllowQuerySemicolons, + ForceHTTPS: l.ForceHTTPS, + SkipAuthRegex: l.SkipAuthRegex, + SkipAuthRoutes: l.SkipAuthRoutes, + SkipAuthPreflight: l.SkipAuthPreflight, + SSLInsecureSkipVerify: l.SSLInsecureSkipVerify, + TrustedIPs: l.TrustedIPs, + + // authentication + AuthenticatedEmailsFile: l.AuthenticatedEmailsFile, + EmailDomains: l.EmailDomains, + WhitelistDomains: l.WhitelistDomains, + HtpasswdFile: l.HtpasswdFile, + HtpasswdUserGroups: l.HtpasswdUserGroups, + SkipJwtBearerTokens: l.SkipJwtBearerTokens, + BearerTokenLoginFallback: l.BearerTokenLoginFallback, + ExtraJwtIssuers: l.ExtraJwtIssuers, + ForceJSONErrors: l.ForceJSONErrors, + + // routing + APIRoutes: l.APIRoutes, + ReverseProxy: l.ReverseProxy, + ProxyPrefix: l.ProxyPrefix, + RedirectURL: l.RawRedirectURL, + RelativeRedirectURL: l.RelativeRedirectURL, + RealClientIPHeader: l.RealClientIPHeader, + SkipProviderButton: l.SkipProviderButton, + EncodeState: l.EncodeState, + + LegacySignatureKey: l.SignatureKey, + } +} diff --git a/pkg/apis/options/load_test.go b/pkg/apis/options/load_test.go index fca8147e..1b54c5ef 100644 --- a/pkg/apis/options/load_test.go +++ b/pkg/apis/options/load_test.go @@ -17,6 +17,11 @@ var _ = Describe("Load", func() { optionsWithNilProvider.Providers = nil legacyOptionsWithNilProvider := &LegacyOptions{ + LegacyProxyOptions: LegacyProxyOptions{ + ProxyPrefix: "/oauth2", + RealClientIPHeader: "X-Real-IP", + BearerTokenLoginFallback: true, + }, LegacyUpstreams: LegacyUpstreams{ PassHostHeader: true, ProxyWebSockets: true, @@ -68,15 +73,10 @@ var _ = Describe("Load", func() { }, Options: Options{ - BearerTokenLoginFallback: true, - ProxyPrefix: "/oauth2", - PingPath: "/ping", - ReadyPath: "/ready", - RealClientIPHeader: "X-Real-IP", - ForceHTTPS: false, - Templates: templatesDefaults(), - SkipAuthPreflight: false, - Logging: loggingDefaults(), + PingPath: "/ping", + ReadyPath: "/ready", + Templates: templatesDefaults(), + Logging: loggingDefaults(), }, } diff --git a/pkg/apis/options/options.go b/pkg/apis/options/options.go index 5440268a..4fa5167e 100644 --- a/pkg/apis/options/options.go +++ b/pkg/apis/options/options.go @@ -18,27 +18,17 @@ type SignatureData struct { // 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"` - PingPath string `flag:"ping-path" cfg:"ping_path"` - PingUserAgent string `flag:"ping-user-agent" cfg:"ping_user_agent"` - ReadyPath string `flag:"ready-path" cfg:"ready_path"` - ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"` - RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"` - TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"` - ForceHTTPS bool `flag:"force-https" cfg:"force_https"` - RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"` - RelativeRedirectURL bool `flag:"relative-redirect-url" cfg:"relative_redirect_url"` + PingPath string `flag:"ping-path" cfg:"ping_path"` + PingUserAgent string `flag:"ping-user-agent" cfg:"ping_user_agent"` + ReadyPath string `flag:"ready-path" cfg:"ready_path"` - AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"` - EmailDomains []string `flag:"email-domain" cfg:"email_domains"` - WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains"` - HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"` - HtpasswdUserGroups []string `flag:"htpasswd-user-group" cfg:"htpasswd_user_groups"` + ProxyOptions ProxyOptions `cfg:",internal"` + Cookie Cookie `cfg:",internal"` + Session SessionOptions `cfg:",internal"` + Logging Logging `cfg:",squash"` + Templates Templates `cfg:",squash"` - Cookie Cookie `cfg:",internal"` - Session SessionOptions `cfg:",internal"` - Logging Logging `cfg:",squash"` - Templates Templates `cfg:",squash"` + GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks"` // Not used in the legacy config, name not allowed to match an external key (upstreams) // TODO(JoelSpeed): Rename when legacy config is removed @@ -52,22 +42,6 @@ type Options struct { Providers Providers `cfg:",internal"` - APIRoutes []string `flag:"api-route" cfg:"api_routes"` - SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"` - SkipAuthRoutes []string `flag:"skip-auth-route" cfg:"skip_auth_routes"` - SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"` - BearerTokenLoginFallback bool `flag:"bearer-token-login-fallback" cfg:"bearer_token_login_fallback"` - ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers"` - SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button"` - SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"` - SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"` - ForceJSONErrors bool `flag:"force-json-errors" cfg:"force_json_errors"` - EncodeState bool `flag:"encode-state" cfg:"encode_state"` - AllowQuerySemicolons bool `flag:"allow-query-semicolons" cfg:"allow_query_semicolons"` - - SignatureKey string `flag:"signature-key" cfg:"signature_key"` - GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks"` - // This is used for backwards compatibility for basic auth users LegacyPreferEmailToUser bool `cfg:",internal"` @@ -98,16 +72,12 @@ func (o *Options) SetRealClientIPParser(s ipapi.RealClientIPParser) { o.re // NewOptions constructs a new Options with defaulted values func NewOptions() *Options { return &Options{ - BearerTokenLoginFallback: true, - ProxyPrefix: "/oauth2", - Providers: providerDefaults(), - PingPath: "/ping", - ReadyPath: "/ready", - RealClientIPHeader: "X-Real-IP", - ForceHTTPS: false, - Templates: templatesDefaults(), - SkipAuthPreflight: false, - Logging: loggingDefaults(), + ProxyOptions: proxyOptionsDefaults(), + Providers: providerDefaults(), + PingPath: "/ping", + ReadyPath: "/ready", + Templates: templatesDefaults(), + Logging: loggingDefaults(), } } @@ -115,35 +85,9 @@ func NewOptions() *Options { func NewFlagSet() *pflag.FlagSet { flagSet := pflag.NewFlagSet("oauth2-proxy", pflag.ExitOnError) - flagSet.Bool("reverse-proxy", false, "are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted") - flagSet.String("real-client-ip-header", "X-Real-IP", "Header used to determine the real IP of the client (one of: X-Forwarded-For, X-Real-IP, X-ProxyUser-IP, X-Envoy-External-Address, or CF-Connecting-IP)") - flagSet.StringSlice("trusted-ip", []string{}, "list of IPs or CIDR ranges to allow to bypass authentication. WARNING: trusting by IP has inherent security flaws, read the configuration documentation for more information.") - flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests") - flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"") - flagSet.Bool("relative-redirect-url", false, "allow relative OAuth Redirect URL.") - flagSet.StringSlice("skip-auth-regex", []string{}, "(DEPRECATED for --skip-auth-route) bypass authentication for requests path's that match (may be given multiple times)") - flagSet.StringSlice("skip-auth-route", []string{}, "bypass authentication for requests that match the method & path. Format: method=path_regex OR method!=path_regex. For all methods: path_regex OR !=path_regex") - flagSet.StringSlice("api-route", []string{}, "return HTTP 401 instead of redirecting to authentication server if token is not valid. Format: path_regex") - flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start") - flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests") - flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS providers") - flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens (default false)") - flagSet.Bool("bearer-token-login-fallback", true, "if skip-jwt-bearer-tokens is set, fall back to normal login redirect with an invalid JWT. If false, 403 instead") - flagSet.Bool("force-json-errors", false, "will force JSON errors instead of HTTP error pages or redirects") - flagSet.Bool("encode-state", false, "will encode oauth state with base64") - flagSet.Bool("allow-query-semicolons", false, "allow the use of semicolons in query args") - flagSet.StringSlice("extra-jwt-issuers", []string{}, "if skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)") - - flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email") - flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . or a *. to allow subdomains (eg .example.com, *.example.com)") - flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)") - flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -B\" for bcrypt encryption") - flagSet.StringSlice("htpasswd-user-group", []string{}, "the groups to be set on sessions for htpasswd users (may be given multiple times)") - flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. //sign_in)") flagSet.String("ping-path", "/ping", "the ping endpoint that can be used for basic health checks") flagSet.String("ping-user-agent", "", "special User-Agent that will be used for basic health checks") flagSet.String("ready-path", "/ready", "the ready endpoint that can be used for deep health checks") - flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)") flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints") flagSet.AddFlagSet(loggingFlagSet()) diff --git a/pkg/apis/options/proxy.go b/pkg/apis/options/proxy.go new file mode 100644 index 00000000..54c994cc --- /dev/null +++ b/pkg/apis/options/proxy.go @@ -0,0 +1,47 @@ +package options + +type ProxyOptions struct { + // security + AllowQuerySemicolons bool `yaml:"allowQuerySemicolons,omitempty"` + ForceHTTPS bool `yaml:"forceHttps,omitempty"` + SkipAuthRegex []string `yaml:"skipAuthRegex,omitempty"` + SkipAuthRoutes []string `yaml:"skipAuthRoutes,omitempty"` + SkipAuthPreflight bool `yaml:"skipAuthPreflight,omitempty"` + SSLInsecureSkipVerify bool `yaml:"sslInsecureSkipVerify,omitempty"` + TrustedIPs []string `yaml:"trustedIPs,omitempty"` + + // authentication + AuthenticatedEmailsFile string `yaml:"authenticatedEmailsFile,omitempty"` + EmailDomains []string `yaml:"emailDomains,omitempty"` + WhitelistDomains []string `yaml:"whitelistDomains,omitempty"` + HtpasswdFile string `yaml:"htpasswdFile,omitempty"` + HtpasswdUserGroups []string `yaml:"htpasswdUserGroups,omitempty"` + SkipJwtBearerTokens bool `yaml:"skipJwtBearerTokens,omitempty"` + BearerTokenLoginFallback bool `yaml:"bearerTokenLoginFallback,omitempty"` + ExtraJwtIssuers []string `yaml:"extraJwtIssuers,omitempty"` + + // routing + APIRoutes []string `yaml:"apiRoutes,omitempty"` + ReverseProxy bool `yaml:"reverseProxy,omitempty"` + ProxyPrefix string `yaml:"proxyPrefix,omitempty"` + RedirectURL string `yaml:"redirectUrl,omitempty"` + RelativeRedirectURL bool `yaml:"relativeRedirectUrl,omitempty"` + RealClientIPHeader string `yaml:"realClientIPHeader,omitempty"` + SkipProviderButton bool `yaml:"skipProviderButton,omitempty"` + EncodeState bool `yaml:"encodeState,omitempty"` + + // Force oauth2-proxy error responses to be JSON + ForceJSONErrors bool `yaml:"forceJsonErrors,omitempty"` + + // This is used for backwards compatibility + LegacyPreferEmailToUser bool `yaml:"legacyPreferEmailToUser,omitempty"` + LegacySignatureKey string `yaml:"legacySignatureKey,omitempty"` +} + +func proxyOptionsDefaults() ProxyOptions { + return ProxyOptions{ + ProxyPrefix: "/oauth2", + RealClientIPHeader: "X-Real-IP", + BearerTokenLoginFallback: true, + } +} diff --git a/pkg/validation/allowlist.go b/pkg/validation/allowlist.go index a74f4ae9..c2953768 100644 --- a/pkg/validation/allowlist.go +++ b/pkg/validation/allowlist.go @@ -18,7 +18,7 @@ func validateAllowlists(o *options.Options) []string { msgs = append(msgs, validateAuthRegexes(o)...) msgs = append(msgs, validateTrustedIPs(o)...) - if len(o.TrustedIPs) > 0 && o.ReverseProxy { + if len(o.ProxyOptions.TrustedIPs) > 0 && o.ProxyOptions.ReverseProxy { _, err := fmt.Fprintln(os.Stderr, "WARNING: mixing --trusted-ip with --reverse-proxy is a potential security vulnerability. An attacker can inject a trusted IP into an X-Real-IP or X-Forwarded-For header if they aren't properly protected outside of oauth2-proxy") if err != nil { panic(err) @@ -31,7 +31,7 @@ func validateAllowlists(o *options.Options) []string { // validateAuthRoutes validates method=path routes passed with options.SkipAuthRoutes func validateAuthRoutes(o *options.Options) []string { msgs := []string{} - for _, route := range o.SkipAuthRoutes { + for _, route := range o.ProxyOptions.SkipAuthRoutes { var regex string parts := strings.SplitN(route, "=", 2) if len(parts) == 1 { @@ -49,13 +49,13 @@ func validateAuthRoutes(o *options.Options) []string { // validateAuthRegexes validates regex paths passed with options.SkipAuthRegex func validateAuthRegexes(o *options.Options) []string { - return validateRegexes(o.SkipAuthRegex) + return validateRegexes(o.ProxyOptions.SkipAuthRegex) } // validateTrustedIPs validates IP/CIDRs for IP based allowlists func validateTrustedIPs(o *options.Options) []string { msgs := []string{} - for i, ipStr := range o.TrustedIPs { + for i, ipStr := range o.ProxyOptions.TrustedIPs { if nil == ip.ParseIPNet(ipStr) { msgs = append(msgs, fmt.Sprintf("trusted_ips[%d] (%s) could not be recognized", i, ipStr)) } @@ -65,7 +65,7 @@ func validateTrustedIPs(o *options.Options) []string { // validateAPIRoutes validates regex paths passed with options.ApiRoutes func validateAPIRoutes(o *options.Options) []string { - return validateRegexes(o.APIRoutes) + return validateRegexes(o.ProxyOptions.APIRoutes) } // validateRegexes validates all regexes and returns a list of messages in case of error diff --git a/pkg/validation/allowlist_test.go b/pkg/validation/allowlist_test.go index 9f6843dd..61efe5aa 100644 --- a/pkg/validation/allowlist_test.go +++ b/pkg/validation/allowlist_test.go @@ -26,7 +26,9 @@ var _ = Describe("Allowlist", func() { DescribeTable("validateRoutes", func(r *validateRoutesTableInput) { opts := &options.Options{ - SkipAuthRoutes: r.routes, + ProxyOptions: options.ProxyOptions{ + SkipAuthRoutes: r.routes, + }, } Expect(validateAuthRoutes(opts)).To(ConsistOf(r.errStrings)) }, @@ -58,7 +60,9 @@ var _ = Describe("Allowlist", func() { DescribeTable("validateRegexes", func(r *validateRegexesTableInput) { opts := &options.Options{ - SkipAuthRegex: r.regexes, + ProxyOptions: options.ProxyOptions{ + SkipAuthRegex: r.regexes, + }, } Expect(validateAuthRegexes(opts)).To(ConsistOf(r.errStrings)) }, @@ -90,7 +94,9 @@ var _ = Describe("Allowlist", func() { DescribeTable("validateTrustedIPs", func(t *validateTrustedIPsTableInput) { opts := &options.Options{ - TrustedIPs: t.trustedIPs, + ProxyOptions: options.ProxyOptions{ + TrustedIPs: t.trustedIPs, + }, } Expect(validateTrustedIPs(opts)).To(ConsistOf(t.errStrings)) }, diff --git a/pkg/validation/options.go b/pkg/validation/options.go index fac2d532..b075146a 100644 --- a/pkg/validation/options.go +++ b/pkg/validation/options.go @@ -31,7 +31,7 @@ func Validate(o *options.Options) error { msgs = configureLogger(o.Logging, msgs) msgs = parseSignatureKey(o, msgs) - if o.SSLInsecureSkipVerify { + if o.ProxyOptions.SSLInsecureSkipVerify { transport := requests.DefaultTransport.(*http.Transport) transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // #nosec G402 -- InsecureSkipVerify is a configurable option we allow } else if len(o.Providers[0].CAFiles) > 0 { @@ -47,16 +47,16 @@ func Validate(o *options.Options) error { } } - if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" { + if o.ProxyOptions.AuthenticatedEmailsFile == "" && len(o.ProxyOptions.EmailDomains) == 0 && o.ProxyOptions.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.SkipJwtBearerTokens { + if o.ProxyOptions.SkipJwtBearerTokens { // Configure extra issuers - if len(o.ExtraJwtIssuers) > 0 { + if len(o.ProxyOptions.ExtraJwtIssuers) > 0 { var jwtIssuers []jwtIssuer - jwtIssuers, msgs = parseJwtIssuers(o.ExtraJwtIssuers, msgs) + jwtIssuers, msgs = parseJwtIssuers(o.ProxyOptions.ExtraJwtIssuers, msgs) for _, jwtIssuer := range jwtIssuers { verifier, err := newVerifierFromJwtIssuer( o.Providers[0].OIDCConfig.AudienceClaims, @@ -72,18 +72,18 @@ func Validate(o *options.Options) error { } var redirectURL *url.URL - redirectURL, msgs = parseURL(o.RawRedirectURL, "redirect", msgs) + redirectURL, msgs = parseURL(o.ProxyOptions.RedirectURL, "redirect", msgs) o.SetRedirectURL(redirectURL) - if o.RawRedirectURL == "" && ptr.Deref(o.Cookie.Insecure, options.DefaultCookieInsecure) && !o.ReverseProxy { + if o.ProxyOptions.RedirectURL == "" && ptr.Deref(o.Cookie.Insecure, options.DefaultCookieInsecure) && !o.ProxyOptions.ReverseProxy { logger.Print("WARNING: no explicit redirect URL: redirects will default to insecure HTTP") } msgs = append(msgs, validateUpstreams(o.UpstreamServers)...) - if o.ReverseProxy { - parser, err := ip.GetRealClientIPParser(o.RealClientIPHeader) + if o.ProxyOptions.ReverseProxy { + parser, err := ip.GetRealClientIPParser(o.ProxyOptions.RealClientIPHeader) if err != nil { - msgs = append(msgs, fmt.Sprintf("real_client_ip_header (%s) not accepted parameter value: %v", o.RealClientIPHeader, err)) + msgs = append(msgs, fmt.Sprintf("real_client_ip_header (%s) not accepted parameter value: %v", o.ProxyOptions.RealClientIPHeader, err)) } o.SetRealClientIPParser(parser) @@ -104,22 +104,22 @@ func Validate(o *options.Options) error { } func parseSignatureKey(o *options.Options, msgs []string) []string { - if o.SignatureKey == "" { + if o.ProxyOptions.LegacySignatureKey == "" { return msgs } logger.Print("WARNING: `--signature-key` is deprecated. It will be removed in a future release") - components := strings.Split(o.SignatureKey, ":") + components := strings.Split(o.ProxyOptions.LegacySignatureKey, ":") if len(components) != 2 { return append(msgs, "invalid signature hash:key spec: "+ - o.SignatureKey) + o.ProxyOptions.LegacySignatureKey) } algorithm, secretKey := components[0], components[1] hash, err := hmacauth.DigestNameToCryptoHash(algorithm) if err != nil { - return append(msgs, "unsupported signature hash algorithm: "+o.SignatureKey) + return append(msgs, "unsupported signature hash algorithm: "+o.ProxyOptions.LegacySignatureKey) } o.SetSignatureData(&options.SignatureData{Hash: hash, Key: secretKey}) return msgs diff --git a/pkg/validation/options_test.go b/pkg/validation/options_test.go index 0d193af8..9c6c2d54 100644 --- a/pkg/validation/options_test.go +++ b/pkg/validation/options_test.go @@ -34,7 +34,7 @@ func testOptions() *options.Options { o.Providers[0].ID = providerID o.Providers[0].ClientID = clientID o.Providers[0].ClientSecret = clientSecret - o.EmailDomains = []string{"*"} + o.ProxyOptions.EmailDomains = []string{"*"} o.EnsureDefaults() return o } @@ -48,7 +48,7 @@ func errorMsg(msgs []string) string { func TestNewOptions(t *testing.T) { o := options.NewOptions() - o.EmailDomains = []string{"*"} + o.ProxyOptions.EmailDomains = []string{"*"} o.EnsureDefaults() err := Validate(o) @@ -117,7 +117,7 @@ func TestInitializedOptions(t *testing.T) { // seems to parse damn near anything. func TestRedirectURL(t *testing.T) { o := testOptions() - o.RawRedirectURL = "https://myhost.com/oauth2/callback" + o.ProxyOptions.RedirectURL = "https://myhost.com/oauth2/callback" assert.Equal(t, nil, Validate(o)) expected := &url.URL{ Scheme: "https", Host: "myhost.com", Path: "/oauth2/callback"} @@ -163,7 +163,7 @@ func TestBase64CookieSecret(t *testing.T) { func TestValidateSignatureKey(t *testing.T) { o := testOptions() - o.SignatureKey = "sha1:secret" + o.ProxyOptions.LegacySignatureKey = "sha1:secret" assert.Equal(t, nil, Validate(o)) assert.Equal(t, o.GetSignatureData().Hash, crypto.SHA1) assert.Equal(t, o.GetSignatureData().Key, "secret") @@ -171,18 +171,18 @@ func TestValidateSignatureKey(t *testing.T) { func TestValidateSignatureKeyInvalidSpec(t *testing.T) { o := testOptions() - o.SignatureKey = "invalid spec" + o.ProxyOptions.LegacySignatureKey = "invalid spec" err := Validate(o) assert.Equal(t, err.Error(), "invalid configuration:\n"+ - " invalid signature hash:key spec: "+o.SignatureKey) + " invalid signature hash:key spec: "+o.ProxyOptions.LegacySignatureKey) } func TestValidateSignatureKeyUnsupportedAlgorithm(t *testing.T) { o := testOptions() - o.SignatureKey = "unsupported:default secret" + o.ProxyOptions.LegacySignatureKey = "unsupported:default secret" err := Validate(o) assert.Equal(t, err.Error(), "invalid configuration:\n"+ - " unsupported signature hash algorithm: "+o.SignatureKey) + " unsupported signature hash algorithm: "+o.ProxyOptions.LegacySignatureKey) } func TestGCPHealthcheck(t *testing.T) { @@ -194,21 +194,21 @@ func TestGCPHealthcheck(t *testing.T) { func TestRealClientIPHeader(t *testing.T) { // Ensure nil if ReverseProxy not set. o := testOptions() - o.RealClientIPHeader = "X-Real-IP" + o.ProxyOptions.RealClientIPHeader = "X-Real-IP" assert.Equal(t, nil, Validate(o)) assert.Nil(t, o.GetRealClientIPParser()) // Ensure simple use case works. o = testOptions() - o.ReverseProxy = true - o.RealClientIPHeader = "X-Forwarded-For" + o.ProxyOptions.ReverseProxy = true + o.ProxyOptions.RealClientIPHeader = "X-Forwarded-For" assert.Equal(t, nil, Validate(o)) assert.NotNil(t, o.GetRealClientIPParser()) // Ensure unknown header format process an error. o = testOptions() - o.ReverseProxy = true - o.RealClientIPHeader = "Forwarded" + o.ProxyOptions.ReverseProxy = true + o.ProxyOptions.RealClientIPHeader = "Forwarded" err := Validate(o) assert.NotEqual(t, nil, err) expected := errorMsg([]string{ @@ -219,8 +219,8 @@ func TestRealClientIPHeader(t *testing.T) { // Ensure invalid header format produces an error. o = testOptions() - o.ReverseProxy = true - o.RealClientIPHeader = "!934invalidheader-23:" + o.ProxyOptions.ReverseProxy = true + o.ProxyOptions.RealClientIPHeader = "!934invalidheader-23:" err = Validate(o) assert.NotEqual(t, nil, err) expected = errorMsg([]string{ diff --git a/pkg/validation/providers.go b/pkg/validation/providers.go index 0c8e28db..99c41969 100644 --- a/pkg/validation/providers.go +++ b/pkg/validation/providers.go @@ -34,7 +34,7 @@ func validateProviders(o *options.Options) []string { if len(o.Providers) == 0 { msgs = append(msgs, "at least one provider has to be defined") } - if o.SkipProviderButton && len(o.Providers) > 1 { + if o.ProxyOptions.SkipProviderButton && len(o.Providers) > 1 { msgs = append(msgs, "SkipProviderButton and multiple providers are mutually exclusive") } diff --git a/pkg/validation/providers_test.go b/pkg/validation/providers_test.go index 3c3531d7..2671e4d3 100644 --- a/pkg/validation/providers_test.go +++ b/pkg/validation/providers_test.go @@ -100,7 +100,9 @@ var _ = Describe("Providers", func() { }), Entry("with multiple providers and skip provider button", &validateProvidersTableInput{ options: &options.Options{ - SkipProviderButton: true, + ProxyOptions: options.ProxyOptions{ + SkipProviderButton: true, + }, Providers: options.Providers{ validProvider, validLoginGovProvider,