From 44b27e02081bd1f2016d97854fcad678aec3a9af Mon Sep 17 00:00:00 2001 From: Joel Speed Date: Mon, 13 Apr 2020 13:50:34 +0100 Subject: [PATCH] Move Options and Validation to package --- http.go | 5 +- http_test.go | 9 +- main.go | 5 +- oauthproxy.go | 36 +-- oauthproxy_test.go | 108 +++---- .../apis/logging/realclientip.go | 20 +- .../apis/logging/realclientip_test.go | 10 +- pkg/apis/options/options.go | 210 ++++++++++++++ options.go => pkg/validation/options.go | 264 ++++-------------- .../validation/options_test.go | 129 +++++---- 10 files changed, 417 insertions(+), 379 deletions(-) rename realclientip.go => pkg/apis/logging/realclientip.go (82%) rename realclientip_test.go => pkg/apis/logging/realclientip_test.go (95%) create mode 100644 pkg/apis/options/options.go rename options.go => pkg/validation/options.go (51%) rename options_test.go => pkg/validation/options_test.go (80%) diff --git a/http.go b/http.go index 228364e3..a2287cb7 100644 --- a/http.go +++ b/http.go @@ -9,13 +9,14 @@ import ( "strings" "time" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) // Server represents an HTTP server type Server struct { Handler http.Handler - Opts *Options + Opts *options.Options stop chan struct{} // channel for waiting shutdown } @@ -167,7 +168,7 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { return tc, nil } -func redirectToHTTPS(opts *Options, h http.Handler) http.Handler { +func redirectToHTTPS(opts *options.Options, h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { proto := r.Header.Get("X-Forwarded-Proto") if opts.ForceHTTPS && (r.TLS == nil || (proto != "" && strings.ToLower(proto) != "https")) { diff --git a/http_test.go b/http_test.go index f5165c5e..bd9c02fa 100644 --- a/http_test.go +++ b/http_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" "github.com/stretchr/testify/assert" ) @@ -110,7 +111,7 @@ func TestGCPHealthcheckNotIngressPut(t *testing.T) { } func TestRedirectToHTTPSTrue(t *testing.T) { - opts := NewOptions() + opts := options.NewOptions() opts.ForceHTTPS = true handler := func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("test")) @@ -125,7 +126,7 @@ func TestRedirectToHTTPSTrue(t *testing.T) { } func TestRedirectToHTTPSFalse(t *testing.T) { - opts := NewOptions() + opts := options.NewOptions() handler := func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("test")) } @@ -139,7 +140,7 @@ func TestRedirectToHTTPSFalse(t *testing.T) { } func TestRedirectNotWhenHTTPS(t *testing.T) { - opts := NewOptions() + opts := options.NewOptions() opts.ForceHTTPS = true handler := func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("test")) @@ -160,7 +161,7 @@ func TestRedirectNotWhenHTTPS(t *testing.T) { } func TestGracefulShutdown(t *testing.T) { - opts := NewOptions() + opts := options.NewOptions() stop := make(chan struct{}, 1) srv := Server{Handler: http.DefaultServeMux, Opts: opts, stop: stop} var wg sync.WaitGroup diff --git a/main.go b/main.go index 4507d8ee..62bed153 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/pkg/validation" "github.com/spf13/pflag" ) @@ -148,14 +149,14 @@ func main() { return } - opts := NewOptions() + opts := options.NewOptions() err := options.Load(*config, flagSet, opts) if err != nil { logger.Printf("ERROR: Failed to load config: %v", err) os.Exit(1) } - err = opts.Validate() + err = validation.Validate(opts) if err != nil { logger.Printf("%s", err) os.Exit(1) diff --git a/oauthproxy.go b/oauthproxy.go index 6a4d699f..1367353c 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -19,6 +19,8 @@ import ( "github.com/coreos/go-oidc" "github.com/mbland/hmacauth" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/logging" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" "github.com/oauth2-proxy/oauth2-proxy/pkg/cookies" "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption" @@ -112,7 +114,7 @@ type OAuthProxy struct { jwtBearerVerifiers []*oidc.IDTokenVerifier compiledRegex []*regexp.Regexp templates *template.Template - realClientIPParser realClientIPParser + realClientIPParser logging.RealClientIPParser Banner string Footer string } @@ -143,7 +145,7 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { // NewReverseProxy creates a new reverse proxy for proxying requests to upstream // servers -func NewReverseProxy(target *url.URL, opts *Options) (proxy *httputil.ReverseProxy) { +func NewReverseProxy(target *url.URL, opts *options.Options) (proxy *httputil.ReverseProxy) { proxy = httputil.NewSingleHostReverseProxy(target) proxy.FlushInterval = opts.FlushInterval if opts.SSLUpstreamInsecureSkipVerify { @@ -181,7 +183,7 @@ func NewFileServer(path string, filesystemPath string) (proxy http.Handler) { } // NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url -func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) http.Handler { +func NewWebSocketOrRestReverseProxy(u *url.URL, opts *options.Options, auth hmacauth.HmacAuth) http.Handler { u.Path = "" proxy := NewReverseProxy(u, opts) if !opts.PassHostHeader { @@ -209,14 +211,14 @@ func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.Hma } // NewOAuthProxy creates a new instance of OAuthProxy from the options provided -func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { +func NewOAuthProxy(opts *options.Options, validator func(string) bool) *OAuthProxy { serveMux := http.NewServeMux() var auth hmacauth.HmacAuth - if sigData := opts.signatureData; sigData != nil { - auth = hmacauth.NewHmacAuth(sigData.hash, []byte(sigData.key), + if sigData := opts.GetSignatureData(); sigData != nil { + auth = hmacauth.NewHmacAuth(sigData.Hash, []byte(sigData.Key), SignatureHeader, SignatureHeaders) } - for _, u := range opts.proxyURLs { + for _, u := range opts.GetProxyURLs() { path := u.Path host := u.Host switch u.Scheme { @@ -252,7 +254,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme)) } } - for _, u := range opts.compiledRegex { + for _, u := range opts.GetCompiledRegex() { logger.Printf("compiled skip-auth-regex => %q", u) } @@ -262,12 +264,12 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer) } } - redirectURL := opts.redirectURL + redirectURL := opts.GetRedirectURL() if redirectURL.Path == "" { redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix) } - logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.provider.Data().ProviderName, opts.ClientID) + logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.ClientID) refresh := "disabled" if opts.Cookie.Refresh != time.Duration(0) { refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh) @@ -298,18 +300,18 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix), ProxyPrefix: opts.ProxyPrefix, - provider: opts.provider, + provider: opts.GetProvider(), providerNameOverride: opts.ProviderName, - sessionStore: opts.sessionStore, + sessionStore: opts.GetSessionStore(), serveMux: serveMux, redirectURL: redirectURL, whitelistDomains: opts.WhitelistDomains, skipAuthRegex: opts.SkipAuthRegex, skipAuthPreflight: opts.SkipAuthPreflight, skipJwtBearerTokens: opts.SkipJwtBearerTokens, - jwtBearerVerifiers: opts.jwtBearerVerifiers, - compiledRegex: opts.compiledRegex, - realClientIPParser: opts.realClientIPParser, + jwtBearerVerifiers: opts.GetJWTBearerVerifiers(), + compiledRegex: opts.GetCompiledRegex(), + realClientIPParser: opts.GetRealClientIPParser(), SetXAuthRequest: opts.SetXAuthRequest, PassBasicAuth: opts.PassBasicAuth, SetBasicAuth: opts.SetBasicAuth, @@ -760,7 +762,7 @@ func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) { // OAuthCallback is the OAuth2 authentication flow callback that finishes the // OAuth2 authentication flow func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { - remoteAddr := getClientString(p.realClientIPParser, req, true) + remoteAddr := logging.GetClientString(p.realClientIPParser, req, true) // finish the oauth cycle err := req.ParseForm() @@ -888,7 +890,7 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R } } - remoteAddr := getClientString(p.realClientIPParser, req, true) + remoteAddr := logging.GetClientString(p.realClientIPParser, req, true) if session == nil { session, err = p.LoadCookiedSession(req) if err != nil { diff --git a/oauthproxy_test.go b/oauthproxy_test.go index a08b1640..20d59f8d 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -18,9 +18,11 @@ import ( "github.com/coreos/go-oidc" "github.com/mbland/hmacauth" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/cookie" + "github.com/oauth2-proxy/oauth2-proxy/pkg/validation" "github.com/oauth2-proxy/oauth2-proxy/providers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -71,7 +73,7 @@ func TestWebSocketProxy(t *testing.T) { backendURL, _ := url.Parse(backend.URL) - options := NewOptions() + options := options.NewOptions() var auth hmacauth.HmacAuth options.PassHostHeader = true proxyHandler := NewWebSocketOrRestReverseProxy(backendURL, options, auth) @@ -121,7 +123,7 @@ func TestNewReverseProxy(t *testing.T) { backendHost := net.JoinHostPort(backendHostname, backendPort) proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/") - proxyHandler := NewReverseProxy(proxyURL, &Options{FlushInterval: time.Second}) + proxyHandler := NewReverseProxy(proxyURL, &options.Options{FlushInterval: time.Second}) setProxyUpstreamHostHeader(proxyHandler, proxyURL) frontend := httptest.NewServer(proxyHandler) defer frontend.Close() @@ -143,7 +145,7 @@ func TestEncodedSlashes(t *testing.T) { defer backend.Close() b, _ := url.Parse(backend.URL) - proxyHandler := NewReverseProxy(b, &Options{FlushInterval: time.Second}) + proxyHandler := NewReverseProxy(b, &options.Options{FlushInterval: time.Second}) setProxyDirector(proxyHandler) frontend := httptest.NewServer(proxyHandler) defer frontend.Close() @@ -161,11 +163,11 @@ func TestEncodedSlashes(t *testing.T) { } func TestRobotsTxt(t *testing.T) { - opts := NewOptions() + opts := options.NewOptions() opts.ClientID = "asdlkjx" opts.ClientSecret = "alkgks" opts.Cookie.Secret = "asdkugkj" - opts.Validate() + validation.Validate(opts) proxy := NewOAuthProxy(opts, func(string) bool { return true }) rw := httptest.NewRecorder() @@ -176,7 +178,7 @@ func TestRobotsTxt(t *testing.T) { } func TestIsValidRedirect(t *testing.T) { - opts := NewOptions() + opts := options.NewOptions() opts.ClientID = "skdlfj" opts.ClientSecret = "fgkdsgj" opts.Cookie.Secret = "ljgiogbj" @@ -189,7 +191,7 @@ func TestIsValidRedirect(t *testing.T) { "anyport.bar:*", ".sub.anyport.bar:*", } - opts.Validate() + validation.Validate(opts) proxy := NewOAuthProxy(opts, func(string) bool { return true }) @@ -451,7 +453,7 @@ func TestBasicAuthPassword(t *testing.T) { w.WriteHeader(200) w.Write([]byte(payload)) })) - opts := NewOptions() + opts := options.NewOptions() opts.Upstreams = append(opts.Upstreams, providerServer.URL) // The CookieSecret must be 32 bytes in order to create the AES // cipher. @@ -464,12 +466,12 @@ func TestBasicAuthPassword(t *testing.T) { opts.PassUserHeaders = true opts.PreferEmailToUser = true opts.BasicAuthPassword = "This is a secure password" - opts.Validate() + validation.Validate(opts) providerURL, _ := url.Parse(providerServer.URL) const emailAddress = "john.doe@example.com" - opts.provider = NewTestProvider(providerURL, emailAddress) + opts.SetProvider(NewTestProvider(providerURL, emailAddress)) proxy := NewOAuthProxy(opts, func(email string) bool { return email == emailAddress }) @@ -518,12 +520,12 @@ func TestBasicAuthPassword(t *testing.T) { } func TestBasicAuthWithEmail(t *testing.T) { - opts := NewOptions() + opts := options.NewOptions() opts.PassBasicAuth = true opts.PassUserHeaders = false opts.PreferEmailToUser = false opts.BasicAuthPassword = "This is a secure password" - opts.Validate() + validation.Validate(opts) const emailAddress = "john.doe@example.com" const userName = "9fcab5c9b889a557" @@ -564,11 +566,11 @@ func TestBasicAuthWithEmail(t *testing.T) { } func TestPassUserHeadersWithEmail(t *testing.T) { - opts := NewOptions() + opts := options.NewOptions() opts.PassBasicAuth = false opts.PassUserHeaders = true opts.PreferEmailToUser = false - opts.Validate() + validation.Validate(opts) const emailAddress = "john.doe@example.com" const userName = "9fcab5c9b889a557" @@ -605,7 +607,7 @@ func TestPassUserHeadersWithEmail(t *testing.T) { type PassAccessTokenTest struct { providerServer *httptest.Server proxy *OAuthProxy - opts *Options + opts *options.Options } type PassAccessTokenTestOptions struct { @@ -632,7 +634,7 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes w.Write([]byte(payload)) })) - t.opts = NewOptions() + t.opts = options.NewOptions() t.opts.Upstreams = append(t.opts.Upstreams, t.providerServer.URL) if opts.ProxyUpstream != "" { t.opts.Upstreams = append(t.opts.Upstreams, opts.ProxyUpstream) @@ -644,12 +646,12 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes t.opts.ClientSecret = "gfjgojl" t.opts.Cookie.Secure = false t.opts.PassAccessToken = opts.PassAccessToken - t.opts.Validate() + validation.Validate(t.opts) providerURL, _ := url.Parse(t.providerServer.URL) const emailAddress = "michael.bland@gsa.gov" - t.opts.provider = NewTestProvider(providerURL, emailAddress) + t.opts.SetProvider(NewTestProvider(providerURL, emailAddress)) t.proxy = NewOAuthProxy(t.opts, func(email string) bool { return email == emailAddress }) @@ -779,7 +781,7 @@ func TestDoNotForwardAccessTokenUpstream(t *testing.T) { } type SignInPageTest struct { - opts *Options + opts *options.Options proxy *OAuthProxy signInRegexp *regexp.Regexp signInProviderRegexp *regexp.Regexp @@ -791,12 +793,12 @@ const signInSkipProvider = `>Found<` func NewSignInPageTest(skipProvider bool) *SignInPageTest { var sipTest SignInPageTest - sipTest.opts = NewOptions() + sipTest.opts = options.NewOptions() sipTest.opts.Cookie.Secret = "adklsj2" sipTest.opts.ClientID = "lkdgj" sipTest.opts.ClientSecret = "sgiufgoi" sipTest.opts.SkipProviderButton = skipProvider - sipTest.opts.Validate() + validation.Validate(sipTest.opts) sipTest.proxy = NewOAuthProxy(sipTest.opts, func(email string) bool { return true @@ -876,7 +878,7 @@ func TestSignInPageSkipProviderDirect(t *testing.T) { } type ProcessCookieTest struct { - opts *Options + opts *options.Options proxy *OAuthProxy rw *httptest.ResponseRecorder req *http.Request @@ -887,12 +889,12 @@ type ProcessCookieTestOpts struct { providerValidateCookieResponse bool } -type OptionsModifier func(*Options) +type OptionsModifier func(*options.Options) func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifier) *ProcessCookieTest { var pcTest ProcessCookieTest - pcTest.opts = NewOptions() + pcTest.opts = options.NewOptions() for _, modifier := range modifiers { modifier(pcTest.opts) } @@ -902,7 +904,7 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi // First, set the CookieRefresh option so proxy.AesCipher is created, // needed to encrypt the access_token. pcTest.opts.Cookie.Refresh = time.Hour - pcTest.opts.Validate() + validation.Validate(pcTest.opts) pcTest.proxy = NewOAuthProxy(pcTest.opts, func(email string) bool { return pcTest.validateUser @@ -971,7 +973,7 @@ func TestProcessCookieNoCookieError(t *testing.T) { } func TestProcessCookieRefreshNotSet(t *testing.T) { - pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) { + pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) { opts.Cookie.Expire = time.Duration(23) * time.Hour }) reference := time.Now().Add(time.Duration(-2) * time.Hour) @@ -988,7 +990,7 @@ func TestProcessCookieRefreshNotSet(t *testing.T) { } func TestProcessCookieFailIfCookieExpired(t *testing.T) { - pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) { + pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) { opts.Cookie.Expire = time.Duration(24) * time.Hour }) reference := time.Now().Add(time.Duration(25) * time.Hour * -1) @@ -1003,7 +1005,7 @@ func TestProcessCookieFailIfCookieExpired(t *testing.T) { } func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) { - pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) { + pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) { opts.Cookie.Expire = time.Duration(24) * time.Hour }) reference := time.Now().Add(time.Duration(25) * time.Hour * -1) @@ -1073,7 +1075,7 @@ func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) { } func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) { - test := NewAuthOnlyEndpointTest(func(opts *Options) { + test := NewAuthOnlyEndpointTest(func(opts *options.Options) { opts.Cookie.Expire = time.Duration(24) * time.Hour }) reference := time.Now().Add(time.Duration(25) * time.Hour * -1) @@ -1103,9 +1105,9 @@ func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) { func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) { var pcTest ProcessCookieTest - pcTest.opts = NewOptions() + pcTest.opts = options.NewOptions() pcTest.opts.SetXAuthRequest = true - pcTest.opts.Validate() + validation.Validate(pcTest.opts) pcTest.proxy = NewOAuthProxy(pcTest.opts, func(email string) bool { return pcTest.validateUser @@ -1133,10 +1135,10 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) { func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) { var pcTest ProcessCookieTest - pcTest.opts = NewOptions() + pcTest.opts = options.NewOptions() pcTest.opts.SetXAuthRequest = true pcTest.opts.SetBasicAuth = true - pcTest.opts.Validate() + validation.Validate(pcTest.opts) pcTest.proxy = NewOAuthProxy(pcTest.opts, func(email string) bool { return pcTest.validateUser @@ -1166,10 +1168,10 @@ func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) { func TestAuthOnlyEndpointSetBasicAuthFalseRequestHeaders(t *testing.T) { var pcTest ProcessCookieTest - pcTest.opts = NewOptions() + pcTest.opts = options.NewOptions() pcTest.opts.SetXAuthRequest = true pcTest.opts.SetBasicAuth = false - pcTest.opts.Validate() + validation.Validate(pcTest.opts) pcTest.proxy = NewOAuthProxy(pcTest.opts, func(email string) bool { return pcTest.validateUser @@ -1202,16 +1204,16 @@ func TestAuthSkippedForPreflightRequests(t *testing.T) { })) defer upstream.Close() - opts := NewOptions() + opts := options.NewOptions() opts.Upstreams = append(opts.Upstreams, upstream.URL) opts.ClientID = "aljsal" opts.ClientSecret = "jglkfsdgj" opts.Cookie.Secret = "dkfjgdls" opts.SkipAuthPreflight = true - opts.Validate() + validation.Validate(opts) upstreamURL, _ := url.Parse(upstream.URL) - opts.provider = NewTestProvider(upstreamURL, "") + opts.SetProvider(NewTestProvider(upstreamURL, "")) proxy := NewOAuthProxy(opts, func(string) bool { return false }) rw := httptest.NewRecorder() @@ -1242,7 +1244,7 @@ func (v *SignatureAuthenticator) Authenticate(w http.ResponseWriter, r *http.Req } type SignatureTest struct { - opts *Options + opts *options.Options upstream *httptest.Server upstreamHost string provider *httptest.Server @@ -1252,7 +1254,7 @@ type SignatureTest struct { } func NewSignatureTest() *SignatureTest { - opts := NewOptions() + opts := options.NewOptions() opts.Cookie.Secret = "cookie secret" opts.ClientID = "client ID" opts.ClientSecret = "client secret" @@ -1269,7 +1271,7 @@ func NewSignatureTest() *SignatureTest { } provider := httptest.NewServer(http.HandlerFunc(providerHandler)) providerURL, _ := url.Parse(provider.URL) - opts.provider = NewTestProvider(providerURL, "mbland@acm.org") + opts.SetProvider(NewTestProvider(providerURL, "mbland@acm.org")) return &SignatureTest{ opts, @@ -1304,7 +1306,7 @@ func (fnc *fakeNetConn) Read(p []byte) (n int, err error) { } func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) { - err := st.opts.Validate() + err := validation.Validate(st.opts) if err != nil { panic(err) } @@ -1360,8 +1362,8 @@ func TestRequestSignaturePostRequest(t *testing.T) { } func TestGetRedirect(t *testing.T) { - options := NewOptions() - _ = options.Validate() + options := options.NewOptions() + _ = validation.Validate(options) require.NotEmpty(t, options.ProxyPrefix) proxy := NewOAuthProxy(options, func(s string) bool { return false }) @@ -1393,17 +1395,17 @@ func TestGetRedirect(t *testing.T) { } type ajaxRequestTest struct { - opts *Options + opts *options.Options proxy *OAuthProxy } func newAjaxRequestTest() *ajaxRequestTest { test := &ajaxRequestTest{} - test.opts = NewOptions() + test.opts = options.NewOptions() test.opts.Cookie.Secret = "sdflsw" test.opts.ClientID = "gkljfdl" test.opts.ClientSecret = "sdflkjs" - test.opts.Validate() + validation.Validate(test.opts) test.proxy = NewOAuthProxy(test.opts, func(email string) bool { return true }) @@ -1457,7 +1459,7 @@ func TestAjaxForbiddendRequest(t *testing.T) { } func TestClearSplitCookie(t *testing.T) { - opts := NewOptions() + opts := options.NewOptions() opts.Cookie.Name = "oauth2" opts.Cookie.Domains = []string{"abc"} store, err := cookie.NewCookieSessionStore(&opts.Session, &opts.Cookie) @@ -1486,7 +1488,7 @@ func TestClearSplitCookie(t *testing.T) { } func TestClearSingleCookie(t *testing.T) { - opts := NewOptions() + opts := options.NewOptions() opts.Cookie.Name = "oauth2" opts.Cookie.Domains = []string{"abc"} store, err := cookie.NewCookieSessionStore(&opts.Session, &opts.Cookie) @@ -1542,12 +1544,12 @@ func TestGetJwtSession(t *testing.T) { verifier := oidc.NewVerifier("https://issuer.example.com", keyset, &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true}) - test := NewAuthOnlyEndpointTest(func(opts *Options) { + test := NewAuthOnlyEndpointTest(func(opts *options.Options) { opts.PassAuthorization = true opts.SetAuthorization = true opts.SetXAuthRequest = true opts.SkipJwtBearerTokens = true - opts.jwtBearerVerifiers = append(opts.jwtBearerVerifiers, verifier) + opts.SetJWTBearerVerifiers(append(opts.GetJWTBearerVerifiers(), verifier)) }) tp, _ := test.proxy.provider.(*TestProvider) tp.GroupValidator = func(s string) bool { @@ -1666,10 +1668,10 @@ func Test_noCacheHeadersDoesNotExistsInResponseHeadersFromUpstream(t *testing.T) })) t.Cleanup(upstream.Close) - opts := NewOptions() + opts := options.NewOptions() opts.Upstreams = []string{upstream.URL} opts.SkipAuthRegex = []string{".*"} - _ = opts.Validate() + _ = validation.Validate(opts) proxy := NewOAuthProxy(opts, func(email string) bool { return true }) diff --git a/realclientip.go b/pkg/apis/logging/realclientip.go similarity index 82% rename from realclientip.go rename to pkg/apis/logging/realclientip.go index b45ef7c2..04043f75 100644 --- a/realclientip.go +++ b/pkg/apis/logging/realclientip.go @@ -1,19 +1,18 @@ -package main +package logging import ( "fmt" "net" "net/http" "strings" - - "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" ) -type realClientIPParser interface { +// RealClientIPParser is an interface for a getting the client's real IP to be used for logging. +type RealClientIPParser interface { GetRealClientIP(http.Header) (net.IP, error) } -func getRealClientIPParser(headerKey string) (realClientIPParser, error) { +func GetRealClientIPParser(headerKey string) (RealClientIPParser, error) { headerKey = http.CanonicalHeaderKey(headerKey) switch headerKey { @@ -73,13 +72,11 @@ func getRemoteIP(req *http.Request) (net.IP, error) { } } -// getClientString obtains the human readable string of the remote IP and optionally the real client IP if available -func getClientString(p realClientIPParser, req *http.Request, full bool) (s string) { +// GetClientString obtains the human readable string of the remote IP and optionally the real client IP if available +func GetClientString(p RealClientIPParser, req *http.Request, full bool) (s string) { var realClientIPStr string if p != nil { - if realClientIP, err := p.GetRealClientIP(req.Header); err != nil { - logger.Printf("Unable to get real client IP: %v", err) - } else if realClientIP != nil { + if realClientIP, err := p.GetRealClientIP(req.Header); err == nil && realClientIP != nil { realClientIPStr = realClientIP.String() } } @@ -87,9 +84,6 @@ func getClientString(p realClientIPParser, req *http.Request, full bool) (s stri var remoteIPStr string if remoteIP, err := getRemoteIP(req); err == nil { remoteIPStr = remoteIP.String() - } else { - // Should not happen, if it does, likely a bug. - logger.Printf("Unable to get remote IP(?!?!): %v", err) } if !full && realClientIPStr != "" { diff --git a/realclientip_test.go b/pkg/apis/logging/realclientip_test.go similarity index 95% rename from realclientip_test.go rename to pkg/apis/logging/realclientip_test.go index 0271e2e3..62da9192 100644 --- a/realclientip_test.go +++ b/pkg/apis/logging/realclientip_test.go @@ -1,4 +1,4 @@ -package main +package logging import ( "net" @@ -26,7 +26,7 @@ func TestGetRealClientIPParser(t *testing.T) { } for _, test := range tests { - p, err := getRealClientIPParser(test.header) + p, err := GetRealClientIPParser(test.header) if test.errString == "" { assert.Nil(t, err) @@ -144,7 +144,7 @@ func TestGetClientString(t *testing.T) { p := &xForwardedForClientIPParser{header: http.CanonicalHeaderKey("X-Forwarded-For")} tests := []struct { - parser realClientIPParser + parser RealClientIPParser remoteAddr string headerValue string expectedClient string @@ -167,10 +167,10 @@ func TestGetClientString(t *testing.T) { RemoteAddr: test.remoteAddr, } - client := getClientString(test.parser, req, false) + client := GetClientString(test.parser, req, false) assert.Equal(t, test.expectedClient, client) - clientFull := getClientString(test.parser, req, true) + clientFull := GetClientString(test.parser, req, true) assert.Equal(t, test.expectedClientFull, clientFull) } } diff --git a/pkg/apis/options/options.go b/pkg/apis/options/options.go new file mode 100644 index 00000000..fc386e8d --- /dev/null +++ b/pkg/apis/options/options.go @@ -0,0 +1,210 @@ +package options + +import ( + "crypto" + "net/url" + "regexp" + "time" + + oidc "github.com/coreos/go-oidc" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/logging" + sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/providers" +) + +// SignatureData holds hmacauth signature hash and key +type SignatureData struct { + Hash crypto.Hash + Key string +} + +// 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" env:"OAUTH2_PROXY_PROXY_PREFIX"` + PingPath string `flag:"ping-path" cfg:"ping_path" env:"OAUTH2_PROXY_PING_PATH"` + ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets" env:"OAUTH2_PROXY_PROXY_WEBSOCKETS"` + HTTPAddress string `flag:"http-address" cfg:"http_address" env:"OAUTH2_PROXY_HTTP_ADDRESS"` + HTTPSAddress string `flag:"https-address" cfg:"https_address" env:"OAUTH2_PROXY_HTTPS_ADDRESS"` + ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy" env:"OAUTH2_PROXY_REVERSE_PROXY"` + RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header" env:"OAUTH2_PROXY_REAL_CLIENT_IP_HEADER"` + ForceHTTPS bool `flag:"force-https" cfg:"force_https" env:"OAUTH2_PROXY_FORCE_HTTPS"` + RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url" env:"OAUTH2_PROXY_REDIRECT_URL"` + ClientID string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"` + ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"` + ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file" env:"OAUTH2_PROXY_CLIENT_SECRET_FILE"` + TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"` + TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"` + + AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"` + KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group" env:"OAUTH2_PROXY_KEYCLOAK_GROUP"` + AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"` + BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team" env:"OAUTH2_PROXY_BITBUCKET_TEAM"` + BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository" env:"OAUTH2_PROXY_BITBUCKET_REPOSITORY"` + EmailDomains []string `flag:"email-domain" cfg:"email_domains" env:"OAUTH2_PROXY_EMAIL_DOMAINS"` + WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains" env:"OAUTH2_PROXY_WHITELIST_DOMAINS"` + GitHubOrg string `flag:"github-org" cfg:"github_org" env:"OAUTH2_PROXY_GITHUB_ORG"` + GitHubTeam string `flag:"github-team" cfg:"github_team" env:"OAUTH2_PROXY_GITHUB_TEAM"` + GitHubRepo string `flag:"github-repo" cfg:"github_repo" env:"OAUTH2_PROXY_GITHUB_REPO"` + GitHubToken string `flag:"github-token" cfg:"github_token" env:"OAUTH2_PROXY_GITHUB_TOKEN"` + GitLabGroup string `flag:"gitlab-group" cfg:"gitlab_group" env:"OAUTH2_PROXY_GITLAB_GROUP"` + GoogleGroups []string `flag:"google-group" cfg:"google_group" env:"OAUTH2_PROXY_GOOGLE_GROUPS"` + GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email" env:"OAUTH2_PROXY_GOOGLE_ADMIN_EMAIL"` + GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json" env:"OAUTH2_PROXY_GOOGLE_SERVICE_ACCOUNT_JSON"` + HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file" env:"OAUTH2_PROXY_HTPASSWD_FILE"` + DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form" env:"OAUTH2_PROXY_DISPLAY_HTPASSWD_FORM"` + CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir" env:"OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR"` + Banner string `flag:"banner" cfg:"banner" env:"OAUTH2_PROXY_BANNER"` + Footer string `flag:"footer" cfg:"footer" env:"OAUTH2_PROXY_FOOTER"` + + Cookie CookieOptions `cfg:",squash"` + Session SessionOptions `cfg:",squash"` + + Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"` + SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"` + SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens" env:"OAUTH2_PROXY_SKIP_JWT_BEARER_TOKENS"` + ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers" env:"OAUTH2_PROXY_EXTRA_JWT_ISSUERS"` + PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"` + SetBasicAuth bool `flag:"set-basic-auth" cfg:"set_basic_auth" env:"OAUTH2_PROXY_SET_BASIC_AUTH"` + PreferEmailToUser bool `flag:"prefer-email-to-user" cfg:"prefer_email_to_user" env:"OAUTH2_PROXY_PREFER_EMAIL_TO_USER"` + BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"` + PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"` + PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header" env:"OAUTH2_PROXY_PASS_HOST_HEADER"` + SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button" env:"OAUTH2_PROXY_SKIP_PROVIDER_BUTTON"` + PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers" env:"OAUTH2_PROXY_PASS_USER_HEADERS"` + SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify" env:"OAUTH2_PROXY_SSL_INSECURE_SKIP_VERIFY"` + SSLUpstreamInsecureSkipVerify bool `flag:"ssl-upstream-insecure-skip-verify" cfg:"ssl_upstream_insecure_skip_verify" env:"OAUTH2_PROXY_SSL_UPSTREAM_INSECURE_SKIP_VERIFY"` + SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest" env:"OAUTH2_PROXY_SET_XAUTHREQUEST"` + SetAuthorization bool `flag:"set-authorization-header" cfg:"set_authorization_header" env:"OAUTH2_PROXY_SET_AUTHORIZATION_HEADER"` + PassAuthorization bool `flag:"pass-authorization-header" cfg:"pass_authorization_header" env:"OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER"` + SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight" env:"OAUTH2_PROXY_SKIP_AUTH_PREFLIGHT"` + FlushInterval time.Duration `flag:"flush-interval" cfg:"flush_interval" env:"OAUTH2_PROXY_FLUSH_INTERVAL"` + + // These options allow for other providers besides Google, with + // potential overrides. + ProviderType string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"` + ProviderName string `flag:"provider-display-name" cfg:"provider_display_name" env:"OAUTH2_PROXY_PROVIDER_DISPLAY_NAME"` + OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url" env:"OAUTH2_PROXY_OIDC_ISSUER_URL"` + InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email" env:"OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL"` + InsecureOIDCSkipIssuerVerification bool `flag:"insecure-oidc-skip-issuer-verification" cfg:"insecure_oidc_skip_issuer_verification" env:"OAUTH2_PROXY_INSECURE_OIDC_SKIP_ISSUER_VERIFICATION"` + SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_PROXY_SKIP_OIDC_DISCOVERY"` + OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_PROXY_OIDC_JWKS_URL"` + LoginURL string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"` + RedeemURL string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"` + ProfileURL string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"` + ProtectedResource string `flag:"resource" cfg:"resource" env:"OAUTH2_PROXY_RESOURCE"` + ValidateURL string `flag:"validate-url" cfg:"validate_url" env:"OAUTH2_PROXY_VALIDATE_URL"` + Scope string `flag:"scope" cfg:"scope" env:"OAUTH2_PROXY_SCOPE"` + Prompt string `flag:"prompt" cfg:"prompt" env:"OAUTH2_PROXY_PROMPT"` + ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"` // Deprecated by OIDC 1.0 + UserIDClaim string `flag:"user-id-claim" cfg:"user_id_claim" env:"OAUTH2_PROXY_USER_ID_CLAIM"` + + // Configuration values for logging + LoggingFilename string `flag:"logging-filename" cfg:"logging_filename" env:"OAUTH2_PROXY_LOGGING_FILENAME"` + LoggingMaxSize int `flag:"logging-max-size" cfg:"logging_max_size" env:"OAUTH2_PROXY_LOGGING_MAX_SIZE"` + LoggingMaxAge int `flag:"logging-max-age" cfg:"logging_max_age" env:"OAUTH2_PROXY_LOGGING_MAX_AGE"` + LoggingMaxBackups int `flag:"logging-max-backups" cfg:"logging_max_backups" env:"OAUTH2_PROXY_LOGGING_MAX_BACKUPS"` + LoggingLocalTime bool `flag:"logging-local-time" cfg:"logging_local_time" env:"OAUTH2_PROXY_LOGGING_LOCAL_TIME"` + LoggingCompress bool `flag:"logging-compress" cfg:"logging_compress" env:"OAUTH2_PROXY_LOGGING_COMPRESS"` + StandardLogging bool `flag:"standard-logging" cfg:"standard_logging" env:"OAUTH2_PROXY_STANDARD_LOGGING"` + StandardLoggingFormat string `flag:"standard-logging-format" cfg:"standard_logging_format" env:"OAUTH2_PROXY_STANDARD_LOGGING_FORMAT"` + RequestLogging bool `flag:"request-logging" cfg:"request_logging" env:"OAUTH2_PROXY_REQUEST_LOGGING"` + RequestLoggingFormat string `flag:"request-logging-format" cfg:"request_logging_format" env:"OAUTH2_PROXY_REQUEST_LOGGING_FORMAT"` + ExcludeLoggingPaths string `flag:"exclude-logging-paths" cfg:"exclude_logging_paths" env:"OAUTH2_PROXY_EXCLUDE_LOGGING_PATHS"` + SilencePingLogging bool `flag:"silence-ping-logging" cfg:"silence_ping_logging" env:"OAUTH2_PROXY_SILENCE_PING_LOGGING"` + AuthLogging bool `flag:"auth-logging" cfg:"auth_logging" env:"OAUTH2_PROXY_LOGGING_AUTH_LOGGING"` + AuthLoggingFormat string `flag:"auth-logging-format" cfg:"auth_logging_format" env:"OAUTH2_PROXY_AUTH_LOGGING_FORMAT"` + SignatureKey string `flag:"signature-key" cfg:"signature_key" env:"OAUTH2_PROXY_SIGNATURE_KEY"` + AcrValues string `flag:"acr-values" cfg:"acr_values" env:"OAUTH2_PROXY_ACR_VALUES"` + JWTKey string `flag:"jwt-key" cfg:"jwt_key" env:"OAUTH2_PROXY_JWT_KEY"` + JWTKeyFile string `flag:"jwt-key-file" cfg:"jwt_key_file" env:"OAUTH2_PROXY_JWT_KEY_FILE"` + PubJWKURL string `flag:"pubjwk-url" cfg:"pubjwk_url" env:"OAUTH2_PROXY_PUBJWK_URL"` + GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks" env:"OAUTH2_PROXY_GCP_HEALTHCHECKS"` + + // internal values that are set after config validation + redirectURL *url.URL + proxyURLs []*url.URL + compiledRegex []*regexp.Regexp + provider providers.Provider + sessionStore sessionsapi.SessionStore + signatureData *SignatureData + oidcVerifier *oidc.IDTokenVerifier + jwtBearerVerifiers []*oidc.IDTokenVerifier + realClientIPParser logging.RealClientIPParser +} + +// Options for Getting internal values +func (o *Options) GetRedirectURL() *url.URL { return o.redirectURL } +func (o *Options) GetProxyURLs() []*url.URL { return o.proxyURLs } +func (o *Options) GetCompiledRegex() []*regexp.Regexp { return o.compiledRegex } +func (o *Options) GetProvider() providers.Provider { return o.provider } +func (o *Options) GetSessionStore() sessionsapi.SessionStore { return o.sessionStore } +func (o *Options) GetSignatureData() *SignatureData { return o.signatureData } +func (o *Options) GetOIDCVerifier() *oidc.IDTokenVerifier { return o.oidcVerifier } +func (o *Options) GetJWTBearerVerifiers() []*oidc.IDTokenVerifier { return o.jwtBearerVerifiers } +func (o *Options) GetRealClientIPParser() logging.RealClientIPParser { return o.realClientIPParser } + +// Options for Setting internal values +func (o *Options) SetRedirectURL(s *url.URL) { o.redirectURL = s } +func (o *Options) SetProxyURLs(s []*url.URL) { o.proxyURLs = s } +func (o *Options) SetCompiledRegex(s []*regexp.Regexp) { o.compiledRegex = s } +func (o *Options) SetProvider(s providers.Provider) { o.provider = s } +func (o *Options) SetSessionStore(s sessionsapi.SessionStore) { o.sessionStore = s } +func (o *Options) SetSignatureData(s *SignatureData) { o.signatureData = s } +func (o *Options) SetOIDCVerifier(s *oidc.IDTokenVerifier) { o.oidcVerifier = s } +func (o *Options) SetJWTBearerVerifiers(s []*oidc.IDTokenVerifier) { o.jwtBearerVerifiers = s } +func (o *Options) SetRealClientIPParser(s logging.RealClientIPParser) { o.realClientIPParser = s } + +// NewOptions constructs a new Options with defaulted values +func NewOptions() *Options { + return &Options{ + ProxyPrefix: "/oauth2", + PingPath: "/ping", + ProxyWebSockets: true, + HTTPAddress: "127.0.0.1:4180", + HTTPSAddress: ":443", + RealClientIPHeader: "X-Real-IP", + ForceHTTPS: false, + DisplayHtpasswdForm: true, + Cookie: CookieOptions{ + Name: "_oauth2_proxy", + Secure: true, + HTTPOnly: true, + Expire: time.Duration(168) * time.Hour, + Refresh: time.Duration(0), + }, + Session: SessionOptions{ + Type: "cookie", + }, + SetXAuthRequest: false, + SkipAuthPreflight: false, + PassBasicAuth: true, + SetBasicAuth: false, + PassUserHeaders: true, + PassAccessToken: false, + PassHostHeader: true, + SetAuthorization: false, + PassAuthorization: false, + PreferEmailToUser: false, + Prompt: "", // Change to "login" when ApprovalPrompt officially deprecated + ApprovalPrompt: "force", + UserIDClaim: "email", + InsecureOIDCAllowUnverifiedEmail: false, + SkipOIDCDiscovery: false, + LoggingFilename: "", + LoggingMaxSize: 100, + LoggingMaxAge: 7, + LoggingMaxBackups: 0, + LoggingLocalTime: true, + LoggingCompress: false, + ExcludeLoggingPaths: "", + SilencePingLogging: false, + StandardLogging: true, + StandardLoggingFormat: logger.DefaultStandardLoggingFormat, + RequestLogging: true, + RequestLoggingFormat: logger.DefaultRequestLoggingFormat, + AuthLogging: true, + AuthLoggingFormat: logger.DefaultAuthLoggingFormat, + } +} diff --git a/options.go b/pkg/validation/options.go similarity index 51% rename from options.go rename to pkg/validation/options.go index ae6ac03d..66b7a74d 100644 --- a/options.go +++ b/pkg/validation/options.go @@ -1,4 +1,4 @@ -package main +package validation import ( "context" @@ -14,11 +14,11 @@ import ( "strings" "time" - oidc "github.com/coreos/go-oidc" + "github.com/coreos/go-oidc" "github.com/dgrijalva/jwt-go" "github.com/mbland/hmacauth" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/logging" "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" - sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption" "github.com/oauth2-proxy/oauth2-proxy/pkg/logger" "github.com/oauth2-proxy/oauth2-proxy/pkg/requests" @@ -27,197 +27,9 @@ import ( "gopkg.in/natefinch/lumberjack.v2" ) -// 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" env:"OAUTH2_PROXY_PROXY_PREFIX"` - PingPath string `flag:"ping-path" cfg:"ping_path" env:"OAUTH2_PROXY_PING_PATH"` - ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets" env:"OAUTH2_PROXY_PROXY_WEBSOCKETS"` - HTTPAddress string `flag:"http-address" cfg:"http_address" env:"OAUTH2_PROXY_HTTP_ADDRESS"` - HTTPSAddress string `flag:"https-address" cfg:"https_address" env:"OAUTH2_PROXY_HTTPS_ADDRESS"` - ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy" env:"OAUTH2_PROXY_REVERSE_PROXY"` - RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header" env:"OAUTH2_PROXY_REAL_CLIENT_IP_HEADER"` - ForceHTTPS bool `flag:"force-https" cfg:"force_https" env:"OAUTH2_PROXY_FORCE_HTTPS"` - RedirectURL string `flag:"redirect-url" cfg:"redirect_url" env:"OAUTH2_PROXY_REDIRECT_URL"` - ClientID string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"` - ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"` - ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file" env:"OAUTH2_PROXY_CLIENT_SECRET_FILE"` - TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"` - TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"` - - AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"` - KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group" env:"OAUTH2_PROXY_KEYCLOAK_GROUP"` - AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"` - BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team" env:"OAUTH2_PROXY_BITBUCKET_TEAM"` - BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository" env:"OAUTH2_PROXY_BITBUCKET_REPOSITORY"` - EmailDomains []string `flag:"email-domain" cfg:"email_domains" env:"OAUTH2_PROXY_EMAIL_DOMAINS"` - WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains" env:"OAUTH2_PROXY_WHITELIST_DOMAINS"` - GitHubOrg string `flag:"github-org" cfg:"github_org" env:"OAUTH2_PROXY_GITHUB_ORG"` - GitHubTeam string `flag:"github-team" cfg:"github_team" env:"OAUTH2_PROXY_GITHUB_TEAM"` - GitHubRepo string `flag:"github-repo" cfg:"github_repo" env:"OAUTH2_PROXY_GITHUB_REPO"` - GitHubToken string `flag:"github-token" cfg:"github_token" env:"OAUTH2_PROXY_GITHUB_TOKEN"` - GitLabGroup string `flag:"gitlab-group" cfg:"gitlab_group" env:"OAUTH2_PROXY_GITLAB_GROUP"` - GoogleGroups []string `flag:"google-group" cfg:"google_group" env:"OAUTH2_PROXY_GOOGLE_GROUPS"` - GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email" env:"OAUTH2_PROXY_GOOGLE_ADMIN_EMAIL"` - GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json" env:"OAUTH2_PROXY_GOOGLE_SERVICE_ACCOUNT_JSON"` - HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file" env:"OAUTH2_PROXY_HTPASSWD_FILE"` - DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form" env:"OAUTH2_PROXY_DISPLAY_HTPASSWD_FORM"` - CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir" env:"OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR"` - Banner string `flag:"banner" cfg:"banner" env:"OAUTH2_PROXY_BANNER"` - Footer string `flag:"footer" cfg:"footer" env:"OAUTH2_PROXY_FOOTER"` - - Cookie options.CookieOptions `cfg:",squash"` - Session options.SessionOptions `cfg:",squash"` - - Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"` - SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"` - SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens" env:"OAUTH2_PROXY_SKIP_JWT_BEARER_TOKENS"` - ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers" env:"OAUTH2_PROXY_EXTRA_JWT_ISSUERS"` - PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"` - SetBasicAuth bool `flag:"set-basic-auth" cfg:"set_basic_auth" env:"OAUTH2_PROXY_SET_BASIC_AUTH"` - PreferEmailToUser bool `flag:"prefer-email-to-user" cfg:"prefer_email_to_user" env:"OAUTH2_PROXY_PREFER_EMAIL_TO_USER"` - BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"` - PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"` - PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header" env:"OAUTH2_PROXY_PASS_HOST_HEADER"` - SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button" env:"OAUTH2_PROXY_SKIP_PROVIDER_BUTTON"` - PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers" env:"OAUTH2_PROXY_PASS_USER_HEADERS"` - SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify" env:"OAUTH2_PROXY_SSL_INSECURE_SKIP_VERIFY"` - SSLUpstreamInsecureSkipVerify bool `flag:"ssl-upstream-insecure-skip-verify" cfg:"ssl_upstream_insecure_skip_verify" env:"OAUTH2_PROXY_SSL_UPSTREAM_INSECURE_SKIP_VERIFY"` - SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest" env:"OAUTH2_PROXY_SET_XAUTHREQUEST"` - SetAuthorization bool `flag:"set-authorization-header" cfg:"set_authorization_header" env:"OAUTH2_PROXY_SET_AUTHORIZATION_HEADER"` - PassAuthorization bool `flag:"pass-authorization-header" cfg:"pass_authorization_header" env:"OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER"` - SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight" env:"OAUTH2_PROXY_SKIP_AUTH_PREFLIGHT"` - FlushInterval time.Duration `flag:"flush-interval" cfg:"flush_interval" env:"OAUTH2_PROXY_FLUSH_INTERVAL"` - - // These options allow for other providers besides Google, with - // potential overrides. - Provider string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"` - ProviderName string `flag:"provider-display-name" cfg:"provider_display_name" env:"OAUTH2_PROXY_PROVIDER_DISPLAY_NAME"` - OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url" env:"OAUTH2_PROXY_OIDC_ISSUER_URL"` - InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email" env:"OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL"` - InsecureOIDCSkipIssuerVerification bool `flag:"insecure-oidc-skip-issuer-verification" cfg:"insecure_oidc_skip_issuer_verification" env:"OAUTH2_PROXY_INSECURE_OIDC_SKIP_ISSUER_VERIFICATION"` - SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_PROXY_SKIP_OIDC_DISCOVERY"` - OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_PROXY_OIDC_JWKS_URL"` - LoginURL string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"` - RedeemURL string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"` - ProfileURL string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"` - ProtectedResource string `flag:"resource" cfg:"resource" env:"OAUTH2_PROXY_RESOURCE"` - ValidateURL string `flag:"validate-url" cfg:"validate_url" env:"OAUTH2_PROXY_VALIDATE_URL"` - Scope string `flag:"scope" cfg:"scope" env:"OAUTH2_PROXY_SCOPE"` - Prompt string `flag:"prompt" cfg:"prompt" env:"OAUTH2_PROXY_PROMPT"` - ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"` // Deprecated by OIDC 1.0 - UserIDClaim string `flag:"user-id-claim" cfg:"user_id_claim" env:"OAUTH2_PROXY_USER_ID_CLAIM"` - - // Configuration values for logging - LoggingFilename string `flag:"logging-filename" cfg:"logging_filename" env:"OAUTH2_PROXY_LOGGING_FILENAME"` - LoggingMaxSize int `flag:"logging-max-size" cfg:"logging_max_size" env:"OAUTH2_PROXY_LOGGING_MAX_SIZE"` - LoggingMaxAge int `flag:"logging-max-age" cfg:"logging_max_age" env:"OAUTH2_PROXY_LOGGING_MAX_AGE"` - LoggingMaxBackups int `flag:"logging-max-backups" cfg:"logging_max_backups" env:"OAUTH2_PROXY_LOGGING_MAX_BACKUPS"` - LoggingLocalTime bool `flag:"logging-local-time" cfg:"logging_local_time" env:"OAUTH2_PROXY_LOGGING_LOCAL_TIME"` - LoggingCompress bool `flag:"logging-compress" cfg:"logging_compress" env:"OAUTH2_PROXY_LOGGING_COMPRESS"` - StandardLogging bool `flag:"standard-logging" cfg:"standard_logging" env:"OAUTH2_PROXY_STANDARD_LOGGING"` - StandardLoggingFormat string `flag:"standard-logging-format" cfg:"standard_logging_format" env:"OAUTH2_PROXY_STANDARD_LOGGING_FORMAT"` - RequestLogging bool `flag:"request-logging" cfg:"request_logging" env:"OAUTH2_PROXY_REQUEST_LOGGING"` - RequestLoggingFormat string `flag:"request-logging-format" cfg:"request_logging_format" env:"OAUTH2_PROXY_REQUEST_LOGGING_FORMAT"` - ExcludeLoggingPaths string `flag:"exclude-logging-paths" cfg:"exclude_logging_paths" env:"OAUTH2_PROXY_EXCLUDE_LOGGING_PATHS"` - SilencePingLogging bool `flag:"silence-ping-logging" cfg:"silence_ping_logging" env:"OAUTH2_PROXY_SILENCE_PING_LOGGING"` - AuthLogging bool `flag:"auth-logging" cfg:"auth_logging" env:"OAUTH2_PROXY_LOGGING_AUTH_LOGGING"` - AuthLoggingFormat string `flag:"auth-logging-format" cfg:"auth_logging_format" env:"OAUTH2_PROXY_AUTH_LOGGING_FORMAT"` - SignatureKey string `flag:"signature-key" cfg:"signature_key" env:"OAUTH2_PROXY_SIGNATURE_KEY"` - AcrValues string `flag:"acr-values" cfg:"acr_values" env:"OAUTH2_PROXY_ACR_VALUES"` - JWTKey string `flag:"jwt-key" cfg:"jwt_key" env:"OAUTH2_PROXY_JWT_KEY"` - JWTKeyFile string `flag:"jwt-key-file" cfg:"jwt_key_file" env:"OAUTH2_PROXY_JWT_KEY_FILE"` - PubJWKURL string `flag:"pubjwk-url" cfg:"pubjwk_url" env:"OAUTH2_PROXY_PUBJWK_URL"` - GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks" env:"OAUTH2_PROXY_GCP_HEALTHCHECKS"` - - // internal values that are set after config validation - redirectURL *url.URL - proxyURLs []*url.URL - compiledRegex []*regexp.Regexp - provider providers.Provider - sessionStore sessionsapi.SessionStore - signatureData *SignatureData - oidcVerifier *oidc.IDTokenVerifier - jwtBearerVerifiers []*oidc.IDTokenVerifier - realClientIPParser realClientIPParser -} - -// SignatureData holds hmacauth signature hash and key -type SignatureData struct { - hash crypto.Hash - key string -} - -// NewOptions constructs a new Options with defaulted values -func NewOptions() *Options { - return &Options{ - ProxyPrefix: "/oauth2", - PingPath: "/ping", - ProxyWebSockets: true, - HTTPAddress: "127.0.0.1:4180", - HTTPSAddress: ":443", - ForceHTTPS: false, - DisplayHtpasswdForm: true, - Cookie: options.CookieOptions{ - Name: "_oauth2_proxy", - Secure: true, - HTTPOnly: true, - Expire: time.Duration(168) * time.Hour, - Refresh: time.Duration(0), - }, - Session: options.SessionOptions{ - Type: "cookie", - }, - SetXAuthRequest: false, - SkipAuthPreflight: false, - PassBasicAuth: true, - SetBasicAuth: false, - PassUserHeaders: true, - PassAccessToken: false, - PassHostHeader: true, - SetAuthorization: false, - PassAuthorization: false, - PreferEmailToUser: false, - Prompt: "", // Change to "login" when ApprovalPrompt officially deprecated - ApprovalPrompt: "force", - UserIDClaim: "email", - InsecureOIDCAllowUnverifiedEmail: false, - SkipOIDCDiscovery: false, - LoggingFilename: "", - LoggingMaxSize: 100, - LoggingMaxAge: 7, - LoggingMaxBackups: 0, - LoggingLocalTime: true, - LoggingCompress: false, - ExcludeLoggingPaths: "", - SilencePingLogging: false, - StandardLogging: true, - StandardLoggingFormat: logger.DefaultStandardLoggingFormat, - RequestLogging: true, - RequestLoggingFormat: logger.DefaultRequestLoggingFormat, - AuthLogging: true, - AuthLoggingFormat: logger.DefaultAuthLoggingFormat, - } -} - -// jwtIssuer hold parsed JWT issuer info that's used to construct a verifier. -type jwtIssuer struct { - issuerURI string - audience string -} - -func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) { - parsed, err := url.Parse(toParse) - if err != nil { - return nil, append(msgs, fmt.Sprintf( - "error parsing %s-url=%q %s", urltype, toParse, err)) - } - return parsed, msgs -} - // Validate checks that required options are set and validates those that they // are of the correct format -func (o *Options) Validate() error { +func Validate(o *options.Options) error { if o.SSLInsecureSkipVerify { // TODO: Accept a certificate bundle. insecureTransport := &http.Transport{ @@ -234,7 +46,7 @@ func (o *Options) Validate() error { msgs = append(msgs, "missing setting: client-id") } // login.gov uses a signed JWT to authenticate, not a client-secret - if o.Provider != "login.gov" { + if o.ProviderType != "login.gov" { if o.ClientSecret == "" && o.ClientSecretFile == "" { msgs = append(msgs, "missing setting: client-secret or client-secret-file") } @@ -311,20 +123,20 @@ func (o *Options) Validate() error { msgs = append(msgs, "missing setting: oidc-jwks-url") } keySet := oidc.NewRemoteKeySet(ctx, o.OIDCJwksURL) - o.oidcVerifier = oidc.NewVerifier(o.OIDCIssuerURL, keySet, &oidc.Config{ + o.SetOIDCVerifier(oidc.NewVerifier(o.OIDCIssuerURL, keySet, &oidc.Config{ ClientID: o.ClientID, SkipIssuerCheck: o.InsecureOIDCSkipIssuerVerification, - }) + })) } else { // Configure discoverable provider data. provider, err := oidc.NewProvider(ctx, o.OIDCIssuerURL) if err != nil { return err } - o.oidcVerifier = provider.Verifier(&oidc.Config{ + o.SetOIDCVerifier(provider.Verifier(&oidc.Config{ ClientID: o.ClientID, SkipIssuerCheck: o.InsecureOIDCSkipIssuerVerification, - }) + })) o.LoginURL = provider.Endpoint().AuthURL o.RedeemURL = provider.Endpoint().TokenURL @@ -340,8 +152,8 @@ func (o *Options) Validate() error { if o.SkipJwtBearerTokens { // If we are using an oidc provider, go ahead and add that provider to the list - if o.oidcVerifier != nil { - o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, o.oidcVerifier) + if o.GetOIDCVerifier() != nil { + o.SetJWTBearerVerifiers(append(o.GetJWTBearerVerifiers(), o.GetOIDCVerifier())) } // Configure extra issuers if len(o.ExtraJwtIssuers) > 0 { @@ -352,12 +164,14 @@ func (o *Options) Validate() error { if err != nil { msgs = append(msgs, fmt.Sprintf("error building verifiers: %s", err)) } - o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, verifier) + o.SetJWTBearerVerifiers(append(o.GetJWTBearerVerifiers(), verifier)) } } } - o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs) + var redirectURL *url.URL + redirectURL, msgs = parseURL(o.RawRedirectURL, "redirect", msgs) + o.SetRedirectURL(redirectURL) for _, u := range o.Upstreams { upstreamURL, err := url.Parse(u) @@ -367,7 +181,7 @@ func (o *Options) Validate() error { if upstreamURL.Path == "" { upstreamURL.Path = "/" } - o.proxyURLs = append(o.proxyURLs, upstreamURL) + o.SetProxyURLs(append(o.GetProxyURLs(), upstreamURL)) } } @@ -377,7 +191,7 @@ func (o *Options) Validate() error { msgs = append(msgs, fmt.Sprintf("error compiling regex=%q %s", u, err)) continue } - o.compiledRegex = append(o.compiledRegex, compiledRegex) + o.SetCompiledRegex(append(o.GetCompiledRegex(), compiledRegex)) } msgs = parseProviderInfo(o, msgs) @@ -418,7 +232,7 @@ func (o *Options) Validate() error { if err != nil { msgs = append(msgs, fmt.Sprintf("error initialising session storage: %v", err)) } else { - o.sessionStore = sessionStore + o.SetSessionStore(sessionStore) } if o.Cookie.Refresh >= o.Cookie.Expire { @@ -458,10 +272,11 @@ func (o *Options) Validate() error { msgs = setupLogger(o, msgs) if o.ReverseProxy { - o.realClientIPParser, err = getRealClientIPParser(o.RealClientIPHeader) + parser, err := logging.GetRealClientIPParser(o.RealClientIPHeader) if err != nil { msgs = append(msgs, fmt.Sprintf("real_client_ip_header (%s) not accepted parameter value: %v", o.RealClientIPHeader, err)) } + o.SetRealClientIPParser(parser) } if len(msgs) != 0 { @@ -471,7 +286,7 @@ func (o *Options) Validate() error { return nil } -func parseProviderInfo(o *Options, msgs []string) []string { +func parseProviderInfo(o *options.Options, msgs []string) []string { p := &providers.ProviderData{ Scope: o.Scope, ClientID: o.ClientID, @@ -487,8 +302,8 @@ func parseProviderInfo(o *Options, msgs []string) []string { p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs) p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs) - o.provider = providers.New(o.Provider, p) - switch p := o.provider.(type) { + o.SetProvider(providers.New(o.ProviderType, p)) + switch p := o.GetProvider().(type) { case *providers.AzureProvider: p.Configure(o.AzureTenant) case *providers.GitHubProvider: @@ -511,18 +326,18 @@ func parseProviderInfo(o *Options, msgs []string) []string { case *providers.OIDCProvider: p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail p.UserIDClaim = o.UserIDClaim - if o.oidcVerifier == nil { + if o.GetOIDCVerifier() == nil { msgs = append(msgs, "oidc provider requires an oidc issuer URL") } else { - p.Verifier = o.oidcVerifier + p.Verifier = o.GetOIDCVerifier() } case *providers.GitLabProvider: p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail p.Group = o.GitLabGroup p.EmailDomains = o.EmailDomains - if o.oidcVerifier != nil { - p.Verifier = o.oidcVerifier + if o.GetOIDCVerifier() != nil { + p.Verifier = o.GetOIDCVerifier() } else { // Initialize with default verifier for gitlab.com ctx := context.Background() @@ -573,7 +388,7 @@ func parseProviderInfo(o *Options, msgs []string) []string { return msgs } -func parseSignatureKey(o *Options, msgs []string) []string { +func parseSignatureKey(o *options.Options, msgs []string) []string { if o.SignatureKey == "" { return msgs } @@ -591,7 +406,7 @@ func parseSignatureKey(o *Options, msgs []string) []string { return append(msgs, "unsupported signature hash algorithm: "+ o.SignatureKey) } - o.signatureData = &SignatureData{hash: hash, key: secretKey} + o.SetSignatureData(&options.SignatureData{Hash: hash, Key: secretKey}) return msgs } @@ -634,7 +449,7 @@ func newVerifierFromJwtIssuer(jwtIssuer jwtIssuer) (*oidc.IDTokenVerifier, error return verifier, nil } -func validateCookieName(o *Options, msgs []string) []string { +func validateCookieName(o *options.Options, msgs []string) []string { cookie := &http.Cookie{Name: o.Cookie.Name} if cookie.String() == "" { return append(msgs, fmt.Sprintf("invalid cookie name: %q", o.Cookie.Name)) @@ -642,7 +457,7 @@ func validateCookieName(o *Options, msgs []string) []string { return msgs } -func setupLogger(o *Options, msgs []string) []string { +func setupLogger(o *options.Options, msgs []string) []string { // Setup the log file if len(o.LoggingFilename) > 0 { // Validate that the file/dir can be written @@ -681,7 +496,7 @@ func setupLogger(o *Options, msgs []string) []string { logger.SetAuthTemplate(o.AuthLoggingFormat) logger.SetReqTemplate(o.RequestLoggingFormat) logger.SetGetClientFunc(func(r *http.Request) string { - return getClientString(o.realClientIPParser, r, false) + return logging.GetClientString(o.GetRealClientIPParser(), r, false) }) excludePaths := make([]string, 0) @@ -698,3 +513,18 @@ func setupLogger(o *Options, msgs []string) []string { return msgs } + +// jwtIssuer hold parsed JWT issuer info that's used to construct a verifier. +type jwtIssuer struct { + issuerURI string + audience string +} + +func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) { + parsed, err := url.Parse(toParse) + if err != nil { + return nil, append(msgs, fmt.Sprintf( + "error parsing %s-url=%q %s", urltype, toParse, err)) + } + return parsed, msgs +} diff --git a/options_test.go b/pkg/validation/options_test.go similarity index 80% rename from options_test.go rename to pkg/validation/options_test.go index c6c2d3b5..c7449172 100644 --- a/options_test.go +++ b/pkg/validation/options_test.go @@ -1,4 +1,4 @@ -package main +package validation import ( "crypto" @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" "github.com/stretchr/testify/assert" ) @@ -19,8 +20,8 @@ const ( clientSecret = "xyzzyplugh" ) -func testOptions() *Options { - o := NewOptions() +func testOptions() *options.Options { + o := options.NewOptions() o.Upstreams = append(o.Upstreams, "http://127.0.0.1:8080/") o.Cookie.Secret = cookieSecret o.ClientID = clientID @@ -37,9 +38,9 @@ func errorMsg(msgs []string) string { } func TestNewOptions(t *testing.T) { - o := NewOptions() + o := options.NewOptions() o.EmailDomains = []string{"*"} - err := o.Validate() + err := Validate(o) assert.NotEqual(t, nil, err) expected := errorMsg([]string{ @@ -50,15 +51,15 @@ func TestNewOptions(t *testing.T) { } func TestClientSecretFileOptionFails(t *testing.T) { - o := NewOptions() + o := options.NewOptions() o.Cookie.Secret = cookieSecret o.ClientID = clientID o.ClientSecretFile = clientSecret o.EmailDomains = []string{"*"} - err := o.Validate() + err := Validate(o) assert.NotEqual(t, nil, err) - p := o.provider.Data() + p := o.GetProvider().Data() assert.Equal(t, clientSecret, p.ClientSecretFile) assert.Equal(t, "", p.ClientSecret) @@ -80,15 +81,15 @@ func TestClientSecretFileOption(t *testing.T) { clientSecretFileName := f.Name() defer os.Remove(clientSecretFileName) - o := NewOptions() + o := options.NewOptions() o.Cookie.Secret = cookieSecret o.ClientID = clientID o.ClientSecretFile = clientSecretFileName o.EmailDomains = []string{"*"} - err = o.Validate() + err = Validate(o) assert.Equal(t, nil, err) - p := o.provider.Data() + p := o.GetProvider().Data() assert.Equal(t, clientSecretFileName, p.ClientSecretFile) assert.Equal(t, "", p.ClientSecret) @@ -100,7 +101,7 @@ func TestClientSecretFileOption(t *testing.T) { func TestGoogleGroupOptions(t *testing.T) { o := testOptions() o.GoogleGroups = []string{"googlegroup"} - err := o.Validate() + err := Validate(o) assert.NotEqual(t, nil, err) expected := errorMsg([]string{ @@ -114,7 +115,7 @@ func TestGoogleGroupInvalidFile(t *testing.T) { o.GoogleGroups = []string{"test_group"} o.GoogleAdminEmail = "admin@example.com" o.GoogleServiceAccountJSON = "file_doesnt_exist.json" - err := o.Validate() + err := Validate(o) assert.NotEqual(t, nil, err) expected := errorMsg([]string{ @@ -125,36 +126,36 @@ func TestGoogleGroupInvalidFile(t *testing.T) { func TestInitializedOptions(t *testing.T) { o := testOptions() - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) } // Note that it's not worth testing nonparseable URLs, since url.Parse() // seems to parse damn near anything. func TestRedirectURL(t *testing.T) { o := testOptions() - o.RedirectURL = "https://myhost.com/oauth2/callback" - assert.Equal(t, nil, o.Validate()) + o.RawRedirectURL = "https://myhost.com/oauth2/callback" + assert.Equal(t, nil, Validate(o)) expected := &url.URL{ Scheme: "https", Host: "myhost.com", Path: "/oauth2/callback"} - assert.Equal(t, expected, o.redirectURL) + assert.Equal(t, expected, o.GetRedirectURL()) } func TestProxyURLs(t *testing.T) { o := testOptions() o.Upstreams = append(o.Upstreams, "http://127.0.0.1:8081") - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) expected := []*url.URL{ {Scheme: "http", Host: "127.0.0.1:8080", Path: "/"}, // note the '/' was added {Scheme: "http", Host: "127.0.0.1:8081", Path: "/"}, } - assert.Equal(t, expected, o.proxyURLs) + assert.Equal(t, expected, o.GetProxyURLs()) } func TestProxyURLsError(t *testing.T) { o := testOptions() o.Upstreams = append(o.Upstreams, "127.0.0.1:8081") - err := o.Validate() + err := Validate(o) assert.NotEqual(t, nil, err) assert.Contains(t, err.Error(), "error parsing upstream") } @@ -163,9 +164,9 @@ func TestCompiledRegex(t *testing.T) { o := testOptions() regexps := []string{"/foo/.*", "/ba[rz]/quux"} o.SkipAuthRegex = regexps - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) actual := make([]string, 0) - for _, regex := range o.compiledRegex { + for _, regex := range o.GetCompiledRegex() { actual = append(actual, regex.String()) } assert.Equal(t, regexps, actual) @@ -174,7 +175,7 @@ func TestCompiledRegex(t *testing.T) { func TestCompiledRegexError(t *testing.T) { o := testOptions() o.SkipAuthRegex = []string{"(foobaz", "barquux)"} - err := o.Validate() + err := Validate(o) assert.NotEqual(t, nil, err) expected := errorMsg([]string{ @@ -185,7 +186,7 @@ func TestCompiledRegexError(t *testing.T) { assert.Equal(t, expected, err.Error()) o.SkipAuthRegex = []string{"foobaz", "barquux)"} - err = o.Validate() + err = Validate(o) assert.NotEqual(t, nil, err) expected = errorMsg([]string{ @@ -196,8 +197,8 @@ func TestCompiledRegexError(t *testing.T) { func TestDefaultProviderApiSettings(t *testing.T) { o := testOptions() - assert.Equal(t, nil, o.Validate()) - p := o.provider.Data() + assert.Equal(t, nil, Validate(o)) + p := o.GetProvider().Data() assert.Equal(t, "https://accounts.google.com/o/oauth2/auth?access_type=offline", p.LoginURL.String()) assert.Equal(t, "https://www.googleapis.com/oauth2/v3/token", @@ -208,76 +209,76 @@ func TestDefaultProviderApiSettings(t *testing.T) { func TestPassAccessTokenRequiresSpecificCookieSecretLengths(t *testing.T) { o := testOptions() - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) assert.Equal(t, false, o.PassAccessToken) o.PassAccessToken = true o.Cookie.Secret = "cookie of invalid length-" - assert.NotEqual(t, nil, o.Validate()) + assert.NotEqual(t, nil, Validate(o)) o.PassAccessToken = false o.Cookie.Refresh = time.Duration(24) * time.Hour - assert.NotEqual(t, nil, o.Validate()) + assert.NotEqual(t, nil, Validate(o)) o.Cookie.Secret = "16 bytes AES-128" - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) o.Cookie.Secret = "24 byte secret AES-192--" - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) o.Cookie.Secret = "32 byte secret for AES-256------" - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) } func TestCookieRefreshMustBeLessThanCookieExpire(t *testing.T) { o := testOptions() - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) o.Cookie.Secret = "0123456789abcdef" o.Cookie.Refresh = o.Cookie.Expire - assert.NotEqual(t, nil, o.Validate()) + assert.NotEqual(t, nil, Validate(o)) o.Cookie.Refresh -= time.Duration(1) - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) } func TestBase64CookieSecret(t *testing.T) { o := testOptions() - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) // 32 byte, base64 (urlsafe) encoded key o.Cookie.Secret = "yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ=" - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) // 32 byte, base64 (urlsafe) encoded key, w/o padding o.Cookie.Secret = "yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ" - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) // 24 byte, base64 (urlsafe) encoded key o.Cookie.Secret = "Kp33Gj-GQmYtz4zZUyUDdqQKx5_Hgkv3" - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) // 16 byte, base64 (urlsafe) encoded key o.Cookie.Secret = "LFEqZYvYUwKwzn0tEuTpLA==" - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) // 16 byte, base64 (urlsafe) encoded key, w/o padding o.Cookie.Secret = "LFEqZYvYUwKwzn0tEuTpLA" - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) } func TestValidateSignatureKey(t *testing.T) { o := testOptions() o.SignatureKey = "sha1:secret" - assert.Equal(t, nil, o.Validate()) - assert.Equal(t, o.signatureData.hash, crypto.SHA1) - assert.Equal(t, o.signatureData.key, "secret") + assert.Equal(t, nil, Validate(o)) + assert.Equal(t, o.GetSignatureData().Hash, crypto.SHA1) + assert.Equal(t, o.GetSignatureData().Key, "secret") } func TestValidateSignatureKeyInvalidSpec(t *testing.T) { o := testOptions() o.SignatureKey = "invalid spec" - err := o.Validate() + err := Validate(o) assert.Equal(t, err.Error(), "invalid configuration:\n"+ " invalid signature hash:key spec: "+o.SignatureKey) } @@ -285,7 +286,7 @@ func TestValidateSignatureKeyInvalidSpec(t *testing.T) { func TestValidateSignatureKeyUnsupportedAlgorithm(t *testing.T) { o := testOptions() o.SignatureKey = "unsupported:default secret" - err := o.Validate() + err := Validate(o) assert.Equal(t, err.Error(), "invalid configuration:\n"+ " unsupported signature hash algorithm: "+o.SignatureKey) } @@ -293,24 +294,24 @@ func TestValidateSignatureKeyUnsupportedAlgorithm(t *testing.T) { func TestValidateCookie(t *testing.T) { o := testOptions() o.Cookie.Name = "_valid_cookie_name" - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) } func TestValidateCookieBadName(t *testing.T) { o := testOptions() o.Cookie.Name = "_bad_cookie_name{}" - err := o.Validate() + err := Validate(o) assert.Equal(t, err.Error(), "invalid configuration:\n"+ fmt.Sprintf(" invalid cookie name: %q", o.Cookie.Name)) } func TestSkipOIDCDiscovery(t *testing.T) { o := testOptions() - o.Provider = "oidc" + o.ProviderType = "oidc" o.OIDCIssuerURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/v2.0/" o.SkipOIDCDiscovery = true - err := o.Validate() + err := Validate(o) assert.Equal(t, "invalid configuration:\n"+ " missing setting: login-url\n missing setting: redeem-url\n missing setting: oidc-jwks-url", err.Error()) @@ -318,54 +319,50 @@ func TestSkipOIDCDiscovery(t *testing.T) { o.RedeemURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_sign_in" o.OIDCJwksURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/discovery/v2.0/keys" - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) } func TestGCPHealthcheck(t *testing.T) { o := testOptions() o.GCPHealthChecks = true - assert.Equal(t, nil, o.Validate()) + assert.Equal(t, nil, Validate(o)) } func TestRealClientIPHeader(t *testing.T) { - var o *Options - var err error - var expected string - // Ensure nil if ReverseProxy not set. - o = testOptions() + o := testOptions() o.RealClientIPHeader = "X-Real-IP" - assert.Equal(t, nil, o.Validate()) - assert.Nil(t, o.realClientIPParser) + 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" - assert.Equal(t, nil, o.Validate()) - assert.NotNil(t, o.realClientIPParser) + 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" - err = o.Validate() + err := Validate(o) assert.NotEqual(t, nil, err) - expected = errorMsg([]string{ + expected := errorMsg([]string{ "real_client_ip_header (Forwarded) not accepted parameter value: the http header key (Forwarded) is either invalid or unsupported", }) assert.Equal(t, expected, err.Error()) - assert.Nil(t, o.realClientIPParser) + assert.Nil(t, o.GetRealClientIPParser()) // Ensure invalid header format produces an error. o = testOptions() o.ReverseProxy = true o.RealClientIPHeader = "!934invalidheader-23:" - err = o.Validate() + err = Validate(o) assert.NotEqual(t, nil, err) expected = errorMsg([]string{ "real_client_ip_header (!934invalidheader-23:) not accepted parameter value: the http header key (!934invalidheader-23:) is either invalid or unsupported", }) assert.Equal(t, expected, err.Error()) - assert.Nil(t, o.realClientIPParser) + assert.Nil(t, o.GetRealClientIPParser()) }