Repository: oauth2-proxy/oauth2-proxy
Pull request #484 (2020-04-12 13:17:49)

Replace configuration loading with Viper
<!--- Provide a general summary of your changes in the Title above -->

## Description

<!--- Describe your changes in detail -->
This is the first in a series of PRs that I'm going to introduce to rework our configuration loading to use Viper and PFlag instead of our more legacy configuration.

This currently retains all existing behaviour of options loading but does the following:
- Uses Viper instead of go-options to merge options (with the same preference)
- Uses AutoEnv so we will be able to remove the env tags from the Options struct
- Tests the loading behaviour of our config (which we didn't really have before)
- Will eventually allow us to remove the slice flag code we use as PFlag has this built in
- Returns an error when loading if an options is provided in the config file that does not match the options struct (Fixes: #500)

## 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. -->

Code clean up/ make config easier (long term goal is for config to be structured)

## 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. -->

Built and ran the code locally setting variables from config file, env and flags, existing behaviour is retained

Not yet tested, but added a test to check the environment will be loaded correctly

## 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.


JoelSpeed: I need to verify that this doesn't cause any issues with flag merging since that's not in our codebase
JoelSpeed: I verified this does in fact work as expected and retains the original behaviour, I thought I had commented but it seems not 🤔 
JoelSpeed: I had left the tags in for now, with the intention of removing them in a follow up PR (#489)
JoelSpeed: One thought I've just had is that this will make flags require `--` rather than allowing either `-` or `--` as in the regular flag package, I'll need to add a note to the changelog that this is a breaking change

I don't think this is too bad of a change, not sure 🤔 
steakunderscore: > One thought I've just had is that this will make flags require `--` rather than allowing either `-` or `--` as in the regular flag package, I'll need to add a note to the changelog that this is a breaking change
I'd be fine with this so long as a breaking change note is added.
steakunderscore: @JoelSpeed Ping me when you have updated this so we can get it merged

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f6301e9..e2a026b0 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 058b90c6..00000000
--- 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/go.mod b/go.mod
index 2170dc9b..618c7c73 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 5d4662da..45e2be1b 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 91111eaa..698c64ad 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 1e9bb7ca..587215fd 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/options.go b/options.go
index 50dee28b..b59c2b14 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/pkg/apis/options/cookie.go b/pkg/apis/options/cookie.go
index 4d267731..2cb77eb6 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 00000000..aeb39b9a
--- /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/sessions.go b/pkg/apis/options/sessions.go
index 3e222780..fd69d609 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 7e31464e..5235bb66 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 1ef0134e..a01d8050 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 4d6ce9cf..7737b960 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()


Test patch:
diff --git a/env_options_test.go b/env_options_test.go
deleted file mode 100644
index eb72a83e..00000000
--- 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/oauthproxy_test.go b/oauthproxy_test.go
index 47b27743..957b4334 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_test.go b/options_test.go
index a6bbc9ce..b14c9e46 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/load_test.go b/pkg/apis/options/load_test.go
new file mode 100644
index 00000000..49268018
--- /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/sessions/session_store_test.go b/pkg/sessions/session_store_test.go
index 14707a2c..ded609ec 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() {