Repository: oauth2-proxy/oauth2-proxy
Pull request #489 (2020-04-13 14:01:35)

Move Options and Validation to packages
<!--- Provide a general summary of your changes in the Title above -->

## Description

<!--- Describe your changes in detail -->

Move the Options structure to the Options package and the option validation to the Validation package.

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

This allows us to "Load" the Options structure and therefore we can validate that all the struct tags are set correctly

## How Has This Been Tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->

Shouldn't be a change in functionality, just refactor

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My change requires a change to the documentation or CHANGELOG.
- [ ] I have updated the documentation/CHANGELOG accordingly.
- [x] I have created a feature (non-master) branch for my PR.

This is currently based off of #484, #486, #487 to reduce conflicts when this would merge 


Patch:
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/main.go b/main.go
index 4507d8ee..16a34cd7 100644
--- a/main.go
+++ b/main.go
@@ -13,134 +13,16 @@ import (
 
 	"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
 	"github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
-	"github.com/spf13/pflag"
+	"github.com/oauth2-proxy/oauth2-proxy/pkg/validation"
 )
 
 func main() {
 	logger.SetFlags(logger.Lshortfile)
-	flagSet := pflag.NewFlagSet("oauth2-proxy", pflag.ExitOnError)
+	flagSet := options.NewFlagSet()
 
 	config := flagSet.String("config", "", "path to config file")
 	showVersion := flagSet.Bool("version", false, "print version string")
 
-	flagSet.String("http-address", "127.0.0.1:4180", "[http://]<addr>:<port> or unix://<path> to listen on for HTTP clients")
-	flagSet.String("https-address", ":443", "<addr>:<port> to listen on for HTTPS clients")
-	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.Bool("force-https", false, "force HTTPS redirect for HTTP requests")
-	flagSet.String("tls-cert-file", "", "path to certificate file")
-	flagSet.String("tls-key-file", "", "path to private key file")
-	flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
-	flagSet.Bool("set-xauthrequest", false, "set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)")
-	flagSet.StringSlice("upstream", []string{}, "the http url(s) of the upstream endpoint, file:// paths for static files or static://<status_code> for static response. Routing is based on the path")
-	flagSet.Bool("pass-basic-auth", true, "pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream")
-	flagSet.Bool("set-basic-auth", false, "set HTTP Basic Auth information in response (useful in Nginx auth_request mode)")
-	flagSet.Bool("prefer-email-to-user", false, "Prefer to use the Email address as the Username when passing information to upstream. Will only use Username if Email is unavailable, eg. htaccess authentication. Used in conjunction with -pass-basic-auth and -pass-user-headers")
-	flagSet.Bool("pass-user-headers", true, "pass X-Forwarded-User and X-Forwarded-Email information to upstream")
-	flagSet.String("basic-auth-password", "", "the password to set when passing the HTTP Basic Auth header")
-	flagSet.Bool("pass-access-token", false, "pass OAuth access_token to upstream via X-Forwarded-Access-Token header")
-	flagSet.Bool("pass-host-header", true, "pass the request Host Header to upstream")
-	flagSet.Bool("pass-authorization-header", false, "pass the Authorization Header to upstream")
-	flagSet.Bool("set-authorization-header", false, "set Authorization response headers (useful in Nginx auth_request mode)")
-	flagSet.StringSlice("skip-auth-regex", []string{}, "bypass authentication for requests path's that match (may be given multiple times)")
-	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("ssl-upstream-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS upstreams")
-	flagSet.Duration("flush-interval", time.Duration(1)*time.Second, "period between response flushing when streaming responses")
-	flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens (default false)")
-	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 . to allow subdomains (eg .example.com)")
-	flagSet.String("keycloak-group", "", "restrict login to members of this group.")
-	flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
-	flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
-	flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")
-	flagSet.String("github-org", "", "restrict logins to members of this organisation")
-	flagSet.String("github-team", "", "restrict logins to members of this team")
-	flagSet.String("github-repo", "", "restrict logins to collaborators of this repository")
-	flagSet.String("github-token", "", "the token to use when verifying repository collaborators (must have push access to the repository)")
-	flagSet.String("gitlab-group", "", "restrict logins to members of this group")
-	flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).")
-	flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls")
-	flagSet.String("google-service-account-json", "", "the path to the service account json credentials")
-	flagSet.String("client-id", "", "the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
-	flagSet.String("client-secret", "", "the OAuth Client Secret")
-	flagSet.String("client-secret-file", "", "the file with OAuth Client Secret")
-	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 -s\" for SHA encryption or \"htpasswd -B\" for bcrypt encryption")
-	flagSet.Bool("display-htpasswd-form", true, "display username / password login form if an htpasswd file is provided")
-	flagSet.String("custom-templates-dir", "", "path to custom html templates")
-	flagSet.String("banner", "", "custom banner string. Use \"-\" to disable default banner.")
-	flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.")
-	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.Bool("proxy-websockets", true, "enables WebSocket proxying")
-
-	flagSet.String("cookie-name", "_oauth2_proxy", "the name of the cookie that the oauth_proxy creates")
-	flagSet.String("cookie-secret", "", "the seed string for secure cookies (optionally base64 encoded)")
-	flagSet.StringSlice("cookie-domain", []string{}, "Optional cookie domains to force cookies to (ie: `.yourcompany.com`). The longest domain matching the request's host will be used (or the shortest cookie domain if there is no match).")
-	flagSet.String("cookie-path", "/", "an optional cookie path to force cookies to (ie: /poc/)*")
-	flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie")
-	flagSet.Duration("cookie-refresh", time.Duration(0), "refresh the cookie after this duration; 0 to disable")
-	flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag")
-	flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag")
-	flagSet.String("cookie-samesite", "", "set SameSite cookie attribute (ie: \"lax\", \"strict\", \"none\", or \"\"). ")
-
-	flagSet.String("session-store-type", "cookie", "the session storage provider to use")
-	flagSet.String("redis-connection-url", "", "URL of redis server for redis session storage (eg: redis://HOST[:PORT])")
-	flagSet.Bool("redis-use-sentinel", false, "Connect to redis via sentinels. Must set --redis-sentinel-master-name and --redis-sentinel-connection-urls to use this feature")
-	flagSet.String("redis-sentinel-master-name", "", "Redis sentinel master name. Used in conjunction with --redis-use-sentinel")
-	flagSet.String("redis-ca-path", "", "Redis custom CA path")
-	flagSet.Bool("redis-insecure-skip-tls-verify", false, "Use insecure TLS connection to redis")
-	flagSet.StringSlice("redis-sentinel-connection-urls", []string{}, "List of Redis sentinel connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-sentinel")
-	flagSet.Bool("redis-use-cluster", false, "Connect to redis cluster. Must set --redis-cluster-connection-urls to use this feature")
-	flagSet.StringSlice("redis-cluster-connection-urls", []string{}, "List of Redis cluster connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-cluster")
-
-	flagSet.String("logging-filename", "", "File to log requests to, empty for stdout")
-	flagSet.Int("logging-max-size", 100, "Maximum size in megabytes of the log file before rotation")
-	flagSet.Int("logging-max-age", 7, "Maximum number of days to retain old log files")
-	flagSet.Int("logging-max-backups", 0, "Maximum number of old log files to retain; 0 to disable")
-	flagSet.Bool("logging-local-time", true, "If the time in log files and backup filenames are local or UTC time")
-	flagSet.Bool("logging-compress", false, "Should rotated log files be compressed using gzip")
-
-	flagSet.Bool("standard-logging", true, "Log standard runtime information")
-	flagSet.String("standard-logging-format", logger.DefaultStandardLoggingFormat, "Template for standard log lines")
-
-	flagSet.Bool("request-logging", true, "Log HTTP requests")
-	flagSet.String("request-logging-format", logger.DefaultRequestLoggingFormat, "Template for HTTP request log lines")
-	flagSet.String("exclude-logging-paths", "", "Exclude logging requests to paths (eg: '/path1,/path2,/path3')")
-	flagSet.Bool("silence-ping-logging", false, "Disable logging of requests to ping endpoint")
-
-	flagSet.Bool("auth-logging", true, "Log authentication attempts")
-	flagSet.String("auth-logging-format", logger.DefaultAuthLoggingFormat, "Template for authentication log lines")
-
-	flagSet.String("provider", "google", "OAuth provider")
-	flagSet.String("provider-display-name", "", "Provider display name")
-	flagSet.String("oidc-issuer-url", "", "OpenID Connect issuer URL (ie: https://accounts.google.com)")
-	flagSet.Bool("insecure-oidc-allow-unverified-email", false, "Don't fail if an email address in an id_token is not verified")
-	flagSet.Bool("insecure-oidc-skip-issuer-verification", false, "Do not verify if issuer matches OIDC discovery URL")
-	flagSet.Bool("skip-oidc-discovery", false, "Skip OIDC discovery and use manually supplied Endpoints")
-	flagSet.String("oidc-jwks-url", "", "OpenID Connect JWKS URL (ie: https://www.googleapis.com/oauth2/v3/certs)")
-	flagSet.String("login-url", "", "Authentication endpoint")
-	flagSet.String("redeem-url", "", "Token redemption endpoint")
-	flagSet.String("profile-url", "", "Profile access endpoint")
-	flagSet.String("resource", "", "The resource that is protected (Azure AD only)")
-	flagSet.String("validate-url", "", "Access token validation endpoint")
-	flagSet.String("scope", "", "OAuth scope specification")
-	flagSet.String("prompt", "", "OIDC prompt")
-	flagSet.String("approval-prompt", "force", "OAuth approval_prompt")
-
-	flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)")
-	flagSet.String("acr-values", "", "acr values string:  optional")
-	flagSet.String("jwt-key", "", "private key in PEM format used to sign JWT, so that you can say something like -jwt-key=\"${OAUTH2_PROXY_JWT_KEY}\": required by login.gov")
-	flagSet.String("jwt-key-file", "", "path to the private key file in PEM format used to sign the JWT so that you can say something like -jwt-key-file=/etc/ssl/private/jwt_signing_key.pem: required by login.gov")
-	flagSet.String("pubjwk-url", "", "JWK pubkey access endpoint: required by login.gov")
-	flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints")
-
-	flagSet.String("user-id-claim", "email", "which claim contains the user ID")
-
 	flagSet.Parse(os.Args[1:])
 
 	if *showVersion {
@@ -148,14 +30,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..c7dc3145 100644
--- a/oauthproxy.go
+++ b/oauthproxy.go
@@ -19,9 +19,12 @@ import (
 
 	"github.com/coreos/go-oidc"
 	"github.com/mbland/hmacauth"
+	ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip"
+	"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"
+	"github.com/oauth2-proxy/oauth2-proxy/pkg/ip"
 	"github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
 	"github.com/oauth2-proxy/oauth2-proxy/providers"
 	"github.com/yhat/wsutil"
@@ -112,7 +115,7 @@ type OAuthProxy struct {
 	jwtBearerVerifiers   []*oidc.IDTokenVerifier
 	compiledRegex        []*regexp.Regexp
 	templates            *template.Template
-	realClientIPParser   realClientIPParser
+	realClientIPParser   ipapi.RealClientIPParser
 	Banner               string
 	Footer               string
 }
@@ -143,7 +146,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 +184,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 +212,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 +255,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 +265,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 +301,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 +763,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 := ip.GetClientString(p.realClientIPParser, req, true)
 
 	// finish the oauth cycle
 	err := req.ParseForm()
@@ -888,7 +891,7 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
 		}
 	}
 
