git.iko.soy/typeable/oauth2-proxy
on master help sign in
Multiple providers in alpha config (#947)

* Initial commit of multiple provider logic: 1. Created new provider options. 2. Created legacy provider options and conversion options. 3. Added Providers to alpha Options. 4. Started Validation migration of multiple providers 5. Tests. * fixed lint issues * additional lint fixes * Nits and alterations based on CR: manliy splitting large providers validation function and adding comments to provider options * fixed typo * removed weird : file * small CR changes * Removed GoogleGroups validation due to new allowed-groups (including tests). Added line in CHANGELOG * Update pkg/apis/options/providers.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/apis/options/providers.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/apis/options/providers.go Co-authored-by: Nick Meves <nick.meves@greenhouse.io> * Initial commit of multiple provider logic: 1. Created new provider options. 2. Created legacy provider options and conversion options. 3. Added Providers to alpha Options. 4. Started Validation migration of multiple providers 5. Tests. * fixed lint issues * additional lint fixes * Nits and alterations based on CR: manliy splitting large providers validation function and adding comments to provider options * small CR changes * auto generates alpha_config.md * rebase (mainly service alpha options related conflicts) * removed : * Nits and alterations based on CR: manliy splitting large providers validation function and adding comments to provider options * small CR changes * Removed GoogleGroups validation due to new allowed-groups (including tests). Added line in CHANGELOG * "cntd. rebase" * ran make generate again * last conflicts * removed duplicate client id validation * 1. Removed provider prefixes 2. altered optionsWithNilProvider logic 3. altered default provider logic 4. moved change in CHANELOG to 7.0.0 * fixed TestGoogleGroupOptions test * ran make generate * moved CHANGLOG line to 7.1.1 * moved changelog comment to 7.1.2 (additional rebase) Co-authored-by: Yana Segal <yana.segal@nielsen.com> Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> Co-authored-by: Nick Meves <nick.meves@greenhouse.io>

commit42475c28f7f235c08777a24027fe8ec170763482 authoryanasega <yanasegal90@gmail.com> dateSat 3 Apr 2021, 19:06 (6 years ago) parent9d20b4e
18 files changed+1014−265
CHANGELOG.md+2 contrib/local-environment/oauth2-proxy-alpha-config.cfg−5 contrib/local-environment/oauth2-proxy-alpha-config.yaml+6 docs/docs/configuration/alpha_config.md+141 go.sum+1 main_test.go+33−9 oauthproxy.go+3−3 oauthproxy_test.go+7−6 pkg/apis/options/alpha_options.go+6 pkg/apis/options/legacy_options.go+209 pkg/apis/options/legacy_options_test.go+90−5 pkg/apis/options/load_test.go+45−2 pkg/apis/options/options.go+18−111 pkg/apis/options/providers.go+180 pkg/validation/options.go+83−106 pkg/validation/options_test.go+23−18 pkg/validation/providers.go+83 pkg/validation/providers_test.go+84
CHANGELOG.md+2
@@ -8,6 +8,8 @@
88
99## Changes since v7.1.2
1010
11+- [#947](https://github.com/oauth2-proxy/oauth2-proxy/pull/947) Multiple provider ingestion and validation in alpha options (first stage: [#926](https://github.com/oauth2-proxy/oauth2-proxy/issues/926)) (@yanasega)
12+
1113# V7.1.2
1214
1315## Release Highlights
contrib/local-environment/oauth2-proxy-alpha-config.cfg−5
@@ -1,10 +1,5 @@
11http_address="0.0.0.0:4180"
22cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
3−provider="oidc"
43email_domains="example.com"
5−oidc_issuer_url="http://dex.localhost:4190/dex"
6−client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
7−client_id="oauth2-proxy"
84cookie_secure="false"
9−
105redirect_url="http://localhost:4180/oauth2/callback"
contrib/local-environment/oauth2-proxy-alpha-config.yaml+6
@@ -15,3 +15,9 @@ injectRequestHeaders:
1515- name: X-Forwarded-Preferred-Username
1616 values:
1717 - claim: preferred_username
18+providers:
19+- provider: oidc
20+ clientSecret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
21+ clientID: oauth2-proxy
22+ oidcConfig:
23+ oidcIssuerURL: http://dex.localhost:4190/dex
docs/docs/configuration/alpha_config.md+141
@@ -119,6 +119,28 @@ They may change between releases without notice.
119119| `injectResponseHeaders` | _[[]Header](#header)_ | InjectResponseHeaders is used to configure headers that should be added<br/>to responses from the proxy.<br/>This is typically used when using the proxy as an external authentication<br/>provider in conjunction with another proxy such as NGINX and its<br/>auth_request module.<br/>Headers may source values from either the authenticated user's session<br/>or from a static secret value. |
120120| `server` | _[Server](#server)_ | Server is used to configure the HTTP(S) server for the proxy application.<br/>You may choose to run both HTTP and HTTPS servers simultaneously.<br/>This can be done by setting the BindAddress and the SecureBindAddress simultaneously.<br/>To use the secure server you must configure a TLS certificate and key. |
121121| `metricsServer` | _[Server](#server)_ | MetricsServer is used to configure the HTTP(S) server for metrics.<br/>You may choose to run both HTTP and HTTPS servers simultaneously.<br/>This can be done by setting the BindAddress and the SecureBindAddress simultaneously.<br/>To use the secure server you must configure a TLS certificate and key. |
122+| `providers` | _[Providers](#providers)_ | Providers is used to configure multiple providers. |
123+
124+### AzureOptions
125+
126+(**Appears on:** [Provider](#provider))
127+
128+
129+
130+| Field | Type | Description |
131+| ----- | ---- | ----------- |
132+| `tenant` | _string_ | Tenant directs to a tenant-specific or common (tenant-independent) endpoint<br/>Default value is 'commmon' |
133+
134+### BitbucketOptions
135+
136+(**Appears on:** [Provider](#provider))
137+
138+
139+
140+| Field | Type | Description |
141+| ----- | ---- | ----------- |
142+| `team` | _string_ | Team sets restrict logins to members of this team |
143+| `repository` | _string_ | Repository sets restrict logins to user with access to this repository |
122144
123145### ClaimSource
124146
@@ -143,6 +165,43 @@ each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45
143165Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
144166
145167
168+### GitHubOptions
169+
170+(**Appears on:** [Provider](#provider))
171+
172+
173+
174+| Field | Type | Description |
175+| ----- | ---- | ----------- |
176+| `org` | _string_ | Org sets restrict logins to members of this organisation |
177+| `team` | _string_ | Team sets restrict logins to members of this team |
178+| `repo` | _string_ | Repo sets restrict logins to collaborators of this repository |
179+| `token` | _string_ | Token is the token to use when verifying repository collaborators<br/>it must have push access to the repository |
180+| `users` | _[]string_ | Users allows users with these usernames to login<br/>even if they do not belong to the specified org and team or collaborators |
181+
182+### GitLabOptions
183+
184+(**Appears on:** [Provider](#provider))
185+
186+
187+
188+| Field | Type | Description |
189+| ----- | ---- | ----------- |
190+| `group` | _[]string_ | Group sets restrict logins to members of this group |
191+| `projects` | _[]string_ | Projects restricts logins to members of any of these projects |
192+
193+### GoogleOptions
194+
195+(**Appears on:** [Provider](#provider))
196+
197+
198+
199+| Field | Type | Description |
200+| ----- | ---- | ----------- |
201+| `group` | _[]string_ | Groups sets restrict logins to members of this google group |
202+| `adminEmail` | _string_ | AdminEmail is the google admin to impersonate for api calls |
203+| `serviceAccountJson` | _string_ | ServiceAccountJSON is the path to the service account json credentials |
204+
146205### Header
147206
148207(**Appears on:** [AlphaOptions](#alphaoptions))
@@ -172,6 +231,88 @@ make up the header value
172231| `prefix` | _string_ | Prefix is an optional prefix that will be prepended to the value of the<br/>claim if it is non-empty. |
173232| `basicAuthPassword` | _[SecretSource](#secretsource)_ | BasicAuthPassword converts this claim into a basic auth header.<br/>Note the value of claim will become the basic auth username and the<br/>basicAuthPassword will be used as the password value. |
174233
234+### KeycloakOptions
235+
236+(**Appears on:** [Provider](#provider))
237+
238+
239+
240+| Field | Type | Description |
241+| ----- | ---- | ----------- |
242+| `groups` | _[]string_ | Group enables to restrict login to members of indicated group |
243+
244+### LoginGovOptions
245+
246+(**Appears on:** [Provider](#provider))
247+
248+
249+
250+| Field | Type | Description |
251+| ----- | ---- | ----------- |
252+| `jwtKey` | _string_ | JWTKey is a private key in PEM format used to sign JWT, |
253+| `jwtKeyFile` | _string_ | JWTKeyFile is a path to the private key file in PEM format used to sign the JWT |
254+| `pubjwkURL` | _string_ | PubJWKURL is the JWK pubkey access endpoint |
255+
256+### OIDCOptions
257+
258+(**Appears on:** [Provider](#provider))
259+
260+
261+
262+| Field | Type | Description |
263+| ----- | ---- | ----------- |
264+| `issuerURL` | _string_ | IssuerURL is the OpenID Connect issuer URL<br/>eg: https://accounts.google.com |
265+| `insecureAllowUnverifiedEmail` | _bool_ | InsecureAllowUnverifiedEmail prevents failures if an email address in an id_token is not verified<br/>default set to 'false' |
266+| `insecureSkipIssuerVerification` | _bool_ | InsecureSkipIssuerVerification skips verification of ID token issuers. When false, ID Token Issuers must match the OIDC discovery URL<br/>default set to 'false' |
267+| `skipDiscovery` | _bool_ | SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints<br/>default set to 'false' |
268+| `jwksURL` | _string_ | JwksURL is the OpenID Connect JWKS URL<br/>eg: https://www.googleapis.com/oauth2/v3/certs |
269+| `emailClaim` | _string_ | EmailClaim indicates which claim contains the user email,<br/>default set to 'email' |
270+| `groupsClaim` | _string_ | GroupsClaim indicates which claim contains the user groups<br/>default set to 'groups' |
271+| `userIDClaim` | _string_ | UserIDClaim indicates which claim contains the user ID<br/>default set to 'email' |
272+
273+### Provider
274+
275+(**Appears on:** [Providers](#providers))
276+
277+Provider holds all configuration for a single provider
278+
279+| Field | Type | Description |
280+| ----- | ---- | ----------- |
281+| `clientID` | _string_ | ClientID is the OAuth Client ID that is defined in the provider<br/>This value is required for all providers. |
282+| `clientSecret` | _string_ | ClientSecret is the OAuth Client Secret that is defined in the provider<br/>This value is required for all providers. |
283+| `clientSecretFile` | _string_ | ClientSecretFile is the name of the file<br/>containing the OAuth Client Secret, it will be used if ClientSecret is not set. |
284+| `keycloakConfig` | _[KeycloakOptions](#keycloakoptions)_ | KeycloakConfig holds all configurations for Keycloak provider. |
285+| `azureConfig` | _[AzureOptions](#azureoptions)_ | AzureConfig holds all configurations for Azure provider. |
286+| `bitbucketConfig` | _[BitbucketOptions](#bitbucketoptions)_ | BitbucketConfig holds all configurations for Bitbucket provider. |
287+| `githubConfig` | _[GitHubOptions](#githuboptions)_ | GitHubConfig holds all configurations for GitHubC provider. |
288+| `gitlabConfig` | _[GitLabOptions](#gitlaboptions)_ | GitLabConfig holds all configurations for GitLab provider. |
289+| `googleConfig` | _[GoogleOptions](#googleoptions)_ | GoogleConfig holds all configurations for Google provider. |
290+| `oidcConfig` | _[OIDCOptions](#oidcoptions)_ | OIDCConfig holds all configurations for OIDC provider<br/>or providers utilize OIDC configurations. |
291+| `loginGovConfig` | _[LoginGovOptions](#logingovoptions)_ | LoginGovConfig holds all configurations for LoginGov provider. |
292+| `id` | _string_ | ID should be a unique identifier for the provider.<br/>This value is required for all providers. |
293+| `provider` | _string_ | Type is the OAuth provider<br/>must be set from the supported providers group,<br/>otherwise 'Google' is set as default |
294+| `name` | _string_ | Name is the providers display name<br/>if set, it will be shown to the users in the login page. |
295+| `caFiles` | _[]string_ | CAFiles is a list of paths to CA certificates that should be used when connecting to the provider.<br/>If not specified, the default Go trust sources are used instead |
296+| `loginURL` | _string_ | LoginURL is the authentication endpoint |
297+| `redeemURL` | _string_ | RedeemURL is the token redemption endpoint |
298+| `profileURL` | _string_ | ProfileURL is the profile access endpoint |
299+| `resource` | _string_ | ProtectedResource is the resource that is protected (Azure AD only) |
300+| `validateURL` | _string_ | ValidateURL is the access token validation endpoint |
301+| `scope` | _string_ | Scope is the OAuth scope specification |
302+| `prompt` | _string_ | Prompt is OIDC prompt |
303+| `approvalPrompt` | _string_ | ApprovalPrompt is the OAuth approval_prompt<br/>default is set to 'force' |
304+| `allowedGroups` | _[]string_ | AllowedGroups is a list of restrict logins to members of this group |
305+| `acrValues` | _string_ | AcrValues is a string of acr values |
306+
307+### Providers
308+
309+#### ([[]Provider](#provider) alias)
310+
311+(**Appears on:** [AlphaOptions](#alphaoptions))
312+
313+Providers is a collection of definitions for providers.
314+
315+
175316### SecretSource
176317
177318(**Appears on:** [ClaimSource](#claimsource), [HeaderValue](#headervalue), [TLS](#tls))
go.sum+1
@@ -398,6 +398,7 @@ github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3
398398github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
399399github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
400400github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
401+github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
401402github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
402403github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
403404github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
main_test.go+33−9
@@ -19,6 +19,8 @@ http_address="127.0.0.1:4180"
1919upstreams="http://httpbin"
2020set_basic_auth="true"
2121basic_auth_password="super-secret-password"
22+client_id="oauth2-proxy"
23+client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
2224`
2325
2426 const testAlphaConfig = `
@@ -57,15 +59,23 @@ injectResponseHeaders:
5759 value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk
5860server:
5961 bindAddress: "127.0.0.1:4180"
62+providers:
63+- provider: google
64+ ID: google=oauth2-proxy
65+ clientSecret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
66+ clientID: oauth2-proxy
67+ approvalPrompt: force
68+ azureConfig:
69+ tenant: common
70+ oidcConfig:
71+ groupsClaim: groups
72+ emailClaim: email
73+ userIDClaim: email
6074`
6175
6276 const testCoreConfig = `
6377cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
64−provider="oidc"
6578email_domains="example.com"
66−oidc_issuer_url="http://dex.localhost:4190/dex"
67−client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
68−client_id="oauth2-proxy"
6979cookie_secure="false"
7080
7181redirect_url="http://localhost:4180/oauth2/callback"
@@ -85,11 +95,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
8595 Expect(err).ToNot(HaveOccurred())
8696
8797 opts.Cookie.Secret = "OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
88− opts.ProviderType = "oidc"
8998 opts.EmailDomains = []string{"example.com"}
90− opts.OIDCIssuerURL = "http://dex.localhost:4190/dex"
91− opts.ClientSecret = "b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
92− opts.ClientID = "oauth2-proxy"
9399 opts.Cookie.Secure = false
94100 opts.RawRedirectURL = "http://localhost:4180/oauth2/callback"
95101
@@ -121,6 +127,24 @@ redirect_url="http://localhost:4180/oauth2/callback"
121127
122128 opts.InjectRequestHeaders = append([]options.Header{authHeader}, opts.InjectRequestHeaders...)
123129 opts.InjectResponseHeaders = append(opts.InjectResponseHeaders, authHeader)
130+
131+ opts.Providers = options.Providers{
132+ {
133+ ID: "google=oauth2-proxy",
134+ Type: "google",
135+ ClientSecret: "b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK",
136+ ClientID: "oauth2-proxy",
137+ AzureConfig: options.AzureOptions{
138+ Tenant: "common",
139+ },
140+ OIDCConfig: options.OIDCOptions{
141+ GroupsClaim: "groups",
142+ EmailClaim: "email",
143+ UserIDClaim: "email",
144+ },
145+ ApprovalPrompt: "force",
146+ },
147+ }
124148 return opts
125149 }
126150
@@ -204,7 +228,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
204228 configContent: testCoreConfig,
205229 alphaConfigContent: testAlphaConfig + ":",
206230 expectedOptions: func() *options.Options { return nil },
207− expectedErr: errors.New("failed to load alpha options: error unmarshalling config: error converting YAML to JSON: yaml: line 36: did not find expected key"),
231+ expectedErr: errors.New("failed to load alpha options: error unmarshalling config: error converting YAML to JSON: yaml: line 48: did not find expected key"),
208232 }),
209233 Entry("with alpha configuration and bad core configuration", loadConfigurationTableInput{
210234 configContent: testCoreConfig + "unknown_field=\"something\"",
oauthproxy.go+3−3
@@ -122,7 +122,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
122122 Footer: opts.Templates.Footer,
123123 Version: VERSION,
124124 Debug: opts.Templates.Debug,
125− ProviderName: buildProviderName(opts.GetProvider(), opts.ProviderName),
125+ ProviderName: buildProviderName(opts.GetProvider(), opts.Providers[0].Name),
126126 SignInMessage: buildSignInMessage(opts),
127127 DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
128128 })
@@ -136,7 +136,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
136136 }
137137
138138 if opts.SkipJwtBearerTokens {
139− logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
139+ logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.Providers[0].OIDCConfig.IssuerURL)
140140 for _, issuer := range opts.ExtraJwtIssuers {
141141 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
142142 }
@@ -146,7 +146,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
146146 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
147147 }
148148
149− logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.ClientID)
149+ logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.Providers[0].ClientID)
150150 refresh := "disabled"
151151 if opts.Cookie.Refresh != time.Duration(0) {
152152 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
oauthproxy_test.go+7−6
@@ -981,7 +981,7 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi
981981 ProviderData: &providers.ProviderData{},
982982 ValidToken: opts.providerValidateCookieResponse,
983983 }
984− pcTest.proxy.provider.(*TestProvider).SetAllowedGroups(pcTest.opts.AllowedGroups)
984+ pcTest.proxy.provider.(*TestProvider).SetAllowedGroups(pcTest.opts.Providers[0].AllowedGroups)
985985
986986 pcTest.rw = httptest.NewRecorder()
987987 pcTest.req, _ = http.NewRequest("GET", "/", strings.NewReader(""))
@@ -1322,7 +1322,7 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
13221322 },
13231323 },
13241324 }
1325− pcTest.opts.AllowedGroups = []string{"oauth_groups"}
1325+ pcTest.opts.Providers[0].AllowedGroups = []string{"oauth_groups"}
13261326 err := validation.Validate(pcTest.opts)
13271327 assert.NoError(t, err)
13281328
@@ -2292,8 +2292,9 @@ func Test_noCacheHeaders(t *testing.T) {
22922292func baseTestOptions() *options.Options {
22932293 opts := options.NewOptions()
22942294 opts.Cookie.Secret = rawCookieSecret
2295− opts.ClientID = clientID
2296− opts.ClientSecret = clientSecret
2295+ opts.Providers[0].ID = "providerID"
2296+ opts.Providers[0].ClientID = clientID
2297+ opts.Providers[0].ClientSecret = clientSecret
22972298 opts.EmailDomains = []string{"*"}
22982299
22992300 // Default injected headers for legacy configuration
@@ -2786,7 +2787,7 @@ func TestProxyAllowedGroups(t *testing.T) {
27862787 t.Cleanup(upstreamServer.Close)
27872788
27882789 test, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
2789− opts.AllowedGroups = tt.allowedGroups
2790+ opts.Providers[0].AllowedGroups = tt.allowedGroups
27902791 opts.UpstreamServers = options.Upstreams{
27912792 {
27922793 ID: upstreamServer.URL,
@@ -2915,7 +2916,7 @@ func TestAuthOnlyAllowedGroups(t *testing.T) {
29152916 }
29162917
29172918 test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {
2918− opts.AllowedGroups = tc.allowedGroups
2919+ opts.Providers[0].AllowedGroups = tc.allowedGroups
29192920 })
29202921 if err != nil {
29212922 t.Fatal(err)
pkg/apis/options/alpha_options.go+6
@@ -40,6 +40,9 @@ type AlphaOptions struct {
4040 // This can be done by setting the BindAddress and the SecureBindAddress simultaneously.
4141 // To use the secure server you must configure a TLS certificate and key.
4242 MetricsServer Server `json:"metricsServer,omitempty"`
43+
44+ // Providers is used to configure multiple providers.
45+ Providers Providers `json:"providers,omitempty"`
4346}
4447
4548// MergeInto replaces alpha options in the Options struct with the values
@@ -50,6 +53,8 @@ func (a *AlphaOptions) MergeInto(opts *Options) {
5053 opts.InjectResponseHeaders = a.InjectResponseHeaders
5154 opts.Server = a.Server
5255 opts.MetricsServer = a.MetricsServer
56+ opts.Providers = a.Providers
57+
5358}
5459
5560// ExtractFrom populates the fields in the AlphaOptions with the values from
@@ -60,4 +65,5 @@ func (a *AlphaOptions) ExtractFrom(opts *Options) {
6065 a.InjectResponseHeaders = opts.InjectResponseHeaders
6166 a.Server = opts.Server
6267 a.MetricsServer = opts.MetricsServer
68+ a.Providers = opts.Providers
6369}
pkg/apis/options/legacy_options.go+209
@@ -8,6 +8,7 @@ import (
88 "time"
99
1010 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
1112 "github.com/spf13/pflag"
1213)
1314
@@ -21,6 +22,9 @@ type LegacyOptions struct {
2122 // Legacy options for the server address and TLS
2223 LegacyServer LegacyServer `cfg:",squash"`
2324
25+ // Legacy options for single provider
26+ LegacyProvider LegacyProvider `cfg:",squash"`
27+
2428 Options Options `cfg:",squash"`
2529}
2630
@@ -43,6 +47,15 @@ func NewLegacyOptions() *LegacyOptions {
4347 HTTPSAddress: ":443",
4448 },
4549
50+ LegacyProvider: LegacyProvider{
51+ ProviderType: "google",
52+ AzureTenant: "common",
53+ ApprovalPrompt: "force",
54+ UserIDClaim: "email",
55+ OIDCEmailClaim: "email",
56+ OIDCGroupsClaim: "groups",
57+ },
58+
4659 Options: *NewOptions(),
4760 }
4861}
@@ -53,6 +66,7 @@ func NewLegacyFlagSet() *pflag.FlagSet {
5366 flagSet.AddFlagSet(legacyUpstreamsFlagSet())
5467 flagSet.AddFlagSet(legacyHeadersFlagSet())
5568 flagSet.AddFlagSet(legacyServerFlagset())
69+ flagSet.AddFlagSet(legacyProviderFlagSet())
5670
5771 return flagSet
5872}
@@ -65,10 +79,17 @@ func (l *LegacyOptions) ToOptions() (*Options, error) {
6579 l.Options.UpstreamServers = upstreams
6680
6781 l.Options.InjectRequestHeaders, l.Options.InjectResponseHeaders = l.LegacyHeaders.convert()
82+
6883 l.Options.Server, l.Options.MetricsServer = l.LegacyServer.convert()
6984
7085 l.Options.LegacyPreferEmailToUser = l.LegacyHeaders.PreferEmailToUser
7186
87+ providers, err := l.LegacyProvider.convert()
88+ if err != nil {
89+ return nil, fmt.Errorf("error converting provider: %v", err)
90+ }
91+ l.Options.Providers = providers
92+
7293 return &l.Options, nil
7394}
7495
@@ -443,6 +464,106 @@ func legacyServerFlagset() *pflag.FlagSet {
443464 return flagSet
444465}
445466
467+type LegacyProvider struct {
468+ ClientID string `flag:"client-id" cfg:"client_id"`
469+ ClientSecret string `flag:"client-secret" cfg:"client_secret"`
470+ ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file"`
471+
472+ KeycloakGroups []string `flag:"keycloak-group" cfg:"keycloak_groups"`
473+ AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
474+ BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team"`
475+ BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository"`
476+ GitHubOrg string `flag:"github-org" cfg:"github_org"`
477+ GitHubTeam string `flag:"github-team" cfg:"github_team"`
478+ GitHubRepo string `flag:"github-repo" cfg:"github_repo"`
479+ GitHubToken string `flag:"github-token" cfg:"github_token"`
480+ GitHubUsers []string `flag:"github-user" cfg:"github_users"`
481+ GitLabGroup []string `flag:"gitlab-group" cfg:"gitlab_groups"`
482+ GitLabProjects []string `flag:"gitlab-project" cfg:"gitlab_projects"`
483+ GoogleGroups []string `flag:"google-group" cfg:"google_group"`
484+ GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"`
485+ GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"`
486+
487+ // These options allow for other providers besides Google, with
488+ // potential overrides.
489+ ProviderType string `flag:"provider" cfg:"provider"`
490+ ProviderName string `flag:"provider-display-name" cfg:"provider_display_name"`
491+ ProviderCAFiles []string `flag:"provider-ca-file" cfg:"provider_ca_files"`
492+ OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"`
493+ InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email"`
494+ InsecureOIDCSkipIssuerVerification bool `flag:"insecure-oidc-skip-issuer-verification" cfg:"insecure_oidc_skip_issuer_verification"`
495+ SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery"`
496+ OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url"`
497+ OIDCEmailClaim string `flag:"oidc-email-claim" cfg:"oidc_email_claim"`
498+ OIDCGroupsClaim string `flag:"oidc-groups-claim" cfg:"oidc_groups_claim"`
499+ LoginURL string `flag:"login-url" cfg:"login_url"`
500+ RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
501+ ProfileURL string `flag:"profile-url" cfg:"profile_url"`
502+ ProtectedResource string `flag:"resource" cfg:"resource"`
503+ ValidateURL string `flag:"validate-url" cfg:"validate_url"`
504+ Scope string `flag:"scope" cfg:"scope"`
505+ Prompt string `flag:"prompt" cfg:"prompt"`
506+ ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0
507+ UserIDClaim string `flag:"user-id-claim" cfg:"user_id_claim"`
508+ AllowedGroups []string `flag:"allowed-group" cfg:"allowed_groups"`
509+
510+ AcrValues string `flag:"acr-values" cfg:"acr_values"`
511+ JWTKey string `flag:"jwt-key" cfg:"jwt_key"`
512+ JWTKeyFile string `flag:"jwt-key-file" cfg:"jwt_key_file"`
513+ PubJWKURL string `flag:"pubjwk-url" cfg:"pubjwk_url"`
514+}
515+
516+func legacyProviderFlagSet() *pflag.FlagSet {
517+ flagSet := pflag.NewFlagSet("provider", pflag.ExitOnError)
518+
519+ flagSet.StringSlice("keycloak-group", []string{}, "restrict logins to members of these groups (may be given multiple times)")
520+ flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
521+ flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
522+ flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")
523+ flagSet.String("github-org", "", "restrict logins to members of this organisation")
524+ flagSet.String("github-team", "", "restrict logins to members of this team")
525+ flagSet.String("github-repo", "", "restrict logins to collaborators of this repository")
526+ flagSet.String("github-token", "", "the token to use when verifying repository collaborators (must have push access to the repository)")
527+ flagSet.StringSlice("github-user", []string{}, "allow users with these usernames to login even if they do not belong to the specified org and team or collaborators (may be given multiple times)")
528+ flagSet.StringSlice("gitlab-group", []string{}, "restrict logins to members of this group (may be given multiple times)")
529+ flagSet.StringSlice("gitlab-project", []string{}, "restrict logins to members of this project (may be given multiple times) (eg `group/project=accesslevel`). Access level should be a value matching Gitlab access levels (see https://docs.gitlab.com/ee/api/members.html#valid-access-levels), defaulted to 20 if absent")
530+ flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).")
531+ flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls")
532+ flagSet.String("google-service-account-json", "", "the path to the service account json credentials")
533+ flagSet.String("client-id", "", "the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
534+ flagSet.String("client-secret", "", "the OAuth Client Secret")
535+ flagSet.String("client-secret-file", "", "the file with OAuth Client Secret")
536+
537+ flagSet.String("provider", "google", "OAuth provider")
538+ flagSet.String("provider-display-name", "", "Provider display name")
539+ flagSet.StringSlice("provider-ca-file", []string{}, "One or more paths to CA certificates that should be used when connecting to the provider. If not specified, the default Go trust sources are used instead.")
540+ flagSet.String("oidc-issuer-url", "", "OpenID Connect issuer URL (ie: https://accounts.google.com)")
541+ flagSet.Bool("insecure-oidc-allow-unverified-email", false, "Don't fail if an email address in an id_token is not verified")
542+ flagSet.Bool("insecure-oidc-skip-issuer-verification", false, "Do not verify if issuer matches OIDC discovery URL")
543+ flagSet.Bool("skip-oidc-discovery", false, "Skip OIDC discovery and use manually supplied Endpoints")
544+ flagSet.String("oidc-jwks-url", "", "OpenID Connect JWKS URL (ie: https://www.googleapis.com/oauth2/v3/certs)")
545+ flagSet.String("oidc-groups-claim", providers.OIDCGroupsClaim, "which OIDC claim contains the user groups")
546+ flagSet.String("oidc-email-claim", providers.OIDCEmailClaim, "which OIDC claim contains the user's email")
547+ flagSet.String("login-url", "", "Authentication endpoint")
548+ flagSet.String("redeem-url", "", "Token redemption endpoint")
549+ flagSet.String("profile-url", "", "Profile access endpoint")
550+ flagSet.String("resource", "", "The resource that is protected (Azure AD only)")
551+ flagSet.String("validate-url", "", "Access token validation endpoint")
552+ flagSet.String("scope", "", "OAuth scope specification")
553+ flagSet.String("prompt", "", "OIDC prompt")
554+ flagSet.String("approval-prompt", "force", "OAuth approval_prompt")
555+
556+ flagSet.String("acr-values", "", "acr values string: optional")
557+ flagSet.String("jwt-key", "", "private key in PEM format used to sign JWT, so that you can say something like -jwt-key=\"${OAUTH2_PROXY_JWT_KEY}\": required by login.gov")
558+ flagSet.String("jwt-key-file", "", "path to the private key file in PEM format used to sign the JWT so that you can say something like -jwt-key-file=/etc/ssl/private/jwt_signing_key.pem: required by login.gov")
559+ flagSet.String("pubjwk-url", "", "JWK pubkey access endpoint: required by login.gov")
560+
561+ flagSet.String("user-id-claim", providers.OIDCEmailClaim, "(DEPRECATED for `oidc-email-claim`) which claim contains the user ID")
562+ flagSet.StringSlice("allowed-group", []string{}, "restrict logins to members of this group (may be given multiple times)")
563+
564+ return flagSet
565+}
566+
446567func (l LegacyServer) convert() (Server, Server) {
447568 appServer := Server{
448569 BindAddress: l.HTTPAddress,
@@ -482,3 +603,91 @@ func (l LegacyServer) convert() (Server, Server) {
482603
483604 return appServer, metricsServer
484605}
606+
607+func (l *LegacyProvider) convert() (Providers, error) {
608+ providers := Providers{}
609+
610+ provider := Provider{
611+ ClientID: l.ClientID,
612+ ClientSecret: l.ClientSecret,
613+ ClientSecretFile: l.ClientSecretFile,
614+ Type: l.ProviderType,
615+ CAFiles: l.ProviderCAFiles,
616+ LoginURL: l.LoginURL,
617+ RedeemURL: l.RedeemURL,
618+ ProfileURL: l.ProfileURL,
619+ ProtectedResource: l.ProtectedResource,
620+ ValidateURL: l.ValidateURL,
621+ Scope: l.Scope,
622+ Prompt: l.Prompt,
623+ ApprovalPrompt: l.ApprovalPrompt,
624+ AllowedGroups: l.AllowedGroups,
625+ AcrValues: l.AcrValues,
626+ }
627+
628+ // This part is out of the switch section for all providers that support OIDC
629+ provider.OIDCConfig = OIDCOptions{
630+ IssuerURL: l.OIDCIssuerURL,
631+ InsecureAllowUnverifiedEmail: l.InsecureOIDCAllowUnverifiedEmail,
632+ InsecureSkipIssuerVerification: l.InsecureOIDCSkipIssuerVerification,
633+ SkipDiscovery: l.SkipOIDCDiscovery,
634+ JwksURL: l.OIDCJwksURL,
635+ UserIDClaim: l.UserIDClaim,
636+ EmailClaim: l.OIDCEmailClaim,
637+ GroupsClaim: l.OIDCGroupsClaim,
638+ }
639+
640+ // This part is out of the switch section because azure has a default tenant
641+ // that needs to be added from legacy options
642+ provider.AzureConfig = AzureOptions{
643+ Tenant: l.AzureTenant,
644+ }
645+
646+ switch provider.Type {
647+ case "github":
648+ provider.GitHubConfig = GitHubOptions{
649+ Org: l.GitHubOrg,
650+ Team: l.GitHubTeam,
651+ Repo: l.GitHubRepo,
652+ Token: l.GitHubToken,
653+ Users: l.GitHubUsers,
654+ }
655+ case "keycloak":
656+ provider.KeycloakConfig = KeycloakOptions{
657+ Groups: l.KeycloakGroups,
658+ }
659+ case "gitlab":
660+ provider.GitLabConfig = GitLabOptions{
661+ Group: l.GitLabGroup,
662+ Projects: l.GitLabProjects,
663+ }
664+ case "login.gov":
665+ provider.LoginGovConfig = LoginGovOptions{
666+ JWTKey: l.JWTKey,
667+ JWTKeyFile: l.JWTKeyFile,
668+ PubJWKURL: l.PubJWKURL,
669+ }
670+ case "bitbucket":
671+ provider.BitbucketConfig = BitbucketOptions{
672+ Team: l.BitbucketTeam,
673+ Repository: l.BitbucketRepository,
674+ }
675+ case "google":
676+ provider.GoogleConfig = GoogleOptions{
677+ Groups: l.GoogleGroups,
678+ AdminEmail: l.GoogleAdminEmail,
679+ ServiceAccountJSON: l.GoogleServiceAccountJSON,
680+ }
681+ }
682+
683+ if l.ProviderName != "" {
684+ provider.ID = l.ProviderName
685+ provider.Name = l.ProviderName
686+ } else {
687+ provider.ID = l.ProviderType + "=" + l.ClientID
688+ }
689+
690+ providers = append(providers, provider)
691+
692+ return providers, nil
693+}
pkg/apis/options/legacy_options_test.go+90−5
@@ -22,6 +22,7 @@ var _ = Describe("Legacy Options", func() {
2222 legacyOpts.LegacyUpstreams.ProxyWebSockets = true
2323 legacyOpts.LegacyUpstreams.SSLUpstreamInsecureSkipVerify = true
2424 legacyOpts.LegacyUpstreams.Upstreams = []string{"http://foo.bar/baz", "file:///var/lib/website#/bar", "static://204"}
25+ legacyOpts.LegacyProvider.ClientID = "oauth-proxy"
2526
2627 truth := true
2728 staticCode := 204
@@ -110,6 +111,9 @@ var _ = Describe("Legacy Options", func() {
110111 BindAddress: "127.0.0.1:4180",
111112 }
112113
114+ opts.Providers[0].ClientID = "oauth-proxy"
115+ opts.Providers[0].ID = "google=oauth-proxy"
116+
113117 converted, err := legacyOpts.ToOptions()
114118 Expect(err).ToNot(HaveOccurred())
115119 Expect(converted).To(Equal(opts))
@@ -196,9 +200,9 @@ var _ = Describe("Legacy Options", func() {
196200 invalidHTTPErrMsg := "could not parse upstream \":foo\": parse \":foo\": missing protocol scheme"
197201
198202 DescribeTable("convertLegacyUpstreams",
199− func(o *convertUpstreamsTableInput) {
203+ func(in *convertUpstreamsTableInput) {
200204 legacyUpstreams := LegacyUpstreams{
201− Upstreams: o.upstreamStrings,
205+ Upstreams: in.upstreamStrings,
202206 SSLUpstreamInsecureSkipVerify: skipVerify,
203207 PassHostHeader: passHostHeader,
204208 ProxyWebSockets: proxyWebSockets,
@@ -207,14 +211,14 @@ var _ = Describe("Legacy Options", func() {
207211
208212 upstreams, err := legacyUpstreams.convert()
209213
210− if o.errMsg != "" {
214+ if in.errMsg != "" {
211215 Expect(err).To(HaveOccurred())
212− Expect(err.Error()).To(Equal(o.errMsg))
216+ Expect(err.Error()).To(Equal(in.errMsg))
213217 } else {
214218 Expect(err).ToNot(HaveOccurred())
215219 }
216220
217− Expect(upstreams).To(ConsistOf(o.expectedUpstreams))
221+ Expect(upstreams).To(ConsistOf(in.expectedUpstreams))
218222 },
219223 Entry("with no upstreams", &convertUpstreamsTableInput{
220224 upstreamStrings: []string{},
@@ -850,6 +854,87 @@ var _ = Describe("Legacy Options", func() {
850854 },
851855 }),
852856 )
857+ })
858+
859+ Context("Legacy Providers", func() {
860+ type convertProvidersTableInput struct {
861+ legacyProvider LegacyProvider
862+ expectedProviders Providers
863+ errMsg string
864+ }
865+
866+ // Non defaults for these options
867+ clientID := "abcd"
868+
869+ defaultProvider := Provider{
870+ ID: "google=" + clientID,
871+ ClientID: clientID,
872+ Type: "google",
873+ }
874+ defaultLegacyProvider := LegacyProvider{
875+ ClientID: clientID,
876+ ProviderType: "google",
877+ }
878+
879+ displayNameProvider := Provider{
880+ ID: "displayName",
881+ Name: "displayName",
882+ ClientID: clientID,
883+ Type: "google",
884+ }
885+
886+ displayNameLegacyProvider := LegacyProvider{
887+ ClientID: clientID,
888+ ProviderName: "displayName",
889+ ProviderType: "google",
890+ }
891+
892+ internalConfigProvider := Provider{
893+ ID: "google=" + clientID,
894+ ClientID: clientID,
895+ Type: "google",
896+ GoogleConfig: GoogleOptions{
897+ AdminEmail: "email@email.com",
898+ ServiceAccountJSON: "test.json",
899+ Groups: []string{"1", "2"},
900+ },
901+ }
902+
903+ internalConfigLegacyProvider := LegacyProvider{
904+ ClientID: clientID,
905+ ProviderType: "google",
906+ GoogleAdminEmail: "email@email.com",
907+ GoogleServiceAccountJSON: "test.json",
908+ GoogleGroups: []string{"1", "2"},
909+ }
910+ DescribeTable("convertLegacyProviders",
911+ func(in *convertProvidersTableInput) {
912+ providers, err := in.legacyProvider.convert()
913+
914+ if in.errMsg != "" {
915+ Expect(err).To(HaveOccurred())
916+ Expect(err.Error()).To(Equal(in.errMsg))
917+ } else {
918+ Expect(err).ToNot(HaveOccurred())
919+ }
853920
921+ Expect(providers).To(ConsistOf(in.expectedProviders))
922+ },
923+ Entry("with default provider", &convertProvidersTableInput{
924+ legacyProvider: defaultLegacyProvider,
925+ expectedProviders: Providers{defaultProvider},
926+ errMsg: "",
927+ }),
928+ Entry("with provider display name", &convertProvidersTableInput{
929+ legacyProvider: displayNameLegacyProvider,
930+ expectedProviders: Providers{displayNameProvider},
931+ errMsg: "",
932+ }),
933+ Entry("with internal provider config", &convertProvidersTableInput{
934+ legacyProvider: internalConfigLegacyProvider,
935+ expectedProviders: Providers{internalConfigProvider},
936+ errMsg: "",
937+ }),
938+ )
854939 })
855940})
pkg/apis/options/load_test.go+45−2
@@ -14,6 +14,49 @@ import (
1414)
1515
1616var _ = Describe("Load", func() {
17+ optionsWithNilProvider := NewOptions()
18+ optionsWithNilProvider.Providers = nil
19+
20+ legacyOptionsWithNilProvider := &LegacyOptions{
21+ LegacyUpstreams: LegacyUpstreams{
22+ PassHostHeader: true,
23+ ProxyWebSockets: true,
24+ FlushInterval: DefaultUpstreamFlushInterval,
25+ },
26+
27+ LegacyHeaders: LegacyHeaders{
28+ PassBasicAuth: true,
29+ PassUserHeaders: true,
30+ SkipAuthStripHeaders: true,
31+ },
32+
33+ LegacyServer: LegacyServer{
34+ HTTPAddress: "127.0.0.1:4180",
35+ HTTPSAddress: ":443",
36+ },
37+
38+ LegacyProvider: LegacyProvider{
39+ ProviderType: "google",
40+ AzureTenant: "common",
41+ ApprovalPrompt: "force",
42+ UserIDClaim: "email",
43+ OIDCEmailClaim: "email",
44+ OIDCGroupsClaim: "groups",
45+ },
46+
47+ Options: Options{
48+ ProxyPrefix: "/oauth2",
49+ PingPath: "/ping",
50+ RealClientIPHeader: "X-Real-IP",
51+ ForceHTTPS: false,
52+ Cookie: cookieDefaults(),
53+ Session: sessionOptionsDefaults(),
54+ Templates: templatesDefaults(),
55+ SkipAuthPreflight: false,
56+ Logging: loggingDefaults(),
57+ },
58+ }
59+
1760 Context("with a testOptions structure", func() {
1861 type TestOptionSubStruct struct {
1962 StringSliceOption []string `flag:"string-slice-option" cfg:"string_slice_option"`
@@ -294,12 +337,12 @@ var _ = Describe("Load", func() {
294337 Entry("with an empty Options struct, should return default values", &testOptionsTableInput{
295338 flagSet: NewFlagSet,
296339 input: &Options{},
297− expectedOutput: NewOptions(),
340+ expectedOutput: optionsWithNilProvider,
298341 }),
299342 Entry("with an empty LegacyOptions struct, should return default values", &testOptionsTableInput{
300343 flagSet: NewLegacyFlagSet,
301344 input: &LegacyOptions{},
302− expectedOutput: NewLegacyOptions(),
345+ expectedOutput: legacyOptionsWithNilProvider,
303346 }),
304347 )
305348 })
pkg/apis/options/options.go+18−111
@@ -27,29 +27,12 @@ type Options struct {
2727 TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
2828 ForceHTTPS bool `flag:"force-https" cfg:"force_https"`
2929 RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
30− ClientID string `flag:"client-id" cfg:"client_id"`
31− ClientSecret string `flag:"client-secret" cfg:"client_secret"`
32− ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file"`
33−
34− AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
35− KeycloakGroups []string `flag:"keycloak-group" cfg:"keycloak_groups"`
36− AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
37− BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team"`
38− BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository"`
39− EmailDomains []string `flag:"email-domain" cfg:"email_domains"`
40− WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains"`
41− GitHubOrg string `flag:"github-org" cfg:"github_org"`
42− GitHubTeam string `flag:"github-team" cfg:"github_team"`
43− GitHubRepo string `flag:"github-repo" cfg:"github_repo"`
44− GitHubToken string `flag:"github-token" cfg:"github_token"`
45− GitHubUsers []string `flag:"github-user" cfg:"github_users"`
46− GitLabGroup []string `flag:"gitlab-group" cfg:"gitlab_groups"`
47− GitlabProjects []string `flag:"gitlab-project" cfg:"gitlab_projects"`
48− GoogleGroups []string `flag:"google-group" cfg:"google_group"`
49− GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"`
50− GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"`
51− HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"`
52− HtpasswdUserGroups []string `flag:"htpasswd-user-group" cfg:"htpasswd_user_groups"`
30+
31+ AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
32+ EmailDomains []string `flag:"email-domain" cfg:"email_domains"`
33+ WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains"`
34+ HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"`
35+ HtpasswdUserGroups []string `flag:"htpasswd-user-group" cfg:"htpasswd_user_groups"`
5336
5437 Cookie Cookie `cfg:",squash"`
5538 Session SessionOptions `cfg:",squash"`
@@ -66,6 +49,8 @@ type Options struct {
6649 Server Server `cfg:",internal"`
6750 MetricsServer Server `cfg:",internal"`
6851
52+ Providers Providers `cfg:",internal"`
53+
6954 SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
7055 SkipAuthRoutes []string `flag:"skip-auth-route" cfg:"skip_auth_routes"`
7156 SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"`
@@ -74,34 +59,7 @@ type Options struct {
7459 SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"`
7560 SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"`
7661
77− // These options allow for other providers besides Google, with
78− // potential overrides.
79− ProviderType string `flag:"provider" cfg:"provider"`
80− ProviderName string `flag:"provider-display-name" cfg:"provider_display_name"`
81− ProviderCAFiles []string `flag:"provider-ca-file" cfg:"provider_ca_files"`
82− OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"`
83− InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email"`
84− InsecureOIDCSkipIssuerVerification bool `flag:"insecure-oidc-skip-issuer-verification" cfg:"insecure_oidc_skip_issuer_verification"`
85− SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery"`
86− OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url"`
87− OIDCEmailClaim string `flag:"oidc-email-claim" cfg:"oidc_email_claim"`
88− OIDCGroupsClaim string `flag:"oidc-groups-claim" cfg:"oidc_groups_claim"`
89− LoginURL string `flag:"login-url" cfg:"login_url"`
90− RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
91− ProfileURL string `flag:"profile-url" cfg:"profile_url"`
92− ProtectedResource string `flag:"resource" cfg:"resource"`
93− ValidateURL string `flag:"validate-url" cfg:"validate_url"`
94− Scope string `flag:"scope" cfg:"scope"`
95− Prompt string `flag:"prompt" cfg:"prompt"`
96− ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0
97− UserIDClaim string `flag:"user-id-claim" cfg:"user_id_claim"`
98− AllowedGroups []string `flag:"allowed-group" cfg:"allowed_groups"`
99−
10062 SignatureKey string `flag:"signature-key" cfg:"signature_key"`
101− AcrValues string `flag:"acr-values" cfg:"acr_values"`
102− JWTKey string `flag:"jwt-key" cfg:"jwt_key"`
103− JWTKeyFile string `flag:"jwt-key-file" cfg:"jwt_key_file"`
104− PubJWKURL string `flag:"pubjwk-url" cfg:"pubjwk_url"`
10563 GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks"`
10664
10765 // This is used for backwards compatibility for basic auth users
@@ -135,24 +93,16 @@ func (o *Options) SetRealClientIPParser(s ipapi.RealClientIPParser) { o.realClie
13593// NewOptions constructs a new Options with defaulted values
13694func NewOptions() *Options {
13795 return &Options{
138− ProxyPrefix: "/oauth2",
139− ProviderType: "google",
140− PingPath: "/ping",
141− RealClientIPHeader: "X-Real-IP",
142− ForceHTTPS: false,
143− Cookie: cookieDefaults(),
144− Session: sessionOptionsDefaults(),
145− Templates: templatesDefaults(),
146− AzureTenant: "common",
147− SkipAuthPreflight: false,
148− Prompt: "", // Change to "login" when ApprovalPrompt officially deprecated
149− ApprovalPrompt: "force",
150− InsecureOIDCAllowUnverifiedEmail: false,
151− SkipOIDCDiscovery: false,
152− Logging: loggingDefaults(),
153− UserIDClaim: providers.OIDCEmailClaim, // Deprecated: Use OIDCEmailClaim
154− OIDCEmailClaim: providers.OIDCEmailClaim,
155− OIDCGroupsClaim: providers.OIDCGroupsClaim,
96+ ProxyPrefix: "/oauth2",
97+ Providers: providerDefaults(),
98+ PingPath: "/ping",
99+ RealClientIPHeader: "X-Real-IP",
100+ ForceHTTPS: false,
101+ Cookie: cookieDefaults(),
102+ Session: sessionOptionsDefaults(),
103+ Templates: templatesDefaults(),
104+ SkipAuthPreflight: false,
105+ Logging: loggingDefaults(),
156106 }
157107}
158108
@@ -175,23 +125,6 @@ func NewFlagSet() *pflag.FlagSet {
175125
176126 flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
177127 flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
178− flagSet.StringSlice("keycloak-group", []string{}, "restrict logins to members of these groups (may be given multiple times)")
179− flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
180− flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
181− flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")
182− flagSet.String("github-org", "", "restrict logins to members of this organisation")
183− flagSet.String("github-team", "", "restrict logins to members of this team")
184− flagSet.String("github-repo", "", "restrict logins to collaborators of this repository")
185− flagSet.String("github-token", "", "the token to use when verifying repository collaborators (must have push access to the repository)")
186− flagSet.StringSlice("github-user", []string{}, "allow users with these usernames to login even if they do not belong to the specified org and team or collaborators (may be given multiple times)")
187− flagSet.StringSlice("gitlab-group", []string{}, "restrict logins to members of this group (may be given multiple times)")
188− flagSet.StringSlice("gitlab-project", []string{}, "restrict logins to members of this project (may be given multiple times) (eg `group/project=accesslevel`). Access level should be a value matching Gitlab access levels (see https://docs.gitlab.com/ee/api/members.html#valid-access-levels), defaulted to 20 if absent")
189− flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).")
190− flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls")
191− flagSet.String("google-service-account-json", "", "the path to the service account json credentials")
192− flagSet.String("client-id", "", "the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
193− flagSet.String("client-secret", "", "the OAuth Client Secret")
194− flagSet.String("client-secret-file", "", "the file with OAuth Client Secret")
195128 flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
196129 flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -B\" for bcrypt encryption")
197130 flagSet.StringSlice("htpasswd-user-group", []string{}, "the groups to be set on sessions for htpasswd users (may be given multiple times)")
@@ -211,35 +144,9 @@ func NewFlagSet() *pflag.FlagSet {
211144 flagSet.Bool("redis-use-cluster", false, "Connect to redis cluster. Must set --redis-cluster-connection-urls to use this feature")
212145 flagSet.StringSlice("redis-cluster-connection-urls", []string{}, "List of Redis cluster connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-cluster")
213146
214− flagSet.String("provider", "google", "OAuth provider")
215− flagSet.String("provider-display-name", "", "Provider display name")
216− flagSet.StringSlice("provider-ca-file", []string{}, "One or more paths to CA certificates that should be used when connecting to the provider. If not specified, the default Go trust sources are used instead.")
217− flagSet.String("oidc-issuer-url", "", "OpenID Connect issuer URL (ie: https://accounts.google.com)")
218− flagSet.Bool("insecure-oidc-allow-unverified-email", false, "Don't fail if an email address in an id_token is not verified")
219− flagSet.Bool("insecure-oidc-skip-issuer-verification", false, "Do not verify if issuer matches OIDC discovery URL")
220− flagSet.Bool("skip-oidc-discovery", false, "Skip OIDC discovery and use manually supplied Endpoints")
221− flagSet.String("oidc-jwks-url", "", "OpenID Connect JWKS URL (ie: https://www.googleapis.com/oauth2/v3/certs)")
222− flagSet.String("oidc-groups-claim", providers.OIDCGroupsClaim, "which OIDC claim contains the user groups")
223− flagSet.String("oidc-email-claim", providers.OIDCEmailClaim, "which OIDC claim contains the user's email")
224− flagSet.String("login-url", "", "Authentication endpoint")
225− flagSet.String("redeem-url", "", "Token redemption endpoint")
226− flagSet.String("profile-url", "", "Profile access endpoint")
227− flagSet.String("resource", "", "The resource that is protected (Azure AD only)")
228− flagSet.String("validate-url", "", "Access token validation endpoint")
229− flagSet.String("scope", "", "OAuth scope specification")
230− flagSet.String("prompt", "", "OIDC prompt")
231− flagSet.String("approval-prompt", "force", "OAuth approval_prompt")
232−
233147 flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)")
234− flagSet.String("acr-values", "", "acr values string: optional")
235− flagSet.String("jwt-key", "", "private key in PEM format used to sign JWT, so that you can say something like -jwt-key=\"${OAUTH2_PROXY_JWT_KEY}\": required by login.gov")
236− flagSet.String("jwt-key-file", "", "path to the private key file in PEM format used to sign the JWT so that you can say something like -jwt-key-file=/etc/ssl/private/jwt_signing_key.pem: required by login.gov")
237− flagSet.String("pubjwk-url", "", "JWK pubkey access endpoint: required by login.gov")
238148 flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints")
239149
240− flagSet.String("user-id-claim", providers.OIDCEmailClaim, "(DEPRECATED for `oidc-email-claim`) which claim contains the user ID")
241− flagSet.StringSlice("allowed-group", []string{}, "restrict logins to members of this group (may be given multiple times)")
242−
243150 flagSet.AddFlagSet(cookieFlagSet())
244151 flagSet.AddFlagSet(loggingFlagSet())
245152 flagSet.AddFlagSet(templatesFlagSet())
pkg/apis/options/providers.gonew file+180
@@ -0,0 +1,180 @@
1+package options
2+
3+import "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
4+
5+// Providers is a collection of definitions for providers.
6+type Providers []Provider
7+
8+// Provider holds all configuration for a single provider
9+type Provider struct {
10+ // ClientID is the OAuth Client ID that is defined in the provider
11+ // This value is required for all providers.
12+ ClientID string `json:"clientID,omitempty"`
13+ // ClientSecret is the OAuth Client Secret that is defined in the provider
14+ // This value is required for all providers.
15+ ClientSecret string `json:"clientSecret,omitempty"`
16+ // ClientSecretFile is the name of the file
17+ // containing the OAuth Client Secret, it will be used if ClientSecret is not set.
18+ ClientSecretFile string `json:"clientSecretFile,omitempty"`
19+
20+ // KeycloakConfig holds all configurations for Keycloak provider.
21+ KeycloakConfig KeycloakOptions `json:"keycloakConfig,omitempty"`
22+ // AzureConfig holds all configurations for Azure provider.
23+ AzureConfig AzureOptions `json:"azureConfig,omitempty"`
24+ // BitbucketConfig holds all configurations for Bitbucket provider.
25+ BitbucketConfig BitbucketOptions `json:"bitbucketConfig,omitempty"`
26+ // GitHubConfig holds all configurations for GitHubC provider.
27+ GitHubConfig GitHubOptions `json:"githubConfig,omitempty"`
28+ // GitLabConfig holds all configurations for GitLab provider.
29+ GitLabConfig GitLabOptions `json:"gitlabConfig,omitempty"`
30+ // GoogleConfig holds all configurations for Google provider.
31+ GoogleConfig GoogleOptions `json:"googleConfig,omitempty"`
32+ // OIDCConfig holds all configurations for OIDC provider
33+ // or providers utilize OIDC configurations.
34+ OIDCConfig OIDCOptions `json:"oidcConfig,omitempty"`
35+ // LoginGovConfig holds all configurations for LoginGov provider.
36+ LoginGovConfig LoginGovOptions `json:"loginGovConfig,omitempty"`
37+
38+ // ID should be a unique identifier for the provider.
39+ // This value is required for all providers.
40+ ID string `json:"id,omitempty"`
41+ // Type is the OAuth provider
42+ // must be set from the supported providers group,
43+ // otherwise 'Google' is set as default
44+ Type string `json:"provider,omitempty"`
45+ // Name is the providers display name
46+ // if set, it will be shown to the users in the login page.
47+ Name string `json:"name,omitempty"`
48+ // CAFiles is a list of paths to CA certificates that should be used when connecting to the provider.
49+ // If not specified, the default Go trust sources are used instead
50+ CAFiles []string `json:"caFiles,omitempty"`
51+
52+ // LoginURL is the authentication endpoint
53+ LoginURL string `json:"loginURL,omitempty"`
54+ // RedeemURL is the token redemption endpoint
55+ RedeemURL string `json:"redeemURL,omitempty"`
56+ // ProfileURL is the profile access endpoint
57+ ProfileURL string `json:"profileURL,omitempty"`
58+ // ProtectedResource is the resource that is protected (Azure AD only)
59+ ProtectedResource string `json:"resource,omitempty"`
60+ // ValidateURL is the access token validation endpoint
61+ ValidateURL string `json:"validateURL,omitempty"`
62+ // Scope is the OAuth scope specification
63+ Scope string `json:"scope,omitempty"`
64+ // Prompt is OIDC prompt
65+ Prompt string `json:"prompt,omitempty"`
66+ // ApprovalPrompt is the OAuth approval_prompt
67+ // default is set to 'force'
68+ ApprovalPrompt string `json:"approvalPrompt,omitempty"`
69+ // AllowedGroups is a list of restrict logins to members of this group
70+ AllowedGroups []string `json:"allowedGroups,omitempty"`
71+
72+ // AcrValues is a string of acr values
73+ AcrValues string `json:"acrValues,omitempty"`
74+}
75+
76+type KeycloakOptions struct {
77+ // Group enables to restrict login to members of indicated group
78+ Groups []string `json:"groups,omitempty"`
79+}
80+
81+type AzureOptions struct {
82+ // Tenant directs to a tenant-specific or common (tenant-independent) endpoint
83+ // Default value is 'commmon'
84+ Tenant string `json:"tenant,omitempty"`
85+}
86+
87+type BitbucketOptions struct {
88+ // Team sets restrict logins to members of this team
89+ Team string `json:"team,omitempty"`
90+ // Repository sets restrict logins to user with access to this repository
91+ Repository string `json:"repository,omitempty"`
92+}
93+
94+type GitHubOptions struct {
95+ // Org sets restrict logins to members of this organisation
96+ Org string `json:"org,omitempty"`
97+ // Team sets restrict logins to members of this team
98+ Team string `json:"team,omitempty"`
99+ // Repo sets restrict logins to collaborators of this repository
100+ Repo string `json:"repo,omitempty"`
101+ // Token is the token to use when verifying repository collaborators
102+ // it must have push access to the repository
103+ Token string `json:"token,omitempty"`
104+ // Users allows users with these usernames to login
105+ // even if they do not belong to the specified org and team or collaborators
106+ Users []string `json:"users,omitempty"`
107+}
108+
109+type GitLabOptions struct {
110+ // Group sets restrict logins to members of this group
111+ Group []string `json:"group,omitempty"`
112+ // Projects restricts logins to members of any of these projects
113+ Projects []string `json:"projects,omitempty"`
114+}
115+
116+type GoogleOptions struct {
117+ // Groups sets restrict logins to members of this google group
118+ Groups []string `json:"group,omitempty"`
119+ // AdminEmail is the google admin to impersonate for api calls
120+ AdminEmail string `json:"adminEmail,omitempty"`
121+ // ServiceAccountJSON is the path to the service account json credentials
122+ ServiceAccountJSON string `json:"serviceAccountJson,omitempty"`
123+}
124+
125+type OIDCOptions struct {
126+ // IssuerURL is the OpenID Connect issuer URL
127+ // eg: https://accounts.google.com
128+ IssuerURL string `json:"issuerURL,omitempty"`
129+ // InsecureAllowUnverifiedEmail prevents failures if an email address in an id_token is not verified
130+ // default set to 'false'
131+ InsecureAllowUnverifiedEmail bool `json:"insecureAllowUnverifiedEmail,omitempty"`
132+ // InsecureSkipIssuerVerification skips verification of ID token issuers. When false, ID Token Issuers must match the OIDC discovery URL
133+ // default set to 'false'
134+ InsecureSkipIssuerVerification bool `json:"insecureSkipIssuerVerification,omitempty"`
135+ // SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints
136+ // default set to 'false'
137+ SkipDiscovery bool `json:"skipDiscovery,omitempty"`
138+ // JwksURL is the OpenID Connect JWKS URL
139+ // eg: https://www.googleapis.com/oauth2/v3/certs
140+ JwksURL string `json:"jwksURL,omitempty"`
141+ // EmailClaim indicates which claim contains the user email,
142+ // default set to 'email'
143+ EmailClaim string `json:"emailClaim,omitempty"`
144+ // GroupsClaim indicates which claim contains the user groups
145+ // default set to 'groups'
146+ GroupsClaim string `json:"groupsClaim,omitempty"`
147+ // UserIDClaim indicates which claim contains the user ID
148+ // default set to 'email'
149+ UserIDClaim string `json:"userIDClaim,omitempty"`
150+}
151+
152+type LoginGovOptions struct {
153+ // JWTKey is a private key in PEM format used to sign JWT,
154+ JWTKey string `json:"jwtKey,omitempty"`
155+ // JWTKeyFile is a path to the private key file in PEM format used to sign the JWT
156+ JWTKeyFile string `json:"jwtKeyFile,omitempty"`
157+ // PubJWKURL is the JWK pubkey access endpoint
158+ PubJWKURL string `json:"pubjwkURL,omitempty"`
159+}
160+
161+func providerDefaults() Providers {
162+ providers := Providers{
163+ {
164+ Type: "google",
165+ Prompt: "", // Change to "login" when ApprovalPrompt officially deprecated
166+ ApprovalPrompt: "force",
167+ AzureConfig: AzureOptions{
168+ Tenant: "common",
169+ },
170+ OIDCConfig: OIDCOptions{
171+ InsecureAllowUnverifiedEmail: false,
172+ SkipDiscovery: false,
173+ UserIDClaim: providers.OIDCEmailClaim, // Deprecated: Use OIDCEmailClaim
174+ EmailClaim: providers.OIDCEmailClaim,
175+ GroupsClaim: providers.OIDCGroupsClaim,
176+ },
177+ },
178+ }
179+ return providers
180+}
pkg/validation/options.go+83−106
@@ -29,6 +29,7 @@ func Validate(o *options.Options) error {
2929 msgs = append(msgs, validateRedisSessionStore(o)...)
3030 msgs = append(msgs, prefixValues("injectRequestHeaders: ", validateHeaders(o.InjectRequestHeaders)...)...)
3131 msgs = append(msgs, prefixValues("injectResponseHeaders: ", validateHeaders(o.InjectResponseHeaders)...)...)
32+ msgs = append(msgs, validateProviders(o)...)
3233 msgs = configureLogger(o.Logging, msgs)
3334 msgs = parseSignatureKey(o, msgs)
3435
@@ -39,8 +40,8 @@ func Validate(o *options.Options) error {
3940 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
4041 }
4142 http.DefaultClient = &http.Client{Transport: insecureTransport}
42− } else if len(o.ProviderCAFiles) > 0 {
43− pool, err := util.GetCertPool(o.ProviderCAFiles)
43+ } else if len(o.Providers[0].CAFiles) > 0 {
44+ pool, err := util.GetCertPool(o.Providers[0].CAFiles)
4445 if err == nil {
4546 transport := http.DefaultTransport.(*http.Transport).Clone()
4647 transport.TLSClientConfig = &tls.Config{
@@ -54,38 +55,23 @@ func Validate(o *options.Options) error {
5455 }
5556 }
5657
57− if o.ClientID == "" {
58− msgs = append(msgs, "missing setting: client-id")
59− }
60− // login.gov uses a signed JWT to authenticate, not a client-secret
61− if o.ProviderType != "login.gov" {
62− if o.ClientSecret == "" && o.ClientSecretFile == "" {
63− msgs = append(msgs, "missing setting: client-secret or client-secret-file")
64− }
65− if o.ClientSecret == "" && o.ClientSecretFile != "" {
66− _, err := ioutil.ReadFile(o.ClientSecretFile)
67− if err != nil {
68− msgs = append(msgs, "could not read client secret file: "+o.ClientSecretFile)
69− }
70− }
71− }
7258 if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" {
7359 msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required."+
7460 "\n use email-domain=* to authorize all email addresses")
7561 }
7662
77− if o.OIDCIssuerURL != "" {
63+ if o.Providers[0].OIDCConfig.IssuerURL != "" {
7864
7965 ctx := context.Background()
8066
81− if o.InsecureOIDCSkipIssuerVerification && !o.SkipOIDCDiscovery {
67+ if o.Providers[0].OIDCConfig.InsecureSkipIssuerVerification && !o.Providers[0].OIDCConfig.SkipDiscovery {
8268 // go-oidc doesn't let us pass bypass the issuer check this in the oidc.NewProvider call
8369 // (which uses discovery to get the URLs), so we'll do a quick check ourselves and if
8470 // we get the URLs, we'll just use the non-discovery path.
8571
8672 logger.Printf("Performing OIDC Discovery...")
8773
88− requestURL := strings.TrimSuffix(o.OIDCIssuerURL, "/") + "/.well-known/openid-configuration"
74+ requestURL := strings.TrimSuffix(o.Providers[0].OIDCConfig.IssuerURL, "/") + "/.well-known/openid-configuration"
8975 body, err := requests.New(requestURL).
9076 WithContext(ctx).
9177 Do().
@@ -96,23 +82,23 @@ func Validate(o *options.Options) error {
9682 // Prefer manually configured URLs. It's a bit unclear
9783 // why you'd be doing discovery and also providing the URLs
9884 // explicitly though...
99− if o.LoginURL == "" {
100− o.LoginURL = body.Get("authorization_endpoint").MustString()
85+ if o.Providers[0].LoginURL == "" {
86+ o.Providers[0].LoginURL = body.Get("authorization_endpoint").MustString()
10187 }
10288
103− if o.RedeemURL == "" {
104− o.RedeemURL = body.Get("token_endpoint").MustString()
89+ if o.Providers[0].RedeemURL == "" {
90+ o.Providers[0].RedeemURL = body.Get("token_endpoint").MustString()
10591 }
10692
107− if o.OIDCJwksURL == "" {
108− o.OIDCJwksURL = body.Get("jwks_uri").MustString()
93+ if o.Providers[0].OIDCConfig.JwksURL == "" {
94+ o.Providers[0].OIDCConfig.JwksURL = body.Get("jwks_uri").MustString()
10995 }
11096
111− if o.ProfileURL == "" {
112− o.ProfileURL = body.Get("userinfo_endpoint").MustString()
97+ if o.Providers[0].ProfileURL == "" {
98+ o.Providers[0].ProfileURL = body.Get("userinfo_endpoint").MustString()
11399 }
114100
115− o.SkipOIDCDiscovery = true
101+ o.Providers[0].OIDCConfig.SkipDiscovery = true
116102 }
117103 }
118104
@@ -120,42 +106,45 @@ func Validate(o *options.Options) error {
120106 // instead of metadata discovery if we enable -skip-oidc-discovery.
121107 // In this case we need to make sure the required endpoints for
122108 // the provider are configured.
123− if o.SkipOIDCDiscovery {
124− if o.LoginURL == "" {
109+ if o.Providers[0].OIDCConfig.SkipDiscovery {
110+ if o.Providers[0].LoginURL == "" {
125111 msgs = append(msgs, "missing setting: login-url")
126112 }
127− if o.RedeemURL == "" {
113+ if o.Providers[0].RedeemURL == "" {
128114 msgs = append(msgs, "missing setting: redeem-url")
129115 }
130− if o.OIDCJwksURL == "" {
116+ if o.Providers[0].OIDCConfig.JwksURL == "" {
131117 msgs = append(msgs, "missing setting: oidc-jwks-url")
132118 }
133− keySet := oidc.NewRemoteKeySet(ctx, o.OIDCJwksURL)
134− o.SetOIDCVerifier(oidc.NewVerifier(o.OIDCIssuerURL, keySet, &oidc.Config{
135− ClientID: o.ClientID,
136− SkipIssuerCheck: o.InsecureOIDCSkipIssuerVerification,
119+ keySet := oidc.NewRemoteKeySet(ctx, o.Providers[0].OIDCConfig.JwksURL)
120+ o.SetOIDCVerifier(oidc.NewVerifier(o.Providers[0].OIDCConfig.IssuerURL, keySet, &oidc.Config{
121+ ClientID: o.Providers[0].ClientID,
122+ SkipIssuerCheck: o.Providers[0].OIDCConfig.InsecureSkipIssuerVerification,
137123 }))
138124 } else {
139125 // Configure discoverable provider data.
140− provider, err := oidc.NewProvider(ctx, o.OIDCIssuerURL)
126+ provider, err := oidc.NewProvider(ctx, o.Providers[0].OIDCConfig.IssuerURL)
141127 if err != nil {
142128 return err
143129 }
144130 o.SetOIDCVerifier(provider.Verifier(&oidc.Config{
145− ClientID: o.ClientID,
146− SkipIssuerCheck: o.InsecureOIDCSkipIssuerVerification,
131+ ClientID: o.Providers[0].ClientID,
132+ SkipIssuerCheck: o.Providers[0].OIDCConfig.InsecureSkipIssuerVerification,
147133 }))
148134
149− o.LoginURL = provider.Endpoint().AuthURL
150− o.RedeemURL = provider.Endpoint().TokenURL
135+ o.Providers[0].LoginURL = provider.Endpoint().AuthURL
136+ o.Providers[0].RedeemURL = provider.Endpoint().TokenURL
151137 }
152− if o.Scope == "" {
153− o.Scope = "openid email profile"
138+ if o.Providers[0].Scope == "" {
139+ o.Providers[0].Scope = "openid email profile"
154140
155− if len(o.AllowedGroups) > 0 {
156− o.Scope += " groups"
141+ if len(o.Providers[0].AllowedGroups) > 0 {
142+ o.Providers[0].Scope += " groups"
157143 }
158144 }
145+ if o.Providers[0].OIDCConfig.UserIDClaim == "" {
146+ o.Providers[0].OIDCConfig.UserIDClaim = "email"
147+ }
159148 }
160149
161150 if o.SkipJwtBearerTokens {
@@ -183,18 +172,6 @@ func Validate(o *options.Options) error {
183172 msgs = append(msgs, validateUpstreams(o.UpstreamServers)...)
184173 msgs = parseProviderInfo(o, msgs)
185174
186− if len(o.GoogleGroups) > 0 || o.GoogleAdminEmail != "" || o.GoogleServiceAccountJSON != "" {
187− if len(o.GoogleGroups) < 1 {
188− msgs = append(msgs, "missing setting: google-group")
189− }
190− if o.GoogleAdminEmail == "" {
191− msgs = append(msgs, "missing setting: google-admin-email")
192− }
193− if o.GoogleServiceAccountJSON == "" {
194− msgs = append(msgs, "missing setting: google-service-account-json")
195− }
196− }
197−
198175 if o.ReverseProxy {
199176 parser, err := ip.GetRealClientIPParser(o.RealClientIPHeader)
200177 if err != nil {
@@ -220,79 +197,79 @@ func Validate(o *options.Options) error {
220197
221198func parseProviderInfo(o *options.Options, msgs []string) []string {
222199 p := &providers.ProviderData{
223− Scope: o.Scope,
224− ClientID: o.ClientID,
225− ClientSecret: o.ClientSecret,
226− ClientSecretFile: o.ClientSecretFile,
227− Prompt: o.Prompt,
228− ApprovalPrompt: o.ApprovalPrompt,
229− AcrValues: o.AcrValues,
200+ Scope: o.Providers[0].Scope,
201+ ClientID: o.Providers[0].ClientID,
202+ ClientSecret: o.Providers[0].ClientSecret,
203+ ClientSecretFile: o.Providers[0].ClientSecretFile,
204+ Prompt: o.Providers[0].Prompt,
205+ ApprovalPrompt: o.Providers[0].ApprovalPrompt,
206+ AcrValues: o.Providers[0].AcrValues,
230207 }
231− p.LoginURL, msgs = parseURL(o.LoginURL, "login", msgs)
232− p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs)
233− p.ProfileURL, msgs = parseURL(o.ProfileURL, "profile", msgs)
234− p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
235− p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs)
208+ p.LoginURL, msgs = parseURL(o.Providers[0].LoginURL, "login", msgs)
209+ p.RedeemURL, msgs = parseURL(o.Providers[0].RedeemURL, "redeem", msgs)
210+ p.ProfileURL, msgs = parseURL(o.Providers[0].ProfileURL, "profile", msgs)
211+ p.ValidateURL, msgs = parseURL(o.Providers[0].ValidateURL, "validate", msgs)
212+ p.ProtectedResource, msgs = parseURL(o.Providers[0].ProtectedResource, "resource", msgs)
236213
237214 // Make the OIDC options available to all providers that support it
238− p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
239− p.EmailClaim = o.OIDCEmailClaim
240− p.GroupsClaim = o.OIDCGroupsClaim
215+ p.AllowUnverifiedEmail = o.Providers[0].OIDCConfig.InsecureAllowUnverifiedEmail
216+ p.EmailClaim = o.Providers[0].OIDCConfig.EmailClaim
217+ p.GroupsClaim = o.Providers[0].OIDCConfig.GroupsClaim
241218 p.Verifier = o.GetOIDCVerifier()
242219
243220 // TODO (@NickMeves) - Remove This
244221 // Backwards Compatibility for Deprecated UserIDClaim option
245− if o.OIDCEmailClaim == providers.OIDCEmailClaim &&
246− o.UserIDClaim != providers.OIDCEmailClaim {
247− p.EmailClaim = o.UserIDClaim
222+ if o.Providers[0].OIDCConfig.EmailClaim == providers.OIDCEmailClaim &&
223+ o.Providers[0].OIDCConfig.UserIDClaim != providers.OIDCEmailClaim {
224+ p.EmailClaim = o.Providers[0].OIDCConfig.UserIDClaim
248225 }
249226
250− p.SetAllowedGroups(o.AllowedGroups)
227+ p.SetAllowedGroups(o.Providers[0].AllowedGroups)
251228
252− provider := providers.New(o.ProviderType, p)
229+ provider := providers.New(o.Providers[0].Type, p)
253230 if provider == nil {
254− msgs = append(msgs, fmt.Sprintf("invalid setting: provider '%s' is not available", o.ProviderType))
231+ msgs = append(msgs, fmt.Sprintf("invalid setting: provider '%s' is not available", o.Providers[0].Type))
255232 return msgs
256233 }
257234 o.SetProvider(provider)
258235
259236 switch p := o.GetProvider().(type) {
260237 case *providers.AzureProvider:
261− p.Configure(o.AzureTenant)
238+ p.Configure(o.Providers[0].AzureConfig.Tenant)
262239 case *providers.GitHubProvider:
263− p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
264− p.SetRepo(o.GitHubRepo, o.GitHubToken)
265− p.SetUsers(o.GitHubUsers)
240+ p.SetOrgTeam(o.Providers[0].GitHubConfig.Org, o.Providers[0].GitHubConfig.Team)
241+ p.SetRepo(o.Providers[0].GitHubConfig.Repo, o.Providers[0].GitHubConfig.Token)
242+ p.SetUsers(o.Providers[0].GitHubConfig.Users)
266243 case *providers.KeycloakProvider:
267244 // Backwards compatibility with `--keycloak-group` option
268− if len(o.KeycloakGroups) > 0 {
269− p.SetAllowedGroups(o.KeycloakGroups)
245+ if len(o.Providers[0].KeycloakConfig.Groups) > 0 {
246+ p.SetAllowedGroups(o.Providers[0].KeycloakConfig.Groups)
270247 }
271248 case *providers.GoogleProvider:
272− if o.GoogleServiceAccountJSON != "" {
273− file, err := os.Open(o.GoogleServiceAccountJSON)
249+ if o.Providers[0].GoogleConfig.ServiceAccountJSON != "" {
250+ file, err := os.Open(o.Providers[0].GoogleConfig.ServiceAccountJSON)
274251 if err != nil {
275− msgs = append(msgs, "invalid Google credentials file: "+o.GoogleServiceAccountJSON)
252+ msgs = append(msgs, "invalid Google credentials file: "+o.Providers[0].GoogleConfig.ServiceAccountJSON)
276253 } else {
277− groups := o.AllowedGroups
254+ groups := o.Providers[0].AllowedGroups
278255 // Backwards compatibility with `--google-group` option
279− if len(o.GoogleGroups) > 0 {
280− groups = o.GoogleGroups
256+ if len(o.Providers[0].GoogleConfig.Groups) > 0 {
257+ groups = o.Providers[0].GoogleConfig.Groups
281258 p.SetAllowedGroups(groups)
282259 }
283− p.SetGroupRestriction(groups, o.GoogleAdminEmail, file)
260+ p.SetGroupRestriction(groups, o.Providers[0].GoogleConfig.AdminEmail, file)
284261 }
285262 }
286263 case *providers.BitbucketProvider:
287− p.SetTeam(o.BitbucketTeam)
288− p.SetRepository(o.BitbucketRepository)
264+ p.SetTeam(o.Providers[0].BitbucketConfig.Team)
265+ p.SetRepository(o.Providers[0].BitbucketConfig.Repository)
289266 case *providers.OIDCProvider:
290267 if p.Verifier == nil {
291268 msgs = append(msgs, "oidc provider requires an oidc issuer URL")
292269 }
293270 case *providers.GitLabProvider:
294− p.Groups = o.GitLabGroup
295− err := p.AddProjects(o.GitlabProjects)
271+ p.Groups = o.Providers[0].GitLabConfig.Group
272+ err := p.AddProjects(o.Providers[0].GitLabConfig.Projects)
296273 if err != nil {
297274 msgs = append(msgs, "failed to setup gitlab project access level")
298275 }
@@ -308,7 +285,7 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
308285 msgs = append(msgs, "failed to initialize oidc provider for gitlab.com")
309286 } else {
310287 p.Verifier = provider.Verifier(&oidc.Config{
311− ClientID: o.ClientID,
288+ ClientID: o.Providers[0].ClientID,
312289 })
313290
314291 p.LoginURL, msgs = parseURL(provider.Endpoint().AuthURL, "login", msgs)
@@ -316,31 +293,31 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
316293 }
317294 }
318295 case *providers.LoginGovProvider:
319− p.PubJWKURL, msgs = parseURL(o.PubJWKURL, "pubjwk", msgs)
296+ p.PubJWKURL, msgs = parseURL(o.Providers[0].LoginGovConfig.PubJWKURL, "pubjwk", msgs)
320297
321298 // JWT key can be supplied via env variable or file in the filesystem, but not both.
322299 switch {
323− case o.JWTKey != "" && o.JWTKeyFile != "":
300+ case o.Providers[0].LoginGovConfig.JWTKey != "" && o.Providers[0].LoginGovConfig.JWTKeyFile != "":
324301 msgs = append(msgs, "cannot set both jwt-key and jwt-key-file options")
325− case o.JWTKey == "" && o.JWTKeyFile == "":
302+ case o.Providers[0].LoginGovConfig.JWTKey == "" && o.Providers[0].LoginGovConfig.JWTKeyFile == "":
326303 msgs = append(msgs, "login.gov provider requires a private key for signing JWTs")
327− case o.JWTKey != "":
304+ case o.Providers[0].LoginGovConfig.JWTKey != "":
328305 // The JWT Key is in the commandline argument
329− signKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(o.JWTKey))
306+ signKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(o.Providers[0].LoginGovConfig.JWTKey))
330307 if err != nil {
331308 msgs = append(msgs, "could not parse RSA Private Key PEM")
332309 } else {
333310 p.JWTKey = signKey
334311 }
335− case o.JWTKeyFile != "":
312+ case o.Providers[0].LoginGovConfig.JWTKeyFile != "":
336313 // The JWT key is in the filesystem
337− keyData, err := ioutil.ReadFile(o.JWTKeyFile)
314+ keyData, err := ioutil.ReadFile(o.Providers[0].LoginGovConfig.JWTKeyFile)
338315 if err != nil {
339− msgs = append(msgs, "could not read key file: "+o.JWTKeyFile)
316+ msgs = append(msgs, "could not read key file: "+o.Providers[0].LoginGovConfig.JWTKeyFile)
340317 }
341318 signKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyData)
342319 if err != nil {
343− msgs = append(msgs, "could not parse private key from PEM file:"+o.JWTKeyFile)
320+ msgs = append(msgs, "could not parse private key from PEM file:"+o.Providers[0].LoginGovConfig.JWTKeyFile)
344321 } else {
345322 p.JWTKey = signKey
346323 }
pkg/validation/options_test.go+23−18
@@ -17,6 +17,7 @@ const (
1717 cookieSecret = "secretthirtytwobytes+abcdefghijk"
1818 clientID = "bazquux"
1919 clientSecret = "xyzzyplugh"
20+ providerID = "providerID"
2021)
2122
2223func testOptions() *options.Options {
@@ -27,8 +28,9 @@ func testOptions() *options.Options {
2728 URI: "http://127.0.0.1:8080/",
2829 })
2930 o.Cookie.Secret = cookieSecret
30− o.ClientID = clientID
31− o.ClientSecret = clientSecret
31+ o.Providers[0].ID = providerID
32+ o.Providers[0].ClientID = clientID
33+ o.Providers[0].ClientSecret = clientSecret
3234 o.EmailDomains = []string{"*"}
3335 return o
3436}
@@ -48,7 +50,8 @@ func TestNewOptions(t *testing.T) {
4850
4951 expected := errorMsg([]string{
5052 "missing setting: cookie-secret",
51− "missing setting: client-id",
53+ "provider has empty id: ids are required for all providers",
54+ "provider missing setting: client-id",
5255 "missing setting: client-secret or client-secret-file"})
5356 assert.Equal(t, expected, err.Error())
5457}
@@ -56,8 +59,9 @@ func TestNewOptions(t *testing.T) {
5659func TestClientSecretFileOptionFails(t *testing.T) {
5760 o := options.NewOptions()
5861 o.Cookie.Secret = cookieSecret
59− o.ClientID = clientID
60− o.ClientSecretFile = clientSecret
62+ o.Providers[0].ID = providerID
63+ o.Providers[0].ClientID = clientID
64+ o.Providers[0].ClientSecretFile = clientSecret
6165 o.EmailDomains = []string{"*"}
6266 err := Validate(o)
6367 assert.NotEqual(t, nil, err)
@@ -93,8 +97,9 @@ func TestClientSecretFileOption(t *testing.T) {
9397
9498 o := options.NewOptions()
9599 o.Cookie.Secret = cookieSecret
96− o.ClientID = clientID
97− o.ClientSecretFile = clientSecretFileName
100+ o.Providers[0].ID = providerID
101+ o.Providers[0].ClientID = clientID
102+ o.Providers[0].ClientSecretFile = clientSecretFileName
98103 o.EmailDomains = []string{"*"}
99104 err = Validate(o)
100105 assert.Equal(t, nil, err)
@@ -110,7 +115,7 @@ func TestClientSecretFileOption(t *testing.T) {
110115
111116func TestGoogleGroupOptions(t *testing.T) {
112117 o := testOptions()
113− o.GoogleGroups = []string{"googlegroup"}
118+ o.Providers[0].GoogleConfig.Groups = []string{"googlegroup"}
114119 err := Validate(o)
115120 assert.NotEqual(t, nil, err)
116121
@@ -122,9 +127,9 @@ func TestGoogleGroupOptions(t *testing.T) {
122127
123128func TestGoogleGroupInvalidFile(t *testing.T) {
124129 o := testOptions()
125− o.GoogleGroups = []string{"test_group"}
126− o.GoogleAdminEmail = "admin@example.com"
127− o.GoogleServiceAccountJSON = "file_doesnt_exist.json"
130+ o.Providers[0].GoogleConfig.Groups = []string{"test_group"}
131+ o.Providers[0].GoogleConfig.AdminEmail = "admin@example.com"
132+ o.Providers[0].GoogleConfig.ServiceAccountJSON = "file_doesnt_exist.json"
128133 err := Validate(o)
129134 assert.NotEqual(t, nil, err)
130135
@@ -225,17 +230,17 @@ func TestValidateSignatureKeyUnsupportedAlgorithm(t *testing.T) {
225230
226231func TestSkipOIDCDiscovery(t *testing.T) {
227232 o := testOptions()
228− o.ProviderType = "oidc"
229− o.OIDCIssuerURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/v2.0/"
230− o.SkipOIDCDiscovery = true
233+ o.Providers[0].Type = "oidc"
234+ o.Providers[0].OIDCConfig.IssuerURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/v2.0/"
235+ o.Providers[0].OIDCConfig.SkipDiscovery = true
231236
232237 err := Validate(o)
233238 assert.Equal(t, "invalid configuration:\n"+
234239 " missing setting: login-url\n missing setting: redeem-url\n missing setting: oidc-jwks-url", err.Error())
235240
236− o.LoginURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1_sign_in"
237− o.RedeemURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_sign_in"
238− o.OIDCJwksURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/discovery/v2.0/keys"
241+ o.Providers[0].LoginURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1_sign_in"
242+ o.Providers[0].RedeemURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_sign_in"
243+ o.Providers[0].OIDCConfig.JwksURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/discovery/v2.0/keys"
239244
240245 assert.Equal(t, nil, Validate(o))
241246}
@@ -292,7 +297,7 @@ func TestProviderCAFilesError(t *testing.T) {
292297 assert.NoError(t, os.Remove(file.Name()))
293298
294299 o := testOptions()
295− o.ProviderCAFiles = append(o.ProviderCAFiles, file.Name())
300+ o.Providers[0].CAFiles = append(o.Providers[0].CAFiles, file.Name())
296301 err = Validate(o)
297302 assert.Error(t, err)
298303 assert.Contains(t, err.Error(), "unable to load provider CA file(s)")
pkg/validation/providers.gonew file+83
@@ -0,0 +1,83 @@
1+package validation
2+
3+import (
4+ "fmt"
5+ "io/ioutil"
6+
7+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
8+)
9+
10+// validateProviders is the initial validation migration for multiple providrers
11+// It currently includes only logic that can verify the providers one by one and does not break the valdation pipe
12+func validateProviders(o *options.Options) []string {
13+ msgs := []string{}
14+
15+ // validate general multiple provider configuration
16+ if len(o.Providers) == 0 {
17+ msgs = append(msgs, "at least one provider has to be defined")
18+ }
19+ if o.SkipProviderButton && len(o.Providers) > 1 {
20+ msgs = append(msgs, "SkipProviderButton and multiple providers are mutually exclusive")
21+ }
22+
23+ providerIDs := make(map[string]struct{})
24+
25+ for _, provider := range o.Providers {
26+ msgs = append(msgs, validateProvider(provider, providerIDs)...)
27+ }
28+
29+ return msgs
30+}
31+
32+func validateProvider(provider options.Provider, providerIDs map[string]struct{}) []string {
33+ msgs := []string{}
34+
35+ if provider.ID == "" {
36+ msgs = append(msgs, "provider has empty id: ids are required for all providers")
37+ }
38+
39+ // Ensure provider IDs are unique
40+ if _, ok := providerIDs[provider.ID]; ok {
41+ msgs = append(msgs, fmt.Sprintf("multiple providers found with id %s: provider ids must be unique", provider.ID))
42+ }
43+ providerIDs[provider.ID] = struct{}{}
44+
45+ if provider.ClientID == "" {
46+ msgs = append(msgs, "provider missing setting: client-id")
47+ }
48+
49+ // login.gov uses a signed JWT to authenticate, not a client-secret
50+ if provider.Type != "login.gov" {
51+ if provider.ClientSecret == "" && provider.ClientSecretFile == "" {
52+ msgs = append(msgs, "missing setting: client-secret or client-secret-file")
53+ }
54+ if provider.ClientSecret == "" && provider.ClientSecretFile != "" {
55+ _, err := ioutil.ReadFile(provider.ClientSecretFile)
56+ if err != nil {
57+ msgs = append(msgs, "could not read client secret file: "+provider.ClientSecretFile)
58+ }
59+ }
60+ }
61+
62+ msgs = append(msgs, validateGoogleConfig(provider)...)
63+
64+ return msgs
65+}
66+
67+func validateGoogleConfig(provider options.Provider) []string {
68+ msgs := []string{}
69+ if len(provider.GoogleConfig.Groups) > 0 ||
70+ provider.GoogleConfig.AdminEmail != "" ||
71+ provider.GoogleConfig.ServiceAccountJSON != "" {
72+ if len(provider.GoogleConfig.Groups) < 1 {
73+ msgs = append(msgs, "missing setting: google-group")
74+ }
75+ if provider.GoogleConfig.AdminEmail == "" {
76+ msgs = append(msgs, "missing setting: google-admin-email")
77+ }
78+ if provider.GoogleConfig.ServiceAccountJSON == "" {
79+ msgs = append(msgs, "missing setting: google-service-account-json")
80+ }
81+ }
82+ return msgs
83+}
pkg/validation/providers_test.gonew file+84
@@ -0,0 +1,84 @@
1+package validation
2+
3+import (
4+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
5+ . "github.com/onsi/ginkgo"
6+ . "github.com/onsi/ginkgo/extensions/table"
7+ . "github.com/onsi/gomega"
8+)
9+
10+var _ = Describe("Providers", func() {
11+ type validateProvidersTableInput struct {
12+ options *options.Options
13+ errStrings []string
14+ }
15+
16+ validProvider := options.Provider{
17+ ID: "ProviderID",
18+ ClientID: "ClientID",
19+ ClientSecret: "ClientSecret",
20+ }
21+
22+ validLoginGovProvider := options.Provider{
23+ Type: "login.gov",
24+ ID: "ProviderIDLoginGov",
25+ ClientID: "ClientID",
26+ ClientSecret: "ClientSecret",
27+ }
28+
29+ missingIDProvider := options.Provider{
30+ ClientID: "ClientID",
31+ ClientSecret: "ClientSecret",
32+ }
33+
34+ missingProvider := "at least one provider has to be defined"
35+ emptyIDMsg := "provider has empty id: ids are required for all providers"
36+ duplicateProviderIDMsg := "multiple providers found with id ProviderID: provider ids must be unique"
37+ skipButtonAndMultipleProvidersMsg := "SkipProviderButton and multiple providers are mutually exclusive"
38+
39+ DescribeTable("validateProviders",
40+ func(o *validateProvidersTableInput) {
41+ Expect(validateProviders(o.options)).To(ConsistOf(o.errStrings))
42+ },
43+ Entry("with no providers", &validateProvidersTableInput{
44+ options: &options.Options{},
45+ errStrings: []string{missingProvider},
46+ }),
47+ Entry("with valid providers", &validateProvidersTableInput{
48+ options: &options.Options{
49+ Providers: options.Providers{
50+ validProvider,
51+ validLoginGovProvider,
52+ },
53+ },
54+ errStrings: []string{},
55+ }),
56+ Entry("with an empty providerID", &validateProvidersTableInput{
57+ options: &options.Options{
58+ Providers: options.Providers{
59+ missingIDProvider,
60+ },
61+ },
62+ errStrings: []string{emptyIDMsg},
63+ }),
64+ Entry("with same providerID", &validateProvidersTableInput{
65+ options: &options.Options{
66+ Providers: options.Providers{
67+ validProvider,
68+ validProvider,
69+ },
70+ },
71+ errStrings: []string{duplicateProviderIDMsg},
72+ }),
73+ Entry("with multiple providers and skip provider button", &validateProvidersTableInput{
74+ options: &options.Options{
75+ SkipProviderButton: true,
76+ Providers: options.Providers{
77+ validProvider,
78+ validLoginGovProvider,
79+ },
80+ },
81+ errStrings: []string{skipButtonAndMultipleProvidersMsg},
82+ }),
83+ )
84+})