This commit is contained in:
Jan Larwig 2026-03-26 23:04:55 +01:00 committed by GitHub
commit e17c7d5ea1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 347 additions and 237 deletions

View File

@ -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)

View File

@ -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),

View File

@ -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()
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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. /<oauth2>/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,
}
}

View File

@ -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(),
},
}

View File

@ -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. /<oauth2>/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())

47
pkg/apis/options/proxy.go Normal file
View File

@ -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,
}
}

View File

@ -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

View File

@ -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))
},

View File

@ -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

View File

@ -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{

View File

@ -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")
}

View File

@ -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,