-	remoteAddr := getClientString(p.realClientIPParser, req, true)
+	remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
 	if session == nil {
 		session, err = p.LoadCookiedSession(req)
 		if err != nil {
diff --git a/pkg/apis/ip/interfaces.go b/pkg/apis/ip/interfaces.go
new file mode 100644
index 00000000..02f3937f
--- /dev/null
+++ b/pkg/apis/ip/interfaces.go
@@ -0,0 +1,11 @@
+package ip
+
+import (
+	"net"
+	"net/http"
+)
+
+// 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)
+}
diff --git a/pkg/apis/options/cookie.go b/pkg/apis/options/cookie.go
index 2cb77eb6..71b4fb73 100644
--- a/pkg/apis/options/cookie.go
+++ b/pkg/apis/options/cookie.go
@@ -4,13 +4,13 @@ import "time"
 
 // CookieOptions contains configuration options relating to Cookie configuration
 type CookieOptions struct {
-	Name     string        `flag:"cookie-name" cfg:"cookie_name" env:"OAUTH2_PROXY_COOKIE_NAME"`
-	Secret   string        `flag:"cookie-secret" cfg:"cookie_secret" env:"OAUTH2_PROXY_COOKIE_SECRET"`
-	Domains  []string      `flag:"cookie-domain" cfg:"cookie_domain" env:"OAUTH2_PROXY_COOKIE_DOMAIN"`
-	Path     string        `flag:"cookie-path" cfg:"cookie_path" env:"OAUTH2_PROXY_COOKIE_PATH"`
-	Expire   time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"OAUTH2_PROXY_COOKIE_EXPIRE"`
-	Refresh  time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"OAUTH2_PROXY_COOKIE_REFRESH"`
-	Secure   bool          `flag:"cookie-secure" cfg:"cookie_secure" env:"OAUTH2_PROXY_COOKIE_SECURE"`
-	HTTPOnly bool          `flag:"cookie-httponly" cfg:"cookie_httponly" env:"OAUTH2_PROXY_COOKIE_HTTPONLY"`
-	SameSite string        `flag:"cookie-samesite" cfg:"cookie_samesite" env:"OAUTH2_PROXY_COOKIE_SAMESITE"`
+	Name     string        `flag:"cookie-name" cfg:"cookie_name"`
+	Secret   string        `flag:"cookie-secret" cfg:"cookie_secret"`
+	Domains  []string      `flag:"cookie-domain" cfg:"cookie_domain"`
+	Path     string        `flag:"cookie-path" cfg:"cookie_path"`
+	Expire   time.Duration `flag:"cookie-expire" cfg:"cookie_expire"`
+	Refresh  time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh"`
+	Secure   bool          `flag:"cookie-secure" cfg:"cookie_secure"`
+	HTTPOnly bool          `flag:"cookie-httponly" cfg:"cookie_httponly"`
+	SameSite string        `flag:"cookie-samesite" cfg:"cookie_samesite"`
 }
diff --git a/pkg/apis/options/options.go b/pkg/apis/options/options.go
new file mode 100644
index 00000000..27b0f66c
--- /dev/null
+++ b/pkg/apis/options/options.go
@@ -0,0 +1,340 @@
+package options
+
+import (
+	"crypto"
+	"net/url"
+	"regexp"
+	"time"
+
+	oidc "github.com/coreos/go-oidc"
+	ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip"
+	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"
+	"github.com/spf13/pflag"
+)
+
+// 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"`
+	PingPath           string `flag:"ping-path" cfg:"ping_path"`
+	ProxyWebSockets    bool   `flag:"proxy-websockets" cfg:"proxy_websockets"`
+	HTTPAddress        string `flag:"http-address" cfg:"http_address"`
+	HTTPSAddress       string `flag:"https-address" cfg:"https_address"`
+	ReverseProxy       bool   `flag:"reverse-proxy" cfg:"reverse_proxy"`
+	RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"`
+	ForceHTTPS         bool   `flag:"force-https" cfg:"force_https"`
+	RawRedirectURL     string `flag:"redirect-url" cfg:"redirect_url"`
+	ClientID           string `flag:"client-id" cfg:"client_id"`
+	ClientSecret       string `flag:"client-secret" cfg:"client_secret"`
+	ClientSecretFile   string `flag:"client-secret-file" cfg:"client_secret_file"`
+	TLSCertFile        string `flag:"tls-cert-file" cfg:"tls_cert_file"`
+	TLSKeyFile         string `flag:"tls-key-file" cfg:"tls_key_file"`
+
+	AuthenticatedEmailsFile  string   `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
+	KeycloakGroup            string   `flag:"keycloak-group" cfg:"keycloak_group"`
+	AzureTenant              string   `flag:"azure-tenant" cfg:"azure_tenant"`
+	BitbucketTeam            string   `flag:"bitbucket-team" cfg:"bitbucket_team"`
+	BitbucketRepository      string   `flag:"bitbucket-repository" cfg:"bitbucket_repository"`
+	EmailDomains             []string `flag:"email-domain" cfg:"email_domains"`
+	WhitelistDomains         []string `flag:"whitelist-domain" cfg:"whitelist_domains"`
+	GitHubOrg                string   `flag:"github-org" cfg:"github_org"`
+	GitHubTeam               string   `flag:"github-team" cfg:"github_team"`
+	GitHubRepo               string   `flag:"github-repo" cfg:"github_repo"`
+	GitHubToken              string   `flag:"github-token" cfg:"github_token"`
+	GitLabGroup              string   `flag:"gitlab-group" cfg:"gitlab_group"`
+	GoogleGroups             []string `flag:"google-group" cfg:"google_group"`
+	GoogleAdminEmail         string   `flag:"google-admin-email" cfg:"google_admin_email"`
+	GoogleServiceAccountJSON string   `flag:"google-service-account-json" cfg:"google_service_account_json"`
+	HtpasswdFile             string   `flag:"htpasswd-file" cfg:"htpasswd_file"`
+	DisplayHtpasswdForm      bool     `flag:"display-htpasswd-form" cfg:"display_htpasswd_form"`
+	CustomTemplatesDir       string   `flag:"custom-templates-dir" cfg:"custom_templates_dir"`
+	Banner                   string   `flag:"banner" cfg:"banner"`
+	Footer                   string   `flag:"footer" cfg:"footer"`
+
+	Cookie  CookieOptions  `cfg:",squash"`
+	Session SessionOptions `cfg:",squash"`
+
+	Upstreams                     []string      `flag:"upstream" cfg:"upstreams"`
+	SkipAuthRegex                 []string      `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
+	SkipJwtBearerTokens           bool          `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"`
+	ExtraJwtIssuers               []string      `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers"`
+	PassBasicAuth                 bool          `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
+	SetBasicAuth                  bool          `flag:"set-basic-auth" cfg:"set_basic_auth"`
+	PreferEmailToUser             bool          `flag:"prefer-email-to-user" cfg:"prefer_email_to_user"`
+	BasicAuthPassword             string        `flag:"basic-auth-password" cfg:"basic_auth_password"`
+	PassAccessToken               bool          `flag:"pass-access-token" cfg:"pass_access_token"`
+	PassHostHeader                bool          `flag:"pass-host-header" cfg:"pass_host_header"`
+	SkipProviderButton            bool          `flag:"skip-provider-button" cfg:"skip_provider_button"`
+	PassUserHeaders               bool          `flag:"pass-user-headers" cfg:"pass_user_headers"`
+	SSLInsecureSkipVerify         bool          `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"`
+	SSLUpstreamInsecureSkipVerify bool          `flag:"ssl-upstream-insecure-skip-verify" cfg:"ssl_upstream_insecure_skip_verify"`
+	SetXAuthRequest               bool          `flag:"set-xauthrequest" cfg:"set_xauthrequest"`
+	SetAuthorization              bool          `flag:"set-authorization-header" cfg:"set_authorization_header"`
+	PassAuthorization             bool          `flag:"pass-authorization-header" cfg:"pass_authorization_header"`
+	SkipAuthPreflight             bool          `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"`
+	FlushInterval                 time.Duration `flag:"flush-interval" cfg:"flush_interval"`
+
+	// These options allow for other providers besides Google, with
+	// potential overrides.
+	ProviderType                       string `flag:"provider" cfg:"provider"`
+	ProviderName                       string `flag:"provider-display-name" cfg:"provider_display_name"`
+	OIDCIssuerURL                      string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"`
+	InsecureOIDCAllowUnverifiedEmail   bool   `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email"`
+	InsecureOIDCSkipIssuerVerification bool   `flag:"insecure-oidc-skip-issuer-verification" cfg:"insecure_oidc_skip_issuer_verification"`
+	SkipOIDCDiscovery                  bool   `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery"`
+	OIDCJwksURL                        string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url"`
+	LoginURL                           string `flag:"login-url" cfg:"login_url"`
+	RedeemURL                          string `flag:"redeem-url" cfg:"redeem_url"`
+	ProfileURL                         string `flag:"profile-url" cfg:"profile_url"`
+	ProtectedResource                  string `flag:"resource" cfg:"resource"`
+	ValidateURL                        string `flag:"validate-url" cfg:"validate_url"`
+	Scope                              string `flag:"scope" cfg:"scope"`
+	Prompt                             string `flag:"prompt" cfg:"prompt"`
+	ApprovalPrompt                     string `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0
+	UserIDClaim                        string `flag:"user-id-claim" cfg:"user_id_claim"`
+
+	// Configuration values for logging
+	LoggingFilename       string `flag:"logging-filename" cfg:"logging_filename"`
+	LoggingMaxSize        int    `flag:"logging-max-size" cfg:"logging_max_size"`
+	LoggingMaxAge         int    `flag:"logging-max-age" cfg:"logging_max_age"`
+	LoggingMaxBackups     int    `flag:"logging-max-backups" cfg:"logging_max_backups"`
+	LoggingLocalTime      bool   `flag:"logging-local-time" cfg:"logging_local_time"`
+	LoggingCompress       bool   `flag:"logging-compress" cfg:"logging_compress"`
+	StandardLogging       bool   `flag:"standard-logging" cfg:"standard_logging"`
+	StandardLoggingFormat string `flag:"standard-logging-format" cfg:"standard_logging_format"`
+	RequestLogging        bool   `flag:"request-logging" cfg:"request_logging"`
+	RequestLoggingFormat  string `flag:"request-logging-format" cfg:"request_logging_format"`
+	ExcludeLoggingPaths   string `flag:"exclude-logging-paths" cfg:"exclude_logging_paths"`
+	SilencePingLogging    bool   `flag:"silence-ping-logging" cfg:"silence_ping_logging"`
+	AuthLogging           bool   `flag:"auth-logging" cfg:"auth_logging"`
+	AuthLoggingFormat     string `flag:"auth-logging-format" cfg:"auth_logging_format"`
+	SignatureKey          string `flag:"signature-key" cfg:"signature_key"`
+	AcrValues             string `flag:"acr-values" cfg:"acr_values"`
+	JWTKey                string `flag:"jwt-key" cfg:"jwt_key"`
+	JWTKeyFile            string `flag:"jwt-key-file" cfg:"jwt_key_file"`
+	PubJWKURL             string `flag:"pubjwk-url" cfg:"pubjwk_url"`
+	GCPHealthChecks       bool   `flag:"gcp-healthchecks" cfg:"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 ipapi.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() ipapi.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 ipapi.RealClientIPParser) { o.realClientIPParser = s }
+
+// NewOptions constructs a new Options with defaulted values
+func NewOptions() *Options {
+	return &Options{
+		ProxyPrefix:         "/oauth2",
+		ProviderType:        "google",
+		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),
+			Path:     "/",
+		},
+		Session: SessionOptions{
+			Type: "cookie",
+		},
+		AzureTenant:                      "common",
+		SetXAuthRequest:                  false,
+		SkipAuthPreflight:                false,
+		FlushInterval:                    time.Duration(1) * time.Second,
+		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,
+	}
+}
+
+// NewFlagSet creates a new FlagSet with all of the flags required by Options
+func NewFlagSet() *pflag.FlagSet {
+	flagSet := pflag.NewFlagSet("oauth2-proxy", pflag.ExitOnError)
+
+	flagSet.String("http-address", "127.0.0.1:4180", "[http://]<addr>:<port> or unix://<path> to listen on for HTTP clients")
+	flagSet.String("https-address", ":443", "<addr>:<port> to listen on for HTTPS clients")
+	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.Bool("force-https", false, "force HTTPS redirect for HTTP requests")
+	flagSet.String("tls-cert-file", "", "path to certificate file")
+	flagSet.String("tls-key-file", "", "path to private key file")
+	flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"")
+	flagSet.Bool("set-xauthrequest", false, "set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)")
+	flagSet.StringSlice("upstream", []string{}, "the http url(s) of the upstream endpoint, file:// paths for static files or static://<status_code> for static response. Routing is based on the path")
+	flagSet.Bool("pass-basic-auth", true, "pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream")
+	flagSet.Bool("set-basic-auth", false, "set HTTP Basic Auth information in response (useful in Nginx auth_request mode)")
+	flagSet.Bool("prefer-email-to-user", false, "Prefer to use the Email address as the Username when passing information to upstream. Will only use Username if Email is unavailable, eg. htaccess authentication. Used in conjunction with -pass-basic-auth and -pass-user-headers")
+	flagSet.Bool("pass-user-headers", true, "pass X-Forwarded-User and X-Forwarded-Email information to upstream")
+	flagSet.String("basic-auth-password", "", "the password to set when passing the HTTP Basic Auth header")
+	flagSet.Bool("pass-access-token", false, "pass OAuth access_token to upstream via X-Forwarded-Access-Token header")
+	flagSet.Bool("pass-host-header", true, "pass the request Host Header to upstream")
+	flagSet.Bool("pass-authorization-header", false, "pass the Authorization Header to upstream")
+	flagSet.Bool("set-authorization-header", false, "set Authorization response headers (useful in Nginx auth_request mode)")
+	flagSet.StringSlice("skip-auth-regex", []string{}, "bypass authentication for requests path's that match (may be given multiple times)")
+	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("ssl-upstream-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS upstreams")
+	flagSet.Duration("flush-interval", time.Duration(1)*time.Second, "period between response flushing when streaming responses")
+	flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens (default false)")
+	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 . to allow subdomains (eg .example.com)")
+	flagSet.String("keycloak-group", "", "restrict login to members of this group.")
+	flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
+	flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
+	flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")
+	flagSet.String("github-org", "", "restrict logins to members of this organisation")
+	flagSet.String("github-team", "", "restrict logins to members of this team")
+	flagSet.String("github-repo", "", "restrict logins to collaborators of this repository")
+	flagSet.String("github-token", "", "the token to use when verifying repository collaborators (must have push access to the repository)")
+	flagSet.String("gitlab-group", "", "restrict logins to members of this group")
+	flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).")
+	flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls")
+	flagSet.String("google-service-account-json", "", "the path to the service account json credentials")
+	flagSet.String("client-id", "", "the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
+	flagSet.String("client-secret", "", "the OAuth Client Secret")
+	flagSet.String("client-secret-file", "", "the file with OAuth Client Secret")
+	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 -s\" for SHA encryption or \"htpasswd -B\" for bcrypt encryption")
+	flagSet.Bool("display-htpasswd-form", true, "display username / password login form if an htpasswd file is provided")
+	flagSet.String("custom-templates-dir", "", "path to custom html templates")
+	flagSet.String("banner", "", "custom banner string. Use \"-\" to disable default banner.")
+	flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.")
+	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.Bool("proxy-websockets", true, "enables WebSocket proxying")
+
+	flagSet.String("cookie-name", "_oauth2_proxy", "the name of the cookie that the oauth_proxy creates")
+	flagSet.String("cookie-secret", "", "the seed string for secure cookies (optionally base64 encoded)")
+	flagSet.StringSlice("cookie-domain", []string{}, "Optional cookie domains to force cookies to (ie: `.yourcompany.com`). The longest domain matching the request's host will be used (or the shortest cookie domain if there is no match).")
+	flagSet.String("cookie-path", "/", "an optional cookie path to force cookies to (ie: /poc/)*")
+	flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie")
+	flagSet.Duration("cookie-refresh", time.Duration(0), "refresh the cookie after this duration; 0 to disable")
+	flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag")
+	flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag")
+	flagSet.String("cookie-samesite", "", "set SameSite cookie attribute (ie: \"lax\", \"strict\", \"none\", or \"\"). ")
+
+	flagSet.String("session-store-type", "cookie", "the session storage provider to use")
+	flagSet.String("redis-connection-url", "", "URL of redis server for redis session storage (eg: redis://HOST[:PORT])")
+	flagSet.Bool("redis-use-sentinel", false, "Connect to redis via sentinels. Must set --redis-sentinel-master-name and --redis-sentinel-connection-urls to use this feature")
+	flagSet.String("redis-sentinel-master-name", "", "Redis sentinel master name. Used in conjunction with --redis-use-sentinel")
+	flagSet.String("redis-ca-path", "", "Redis custom CA path")
+	flagSet.Bool("redis-insecure-skip-tls-verify", false, "Use insecure TLS connection to redis")
+	flagSet.StringSlice("redis-sentinel-connection-urls", []string{}, "List of Redis sentinel connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-sentinel")
+	flagSet.Bool("redis-use-cluster", false, "Connect to redis cluster. Must set --redis-cluster-connection-urls to use this feature")
+	flagSet.StringSlice("redis-cluster-connection-urls", []string{}, "List of Redis cluster connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-cluster")
+
+	flagSet.String("logging-filename", "", "File to log requests to, empty for stdout")
+	flagSet.Int("logging-max-size", 100, "Maximum size in megabytes of the log file before rotation")
+	flagSet.Int("logging-max-age", 7, "Maximum number of days to retain old log files")
+	flagSet.Int("logging-max-backups", 0, "Maximum number of old log files to retain; 0 to disable")
+	flagSet.Bool("logging-local-time", true, "If the time in log files and backup filenames are local or UTC time")
+	flagSet.Bool("logging-compress", false, "Should rotated log files be compressed using gzip")
+
+	flagSet.Bool("standard-logging", true, "Log standard runtime information")
+	flagSet.String("standard-logging-format", logger.DefaultStandardLoggingFormat, "Template for standard log lines")
+
+	flagSet.Bool("request-logging", true, "Log HTTP requests")
+	flagSet.String("request-logging-format", logger.DefaultRequestLoggingFormat, "Template for HTTP request log lines")
+	flagSet.String("exclude-logging-paths", "", "Exclude logging requests to paths (eg: '/path1,/path2,/path3')")
+	flagSet.Bool("silence-ping-logging", false, "Disable logging of requests to ping endpoint")
+
+	flagSet.Bool("auth-logging", true, "Log authentication attempts")
+	flagSet.String("auth-logging-format", logger.DefaultAuthLoggingFormat, "Template for authentication log lines")
+
+	flagSet.String("provider", "google", "OAuth provider")
+	flagSet.String("provider-display-name", "", "Provider display name")
+	flagSet.String("oidc-issuer-url", "", "OpenID Connect issuer URL (ie: https://accounts.google.com)")
+	flagSet.Bool("insecure-oidc-allow-unverified-email", false, "Don't fail if an email address in an id_token is not verified")
+	flagSet.Bool("insecure-oidc-skip-issuer-verification", false, "Do not verify if issuer matches OIDC discovery URL")
+	flagSet.Bool("skip-oidc-discovery", false, "Skip OIDC discovery and use manually supplied Endpoints")
+	flagSet.String("oidc-jwks-url", "", "OpenID Connect JWKS URL (ie: https://www.googleapis.com/oauth2/v3/certs)")
+	flagSet.String("login-url", "", "Authentication endpoint")
+	flagSet.String("redeem-url", "", "Token redemption endpoint")
+	flagSet.String("profile-url", "", "Profile access endpoint")
+	flagSet.String("resource", "", "The resource that is protected (Azure AD only)")
+	flagSet.String("validate-url", "", "Access token validation endpoint")
+	flagSet.String("scope", "", "OAuth scope specification")
+	flagSet.String("prompt", "", "OIDC prompt")
+	flagSet.String("approval-prompt", "force", "OAuth approval_prompt")
+
+	flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)")
+	flagSet.String("acr-values", "", "acr values string:  optional")
+	flagSet.String("jwt-key", "", "private key in PEM format used to sign JWT, so that you can say something like -jwt-key=\"${OAUTH2_PROXY_JWT_KEY}\": required by login.gov")
+	flagSet.String("jwt-key-file", "", "path to the private key file in PEM format used to sign the JWT so that you can say something like -jwt-key-file=/etc/ssl/private/jwt_signing_key.pem: required by login.gov")
+	flagSet.String("pubjwk-url", "", "JWK pubkey access endpoint: required by login.gov")
+	flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints")
+
+	flagSet.String("user-id-claim", "email", "which claim contains the user ID")
+
+	return flagSet
+}
diff --git a/pkg/apis/options/sessions.go b/pkg/apis/options/sessions.go
index fd69d609..b490baf7 100644
--- a/pkg/apis/options/sessions.go
+++ b/pkg/apis/options/sessions.go
@@ -4,7 +4,7 @@ import "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
 
 // SessionOptions contains configuration options for the SessionStore providers.
 type SessionOptions struct {
-	Type   string             `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"`
+	Type   string             `flag:"session-store-type" cfg:"session_store_type"`
 	Cipher *encryption.Cipher `cfg:",internal"`
 	Redis  RedisStoreOptions  `cfg:",squash"`
 }
