diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f6301e9ae150dbd38f2648f7f2bff270b7bb258..e2a026b0dbce4d3482625b165c4d77be8bb531bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,14 +11,18 @@
 - Migration from Pusher to independent org may have introduced breaking changes for your environment.
   - See the changes listed below for PR [#464](https://github.com/oauth2-proxy/oauth2-proxy/pull/464) for full details
   - Binaries renamed from `oauth2_proxy` to `oauth2-proxy`
-
 - [#440](https://github.com/oauth2-proxy/oauth2-proxy/pull/440) Switch Azure AD Graph API to Microsoft Graph API (@johejo)
-    - The Azure AD Graph API has been [deprecated](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-graph-api) and is being replaced by the Microsoft Graph API.
+  - The Azure AD Graph API has been [deprecated](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-graph-api) and is being replaced by the Microsoft Graph API.
       If your application relies on the access token being passed to it to access the Azure AD Graph API, you should migrate your application to use the Microsoft Graph API.
       Existing behaviour can be retained by setting  `-resource=https://graph.windows.net`.
+- [#484](https://github.com/oauth2-proxy/oauth2-proxy/pull/484) Configuration loading has been replaced with Viper and PFlag
+  - Flags now require a `--` prefix before the option
+  - Previously flags allowed either `-` or `--` to prefix the option name
+  - Eg `-provider` must now be `--provider`
 
 ## Changes since v5.1.0
 
+- [#484](https://github.com/oauth2-proxy/oauth2-proxy/pull/484) Replace configuration loading with Viper (@JoelSpeed)
 - [#499](https://github.com/oauth2-proxy/oauth2-proxy/pull/469) Add `-user-id-claim` to support generic claims in addition to email
 - [#486](https://github.com/oauth2-proxy/oauth2-proxy/pull/486) Add new linters (@johejo)
 - [#440](https://github.com/oauth2-proxy/oauth2-proxy/pull/440) Switch Azure AD Graph API to Microsoft Graph API (@johejo)
diff --git a/env_options.go b/env_options.go
deleted file mode 100644
index 058b90c6ce4538ec3697c4c7c3faf87d0f7ebf10..0000000000000000000000000000000000000000
--- a/env_options.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package main
-
-import (
-	"os"
-	"reflect"
-	"strings"
-)
-
-// EnvOptions holds program options loaded from the process environment
-type EnvOptions map[string]interface{}
-
-// LoadEnvForStruct loads environment variables for each field in an options
-// struct passed into it.
-//
-// Fields in the options struct must have an `env` and `cfg` tag to be read
-// from the environment
-func (cfg EnvOptions) LoadEnvForStruct(options interface{}) {
-	val := reflect.ValueOf(options)
-	var typ reflect.Type
-	if val.Kind() == reflect.Ptr {
-		typ = val.Elem().Type()
-	} else {
-		typ = val.Type()
-	}
-
-	for i := 0; i < typ.NumField(); i++ {
-		// pull out the struct tags:
-		//    flag - the name of the command line flag
-		//    deprecated - (optional) the name of the deprecated command line flag
-		//    cfg - (optional, defaults to underscored flag) the name of the config file option
-		field := typ.Field(i)
-		fieldV := reflect.Indirect(val).Field(i)
-
-		if field.Type.Kind() == reflect.Struct && field.Anonymous {
-			cfg.LoadEnvForStruct(fieldV.Interface())
-			continue
-		}
-
-		flagName := field.Tag.Get("flag")
-		envName := field.Tag.Get("env")
-		cfgName := field.Tag.Get("cfg")
-		if cfgName == "" && flagName != "" {
-			cfgName = strings.ReplaceAll(flagName, "-", "_")
-		}
-		if envName == "" || cfgName == "" {
-			// resolvable fields must have the `env` and `cfg` struct tag
-			continue
-		}
-		v := os.Getenv(envName)
-		if v != "" {
-			cfg[cfgName] = v
-		}
-	}
-}
diff --git a/env_options_test.go b/env_options_test.go
deleted file mode 100644
index eb72a83ef442080dbf9089f3621117f448282884..0000000000000000000000000000000000000000
--- a/env_options_test.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package main_test
-
-import (
-	"os"
-	"testing"
-
-	proxy "github.com/oauth2-proxy/oauth2-proxy"
-	"github.com/stretchr/testify/assert"
-)
-
-type EnvTest struct {
-	TestField string `cfg:"target_field" env:"TEST_ENV_FIELD"`
-	EnvTestEmbed
-}
-
-type EnvTestEmbed struct {
-	TestFieldEmbed string `cfg:"target_field_embed" env:"TEST_ENV_FIELD_EMBED"`
-}
-
-func TestLoadEnvForStruct(t *testing.T) {
-
-	cfg := make(proxy.EnvOptions)
-	cfg.LoadEnvForStruct(&EnvTest{})
-
-	_, ok := cfg["target_field"]
-	assert.Equal(t, ok, false)
-
-	os.Setenv("TEST_ENV_FIELD", "1234abcd")
-	cfg.LoadEnvForStruct(&EnvTest{})
-	v := cfg["target_field"]
-	assert.Equal(t, v, "1234abcd")
-}
-
-func TestLoadEnvForStructWithEmbeddedFields(t *testing.T) {
-
-	cfg := make(proxy.EnvOptions)
-	cfg.LoadEnvForStruct(&EnvTest{})
-
-	_, ok := cfg["target_field_embed"]
-	assert.Equal(t, ok, false)
-
-	os.Setenv("TEST_ENV_FIELD_EMBED", "1234abcd")
-	cfg.LoadEnvForStruct(&EnvTest{})
-	v := cfg["target_field_embed"]
-	assert.Equal(t, v, "1234abcd")
-}
diff --git a/go.mod b/go.mod
index 2170dc9bc7fe102b498580ad943759a92f913a11..618c7c7393ec07a432c6ca541557dd3b4a2156b9 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,6 @@ module github.com/oauth2-proxy/oauth2-proxy
 go 1.14
 
 require (
-	github.com/BurntSushi/toml v0.3.1
 	github.com/alicebob/miniredis/v2 v2.11.2
 	github.com/bitly/go-simplejson v0.5.0
 	github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
@@ -13,10 +12,12 @@ require (
 	github.com/go-redis/redis/v7 v7.2.0
 	github.com/kr/pretty v0.2.0 // indirect
 	github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa
-	github.com/mreiferson/go-options v1.0.0
+	github.com/mitchellh/mapstructure v1.1.2
 	github.com/onsi/ginkgo v1.12.0
 	github.com/onsi/gomega v1.9.0
 	github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
+	github.com/spf13/pflag v1.0.3
+	github.com/spf13/viper v1.6.3
 	github.com/stretchr/testify v1.5.1
 	github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997
 	golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
diff --git a/go.sum b/go.sum
index 5d4662da5d7ab1ed8678d93d33be9f0062f016af..45e2be1b3a86e7fab01a98506932a191fc020ee8 100644
--- a/go.sum
+++ b/go.sum
@@ -4,67 +4,118 @@ cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
 cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 h1:45bxf7AZMwWcqkLzDAQugVEwedisr5nRJ1r+7LYnv0U=
 github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
 github.com/alicebob/miniredis/v2 v2.11.2 h1:OtWO7akm5otuhssnE/sNfsWxG4gZ8DbGhShDtRrByJs=
 github.com/alicebob/miniredis/v2 v2.11.2/go.mod h1:VL3UDEfAH59bSa7MuHMuFToxkqyHh69s/WUbYlOAuyg=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
 github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
 github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-redis/redis/v7 v7.2.0 h1:CrCexy/jYWZjW0AyVoHlcJUeZN19VWlbepTh1Vq6dJs=
 github.com/go-redis/redis/v7 v7.2.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3 h1:6amM4HsNPOvMLVc2ZnyqrjeQ92YAVWn7T4WBKK87inY=
 github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
 github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
+github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa h1:hI1uC2A3vJFjwvBn0G0a7QBRdBUp6Y048BtLAHRTKPo=
 github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa/go.mod h1:8vxFeeg++MqgCHwehSuwTlYCF0ALyDJbYJ1JsKi7v6s=
-github.com/mreiferson/go-options v1.0.0 h1:RMLidydGlDWpL+lQTXo0bVIf/XT2CTq7AEJMoz5/VWs=
-github.com/mreiferson/go-options v1.0.0/go.mod h1:zHtCks/HQvOt8ATyfwVe3JJq2PPuImzXINPRTC03+9w=
+github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
@@ -73,20 +124,63 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=
 github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
+github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs=
+github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997 h1:1+FQ4Ns+UZtUiQ4lP0sTCyKSQ0EXoiwAdHZB0Pd5t9Q=
 github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997/go.mod h1:DIGbh/f5XMAessMV/uaIik81gkDVjUeQ9ApdaU7wRKE=
 github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
 github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -98,11 +192,14 @@ golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHl
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
 golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -117,7 +214,10 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA=
@@ -131,11 +231,14 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -152,21 +255,28 @@ google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRn
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
+gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y=
 gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/main.go b/main.go
index 91111eaa691a1d796259818ae868057aa417b329..698c64adcc31c9d112a9003b268def2d5de80c6f 100644
--- a/main.go
+++ b/main.go
@@ -12,9 +12,9 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/BurntSushi/toml"
-	options "github.com/mreiferson/go-options"
+	"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
 	"github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
+	"github.com/spf13/pflag"
 )
 
 func main() {
@@ -149,7 +149,10 @@ func main() {
 
 	flagSet.String("user-id-claim", "email", "which claim contains the user ID")
 
-	flagSet.Parse(os.Args[1:])
+	pflagSet := pflag.NewFlagSet("oauth2-proxy", pflag.ExitOnError)
+	pflagSet.AddGoFlagSet(flagSet)
+
+	pflagSet.Parse(os.Args[1:])
 
 	if *showVersion {
 		fmt.Printf("oauth2-proxy %s (built with %s)\n", VERSION, runtime.Version())
@@ -157,18 +160,13 @@ func main() {
 	}
 
 	opts := NewOptions()
-
-	cfg := make(EnvOptions)
-	if *config != "" {
-		_, err := toml.DecodeFile(*config, &cfg)
-		if err != nil {
-			logger.Fatalf("ERROR: failed to load config file %s - %s", *config, err)
-		}
+	err := options.Load(*config, pflagSet, opts)
+	if err != nil {
+		logger.Printf("ERROR: Failed to load config: %v", err)
+		os.Exit(1)
 	}
-	cfg.LoadEnvForStruct(opts)
-	options.Resolve(opts, flagSet, cfg)
 
-	err := opts.Validate()
+	err = opts.Validate()
 	if err != nil {
 		logger.Printf("%s", err)
 		os.Exit(1)
diff --git a/oauthproxy.go b/oauthproxy.go
index 1e9bb7cafe873fa438a79e80b78f7289cfdfcb7c..587215fd2a58b4c036850c3723064172872bfb9f 100644
--- a/oauthproxy.go
+++ b/oauthproxy.go
@@ -247,7 +247,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.compiledRegex {
 		logger.Printf("compiled skip-auth-regex => %q", u)
 	}
 
@@ -264,23 +264,23 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
 
 	logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.provider.Data().ProviderName, opts.ClientID)
 	refresh := "disabled"
-	if opts.CookieRefresh != time.Duration(0) {
-		refresh = fmt.Sprintf("after %s", opts.CookieRefresh)
+	if opts.Cookie.Refresh != time.Duration(0) {
+		refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
 	}
 
-	logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domains:%s path:%s samesite:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHTTPOnly, opts.CookieExpire, strings.Join(opts.CookieDomains, ","), opts.CookiePath, opts.CookieSameSite, refresh)
+	logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domains:%s path:%s samesite:%s refresh:%s", opts.Cookie.Name, opts.Cookie.Secure, opts.Cookie.HTTPOnly, opts.Cookie.Expire, strings.Join(opts.Cookie.Domains, ","), opts.Cookie.Path, opts.Cookie.SameSite, refresh)
 
 	return &OAuthProxy{
-		CookieName:     opts.CookieName,
-		CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"),
-		CookieSeed:     opts.CookieSecret,
-		CookieDomains:  opts.CookieDomains,
-		CookiePath:     opts.CookiePath,
-		CookieSecure:   opts.CookieSecure,
-		CookieHTTPOnly: opts.CookieHTTPOnly,
-		CookieExpire:   opts.CookieExpire,
-		CookieRefresh:  opts.CookieRefresh,
-		CookieSameSite: opts.CookieSameSite,
+		CookieName:     opts.Cookie.Name,
+		CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
+		CookieSeed:     opts.Cookie.Secret,
+		CookieDomains:  opts.Cookie.Domains,
+		CookiePath:     opts.Cookie.Path,
+		CookieSecure:   opts.Cookie.Secure,
+		CookieHTTPOnly: opts.Cookie.HTTPOnly,
+		CookieExpire:   opts.Cookie.Expire,
+		CookieRefresh:  opts.Cookie.Refresh,
+		CookieSameSite: opts.Cookie.SameSite,
 		Validator:      validator,
 
 		RobotsPath:        "/robots.txt",
@@ -303,7 +303,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
 		skipAuthPreflight:    opts.SkipAuthPreflight,
 		skipJwtBearerTokens:  opts.SkipJwtBearerTokens,
 		jwtBearerVerifiers:   opts.jwtBearerVerifiers,
-		compiledRegex:        opts.CompiledRegex,
+		compiledRegex:        opts.compiledRegex,
 		SetXAuthRequest:      opts.SetXAuthRequest,
 		PassBasicAuth:        opts.PassBasicAuth,
 		SetBasicAuth:         opts.SetBasicAuth,
diff --git a/oauthproxy_test.go b/oauthproxy_test.go
index 47b277437f9381b02434b58c8ba145461e8e65ab..957b4334a392b0799fd5fe952be1ea10071ca09c 100644
--- a/oauthproxy_test.go
+++ b/oauthproxy_test.go
@@ -164,7 +164,7 @@ func TestRobotsTxt(t *testing.T) {
 	opts := NewOptions()
 	opts.ClientID = "asdlkjx"
 	opts.ClientSecret = "alkgks"
-	opts.CookieSecret = "asdkugkj"
+	opts.Cookie.Secret = "asdkugkj"
 	opts.Validate()
 
 	proxy := NewOAuthProxy(opts, func(string) bool { return true })
@@ -179,7 +179,7 @@ func TestIsValidRedirect(t *testing.T) {
 	opts := NewOptions()
 	opts.ClientID = "skdlfj"
 	opts.ClientSecret = "fgkdsgj"
-	opts.CookieSecret = "ljgiogbj"
+	opts.Cookie.Secret = "ljgiogbj"
 	// Should match domains that are exactly foo.bar and any subdomain of bar.foo
 	opts.WhitelistDomains = []string{
 		"foo.bar",
@@ -398,10 +398,10 @@ func TestBasicAuthPassword(t *testing.T) {
 	opts.Upstreams = append(opts.Upstreams, providerServer.URL)
 	// The CookieSecret must be 32 bytes in order to create the AES
 	// cipher.
-	opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp"
+	opts.Cookie.Secret = "xyzzyplughxyzzyplughxyzzyplughxp"
 	opts.ClientID = "dlgkj"
 	opts.ClientSecret = "alkgret"
-	opts.CookieSecure = false
+	opts.Cookie.Secure = false
 	opts.PassBasicAuth = true
 	opts.SetBasicAuth = true
 	opts.PassUserHeaders = true
@@ -582,10 +582,10 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes
 	}
 	// The CookieSecret must be 32 bytes in order to create the AES
 	// cipher.
-	t.opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp"
+	t.opts.Cookie.Secret = "xyzzyplughxyzzyplughxyzzyplughxp"
 	t.opts.ClientID = "slgkj"
 	t.opts.ClientSecret = "gfjgojl"
-	t.opts.CookieSecure = false
+	t.opts.Cookie.Secure = false
 	t.opts.PassAccessToken = opts.PassAccessToken
 	t.opts.Validate()
 
@@ -735,7 +735,7 @@ func NewSignInPageTest(skipProvider bool) *SignInPageTest {
 	var sipTest SignInPageTest
 
 	sipTest.opts = NewOptions()
-	sipTest.opts.CookieSecret = "adklsj2"
+	sipTest.opts.Cookie.Secret = "adklsj2"
 	sipTest.opts.ClientID = "lkdgj"
 	sipTest.opts.ClientSecret = "sgiufgoi"
 	sipTest.opts.SkipProviderButton = skipProvider
@@ -841,10 +841,10 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi
 	}
 	pcTest.opts.ClientID = "asdfljk"
 	pcTest.opts.ClientSecret = "lkjfdsig"
-	pcTest.opts.CookieSecret = "0123456789abcdefabcd"
+	pcTest.opts.Cookie.Secret = "0123456789abcdefabcd"
 	// First, set the CookieRefresh option so proxy.AesCipher is created,
 	// needed to encrypt the access_token.
-	pcTest.opts.CookieRefresh = time.Hour
+	pcTest.opts.Cookie.Refresh = time.Hour
 	pcTest.opts.Validate()
 
 	pcTest.proxy = NewOAuthProxy(pcTest.opts, func(email string) bool {
@@ -915,7 +915,7 @@ func TestProcessCookieNoCookieError(t *testing.T) {
 
 func TestProcessCookieRefreshNotSet(t *testing.T) {
 	pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) {
-		opts.CookieExpire = time.Duration(23) * time.Hour
+		opts.Cookie.Expire = time.Duration(23) * time.Hour
 	})
 	reference := time.Now().Add(time.Duration(-2) * time.Hour)
 
@@ -932,7 +932,7 @@ func TestProcessCookieRefreshNotSet(t *testing.T) {
 
 func TestProcessCookieFailIfCookieExpired(t *testing.T) {
 	pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) {
-		opts.CookieExpire = time.Duration(24) * time.Hour
+		opts.Cookie.Expire = time.Duration(24) * time.Hour
 	})
 	reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
 	startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: reference}
@@ -947,7 +947,7 @@ func TestProcessCookieFailIfCookieExpired(t *testing.T) {
 
 func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
 	pcTest := NewProcessCookieTestWithOptionsModifiers(func(opts *Options) {
-		opts.CookieExpire = time.Duration(24) * time.Hour
+		opts.Cookie.Expire = time.Duration(24) * time.Hour
 	})
 	reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
 	startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: reference}
@@ -1017,7 +1017,7 @@ func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
 
 func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
 	test := NewAuthOnlyEndpointTest(func(opts *Options) {
-		opts.CookieExpire = time.Duration(24) * time.Hour
+		opts.Cookie.Expire = time.Duration(24) * time.Hour
 	})
 	reference := time.Now().Add(time.Duration(25) * time.Hour * -1)
 	startSession := &sessions.SessionState{
@@ -1149,7 +1149,7 @@ func TestAuthSkippedForPreflightRequests(t *testing.T) {
 	opts.Upstreams = append(opts.Upstreams, upstream.URL)
 	opts.ClientID = "aljsal"
 	opts.ClientSecret = "jglkfsdgj"
-	opts.CookieSecret = "dkfjgdls"
+	opts.Cookie.Secret = "dkfjgdls"
 	opts.SkipAuthPreflight = true
 	opts.Validate()
 
@@ -1196,7 +1196,7 @@ type SignatureTest struct {
 
 func NewSignatureTest() *SignatureTest {
 	opts := NewOptions()
-	opts.CookieSecret = "cookie secret"
+	opts.Cookie.Secret = "cookie secret"
 	opts.ClientID = "client ID"
 	opts.ClientSecret = "client secret"
 	opts.EmailDomains = []string{"acm.org"}
@@ -1343,7 +1343,7 @@ type ajaxRequestTest struct {
 func newAjaxRequestTest() *ajaxRequestTest {
 	test := &ajaxRequestTest{}
 	test.opts = NewOptions()
-	test.opts.CookieSecret = "sdflsw"
+	test.opts.Cookie.Secret = "sdflsw"
 	test.opts.ClientID = "gkljfdl"
 	test.opts.ClientSecret = "sdflkjs"
 	test.opts.Validate()
@@ -1401,11 +1401,11 @@ func TestAjaxForbiddendRequest(t *testing.T) {
 
 func TestClearSplitCookie(t *testing.T) {
 	opts := NewOptions()
-	opts.CookieName = "oauth2"
-	opts.CookieDomains = []string{"abc"}
-	store, err := cookie.NewCookieSessionStore(&opts.SessionOptions, &opts.CookieOptions)
+	opts.Cookie.Name = "oauth2"
+	opts.Cookie.Domains = []string{"abc"}
+	store, err := cookie.NewCookieSessionStore(&opts.Session, &opts.Cookie)
 	assert.Equal(t, err, nil)
-	p := OAuthProxy{CookieName: opts.CookieName, CookieDomains: opts.CookieDomains, sessionStore: store}
+	p := OAuthProxy{CookieName: opts.Cookie.Name, CookieDomains: opts.Cookie.Domains, sessionStore: store}
 	var rw = httptest.NewRecorder()
 	req := httptest.NewRequest("get", "/", nil)
 
@@ -1430,11 +1430,11 @@ func TestClearSplitCookie(t *testing.T) {
 
 func TestClearSingleCookie(t *testing.T) {
 	opts := NewOptions()
-	opts.CookieName = "oauth2"
-	opts.CookieDomains = []string{"abc"}
-	store, err := cookie.NewCookieSessionStore(&opts.SessionOptions, &opts.CookieOptions)
+	opts.Cookie.Name = "oauth2"
+	opts.Cookie.Domains = []string{"abc"}
+	store, err := cookie.NewCookieSessionStore(&opts.Session, &opts.Cookie)
 	assert.Equal(t, err, nil)
-	p := OAuthProxy{CookieName: opts.CookieName, CookieDomains: opts.CookieDomains, sessionStore: store}
+	p := OAuthProxy{CookieName: opts.Cookie.Name, CookieDomains: opts.Cookie.Domains, sessionStore: store}
 	var rw = httptest.NewRecorder()
 	req := httptest.NewRequest("get", "/", nil)
 
diff --git a/options.go b/options.go
index 50dee28b85495b8fddf0aacfdce04d750f223e8d..b59c2b14ee3bd95fa04e9b60c53a4a1652966848 100644
--- a/options.go
+++ b/options.go
@@ -64,11 +64,8 @@ type Options struct {
 	Banner                   string   `flag:"banner" cfg:"banner" env:"OAUTH2_PROXY_BANNER"`
 	Footer                   string   `flag:"footer" cfg:"footer" env:"OAUTH2_PROXY_FOOTER"`
 
-	// Embed CookieOptions
-	options.CookieOptions
-
-	// Embed SessionOptions
-	options.SessionOptions
+	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"`
@@ -134,7 +131,7 @@ type Options struct {
 	// internal values that are set after config validation
 	redirectURL        *url.URL
 	proxyURLs          []*url.URL
-	CompiledRegex      []*regexp.Regexp
+	compiledRegex      []*regexp.Regexp
 	provider           providers.Provider
 	sessionStore       sessionsapi.SessionStore
 	signatureData      *SignatureData
@@ -158,14 +155,14 @@ func NewOptions() *Options {
 		HTTPSAddress:        ":443",
 		ForceHTTPS:          false,
 		DisplayHtpasswdForm: true,
-		CookieOptions: options.CookieOptions{
-			CookieName:     "_oauth2_proxy",
-			CookieSecure:   true,
-			CookieHTTPOnly: true,
-			CookieExpire:   time.Duration(168) * time.Hour,
-			CookieRefresh:  time.Duration(0),
+		Cookie: options.CookieOptions{
+			Name:     "_oauth2_proxy",
+			Secure:   true,
+			HTTPOnly: true,
+			Expire:   time.Duration(168) * time.Hour,
+			Refresh:  time.Duration(0),
 		},
-		SessionOptions: options.SessionOptions{
+		Session: options.SessionOptions{
 			Type: "cookie",
 		},
 		SetXAuthRequest:                  false,
@@ -227,7 +224,7 @@ func (o *Options) Validate() error {
 	}
 
 	msgs := make([]string, 0)
-	if o.CookieSecret == "" {
+	if o.Cookie.Secret == "" {
 		msgs = append(msgs, "missing setting: cookie-secret")
 	}
 	if o.ClientID == "" {
@@ -372,61 +369,61 @@ func (o *Options) Validate() error {
 	}
 
 	for _, u := range o.SkipAuthRegex {
-		CompiledRegex, err := regexp.Compile(u)
+		compiledRegex, err := regexp.Compile(u)
 		if err != nil {
 			msgs = append(msgs, fmt.Sprintf("error compiling regex=%q %s", u, err))
 			continue
 		}
-		o.CompiledRegex = append(o.CompiledRegex, CompiledRegex)
+		o.compiledRegex = append(o.compiledRegex, compiledRegex)
 	}
 	msgs = parseProviderInfo(o, msgs)
 
 	var cipher *encryption.Cipher
-	if o.PassAccessToken || o.SetAuthorization || o.PassAuthorization || (o.CookieRefresh != time.Duration(0)) {
+	if o.PassAccessToken || o.SetAuthorization || o.PassAuthorization || (o.Cookie.Refresh != time.Duration(0)) {
 		validCookieSecretSize := false
 		for _, i := range []int{16, 24, 32} {
-			if len(secretBytes(o.CookieSecret)) == i {
+			if len(secretBytes(o.Cookie.Secret)) == i {
 				validCookieSecretSize = true
 			}
 		}
 		var decoded bool
-		if string(secretBytes(o.CookieSecret)) != o.CookieSecret {
+		if string(secretBytes(o.Cookie.Secret)) != o.Cookie.Secret {
 			decoded = true
 		}
 		if !validCookieSecretSize {
 			var suffix string
 			if decoded {
-				suffix = fmt.Sprintf(" note: cookie secret was base64 decoded from %q", o.CookieSecret)
+				suffix = fmt.Sprintf(" note: cookie secret was base64 decoded from %q", o.Cookie.Secret)
 			}
 			msgs = append(msgs, fmt.Sprintf(
 				"cookie_secret must be 16, 24, or 32 bytes "+
 					"to create an AES cipher when "+
 					"pass_access_token == true or "+
 					"cookie_refresh != 0, but is %d bytes.%s",
-				len(secretBytes(o.CookieSecret)), suffix))
+				len(secretBytes(o.Cookie.Secret)), suffix))
 		} else {
 			var err error
-			cipher, err = encryption.NewCipher(secretBytes(o.CookieSecret))
+			cipher, err = encryption.NewCipher(secretBytes(o.Cookie.Secret))
 			if err != nil {
 				msgs = append(msgs, fmt.Sprintf("cookie-secret error: %v", err))
 			}
 		}
 	}
 
-	o.SessionOptions.Cipher = cipher
-	sessionStore, err := sessions.NewSessionStore(&o.SessionOptions, &o.CookieOptions)
+	o.Session.Cipher = cipher
+	sessionStore, err := sessions.NewSessionStore(&o.Session, &o.Cookie)
 	if err != nil {
 		msgs = append(msgs, fmt.Sprintf("error initialising session storage: %v", err))
 	} else {
 		o.sessionStore = sessionStore
 	}
 
-	if o.CookieRefresh >= o.CookieExpire {
+	if o.Cookie.Refresh >= o.Cookie.Expire {
 		msgs = append(msgs, fmt.Sprintf(
 			"cookie_refresh (%s) must be less than "+
 				"cookie_expire (%s)",
-			o.CookieRefresh.String(),
-			o.CookieExpire.String()))
+			o.Cookie.Refresh.String(),
+			o.Cookie.Expire.String()))
 	}
 
 	if len(o.GoogleGroups) > 0 || o.GoogleAdminEmail != "" || o.GoogleServiceAccountJSON != "" {
@@ -441,16 +438,16 @@ func (o *Options) Validate() error {
 		}
 	}
 
-	switch o.CookieSameSite {
+	switch o.Cookie.SameSite {
 	case "", "none", "lax", "strict":
 	default:
-		msgs = append(msgs, fmt.Sprintf("cookie_samesite (%s) must be one of ['', 'lax', 'strict', 'none']", o.CookieSameSite))
+		msgs = append(msgs, fmt.Sprintf("cookie_samesite (%s) must be one of ['', 'lax', 'strict', 'none']", o.Cookie.SameSite))
 	}
 
 	// Sort cookie domains by length, so that we try longer (and more specific)
 	// domains first
-	sort.Slice(o.CookieDomains, func(i, j int) bool {
-		return len(o.CookieDomains[i]) > len(o.CookieDomains[j])
+	sort.Slice(o.Cookie.Domains, func(i, j int) bool {
+		return len(o.Cookie.Domains[i]) > len(o.Cookie.Domains[j])
 	})
 
 	msgs = parseSignatureKey(o, msgs)
@@ -627,9 +624,9 @@ func newVerifierFromJwtIssuer(jwtIssuer jwtIssuer) (*oidc.IDTokenVerifier, error
 }
 
 func validateCookieName(o *Options, msgs []string) []string {
-	cookie := &http.Cookie{Name: o.CookieName}
+	cookie := &http.Cookie{Name: o.Cookie.Name}
 	if cookie.String() == "" {
-		return append(msgs, fmt.Sprintf("invalid cookie name: %q", o.CookieName))
+		return append(msgs, fmt.Sprintf("invalid cookie name: %q", o.Cookie.Name))
 	}
 	return msgs
 }
diff --git a/options_test.go b/options_test.go
index a6bbc9ce1d8e0cbb93c301a6d5787d1d1ddc7cf8..b14c9e465a2d8ab55faba00225ceff3eea7484b2 100644
--- a/options_test.go
+++ b/options_test.go
@@ -22,7 +22,7 @@ const (
 func testOptions() *Options {
 	o := NewOptions()
 	o.Upstreams = append(o.Upstreams, "http://127.0.0.1:8080/")
-	o.CookieSecret = cookieSecret
+	o.Cookie.Secret = cookieSecret
 	o.ClientID = clientID
 	o.ClientSecret = clientSecret
 	o.EmailDomains = []string{"*"}
@@ -51,7 +51,7 @@ func TestNewOptions(t *testing.T) {
 
 func TestClientSecretFileOptionFails(t *testing.T) {
 	o := NewOptions()
-	o.CookieSecret = cookieSecret
+	o.Cookie.Secret = cookieSecret
 	o.ClientID = clientID
 	o.ClientSecretFile = clientSecret
 	o.EmailDomains = []string{"*"}
@@ -81,7 +81,7 @@ func TestClientSecretFileOption(t *testing.T) {
 	defer os.Remove(clientSecretFileName)
 
 	o := NewOptions()
-	o.CookieSecret = cookieSecret
+	o.Cookie.Secret = cookieSecret
 	o.ClientID = clientID
 	o.ClientSecretFile = clientSecretFileName
 	o.EmailDomains = []string{"*"}
@@ -165,7 +165,7 @@ func TestCompiledRegex(t *testing.T) {
 	o.SkipAuthRegex = regexps
 	assert.Equal(t, nil, o.Validate())
 	actual := make([]string, 0)
-	for _, regex := range o.CompiledRegex {
+	for _, regex := range o.compiledRegex {
 		actual = append(actual, regex.String())
 	}
 	assert.Equal(t, regexps, actual)
@@ -212,20 +212,20 @@ func TestPassAccessTokenRequiresSpecificCookieSecretLengths(t *testing.T) {
 
 	assert.Equal(t, false, o.PassAccessToken)
 	o.PassAccessToken = true
-	o.CookieSecret = "cookie of invalid length-"
+	o.Cookie.Secret = "cookie of invalid length-"
 	assert.NotEqual(t, nil, o.Validate())
 
 	o.PassAccessToken = false
-	o.CookieRefresh = time.Duration(24) * time.Hour
+	o.Cookie.Refresh = time.Duration(24) * time.Hour
 	assert.NotEqual(t, nil, o.Validate())
 
-	o.CookieSecret = "16 bytes AES-128"
+	o.Cookie.Secret = "16 bytes AES-128"
 	assert.Equal(t, nil, o.Validate())
 
-	o.CookieSecret = "24 byte secret AES-192--"
+	o.Cookie.Secret = "24 byte secret AES-192--"
 	assert.Equal(t, nil, o.Validate())
 
-	o.CookieSecret = "32 byte secret for AES-256------"
+	o.Cookie.Secret = "32 byte secret for AES-256------"
 	assert.Equal(t, nil, o.Validate())
 }
 
@@ -233,11 +233,11 @@ func TestCookieRefreshMustBeLessThanCookieExpire(t *testing.T) {
 	o := testOptions()
 	assert.Equal(t, nil, o.Validate())
 
-	o.CookieSecret = "0123456789abcdefabcd"
-	o.CookieRefresh = o.CookieExpire
+	o.Cookie.Secret = "0123456789abcdefabcd"
+	o.Cookie.Refresh = o.Cookie.Expire
 	assert.NotEqual(t, nil, o.Validate())
 
-	o.CookieRefresh -= time.Duration(1)
+	o.Cookie.Refresh -= time.Duration(1)
 	assert.Equal(t, nil, o.Validate())
 }
 
@@ -246,23 +246,23 @@ func TestBase64CookieSecret(t *testing.T) {
 	assert.Equal(t, nil, o.Validate())
 
 	// 32 byte, base64 (urlsafe) encoded key
-	o.CookieSecret = "yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ="
+	o.Cookie.Secret = "yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ="
 	assert.Equal(t, nil, o.Validate())
 
 	// 32 byte, base64 (urlsafe) encoded key, w/o padding
-	o.CookieSecret = "yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ"
+	o.Cookie.Secret = "yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ"
 	assert.Equal(t, nil, o.Validate())
 
 	// 24 byte, base64 (urlsafe) encoded key
-	o.CookieSecret = "Kp33Gj-GQmYtz4zZUyUDdqQKx5_Hgkv3"
+	o.Cookie.Secret = "Kp33Gj-GQmYtz4zZUyUDdqQKx5_Hgkv3"
 	assert.Equal(t, nil, o.Validate())
 
 	// 16 byte, base64 (urlsafe) encoded key
-	o.CookieSecret = "LFEqZYvYUwKwzn0tEuTpLA=="
+	o.Cookie.Secret = "LFEqZYvYUwKwzn0tEuTpLA=="
 	assert.Equal(t, nil, o.Validate())
 
 	// 16 byte, base64 (urlsafe) encoded key, w/o padding
-	o.CookieSecret = "LFEqZYvYUwKwzn0tEuTpLA"
+	o.Cookie.Secret = "LFEqZYvYUwKwzn0tEuTpLA"
 	assert.Equal(t, nil, o.Validate())
 }
 
@@ -292,16 +292,16 @@ func TestValidateSignatureKeyUnsupportedAlgorithm(t *testing.T) {
 
 func TestValidateCookie(t *testing.T) {
 	o := testOptions()
-	o.CookieName = "_valid_cookie_name"
+	o.Cookie.Name = "_valid_cookie_name"
 	assert.Equal(t, nil, o.Validate())
 }
 
 func TestValidateCookieBadName(t *testing.T) {
 	o := testOptions()
-	o.CookieName = "_bad_cookie_name{}"
+	o.Cookie.Name = "_bad_cookie_name{}"
 	err := o.Validate()
 	assert.Equal(t, err.Error(), "invalid configuration:\n"+
-		fmt.Sprintf("  invalid cookie name: %q", o.CookieName))
+		fmt.Sprintf("  invalid cookie name: %q", o.Cookie.Name))
 }
 
 func TestSkipOIDCDiscovery(t *testing.T) {
diff --git a/pkg/apis/options/cookie.go b/pkg/apis/options/cookie.go
index 4d26773145138d853881a720a07a740c62b893cd..2cb77eb61afffd80fb1269660201f6f6d7ff4f6f 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 {
-	CookieName     string        `flag:"cookie-name" cfg:"cookie_name" env:"OAUTH2_PROXY_COOKIE_NAME"`
-	CookieSecret   string        `flag:"cookie-secret" cfg:"cookie_secret" env:"OAUTH2_PROXY_COOKIE_SECRET"`
-	CookieDomains  []string      `flag:"cookie-domain" cfg:"cookie_domain" env:"OAUTH2_PROXY_COOKIE_DOMAIN"`
-	CookiePath     string        `flag:"cookie-path" cfg:"cookie_path" env:"OAUTH2_PROXY_COOKIE_PATH"`
-	CookieExpire   time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"OAUTH2_PROXY_COOKIE_EXPIRE"`
-	CookieRefresh  time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"OAUTH2_PROXY_COOKIE_REFRESH"`
-	CookieSecure   bool          `flag:"cookie-secure" cfg:"cookie_secure" env:"OAUTH2_PROXY_COOKIE_SECURE"`
-	CookieHTTPOnly bool          `flag:"cookie-httponly" cfg:"cookie_httponly" env:"OAUTH2_PROXY_COOKIE_HTTPONLY"`
-	CookieSameSite string        `flag:"cookie-samesite" cfg:"cookie_samesite" env:"OAUTH2_PROXY_COOKIE_SAMESITE"`
+	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"`
 }
diff --git a/pkg/apis/options/load.go b/pkg/apis/options/load.go
new file mode 100644
index 0000000000000000000000000000000000000000..aeb39b9ae0b5fd0216894ceaa581129be1471825
--- /dev/null
+++ b/pkg/apis/options/load.go
@@ -0,0 +1,134 @@
+package options
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/spf13/pflag"
+	"github.com/spf13/viper"
+)
+
+// Load reads in the config file at the path given, then merges in environment
+// variables (prefixed with `OAUTH2_PROXY`) and finally merges in flags from the flagSet.
+// If a config value is unset and the flag has a non-zero value default, this default will be used.
+// Eg. A field defined:
+//    FooBar `cfg:"foo_bar" flag:"foo-bar"`
+// Can be set in the config file as `foo_bar="baz"`, in the environment as `OAUTH2_PROXY_FOO_BAR=baz`,
+// or via the command line flag `--foo-bar=baz`.
+func Load(configFileName string, flagSet *pflag.FlagSet, into interface{}) error {
+	v := viper.New()
+	v.SetConfigFile(configFileName)
+	v.SetConfigType("toml") // Config is in toml format
+	v.SetEnvPrefix("OAUTH2_PROXY")
+	v.AutomaticEnv()
+	v.SetTypeByDefaultValue(true)
+
+	if configFileName != "" {
+		err := v.ReadInConfig()
+		if err != nil {
+			return fmt.Errorf("unable to load config file: %w", err)
+		}
+	}
+
+	err := registerFlags(v, "", flagSet, into)
+	if err != nil {
+		// This should only happen if there is a programming error
+		return fmt.Errorf("unable to register flags: %w", err)
+	}
+
+	// UnmarhsalExact will return an error if the config includes options that are
+	// not mapped to felds of the into struct
+	err = v.UnmarshalExact(into, decodeFromCfgTag)
+	if err != nil {
+		return fmt.Errorf("error unmarshalling config: %w", err)
+	}
+
+	return nil
+}
+
+// registerFlags uses `cfg` and `flag` tags to associate flags in the flagSet
+// to the fields in the options interface provided.
+// Each exported field in the options must have a `cfg` tag otherwise an error will occur.
+// - For fields, set `cfg` and `flag` so that `flag` is the name of the flag associated to this config option
+// - For exported fields that are not user facing, set the `cfg` to `,internal`
+// - For structs containing user facing fields, set the `cfg` to `,squash`
+func registerFlags(v *viper.Viper, prefix string, flagSet *pflag.FlagSet, options interface{}) error {
+	val := reflect.ValueOf(options)
+	var typ reflect.Type
+	if val.Kind() == reflect.Ptr {
+		typ = val.Elem().Type()
+	} else {
+		typ = val.Type()
+	}
+
+	for i := 0; i < typ.NumField(); i++ {
+		// pull out the struct tags:
+		//    flag - the name of the command line flag
+		//    cfg - the name of the config file option
+		field := typ.Field(i)
+		fieldV := reflect.Indirect(val).Field(i)
+		fieldName := strings.Join([]string{prefix, field.Name}, ".")
+
+		cfgName := field.Tag.Get("cfg")
+		if cfgName == ",internal" {
+			// Public but internal types that should not be exposed to users, skip them
+			continue
+		}
+
+		if isUnexported(field.Name) {
+			// Unexported fields cannot be set by a user, so won't have tags or flags, skip them
+			continue
+		}
+
+		if field.Type.Kind() == reflect.Struct {
+			if cfgName != ",squash" {
+				return fmt.Errorf("field %q does not have required cfg tag: `,squash`", fieldName)
+			}
+			err := registerFlags(v, fieldName, flagSet, fieldV.Interface())
+			if err != nil {
+				return err
+			}
+			continue
+		}
+
+		flagName := field.Tag.Get("flag")
+		if flagName == "" || cfgName == "" {
+			return fmt.Errorf("field %q does not have required tags (cfg, flag)", fieldName)
+		}
+
+		if flagSet == nil {
+			return fmt.Errorf("flagset cannot be nil")
+		}
+
+		f := flagSet.Lookup(flagName)
+		if f == nil {
+			return fmt.Errorf("field %q does not have a registered flag", flagName)
+		}
+		err := v.BindPFlag(cfgName, f)
+		if err != nil {
+			return fmt.Errorf("error binding flag for field %q: %w", fieldName, err)
+		}
+	}
+
+	return nil
+}
+
+// decodeFromCfgTag sets the Viper decoder to read the names from the `cfg` tag
+// on each struct entry.
+func decodeFromCfgTag(c *mapstructure.DecoderConfig) {
+	c.TagName = "cfg"
+}
+
+// isUnexported checks if a field name starts with a lowercase letter and therefore
+// if it is unexported.
+func isUnexported(name string) bool {
+	if len(name) == 0 {
+		// This should never happen
+		panic("field name has len 0")
+	}
+
+	first := string(name[0])
+	return first == strings.ToLower(first)
+}
diff --git a/pkg/apis/options/load_test.go b/pkg/apis/options/load_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..492680187bc8480054fc5de1de26b9a7ac29cfc2
--- /dev/null
+++ b/pkg/apis/options/load_test.go
@@ -0,0 +1,300 @@
+package options
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+	"github.com/spf13/pflag"
+)
+
+func TestOptionsSuite(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Options Suite")
+}
+
+var _ = Describe("Load", func() {
+	Context("with a testOptions structure", func() {
+		type TestOptionSubStruct struct {
+			StringSliceOption []string `flag:"string-slice-option" cfg:"string_slice_option"`
+		}
+
+		type TestOptions struct {
+			StringOption string              `flag:"string-option" cfg:"string_option"`
+			Sub          TestOptionSubStruct `cfg:",squash"`
+			// Check exported but internal fields do not break loading
+			Internal *string `cfg:",internal"`
+			// Check unexported fields do not break loading
+			unexported string
+		}
+
+		type MissingSquashTestOptions struct {
+			StringOption string `flag:"string-option" cfg:"string_option"`
+			Sub          TestOptionSubStruct
+		}
+
+		type MissingCfgTestOptions struct {
+			StringOption string              `flag:"string-option"`
+			Sub          TestOptionSubStruct `cfg:",squash"`
+		}
+
+		type MissingFlagTestOptions struct {
+			StringOption string              `cfg:"string_option"`
+			Sub          TestOptionSubStruct `cfg:",squash"`
+		}
+
+		var testOptionsConfigBytes = []byte(`
+			string_option="foo"
+			string_slice_option="a,b,c,d"
+		`)
+
+		var testOptionsFlagSet *pflag.FlagSet
+
+		type testOptionsTableInput struct {
+			env            map[string]string
+			args           []string
+			configFile     []byte
+			flagSet        func() *pflag.FlagSet
+			expectedErr    error
+			input          interface{}
+			expectedOutput interface{}
+		}
+
+		BeforeEach(func() {
+			testOptionsFlagSet = pflag.NewFlagSet("testFlagSet", pflag.ExitOnError)
+			testOptionsFlagSet.String("string-option", "default", "")
+			testOptionsFlagSet.StringSlice("string-slice-option", []string{"a", "b"}, "")
+		})
+
+		DescribeTable("Load",
+			func(o *testOptionsTableInput) {
+				var configFileName string
+
+				if o.configFile != nil {
+					By("Creating a config file")
+					configFile, err := ioutil.TempFile("", "oauth2-proxy-test-legacy-config-file")
+					Expect(err).ToNot(HaveOccurred())
+					defer configFile.Close()
+
+					_, err = configFile.Write(o.configFile)
+					Expect(err).ToNot(HaveOccurred())
+					defer os.Remove(configFile.Name())
+
+					configFileName = configFile.Name()
+				}
+
+				if len(o.env) > 0 {
+					By("Setting environment variables")
+					for k, v := range o.env {
+						os.Setenv(k, v)
+						defer os.Unsetenv(k)
+					}
+				}
+
+				Expect(o.flagSet).ToNot(BeNil())
+				flagSet := o.flagSet()
+				Expect(flagSet).ToNot(BeNil())
+
+				if len(o.args) > 0 {
+					By("Parsing flag arguments")
+					Expect(flagSet.Parse(o.args)).To(Succeed())
+				}
+
+				var input interface{}
+				if o.input != nil {
+					input = o.input
+				} else {
+					input = &TestOptions{}
+				}
+				err := Load(configFileName, flagSet, input)
+				if o.expectedErr != nil {
+					Expect(err).To(MatchError(o.expectedErr.Error()))
+				} else {
+					Expect(err).ToNot(HaveOccurred())
+				}
+				Expect(input).To(Equal(o.expectedOutput))
+			},
+			Entry("with just a config file", &testOptionsTableInput{
+				configFile: testOptionsConfigBytes,
+				flagSet:    func() *pflag.FlagSet { return testOptionsFlagSet },
+				expectedOutput: &TestOptions{
+					StringOption: "foo",
+					Sub: TestOptionSubStruct{
+						StringSliceOption: []string{"a", "b", "c", "d"},
+					},
+				},
+			}),
+			Entry("when setting env variables", &testOptionsTableInput{
+				configFile: testOptionsConfigBytes,
+				env: map[string]string{
+					"OAUTH2_PROXY_STRING_OPTION":       "bar",
+					"OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c",
+				},
+				flagSet: func() *pflag.FlagSet { return testOptionsFlagSet },
+				expectedOutput: &TestOptions{
+					StringOption: "bar",
+					Sub: TestOptionSubStruct{
+						StringSliceOption: []string{"a", "b", "c"},
+					},
+				},
+			}),
+			Entry("when setting flags", &testOptionsTableInput{
+				configFile: testOptionsConfigBytes,
+				env: map[string]string{
+					"OAUTH2_PROXY_STRING_OPTION":       "bar",
+					"OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c",
+				},
+				args: []string{
+					"--string-option", "baz",
+					"--string-slice-option", "a,b,c,d,e",
+				},
+				flagSet: func() *pflag.FlagSet { return testOptionsFlagSet },
+				expectedOutput: &TestOptions{
+					StringOption: "baz",
+					Sub: TestOptionSubStruct{
+						StringSliceOption: []string{"a", "b", "c", "d", "e"},
+					},
+				},
+			}),
+			Entry("when setting flags multiple times", &testOptionsTableInput{
+				configFile: testOptionsConfigBytes,
+				env: map[string]string{
+					"OAUTH2_PROXY_STRING_OPTION":       "bar",
+					"OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c",
+				},
+				args: []string{
+					"--string-option", "baz",
+					"--string-slice-option", "x",
+					"--string-slice-option", "y",
+					"--string-slice-option", "z",
+				},
+				flagSet: func() *pflag.FlagSet { return testOptionsFlagSet },
+				expectedOutput: &TestOptions{
+					StringOption: "baz",
+					Sub: TestOptionSubStruct{
+						StringSliceOption: []string{"x", "y", "z"},
+					},
+				},
+			}),
+			Entry("when setting env variables without a config file", &testOptionsTableInput{
+				env: map[string]string{
+					"OAUTH2_PROXY_STRING_OPTION":       "bar",
+					"OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c",
+				},
+				flagSet: func() *pflag.FlagSet { return testOptionsFlagSet },
+				expectedOutput: &TestOptions{
+					StringOption: "bar",
+					Sub: TestOptionSubStruct{
+						StringSliceOption: []string{"a", "b", "c"},
+					},
+				},
+			}),
+			Entry("when setting flags without a config file", &testOptionsTableInput{
+				env: map[string]string{
+					"OAUTH2_PROXY_STRING_OPTION":       "bar",
+					"OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c",
+				},
+				args: []string{
+					"--string-option", "baz",
+					"--string-slice-option", "a,b,c,d,e",
+				},
+				flagSet: func() *pflag.FlagSet { return testOptionsFlagSet },
+				expectedOutput: &TestOptions{
+					StringOption: "baz",
+					Sub: TestOptionSubStruct{
+						StringSliceOption: []string{"a", "b", "c", "d", "e"},
+					},
+				},
+			}),
+			Entry("when setting flags without a config file", &testOptionsTableInput{
+				env: map[string]string{
+					"OAUTH2_PROXY_STRING_OPTION":       "bar",
+					"OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c",
+				},
+				args: []string{
+					"--string-option", "baz",
+					"--string-slice-option", "a,b,c,d,e",
+				},
+				flagSet: func() *pflag.FlagSet { return testOptionsFlagSet },
+				expectedOutput: &TestOptions{
+					StringOption: "baz",
+					Sub: TestOptionSubStruct{
+						StringSliceOption: []string{"a", "b", "c", "d", "e"},
+					},
+				},
+			}),
+			Entry("when nothing is set it should use flag defaults", &testOptionsTableInput{
+				flagSet: func() *pflag.FlagSet { return testOptionsFlagSet },
+				expectedOutput: &TestOptions{
+					StringOption: "default",
+					Sub: TestOptionSubStruct{
+						StringSliceOption: []string{"a", "b"},
+					},
+				},
+			}),
+			Entry("with an invalid config file", &testOptionsTableInput{
+				configFile:     []byte(`slice_option = foo`),
+				flagSet:        func() *pflag.FlagSet { return testOptionsFlagSet },
+				expectedErr:    fmt.Errorf("unable to load config file: While parsing config: (1, 16): never reached"),
+				expectedOutput: &TestOptions{},
+			}),
+			Entry("with an invalid flagset", &testOptionsTableInput{
+				flagSet: func() *pflag.FlagSet {
+					// Missing a flag
+					f := pflag.NewFlagSet("testFlagSet", pflag.ExitOnError)
+					f.String("string-option", "default", "")
+					return f
+				},
+				expectedErr:    fmt.Errorf("unable to register flags: field \"string-slice-option\" does not have a registered flag"),
+				expectedOutput: &TestOptions{},
+			}),
+			Entry("with an struct is missing the squash tag", &testOptionsTableInput{
+				flagSet:        func() *pflag.FlagSet { return testOptionsFlagSet },
+				expectedErr:    fmt.Errorf("unable to register flags: field \".Sub\" does not have required cfg tag: `,squash`"),
+				input:          &MissingSquashTestOptions{},
+				expectedOutput: &MissingSquashTestOptions{},
+			}),
+			Entry("with a field is missing the cfg tag", &testOptionsTableInput{
+				flagSet:        func() *pflag.FlagSet { return testOptionsFlagSet },
+				expectedErr:    fmt.Errorf("unable to register flags: field \".StringOption\" does not have required tags (cfg, flag)"),
+				input:          &MissingCfgTestOptions{},
+				expectedOutput: &MissingCfgTestOptions{},
+			}),
+			Entry("with a field is missing the flag tag", &testOptionsTableInput{
+				flagSet:        func() *pflag.FlagSet { return testOptionsFlagSet },
+				expectedErr:    fmt.Errorf("unable to register flags: field \".StringOption\" does not have required tags (cfg, flag)"),
+				input:          &MissingFlagTestOptions{},
+				expectedOutput: &MissingFlagTestOptions{},
+			}),
+			Entry("with existing unexported fields", &testOptionsTableInput{
+				flagSet: func() *pflag.FlagSet { return testOptionsFlagSet },
+				input: &TestOptions{
+					unexported: "unexported",
+				},
+				expectedOutput: &TestOptions{
+					StringOption: "default",
+					Sub: TestOptionSubStruct{
+						StringSliceOption: []string{"a", "b"},
+					},
+					unexported: "unexported",
+				},
+			}),
+			Entry("with an unknown option in the config file", &testOptionsTableInput{
+				configFile:  []byte(`unknown_option="foo"`),
+				flagSet:     func() *pflag.FlagSet { return testOptionsFlagSet },
+				expectedErr: fmt.Errorf("error unmarshalling config: 1 error(s) decoding:\n\n* '' has invalid keys: unknown_option"),
+				// Viper will unmarshal before returning the error, so this is the default output
+				expectedOutput: &TestOptions{
+					StringOption: "default",
+					Sub: TestOptionSubStruct{
+						StringSliceOption: []string{"a", "b"},
+					},
+				},
+			}),
+		)
+	})
+})
diff --git a/pkg/apis/options/sessions.go b/pkg/apis/options/sessions.go
index 3e222780801fcabe4e739a510dad6eef05ca4e2d..fd69d6095932a4accfb563071d9bc85f86db7dab 100644
--- a/pkg/apis/options/sessions.go
+++ b/pkg/apis/options/sessions.go
@@ -4,31 +4,27 @@ 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"`
-	Cipher *encryption.Cipher
-	CookieStoreOptions
-	RedisStoreOptions
+	Type   string             `flag:"session-store-type" cfg:"session_store_type" env:"OAUTH2_PROXY_SESSION_STORE_TYPE"`
+	Cipher *encryption.Cipher `cfg:",internal"`
+	Redis  RedisStoreOptions  `cfg:",squash"`
 }
 
 // CookieSessionStoreType is used to indicate the CookieSessionStore should be
 // used for storing sessions.
 var CookieSessionStoreType = "cookie"
 
-// CookieStoreOptions contains configuration options for the CookieSessionStore.
-type CookieStoreOptions struct{}
-
 // RedisSessionStoreType is used to indicate the RedisSessionStore should be
 // used for storing sessions.
 var RedisSessionStoreType = "redis"
 
 // RedisStoreOptions contains configuration options for the RedisSessionStore.
 type RedisStoreOptions struct {
-	RedisConnectionURL     string   `flag:"redis-connection-url" cfg:"redis_connection_url" env:"OAUTH2_PROXY_REDIS_CONNECTION_URL"`
+	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"`
-	RedisCAPath            string   `flag:"redis-ca-path" cfg:"redis_ca_path" env:"OAUTH2_PROXY_REDIS_CA_PATH"`
-	RedisInsecureTLS       bool     `flag:"redis-insecure-skip-tls-verify" cfg:"redis_insecure_skip_tls_verify" env:"OAUTH2_PROXY_REDIS_INSECURE_SKIP_TLS_VERIFY"`
+	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"`
 }
diff --git a/pkg/cookies/cookies.go b/pkg/cookies/cookies.go
index 7e31464eeb910bc045a113debf68000a134d0ccc..5235bb66ced784918a34415a4b61c1ee6fbed870 100644
--- a/pkg/cookies/cookies.go
+++ b/pkg/cookies/cookies.go
@@ -38,19 +38,19 @@ func MakeCookie(req *http.Request, name string, value string, path string, domai
 
 // MakeCookieFromOptions constructs a cookie based on the given *options.CookieOptions,
 // value and creation time
-func MakeCookieFromOptions(req *http.Request, name string, value string, opts *options.CookieOptions, expiration time.Duration, now time.Time) *http.Cookie {
-	domain := GetCookieDomain(req, opts.CookieDomains)
+func MakeCookieFromOptions(req *http.Request, name string, value string, cookieOpts *options.CookieOptions, expiration time.Duration, now time.Time) *http.Cookie {
+	domain := GetCookieDomain(req, cookieOpts.Domains)
 
 	if domain != "" {
-		return MakeCookie(req, name, value, opts.CookiePath, domain, opts.CookieHTTPOnly, opts.CookieSecure, expiration, now, ParseSameSite(opts.CookieSameSite))
+		return MakeCookie(req, name, value, cookieOpts.Path, domain, cookieOpts.HTTPOnly, cookieOpts.Secure, expiration, now, ParseSameSite(cookieOpts.SameSite))
 	}
 	// If nothing matches, create the cookie with the shortest domain
-	logger.Printf("Warning: request host %q did not match any of the specific cookie domains of %q", GetRequestHost(req), strings.Join(opts.CookieDomains, ","))
+	logger.Printf("Warning: request host %q did not match any of the specific cookie domains of %q", GetRequestHost(req), strings.Join(cookieOpts.Domains, ","))
 	defaultDomain := ""
-	if len(opts.CookieDomains) > 0 {
-		defaultDomain = opts.CookieDomains[len(opts.CookieDomains)-1]
+	if len(cookieOpts.Domains) > 0 {
+		defaultDomain = cookieOpts.Domains[len(cookieOpts.Domains)-1]
 	}
-	return MakeCookie(req, name, value, opts.CookiePath, defaultDomain, opts.CookieHTTPOnly, opts.CookieSecure, expiration, now, ParseSameSite(opts.CookieSameSite))
+	return MakeCookie(req, name, value, cookieOpts.Path, defaultDomain, cookieOpts.HTTPOnly, cookieOpts.Secure, expiration, now, ParseSameSite(cookieOpts.SameSite))
 }
 
 // GetCookieDomain returns the correct cookie domain given a list of domains
diff --git a/pkg/sessions/cookie/session_store.go b/pkg/sessions/cookie/session_store.go
index 1ef0134e4c950aff3fbcc7a2e6fefea6de3599fc..a01d8050541c3f5b4cd38da3dd907e69ce8eca9b 100644
--- a/pkg/sessions/cookie/session_store.go
+++ b/pkg/sessions/cookie/session_store.go
@@ -49,12 +49,12 @@ func (s *SessionStore) Save(rw http.ResponseWriter, req *http.Request, ss *sessi
 // Load reads sessions.SessionState information from Cookies within the
 // HTTP request object
 func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
-	c, err := loadCookie(req, s.CookieOptions.CookieName)
+	c, err := loadCookie(req, s.CookieOptions.Name)
 	if err != nil {
 		// always http.ErrNoCookie
-		return nil, fmt.Errorf("cookie %q not present", s.CookieOptions.CookieName)
+		return nil, fmt.Errorf("cookie %q not present", s.CookieOptions.Name)
 	}
-	val, _, ok := encryption.Validate(c, s.CookieOptions.CookieSecret, s.CookieOptions.CookieExpire)
+	val, _, ok := encryption.Validate(c, s.CookieOptions.Secret, s.CookieOptions.Expire)
 	if !ok {
 		return nil, errors.New("cookie signature not valid")
 	}
@@ -70,7 +70,7 @@ func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
 // clear the session
 func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
 	// matches CookieName, CookieName_<number>
-	var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", s.CookieOptions.CookieName))
+	var cookieNameRegex = regexp.MustCompile(fmt.Sprintf("^%s(_\\d+)?$", s.CookieOptions.Name))
 
 	for _, c := range req.Cookies() {
 		if cookieNameRegex.MatchString(c.Name) {
@@ -94,10 +94,10 @@ func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Reques
 // authentication details
 func (s *SessionStore) makeSessionCookie(req *http.Request, value string, now time.Time) []*http.Cookie {
 	if value != "" {
-		value = encryption.SignedValue(s.CookieOptions.CookieSecret, s.CookieOptions.CookieName, value, now)
+		value = encryption.SignedValue(s.CookieOptions.Secret, s.CookieOptions.Name, value, now)
 	}
-	c := s.makeCookie(req, s.CookieOptions.CookieName, value, s.CookieOptions.CookieExpire, now)
-	if len(c.Value) > 4096-len(s.CookieOptions.CookieName) {
+	c := s.makeCookie(req, s.CookieOptions.Name, value, s.CookieOptions.Expire, now)
+	if len(c.Value) > 4096-len(s.CookieOptions.Name) {
 		return splitCookie(c)
 	}
 	return []*http.Cookie{c}
diff --git a/pkg/sessions/redis/redis_store.go b/pkg/sessions/redis/redis_store.go
index 4d6ce9cf6b8dccf3f642d3ca93549225484af1f5..7737b960b4289c8bfe39802a8326b8924fb99f95 100644
--- a/pkg/sessions/redis/redis_store.go
+++ b/pkg/sessions/redis/redis_store.go
@@ -40,7 +40,7 @@ type SessionStore struct {
 // NewRedisSessionStore initialises a new instance of the SessionStore from
 // the configuration given
 func NewRedisSessionStore(opts *options.SessionOptions, cookieOpts *options.CookieOptions) (sessions.SessionStore, error) {
-	client, err := newRedisCmdable(opts.RedisStoreOptions)
+	client, err := newRedisCmdable(opts.Redis)
 	if err != nil {
 		return nil, fmt.Errorf("error constructing redis client: %v", err)
 	}
@@ -74,16 +74,16 @@ func newRedisCmdable(opts options.RedisStoreOptions) (Client, error) {
 		return newClusterClient(client), nil
 	}
 
-	opt, err := redis.ParseURL(opts.RedisConnectionURL)
+	opt, err := redis.ParseURL(opts.ConnectionURL)
 	if err != nil {
 		return nil, fmt.Errorf("unable to parse redis url: %s", err)
 	}
 
-	if opts.RedisInsecureTLS {
+	if opts.InsecureSkipTLSVerify {
 		opt.TLSConfig.InsecureSkipVerify = true
 	}
 
-	if opts.RedisCAPath != "" {
+	if opts.CAPath != "" {
 		rootCAs, err := x509.SystemCertPool()
 		if err != nil {
 			logger.Printf("failed to load system cert pool for redis connection, falling back to empty cert pool")
@@ -91,9 +91,9 @@ func newRedisCmdable(opts options.RedisStoreOptions) (Client, error) {
 		if rootCAs == nil {
 			rootCAs = x509.NewCertPool()
 		}
-		certs, err := ioutil.ReadFile(opts.RedisCAPath)
+		certs, err := ioutil.ReadFile(opts.CAPath)
 		if err != nil {
-			return nil, fmt.Errorf("failed to load %q, %v", opts.RedisCAPath, err)
+			return nil, fmt.Errorf("failed to load %q, %v", opts.CAPath, err)
 		}
 
 		// Append our cert to the system pool
@@ -117,13 +117,13 @@ func (store *SessionStore) Save(rw http.ResponseWriter, req *http.Request, s *se
 
 	// Old sessions that we are refreshing would have a request cookie
 	// New sessions don't, so we ignore the error. storeValue will check requestCookie
-	requestCookie, _ := req.Cookie(store.CookieOptions.CookieName)
+	requestCookie, _ := req.Cookie(store.CookieOptions.Name)
 	value, err := s.EncodeSessionState(store.CookieCipher)
 	if err != nil {
 		return err
 	}
 	ctx := req.Context()
-	ticketString, err := store.storeValue(ctx, value, store.CookieOptions.CookieExpire, requestCookie)
+	ticketString, err := store.storeValue(ctx, value, store.CookieOptions.Expire, requestCookie)
 	if err != nil {
 		return err
 	}
@@ -131,7 +131,7 @@ func (store *SessionStore) Save(rw http.ResponseWriter, req *http.Request, s *se
 	ticketCookie := store.makeCookie(
 		req,
 		ticketString,
-		store.CookieOptions.CookieExpire,
+		store.CookieOptions.Expire,
 		s.CreatedAt,
 	)
 
@@ -142,12 +142,12 @@ func (store *SessionStore) Save(rw http.ResponseWriter, req *http.Request, s *se
 // Load reads sessions.SessionState information from a ticket
 // cookie within the HTTP request object
 func (store *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
-	requestCookie, err := req.Cookie(store.CookieOptions.CookieName)
+	requestCookie, err := req.Cookie(store.CookieOptions.Name)
 	if err != nil {
 		return nil, fmt.Errorf("error loading session: %s", err)
 	}
 
-	val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
+	val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.Secret, store.CookieOptions.Expire)
 	if !ok {
 		return nil, fmt.Errorf("cookie signature not valid")
 	}
@@ -161,12 +161,12 @@ func (store *SessionStore) Load(req *http.Request) (*sessions.SessionState, erro
 
 // loadSessionFromString loads the session based on the ticket value
 func (store *SessionStore) loadSessionFromString(ctx context.Context, value string) (*sessions.SessionState, error) {
-	ticket, err := decodeTicket(store.CookieOptions.CookieName, value)
+	ticket, err := decodeTicket(store.CookieOptions.Name, value)
 	if err != nil {
 		return nil, err
 	}
 
-	resultBytes, err := store.Client.Get(ctx, ticket.asHandle(store.CookieOptions.CookieName))
+	resultBytes, err := store.Client.Get(ctx, ticket.asHandle(store.CookieOptions.Name))
 	if err != nil {
 		return nil, err
 	}
@@ -199,7 +199,7 @@ func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro
 	http.SetCookie(rw, clearCookie)
 
 	// If there was an existing cookie we should clear the session in redis
-	requestCookie, err := req.Cookie(store.CookieOptions.CookieName)
+	requestCookie, err := req.Cookie(store.CookieOptions.Name)
 	if err != nil && err == http.ErrNoCookie {
 		// No existing cookie so can't clear redis
 		return nil
@@ -207,17 +207,17 @@ func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro
 		return fmt.Errorf("error retrieving cookie: %v", err)
 	}
 
-	val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
+	val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.Secret, store.CookieOptions.Expire)
 	if !ok {
 		return fmt.Errorf("cookie signature not valid")
 	}
 
 	// We only return an error if we had an issue with redis
 	// If there's an issue decoding the ticket, ignore it
-	ticket, _ := decodeTicket(store.CookieOptions.CookieName, val)
+	ticket, _ := decodeTicket(store.CookieOptions.Name, val)
 	if ticket != nil {
 		ctx := req.Context()
-		err := store.Client.Del(ctx, ticket.asHandle(store.CookieOptions.CookieName))
+		err := store.Client.Del(ctx, ticket.asHandle(store.CookieOptions.Name))
 		if err != nil {
 			return fmt.Errorf("error clearing cookie from redis: %s", err)
 		}
@@ -228,11 +228,11 @@ func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro
 // makeCookie makes a cookie, signing the value if present
 func (store *SessionStore) makeCookie(req *http.Request, value string, expires time.Duration, now time.Time) *http.Cookie {
 	if value != "" {
-		value = encryption.SignedValue(store.CookieOptions.CookieSecret, store.CookieOptions.CookieName, value, now)
+		value = encryption.SignedValue(store.CookieOptions.Secret, store.CookieOptions.Name, value, now)
 	}
 	return cookies.MakeCookieFromOptions(
 		req,
-		store.CookieOptions.CookieName,
+		store.CookieOptions.Name,
 		value,
 		store.CookieOptions,
 		expires,
@@ -256,12 +256,12 @@ func (store *SessionStore) storeValue(ctx context.Context, value string, expirat
 	stream := cipher.NewCFBEncrypter(block, ticket.Secret)
 	stream.XORKeyStream(ciphertext, []byte(value))
 
-	handle := ticket.asHandle(store.CookieOptions.CookieName)
+	handle := ticket.asHandle(store.CookieOptions.Name)
 	err = store.Client.Set(ctx, handle, ciphertext, expiration)
 	if err != nil {
 		return "", err
 	}
-	return ticket.encodeTicket(store.CookieOptions.CookieName), nil
+	return ticket.encodeTicket(store.CookieOptions.Name), nil
 }
 
 // getTicket retrieves an existing ticket from the cookie if present,
@@ -272,14 +272,14 @@ func (store *SessionStore) getTicket(requestCookie *http.Cookie) (*TicketData, e
 	}
 
 	// An existing cookie exists, try to retrieve the ticket
-	val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.CookieSecret, store.CookieOptions.CookieExpire)
+	val, _, ok := encryption.Validate(requestCookie, store.CookieOptions.Secret, store.CookieOptions.Expire)
 	if !ok {
 		// Cookie is invalid, create a new ticket
 		return newTicket()
 	}
 
 	// Valid cookie, decode the ticket
-	ticket, err := decodeTicket(store.CookieOptions.CookieName, val)
+	ticket, err := decodeTicket(store.CookieOptions.Name, val)
 	if err != nil {
 		// If we can't decode the ticket we have to create a new one
 		return newTicket()
diff --git a/pkg/sessions/session_store_test.go b/pkg/sessions/session_store_test.go
index 14707a2c960080f68a1e9bfa2d9d96e8db65383e..ded609ec3a81149f340d7b9aed6c602dc354cedc 100644
--- a/pkg/sessions/session_store_test.go
+++ b/pkg/sessions/session_store_test.go
@@ -47,25 +47,25 @@ var _ = Describe("NewSessionStore", func() {
 
 			It("have the correct name set", func() {
 				if len(cookies) == 1 {
-					Expect(cookies[0].Name).To(Equal(cookieOpts.CookieName))
+					Expect(cookies[0].Name).To(Equal(cookieOpts.Name))
 				} else {
 					for _, cookie := range cookies {
-						Expect(cookie.Name).To(ContainSubstring(cookieOpts.CookieName))
+						Expect(cookie.Name).To(ContainSubstring(cookieOpts.Name))
 					}
 				}
 			})
 
 			It("have the correct path set", func() {
 				for _, cookie := range cookies {
-					Expect(cookie.Path).To(Equal(cookieOpts.CookiePath))
+					Expect(cookie.Path).To(Equal(cookieOpts.Path))
 				}
 			})
 
 			It("have the correct domain set", func() {
 				for _, cookie := range cookies {
 					specifiedDomain := ""
-					if len(cookieOpts.CookieDomains) > 0 {
-						specifiedDomain = cookieOpts.CookieDomains[0]
+					if len(cookieOpts.Domains) > 0 {
+						specifiedDomain = cookieOpts.Domains[0]
 					}
 					Expect(cookie.Domain).To(Equal(specifiedDomain))
 				}
@@ -73,19 +73,19 @@ var _ = Describe("NewSessionStore", func() {
 
 			It("have the correct HTTPOnly set", func() {
 				for _, cookie := range cookies {
-					Expect(cookie.HttpOnly).To(Equal(cookieOpts.CookieHTTPOnly))
+					Expect(cookie.HttpOnly).To(Equal(cookieOpts.HTTPOnly))
 				}
 			})
 
 			It("have the correct secure set", func() {
 				for _, cookie := range cookies {
-					Expect(cookie.Secure).To(Equal(cookieOpts.CookieSecure))
+					Expect(cookie.Secure).To(Equal(cookieOpts.Secure))
 				}
 			})
 
 			It("have the correct SameSite set", func() {
 				for _, cookie := range cookies {
-					Expect(cookie.SameSite).To(Equal(cookiesapi.ParseSameSite(cookieOpts.CookieSameSite)))
+					Expect(cookie.SameSite).To(Equal(cookiesapi.ParseSameSite(cookieOpts.SameSite)))
 				}
 			})
 
@@ -168,8 +168,8 @@ var _ = Describe("NewSessionStore", func() {
 				BeforeEach(func() {
 					By("Using a valid cookie with a different providers session encoding")
 					broken := "BrokenSessionFromADifferentSessionImplementation"
-					value := encryption.SignedValue(cookieOpts.CookieSecret, cookieOpts.CookieName, broken, time.Now())
-					cookie := cookiesapi.MakeCookieFromOptions(request, cookieOpts.CookieName, value, cookieOpts, cookieOpts.CookieExpire, time.Now())
+					value := encryption.SignedValue(cookieOpts.Secret, cookieOpts.Name, broken, time.Now())
+					cookie := cookiesapi.MakeCookieFromOptions(request, cookieOpts.Name, value, cookieOpts, cookieOpts.Expire, time.Now())
 					request.AddCookie(cookie)
 
 					err := ss.Save(response, request, session)
@@ -245,7 +245,7 @@ var _ = Describe("NewSessionStore", func() {
 				})
 
 				It("loads a session equal to the original session", func() {
-					if cookieOpts.CookieSecret == "" {
+					if cookieOpts.Secret == "" {
 						// Only Email and User stored in session when encrypted
 						Expect(loadedSession.Email).To(Equal(session.Email))
 						Expect(loadedSession.User).To(Equal(session.User))
@@ -290,7 +290,7 @@ var _ = Describe("NewSessionStore", func() {
 					BeforeEach(func() {
 						switch ss.(type) {
 						case *redis.SessionStore:
-							mr.FastForward(cookieOpts.CookieRefresh + time.Minute)
+							mr.FastForward(cookieOpts.Refresh + time.Minute)
 						}
 					})
 
@@ -304,7 +304,7 @@ var _ = Describe("NewSessionStore", func() {
 					BeforeEach(func() {
 						switch ss.(type) {
 						case *redis.SessionStore:
-							mr.FastForward(cookieOpts.CookieExpire + time.Minute)
+							mr.FastForward(cookieOpts.Expire + time.Minute)
 						}
 
 						loadedSession, err = ss.Load(request)
@@ -341,14 +341,14 @@ var _ = Describe("NewSessionStore", func() {
 		Context("with non-default options", func() {
 			BeforeEach(func() {
 				cookieOpts = &options.CookieOptions{
-					CookieName:     "_cookie_name",
-					CookiePath:     "/path",
-					CookieExpire:   time.Duration(72) * time.Hour,
-					CookieRefresh:  time.Duration(2) * time.Hour,
-					CookieSecure:   false,
-					CookieHTTPOnly: false,
-					CookieDomains:  []string{"example.com"},
-					CookieSameSite: "strict",
+					Name:     "_cookie_name",
+					Path:     "/path",
+					Expire:   time.Duration(72) * time.Hour,
+					Refresh:  time.Duration(2) * time.Hour,
+					Secure:   false,
+					HTTPOnly: false,
+					Domains:  []string{"example.com"},
+					SameSite: "strict",
 				}
 
 				var err error
@@ -364,8 +364,8 @@ var _ = Describe("NewSessionStore", func() {
 				secret := make([]byte, 32)
 				_, err := rand.Read(secret)
 				Expect(err).ToNot(HaveOccurred())
-				cookieOpts.CookieSecret = base64.URLEncoding.EncodeToString(secret)
-				cipher, err := encryption.NewCipher(utils.SecretBytes(cookieOpts.CookieSecret))
+				cookieOpts.Secret = base64.URLEncoding.EncodeToString(secret)
+				cipher, err := encryption.NewCipher(utils.SecretBytes(cookieOpts.Secret))
 				Expect(err).ToNot(HaveOccurred())
 				Expect(cipher).ToNot(BeNil())
 				opts.Cipher = cipher
@@ -384,13 +384,13 @@ var _ = Describe("NewSessionStore", func() {
 
 		// Set default options in CookieOptions
 		cookieOpts = &options.CookieOptions{
-			CookieName:     "_oauth2_proxy",
-			CookiePath:     "/",
-			CookieExpire:   time.Duration(168) * time.Hour,
-			CookieRefresh:  time.Duration(1) * time.Hour,
-			CookieSecure:   true,
-			CookieHTTPOnly: true,
-			CookieSameSite: "",
+			Name:     "_oauth2_proxy",
+			Path:     "/",
+			Expire:   time.Duration(168) * time.Hour,
+			Refresh:  time.Duration(1) * time.Hour,
+			Secure:   true,
+			HTTPOnly: true,
+			SameSite: "",
 		}
 
 		session = &sessionsapi.SessionState{
@@ -428,7 +428,7 @@ var _ = Describe("NewSessionStore", func() {
 			mr, err = miniredis.Run()
 			Expect(err).ToNot(HaveOccurred())
 			opts.Type = options.RedisSessionStoreType
-			opts.RedisConnectionURL = "redis://" + mr.Addr()
+			opts.Redis.ConnectionURL = "redis://" + mr.Addr()
 		})
 
 		AfterEach(func() {