@@ -19,12 +19,12 @@ var RedisSessionStoreType = "redis"
 
 // RedisStoreOptions contains configuration options for the RedisSessionStore.
 type RedisStoreOptions struct {
-	ConnectionURL          string   `flag:"redis-connection-url" cfg:"redis_connection_url" env:"OAUTH2_PROXY_REDIS_CONNECTION_URL"`
-	UseSentinel            bool     `flag:"redis-use-sentinel" cfg:"redis_use_sentinel" env:"OAUTH2_PROXY_REDIS_USE_SENTINEL"`
-	SentinelMasterName     string   `flag:"redis-sentinel-master-name" cfg:"redis_sentinel_master_name" env:"OAUTH2_PROXY_REDIS_SENTINEL_MASTER_NAME"`
-	SentinelConnectionURLs []string `flag:"redis-sentinel-connection-urls" cfg:"redis_sentinel_connection_urls" env:"OAUTH2_PROXY_REDIS_SENTINEL_CONNECTION_URLS"`
-	UseCluster             bool     `flag:"redis-use-cluster" cfg:"redis_use_cluster" env:"OAUTH2_PROXY_REDIS_USE_CLUSTER"`
-	ClusterConnectionURLs  []string `flag:"redis-cluster-connection-urls" cfg:"redis_cluster_connection_urls" env:"OAUTH2_PROXY_REDIS_CLUSTER_CONNECTION_URLS"`
-	CAPath                 string   `flag:"redis-ca-path" cfg:"redis_ca_path" env:"OAUTH2_PROXY_REDIS_CA_PATH"`
-	InsecureSkipTLSVerify  bool     `flag:"redis-insecure-skip-tls-verify" cfg:"redis_insecure_skip_tls_verify" env:"OAUTH2_PROXY_REDIS_INSECURE_SKIP_TLS_VERIFY"`
+	ConnectionURL          string   `flag:"redis-connection-url" cfg:"redis_connection_url"`
+	UseSentinel            bool     `flag:"redis-use-sentinel" cfg:"redis_use_sentinel"`
+	SentinelMasterName     string   `flag:"redis-sentinel-master-name" cfg:"redis_sentinel_master_name"`
+	SentinelConnectionURLs []string `flag:"redis-sentinel-connection-urls" cfg:"redis_sentinel_connection_urls"`
+	UseCluster             bool     `flag:"redis-use-cluster" cfg:"redis_use_cluster"`
+	ClusterConnectionURLs  []string `flag:"redis-cluster-connection-urls" cfg:"redis_cluster_connection_urls"`
+	CAPath                 string   `flag:"redis-ca-path" cfg:"redis_ca_path"`
+	InsecureSkipTLSVerify  bool     `flag:"redis-insecure-skip-tls-verify" cfg:"redis_insecure_skip_tls_verify"`
 }
diff --git a/realclientip.go b/pkg/ip/realclientip.go
similarity index 80%
rename from realclientip.go
rename to pkg/ip/realclientip.go
index b45ef7c2..0fb43e9b 100644
--- a/realclientip.go
+++ b/pkg/ip/realclientip.go
@@ -1,4 +1,4 @@
-package main
+package ip
 
 import (
 	"fmt"
@@ -6,14 +6,10 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
+	ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip"
 )
 
-type realClientIPParser interface {
-	GetRealClientIP(http.Header) (net.IP, error)
-}
-
-func getRealClientIPParser(headerKey string) (realClientIPParser, error) {
+func GetRealClientIPParser(headerKey string) (ipapi.RealClientIPParser, error) {
 	headerKey = http.CanonicalHeaderKey(headerKey)
 
 	switch headerKey {
@@ -73,13 +69,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 ipapi.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 +81,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/options.go b/pkg/validation/options.go
similarity index 51%
rename from options.go
rename to pkg/validation/options.go
index ae6ac03d..cd4aa0ca 100644
--- a/options.go
+++ b/pkg/validation/options.go
@@ -1,4 +1,4 @@
-package main
+package validation
 
 import (
 	"context"
@@ -14,12 +14,12 @@ 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/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/ip"
 	"github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
 	"github.com/oauth2-proxy/oauth2-proxy/pkg/requests"
 	"github.com/oauth2-proxy/oauth2-proxy/pkg/sessions"
@@ -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 := ip.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 ip.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
+}


Test patch:
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/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/pkg/apis/options/load_test.go b/pkg/apis/options/load_test.go
index 49268018..0e61d707 100644
--- a/pkg/apis/options/load_test.go
+++ b/pkg/apis/options/load_test.go
@@ -295,6 +295,11 @@ var _ = Describe("Load", func() {
 					},
 				},
 			}),
+			Entry("with an empty Options struct, should return default values", &testOptionsTableInput{
+				flagSet:        NewFlagSet,
+				input:          &Options{},
+				expectedOutput: NewOptions(),
+			}),
 		)
 	})
 })
diff --git a/realclientip_test.go b/pkg/ip/realclientip_test.go
similarity index 94%
rename from realclientip_test.go
rename to pkg/ip/realclientip_test.go
index 0271e2e3..b24b733b 100644
--- a/realclientip_test.go
+++ b/pkg/ip/realclientip_test.go
@@ -1,4 +1,4 @@
-package main
+package ip
 
 import (
 	"net"
@@ -6,6 +6,7 @@ import (
 	"reflect"
 	"testing"
 
+	ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -26,7 +27,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 +145,7 @@ func TestGetClientString(t *testing.T) {
 	p := &xForwardedForClientIPParser{header: http.CanonicalHeaderKey("X-Forwarded-For")}
 
 	tests := []struct {
-		parser             realClientIPParser
+		parser             ipapi.RealClientIPParser
 		remoteAddr         string
 		headerValue        string
 		expectedClient     string
@@ -167,10 +168,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/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())
 }