git.iko.soy/typeable/oauth2-proxy
on master help sign in
Multiple providers in alpha config (#947)
42475c2yanasega
CHANGELOG.md+2
@@ -8,6 +8,8 @@
8
8
9## Changes since v7.1.2
9## Changes since v7.1.2
10
10
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+
11# V7.1.2
13# V7.1.2
12
14
13## Release Highlights
15## Release Highlights
contrib/local-environment/oauth2-proxy-alpha-config.cfg−5
@@ -1,10 +1,5 @@
1http_address="0.0.0.0:4180"
1http_address="0.0.0.0:4180"
2cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
2cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
3−provider="oidc"
4email_domains="example.com"
3email_domains="example.com"
5−oidc_issuer_url="http://dex.localhost:4190/dex"
6−client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
7−client_id="oauth2-proxy"
8cookie_secure="false"
4cookie_secure="false"
9−
10redirect_url="http://localhost:4180/oauth2/callback"
5redirect_url="http://localhost:4180/oauth2/callback"
contrib/local-environment/oauth2-proxy-alpha-config.yaml+6
@@ -15,3 +15,9 @@ injectRequestHeaders:
15- name: X-Forwarded-Preferred-Username
15- name: X-Forwarded-Preferred-Username
16 values:
16 values:
17 - claim: preferred_username
17 - 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.
119| `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. |
119| `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. |
120| `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. |
120| `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. |
121| `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. |
121| `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 |
122
144
123### ClaimSource
145### ClaimSource
124
146
@@ -143,6 +165,43 @@ each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45
143Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
165Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
144
166
145
167
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+
146### Header
205### Header
147
206
148(**Appears on:** [AlphaOptions](#alphaoptions))
207(**Appears on:** [AlphaOptions](#alphaoptions))
@@ -172,6 +231,88 @@ make up the header value
172| `prefix` | _string_ | Prefix is an optional prefix that will be prepended to the value of the<br/>claim if it is non-empty. |
231| `prefix` | _string_ | Prefix is an optional prefix that will be prepended to the value of the<br/>claim if it is non-empty. |
173| `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. |
232| `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. |
174
233
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+
175### SecretSource
316### SecretSource
176
317
177(**Appears on:** [ClaimSource](#claimsource), [HeaderValue](#headervalue), [TLS](#tls))
318(**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
398github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
398github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
399github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
399github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
400github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
400github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
401+github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
401github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
402github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
402github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
403github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
403github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
404github.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"
19upstreams="http://httpbin"
19upstreams="http://httpbin"
20set_basic_auth="true"
20set_basic_auth="true"
21basic_auth_password="super-secret-password"
21basic_auth_password="super-secret-password"
22+client_id="oauth2-proxy"
23+client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
22`
24`
23
25
24 const testAlphaConfig = `
26 const testAlphaConfig = `
@@ -57,15 +59,23 @@ injectResponseHeaders:
57 value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk
59 value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk
58server:
60server:
59 bindAddress: "127.0.0.1:4180"
61 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
60`
74`
61
75
62 const testCoreConfig = `
76 const testCoreConfig = `
63cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
77cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
64−provider="oidc"
65email_domains="example.com"
78email_domains="example.com"
66−oidc_issuer_url="http://dex.localhost:4190/dex"
67−client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
68−client_id="oauth2-proxy"
69cookie_secure="false"
79cookie_secure="false"
70
80
71redirect_url="http://localhost:4180/oauth2/callback"
81redirect_url="http://localhost:4180/oauth2/callback"
@@ -85,11 +95,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
85 Expect(err).ToNot(HaveOccurred())
95 Expect(err).ToNot(HaveOccurred())
86
96
87 opts.Cookie.Secret = "OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
97 opts.Cookie.Secret = "OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
88− opts.ProviderType = "oidc"
89 opts.EmailDomains = []string{"example.com"}
98 opts.EmailDomains = []string{"example.com"}
90− opts.OIDCIssuerURL = "http://dex.localhost:4190/dex"
91− opts.ClientSecret = "b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
92− opts.ClientID = "oauth2-proxy"
93 opts.Cookie.Secure = false
99 opts.Cookie.Secure = false
94 opts.RawRedirectURL = "http://localhost:4180/oauth2/callback"
100 opts.RawRedirectURL = "http://localhost:4180/oauth2/callback"
95
101
@@ -121,6 +127,24 @@ redirect_url="http://localhost:4180/oauth2/callback"
121
127
122 opts.InjectRequestHeaders = append([]options.Header{authHeader}, opts.InjectRequestHeaders...)
128 opts.InjectRequestHeaders = append([]options.Header{authHeader}, opts.InjectRequestHeaders...)
123 opts.InjectResponseHeaders = append(opts.InjectResponseHeaders, authHeader)
129 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+ }
124 return opts
148 return opts
125 }
149 }
126
150
@@ -204,7 +228,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
204 configContent: testCoreConfig,
228 configContent: testCoreConfig,
205 alphaConfigContent: testAlphaConfig + ":",
229 alphaConfigContent: testAlphaConfig + ":",
206 expectedOptions: func() *options.Options { return nil },
230 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"),
208 }),
232 }),
209 Entry("with alpha configuration and bad core configuration", loadConfigurationTableInput{
233 Entry("with alpha configuration and bad core configuration", loadConfigurationTableInput{
210 configContent: testCoreConfig + "unknown_field=\"something\"",
234 configContent: testCoreConfig + "unknown_field=\"something\"",
oauthproxy.go+3−3
@@ -122,7 +122,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
122 Footer: opts.Templates.Footer,
122 Footer: opts.Templates.Footer,
123 Version: VERSION,
123 Version: VERSION,
124 Debug: opts.Templates.Debug,
124 Debug: opts.Templates.Debug,
125− ProviderName: buildProviderName(opts.GetProvider(), opts.ProviderName),
125+ ProviderName: buildProviderName(opts.GetProvider(), opts.Providers[0].Name),
126 SignInMessage: buildSignInMessage(opts),
126 SignInMessage: buildSignInMessage(opts),
127 DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
127 DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
128 })
128 })
@@ -136,7 +136,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
136 }
136 }
137
137
138 if opts.SkipJwtBearerTokens {
138 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)
140 for _, issuer := range opts.ExtraJwtIssuers {
140 for _, issuer := range opts.ExtraJwtIssuers {
141 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
141 logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
142 }
142 }
@@ -146,7 +146,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
146 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
146 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
147 }
147 }
148
148
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)
150 refresh := "disabled"
150 refresh := "disabled"
151 if opts.Cookie.Refresh != time.Duration(0) {
151 if opts.Cookie.Refresh != time.Duration(0) {
152 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
152 refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)
oauthproxy_test.go+7−6
@@ -981,7 +981,7 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi
981 ProviderData: &providers.ProviderData{},
981 ProviderData: &providers.ProviderData{},
982 ValidToken: opts.providerValidateCookieResponse,
982 ValidToken: opts.providerValidateCookieResponse,
983 }
983 }
984− pcTest.proxy.provider.(*TestProvider).SetAllowedGroups(pcTest.opts.AllowedGroups)
984+ pcTest.proxy.provider.(*TestProvider).SetAllowedGroups(pcTest.opts.Providers[0].AllowedGroups)
985
985
986 pcTest.rw = httptest.NewRecorder()
986 pcTest.rw = httptest.NewRecorder()
987 pcTest.req, _ = http.NewRequest("GET", "/", strings.NewReader(""))
987 pcTest.req, _ = http.NewRequest("GET", "/", strings.NewReader(""))
@@ -1322,7 +1322,7 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
1322 },
1322 },
1323 },
1323 },
1324 }
1324 }
1325− pcTest.opts.AllowedGroups = []string{"oauth_groups"}
1325+ pcTest.opts.Providers[0].AllowedGroups = []string{"oauth_groups"}
1326 err := validation.Validate(pcTest.opts)
1326 err := validation.Validate(pcTest.opts)
1327 assert.NoError(t, err)
1327 assert.NoError(t, err)
1328
1328
@@ -2292,8 +2292,9 @@ func Test_noCacheHeaders(t *testing.T) {
2292func baseTestOptions() *options.Options {
2292func baseTestOptions() *options.Options {
2293 opts := options.NewOptions()
2293 opts := options.NewOptions()
2294 opts.Cookie.Secret = rawCookieSecret
2294 opts.Cookie.Secret = rawCookieSecret
2295+ opts.Providers[0].ID = "providerID"
2295− opts.ClientID = clientID
2296+ opts.Providers[0].ClientID = clientID
2296− opts.ClientSecret = clientSecret
2297+ opts.Providers[0].ClientSecret = clientSecret
2297 opts.EmailDomains = []string{"*"}
2298 opts.EmailDomains = []string{"*"}
2298
2299
2299 // Default injected headers for legacy configuration
2300 // Default injected headers for legacy configuration
@@ -2786,7 +2787,7 @@ func TestProxyAllowedGroups(t *testing.T) {
2786 t.Cleanup(upstreamServer.Close)
2787 t.Cleanup(upstreamServer.Close)
2787
2788
2788 test, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
2789 test, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
2789− opts.AllowedGroups = tt.allowedGroups
2790+ opts.Providers[0].AllowedGroups = tt.allowedGroups
2790 opts.UpstreamServers = options.Upstreams{
2791 opts.UpstreamServers = options.Upstreams{
2791 {
2792 {
2792 ID: upstreamServer.URL,
2793 ID: upstreamServer.URL,
@@ -2915,7 +2916,7 @@ func TestAuthOnlyAllowedGroups(t *testing.T) {
2915 }
2916 }
2916
2917
2917 test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {
2918 test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {
2918− opts.AllowedGroups = tc.allowedGroups
2919+ opts.Providers[0].AllowedGroups = tc.allowedGroups
2919 })
2920 })
2920 if err != nil {
2921 if err != nil {
2921 t.Fatal(err)
2922 t.Fatal(err)
pkg/apis/options/alpha_options.go+6
@@ -40,6 +40,9 @@ type AlphaOptions struct {
40 // This can be done by setting the BindAddress and the SecureBindAddress simultaneously.
40 // This can be done by setting the BindAddress and the SecureBindAddress simultaneously.
41 // To use the secure server you must configure a TLS certificate and key.
41 // To use the secure server you must configure a TLS certificate and key.
42 MetricsServer Server `json:"metricsServer,omitempty"`
42 MetricsServer Server `json:"metricsServer,omitempty"`
43+
44+ // Providers is used to configure multiple providers.
45+ Providers Providers `json:"providers,omitempty"`
43}
46}
44
47
45// MergeInto replaces alpha options in the Options struct with the values
48// MergeInto replaces alpha options in the Options struct with the values
@@ -50,6 +53,8 @@ func (a *AlphaOptions) MergeInto(opts *Options) {
50 opts.InjectResponseHeaders = a.InjectResponseHeaders
53 opts.InjectResponseHeaders = a.InjectResponseHeaders
51 opts.Server = a.Server
54 opts.Server = a.Server
52 opts.MetricsServer = a.MetricsServer
55 opts.MetricsServer = a.MetricsServer
56+ opts.Providers = a.Providers
57+
53}
58}
54
59
55// ExtractFrom populates the fields in the AlphaOptions with the values from
60// ExtractFrom populates the fields in the AlphaOptions with the values from
@@ -60,4 +65,5 @@ func (a *AlphaOptions) ExtractFrom(opts *Options) {
60 a.InjectResponseHeaders = opts.InjectResponseHeaders
65 a.InjectResponseHeaders = opts.InjectResponseHeaders
61 a.Server = opts.Server
66 a.Server = opts.Server
62 a.MetricsServer = opts.MetricsServer
67 a.MetricsServer = opts.MetricsServer
68+ a.Providers = opts.Providers
63}
69}
pkg/apis/options/legacy_options.go+209
@@ -8,6 +8,7 @@ import (
8 "time"
8 "time"
9
9
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
11 "github.com/spf13/pflag"
12 "github.com/spf13/pflag"
12)
13)
13
14
@@ -21,6 +22,9 @@ type LegacyOptions struct {
21 // Legacy options for the server address and TLS
22 // Legacy options for the server address and TLS
22 LegacyServer LegacyServer `cfg:",squash"`
23 LegacyServer LegacyServer `cfg:",squash"`
23
24
25+ // Legacy options for single provider
26+ LegacyProvider LegacyProvider `cfg:",squash"`
27+
24 Options Options `cfg:",squash"`
28 Options Options `cfg:",squash"`
25}
29}
26
30
@@ -43,6 +47,15 @@ func NewLegacyOptions() *LegacyOptions {
43 HTTPSAddress: ":443",
47 HTTPSAddress: ":443",
44 },
48 },
45
49
50+ LegacyProvider: LegacyProvider{
51+ ProviderType: "google",
52+ AzureTenant: "common",
53+ ApprovalPrompt: "force",
54+ UserIDClaim: "email",
55+ OIDCEmailClaim: "email",
56+ OIDCGroupsClaim: "groups",
57+ },
58+
46 Options: *NewOptions(),
59 Options: *NewOptions(),
47 }
60 }
48}
61}
@@ -53,6 +66,7 @@ func NewLegacyFlagSet() *pflag.FlagSet {
53 flagSet.AddFlagSet(legacyUpstreamsFlagSet())
66 flagSet.AddFlagSet(legacyUpstreamsFlagSet())
54 flagSet.AddFlagSet(legacyHeadersFlagSet())
67 flagSet.AddFlagSet(legacyHeadersFlagSet())
55 flagSet.AddFlagSet(legacyServerFlagset())
68 flagSet.AddFlagSet(legacyServerFlagset())
69+ flagSet.AddFlagSet(legacyProviderFlagSet())
56
70
57 return flagSet
71 return flagSet
58}
72}
@@ -65,10 +79,17 @@ func (l *LegacyOptions) ToOptions() (*Options, error) {
65 l.Options.UpstreamServers = upstreams
79 l.Options.UpstreamServers = upstreams
66
80
67 l.Options.InjectRequestHeaders, l.Options.InjectResponseHeaders = l.LegacyHeaders.convert()
81 l.Options.InjectRequestHeaders, l.Options.InjectResponseHeaders = l.LegacyHeaders.convert()
82+
68 l.Options.Server, l.Options.MetricsServer = l.LegacyServer.convert()
83 l.Options.Server, l.Options.MetricsServer = l.LegacyServer.convert()
69
84
70 l.Options.LegacyPreferEmailToUser = l.LegacyHeaders.PreferEmailToUser
85 l.Options.LegacyPreferEmailToUser = l.LegacyHeaders.PreferEmailToUser
71
86
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+
72 return &l.Options, nil
93 return &l.Options, nil
73}
94}
74
95
@@ -443,6 +464,106 @@ func legacyServerFlagset() *pflag.FlagSet {
443 return flagSet
464 return flagSet
444}
465}
445
466
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+
446func (l LegacyServer) convert() (Server, Server) {
567func (l LegacyServer) convert() (Server, Server) {
447 appServer := Server{
568 appServer := Server{
448 BindAddress: l.HTTPAddress,
569 BindAddress: l.HTTPAddress,
@@ -482,3 +603,91 @@ func (l LegacyServer) convert() (Server, Server) {
482
603
483 return appServer, metricsServer
604 return appServer, metricsServer
484}
605}
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() {
22 legacyOpts.LegacyUpstreams.ProxyWebSockets = true
22 legacyOpts.LegacyUpstreams.ProxyWebSockets = true
23 legacyOpts.LegacyUpstreams.SSLUpstreamInsecureSkipVerify = true
23 legacyOpts.LegacyUpstreams.SSLUpstreamInsecureSkipVerify = true
24 legacyOpts.LegacyUpstreams.Upstreams = []string{"http://foo.bar/baz", "file:///var/lib/website#/bar", "static://204"}
24 legacyOpts.LegacyUpstreams.Upstreams = []string{"http://foo.bar/baz", "file:///var/lib/website#/bar", "static://204"}
25+ legacyOpts.LegacyProvider.ClientID = "oauth-proxy"
25
26
26 truth := true
27 truth := true
27 staticCode := 204
28 staticCode := 204
@@ -110,6 +111,9 @@ var _ = Describe("Legacy Options", func() {
110 BindAddress: "127.0.0.1:4180",
111 BindAddress: "127.0.0.1:4180",
111 }
112 }
112
113
114+ opts.Providers[0].ClientID = "oauth-proxy"
115+ opts.Providers[0].ID = "google=oauth-proxy"
116+
113 converted, err := legacyOpts.ToOptions()
117 converted, err := legacyOpts.ToOptions()
114 Expect(err).ToNot(HaveOccurred())
118 Expect(err).ToNot(HaveOccurred())
115 Expect(converted).To(Equal(opts))
119 Expect(converted).To(Equal(opts))
@@ -196,9 +200,9 @@ var _ = Describe("Legacy Options", func() {
196 invalidHTTPErrMsg := "could not parse upstream \":foo\": parse \":foo\": missing protocol scheme"
200 invalidHTTPErrMsg := "could not parse upstream \":foo\": parse \":foo\": missing protocol scheme"
197
201
198 DescribeTable("convertLegacyUpstreams",
202 DescribeTable("convertLegacyUpstreams",
199− func(o *convertUpstreamsTableInput) {
203+ func(in *convertUpstreamsTableInput) {
200 legacyUpstreams := LegacyUpstreams{
204 legacyUpstreams := LegacyUpstreams{
201− Upstreams: o.upstreamStrings,
205+ Upstreams: in.upstreamStrings,
202 SSLUpstreamInsecureSkipVerify: skipVerify,
206 SSLUpstreamInsecureSkipVerify: skipVerify,
203 PassHostHeader: passHostHeader,
207 PassHostHeader: passHostHeader,
204 ProxyWebSockets: proxyWebSockets,
208 ProxyWebSockets: proxyWebSockets,
@@ -207,14 +211,14 @@ var _ = Describe("Legacy Options", func() {
207
211
208 upstreams, err := legacyUpstreams.convert()
212 upstreams, err := legacyUpstreams.convert()
209
213
210− if o.errMsg != "" {
214+ if in.errMsg != "" {
211 Expect(err).To(HaveOccurred())
215 Expect(err).To(HaveOccurred())
212− Expect(err.Error()).To(Equal(o.errMsg))
216+ Expect(err.Error()).To(Equal(in.errMsg))
213 } else {
217 } else {
214 Expect(err).ToNot(HaveOccurred())
218 Expect(err).ToNot(HaveOccurred())
215 }
219 }
216
220
217− Expect(upstreams).To(ConsistOf(o.expectedUpstreams))
221+ Expect(upstreams).To(ConsistOf(in.expectedUpstreams))
218 },
222 },
219 Entry("with no upstreams", &convertUpstreamsTableInput{
223 Entry("with no upstreams", &convertUpstreamsTableInput{
220 upstreamStrings: []string{},
224 upstreamStrings: []string{},
@@ -850,6 +854,87 @@ var _ = Describe("Legacy Options", func() {
850 },
854 },
851 }),
855 }),
852 )
856 )
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+ }
853
920
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+ )
854 })
939 })
855})
940})
pkg/apis/options/load_test.go+45−2
@@ -14,6 +14,49 @@ import (
14)
14)
15
15
16var _ = Describe("Load", func() {
16var _ = 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+
17 Context("with a testOptions structure", func() {
60 Context("with a testOptions structure", func() {
18 type TestOptionSubStruct struct {
61 type TestOptionSubStruct struct {
19 StringSliceOption []string `flag:"string-slice-option" cfg:"string_slice_option"`
62 StringSliceOption []string `flag:"string-slice-option" cfg:"string_slice_option"`
@@ -294,12 +337,12 @@ var _ = Describe("Load", func() {
294 Entry("with an empty Options struct, should return default values", &testOptionsTableInput{
337 Entry("with an empty Options struct, should return default values", &testOptionsTableInput{
295 flagSet: NewFlagSet,
338 flagSet: NewFlagSet,
296 input: &Options{},
339 input: &Options{},
297− expectedOutput: NewOptions(),
340+ expectedOutput: optionsWithNilProvider,
298 }),
341 }),
299 Entry("with an empty LegacyOptions struct, should return default values", &testOptionsTableInput{
342 Entry("with an empty LegacyOptions struct, should return default values", &testOptionsTableInput{
300 flagSet: NewLegacyFlagSet,
343 flagSet: NewLegacyFlagSet,
301 input: &LegacyOptions{},
344 input: &LegacyOptions{},
302− expectedOutput: NewLegacyOptions(),
345+ expectedOutput: legacyOptionsWithNilProvider,
303 }),
346 }),
304 )
347 )
305 })
348 })
pkg/apis/options/options.go+18−111
@@ -27,29 +27,12 @@ type Options struct {
27 TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
27 TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
28 ForceHTTPS bool `flag:"force-https" cfg:"force_https"`
28 ForceHTTPS bool `flag:"force-https" cfg:"force_https"`
29 RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
29 RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
30− ClientID string `flag:"client-id" cfg:"client_id"`
30+
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"`
31+ 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"`
32+ EmailDomains []string `flag:"email-domain" cfg:"email_domains"`
40− WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains"`
33+ 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"`
34+ HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"`
52− HtpasswdUserGroups []string `flag:"htpasswd-user-group" cfg:"htpasswd_user_groups"`
35+ HtpasswdUserGroups []string `flag:"htpasswd-user-group" cfg:"htpasswd_user_groups"`
53
36
54 Cookie Cookie `cfg:",squash"`
37 Cookie Cookie `cfg:",squash"`
55 Session SessionOptions `cfg:",squash"`
38 Session SessionOptions `cfg:",squash"`
@@ -66,6 +49,8 @@ type Options struct {
66 Server Server `cfg:",internal"`
49 Server Server `cfg:",internal"`
67 MetricsServer Server `cfg:",internal"`
50 MetricsServer Server `cfg:",internal"`
68
51
52+ Providers Providers `cfg:",internal"`
53+
69 SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
54 SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
70 SkipAuthRoutes []string `flag:"skip-auth-route" cfg:"skip_auth_routes"`
55 SkipAuthRoutes []string `flag:"skip-auth-route" cfg:"skip_auth_routes"`
71 SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"`
56 SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"`
@@ -74,34 +59,7 @@ type Options struct {
74 SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"`
59 SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"`
75 SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"`
60 SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"`
76
61
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−
100 SignatureKey string `flag:"signature-key" cfg:"signature_key"`
62 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"`
105 GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks"`
63 GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks"`
106
64
107 // This is used for backwards compatibility for basic auth users
65 // This is used for backwards compatibility for basic auth users
@@ -135,24 +93,16 @@ func (o *Options) SetRealClientIPParser(s ipapi.RealClientIPParser) { o.realClie
135// NewOptions constructs a new Options with defaulted values
93// NewOptions constructs a new Options with defaulted values
136func NewOptions() *Options {
94func NewOptions() *Options {
137 return &Options{
95 return &Options{
138− ProxyPrefix: "/oauth2",
96+ ProxyPrefix: "/oauth2",
139− ProviderType: "google",
97+ Providers: providerDefaults(),
140− PingPath: "/ping",
98+ PingPath: "/ping",
141− RealClientIPHeader: "X-Real-IP",
99+ RealClientIPHeader: "X-Real-IP",
142− ForceHTTPS: false,
100+ ForceHTTPS: false,
143− Cookie: cookieDefaults(),
101+ Cookie: cookieDefaults(),
144− Session: sessionOptionsDefaults(),
102+ Session: sessionOptionsDefaults(),
145− Templates: templatesDefaults(),
103+ Templates: templatesDefaults(),
146− AzureTenant: "common",
147− SkipAuthPreflight: false,
104+ SkipAuthPreflight: false,
148− Prompt: "", // Change to "login" when ApprovalPrompt officially deprecated
149− ApprovalPrompt: "force",
150− InsecureOIDCAllowUnverifiedEmail: false,
151− SkipOIDCDiscovery: false,
152− Logging: loggingDefaults(),
105+ Logging: loggingDefaults(),
153− UserIDClaim: providers.OIDCEmailClaim, // Deprecated: Use OIDCEmailClaim
154− OIDCEmailClaim: providers.OIDCEmailClaim,
155− OIDCGroupsClaim: providers.OIDCGroupsClaim,
156 }
106 }
157}
107}
158
108
@@ -175,23 +125,6 @@ func NewFlagSet() *pflag.FlagSet {
175
125
176 flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
126 flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
177 flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
127 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")
195 flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
128 flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
196 flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -B\" for bcrypt encryption")
129 flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -B\" for bcrypt encryption")
197 flagSet.StringSlice("htpasswd-user-group", []string{}, "the groups to be set on sessions for htpasswd users (may be given multiple times)")
130 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 {
211 flagSet.Bool("redis-use-cluster", false, "Connect to redis cluster. Must set --redis-cluster-connection-urls to use this feature")
144 flagSet.Bool("redis-use-cluster", false, "Connect to redis cluster. Must set --redis-cluster-connection-urls to use this feature")
212 flagSet.StringSlice("redis-cluster-connection-urls", []string{}, "List of Redis cluster connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-cluster")
145 flagSet.StringSlice("redis-cluster-connection-urls", []string{}, "List of Redis cluster connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-cluster")
213
146
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−
233 flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)")
147 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")
238 flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints")
148 flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints")
239
149
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−
243 flagSet.AddFlagSet(cookieFlagSet())
150 flagSet.AddFlagSet(cookieFlagSet())
244 flagSet.AddFlagSet(loggingFlagSet())
151 flagSet.AddFlagSet(loggingFlagSet())
245 flagSet.AddFlagSet(templatesFlagSet())
152 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 {
29 msgs = append(msgs, validateRedisSessionStore(o)...)
29 msgs = append(msgs, validateRedisSessionStore(o)...)
30 msgs = append(msgs, prefixValues("injectRequestHeaders: ", validateHeaders(o.InjectRequestHeaders)...)...)
30 msgs = append(msgs, prefixValues("injectRequestHeaders: ", validateHeaders(o.InjectRequestHeaders)...)...)
31 msgs = append(msgs, prefixValues("injectResponseHeaders: ", validateHeaders(o.InjectResponseHeaders)...)...)
31 msgs = append(msgs, prefixValues("injectResponseHeaders: ", validateHeaders(o.InjectResponseHeaders)...)...)
32+ msgs = append(msgs, validateProviders(o)...)
32 msgs = configureLogger(o.Logging, msgs)
33 msgs = configureLogger(o.Logging, msgs)
33 msgs = parseSignatureKey(o, msgs)
34 msgs = parseSignatureKey(o, msgs)
34
35
@@ -39,8 +40,8 @@ func Validate(o *options.Options) error {
39 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
40 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
40 }
41 }
41 http.DefaultClient = &http.Client{Transport: insecureTransport}
42 http.DefaultClient = &http.Client{Transport: insecureTransport}
42− } else if len(o.ProviderCAFiles) > 0 {
43+ } else if len(o.Providers[0].CAFiles) > 0 {
43− pool, err := util.GetCertPool(o.ProviderCAFiles)
44+ pool, err := util.GetCertPool(o.Providers[0].CAFiles)
44 if err == nil {
45 if err == nil {
45 transport := http.DefaultTransport.(*http.Transport).Clone()
46 transport := http.DefaultTransport.(*http.Transport).Clone()
46 transport.TLSClientConfig = &tls.Config{
47 transport.TLSClientConfig = &tls.Config{
@@ -54,38 +55,23 @@ func Validate(o *options.Options) error {
54 }
55 }
55 }
56 }
56
57
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− }
72 if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" {
58 if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" {
73 msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required."+
59 msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required."+
74 "\n use email-domain=* to authorize all email addresses")
60 "\n use email-domain=* to authorize all email addresses")
75 }
61 }
76
62
77− if o.OIDCIssuerURL != "" {
63+ if o.Providers[0].OIDCConfig.IssuerURL != "" {
78
64
79 ctx := context.Background()
65 ctx := context.Background()
80
66
81− if o.InsecureOIDCSkipIssuerVerification && !o.SkipOIDCDiscovery {
67+ if o.Providers[0].OIDCConfig.InsecureSkipIssuerVerification && !o.Providers[0].OIDCConfig.SkipDiscovery {
82 // go-oidc doesn't let us pass bypass the issuer check this in the oidc.NewProvider call
68 // go-oidc doesn't let us pass bypass the issuer check this in the oidc.NewProvider call
83 // (which uses discovery to get the URLs), so we'll do a quick check ourselves and if
69 // (which uses discovery to get the URLs), so we'll do a quick check ourselves and if
84 // we get the URLs, we'll just use the non-discovery path.
70 // we get the URLs, we'll just use the non-discovery path.
85
71
86 logger.Printf("Performing OIDC Discovery...")
72 logger.Printf("Performing OIDC Discovery...")
87
73
88− requestURL := strings.TrimSuffix(o.OIDCIssuerURL, "/") + "/.well-known/openid-configuration"
74+ requestURL := strings.TrimSuffix(o.Providers[0].OIDCConfig.IssuerURL, "/") + "/.well-known/openid-configuration"
89 body, err := requests.New(requestURL).
75 body, err := requests.New(requestURL).
90 WithContext(ctx).
76 WithContext(ctx).
91 Do().
77 Do().
@@ -96,23 +82,23 @@ func Validate(o *options.Options) error {
96 // Prefer manually configured URLs. It's a bit unclear
82 // Prefer manually configured URLs. It's a bit unclear
97 // why you'd be doing discovery and also providing the URLs
83 // why you'd be doing discovery and also providing the URLs
98 // explicitly though...
84 // explicitly though...
99− if o.LoginURL == "" {
85+ if o.Providers[0].LoginURL == "" {
100− o.LoginURL = body.Get("authorization_endpoint").MustString()
86+ o.Providers[0].LoginURL = body.Get("authorization_endpoint").MustString()
101 }
87 }
102
88
103− if o.RedeemURL == "" {
89+ if o.Providers[0].RedeemURL == "" {
104− o.RedeemURL = body.Get("token_endpoint").MustString()
90+ o.Providers[0].RedeemURL = body.Get("token_endpoint").MustString()
105 }
91 }
106
92
107− if o.OIDCJwksURL == "" {
93+ if o.Providers[0].OIDCConfig.JwksURL == "" {
108− o.OIDCJwksURL = body.Get("jwks_uri").MustString()
94+ o.Providers[0].OIDCConfig.JwksURL = body.Get("jwks_uri").MustString()
109 }
95 }
110
96
111− if o.ProfileURL == "" {
97+ if o.Providers[0].ProfileURL == "" {
112− o.ProfileURL = body.Get("userinfo_endpoint").MustString()
98+ o.Providers[0].ProfileURL = body.Get("userinfo_endpoint").MustString()
113 }
99 }
114
100
115− o.SkipOIDCDiscovery = true
101+ o.Providers[0].OIDCConfig.SkipDiscovery = true
116 }
102 }
117 }
103 }
118
104
@@ -120,42 +106,45 @@ func Validate(o *options.Options) error {
120 // instead of metadata discovery if we enable -skip-oidc-discovery.
106 // instead of metadata discovery if we enable -skip-oidc-discovery.
121 // In this case we need to make sure the required endpoints for
107 // In this case we need to make sure the required endpoints for
122 // the provider are configured.
108 // the provider are configured.
123− if o.SkipOIDCDiscovery {
109+ if o.Providers[0].OIDCConfig.SkipDiscovery {
124− if o.LoginURL == "" {
110+ if o.Providers[0].LoginURL == "" {
125 msgs = append(msgs, "missing setting: login-url")
111 msgs = append(msgs, "missing setting: login-url")
126 }
112 }
127− if o.RedeemURL == "" {
113+ if o.Providers[0].RedeemURL == "" {
128 msgs = append(msgs, "missing setting: redeem-url")
114 msgs = append(msgs, "missing setting: redeem-url")
129 }
115 }
130− if o.OIDCJwksURL == "" {
116+ if o.Providers[0].OIDCConfig.JwksURL == "" {
131 msgs = append(msgs, "missing setting: oidc-jwks-url")
117 msgs = append(msgs, "missing setting: oidc-jwks-url")
132 }
118 }
133− keySet := oidc.NewRemoteKeySet(ctx, o.OIDCJwksURL)
119+ keySet := oidc.NewRemoteKeySet(ctx, o.Providers[0].OIDCConfig.JwksURL)
134− o.SetOIDCVerifier(oidc.NewVerifier(o.OIDCIssuerURL, keySet, &oidc.Config{
120+ o.SetOIDCVerifier(oidc.NewVerifier(o.Providers[0].OIDCConfig.IssuerURL, keySet, &oidc.Config{
135− ClientID: o.ClientID,
121+ ClientID: o.Providers[0].ClientID,
136− SkipIssuerCheck: o.InsecureOIDCSkipIssuerVerification,
122+ SkipIssuerCheck: o.Providers[0].OIDCConfig.InsecureSkipIssuerVerification,
137 }))
123 }))
138 } else {
124 } else {
139 // Configure discoverable provider data.
125 // Configure discoverable provider data.
140− provider, err := oidc.NewProvider(ctx, o.OIDCIssuerURL)
126+ provider, err := oidc.NewProvider(ctx, o.Providers[0].OIDCConfig.IssuerURL)
141 if err != nil {
127 if err != nil {
142 return err
128 return err
143 }
129 }
144 o.SetOIDCVerifier(provider.Verifier(&oidc.Config{
130 o.SetOIDCVerifier(provider.Verifier(&oidc.Config{
145− ClientID: o.ClientID,
131+ ClientID: o.Providers[0].ClientID,
146− SkipIssuerCheck: o.InsecureOIDCSkipIssuerVerification,
132+ SkipIssuerCheck: o.Providers[0].OIDCConfig.InsecureSkipIssuerVerification,
147 }))
133 }))
148
134
149− o.LoginURL = provider.Endpoint().AuthURL
135+ o.Providers[0].LoginURL = provider.Endpoint().AuthURL
150− o.RedeemURL = provider.Endpoint().TokenURL
136+ o.Providers[0].RedeemURL = provider.Endpoint().TokenURL
151 }
137 }
152− if o.Scope == "" {
138+ if o.Providers[0].Scope == "" {
153− o.Scope = "openid email profile"
139+ o.Providers[0].Scope = "openid email profile"
154
140
155− if len(o.AllowedGroups) > 0 {
141+ if len(o.Providers[0].AllowedGroups) > 0 {
156− o.Scope += " groups"
142+ o.Providers[0].Scope += " groups"
157 }
143 }
158 }
144 }
145+ if o.Providers[0].OIDCConfig.UserIDClaim == "" {
146+ o.Providers[0].OIDCConfig.UserIDClaim = "email"
147+ }
159 }
148 }
160
149
161 if o.SkipJwtBearerTokens {
150 if o.SkipJwtBearerTokens {
@@ -183,18 +172,6 @@ func Validate(o *options.Options) error {
183 msgs = append(msgs, validateUpstreams(o.UpstreamServers)...)
172 msgs = append(msgs, validateUpstreams(o.UpstreamServers)...)
184 msgs = parseProviderInfo(o, msgs)
173 msgs = parseProviderInfo(o, msgs)
185
174
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−
198 if o.ReverseProxy {
175 if o.ReverseProxy {
199 parser, err := ip.GetRealClientIPParser(o.RealClientIPHeader)
176 parser, err := ip.GetRealClientIPParser(o.RealClientIPHeader)
200 if err != nil {
177 if err != nil {
@@ -220,79 +197,79 @@ func Validate(o *options.Options) error {
220
197
221func parseProviderInfo(o *options.Options, msgs []string) []string {
198func parseProviderInfo(o *options.Options, msgs []string) []string {
222 p := &providers.ProviderData{
199 p := &providers.ProviderData{
223− Scope: o.Scope,
200+ Scope: o.Providers[0].Scope,
224− ClientID: o.ClientID,
201+ ClientID: o.Providers[0].ClientID,
225− ClientSecret: o.ClientSecret,
202+ ClientSecret: o.Providers[0].ClientSecret,
226− ClientSecretFile: o.ClientSecretFile,
203+ ClientSecretFile: o.Providers[0].ClientSecretFile,
227− Prompt: o.Prompt,
204+ Prompt: o.Providers[0].Prompt,
228− ApprovalPrompt: o.ApprovalPrompt,
205+ ApprovalPrompt: o.Providers[0].ApprovalPrompt,
229− AcrValues: o.AcrValues,
206+ AcrValues: o.Providers[0].AcrValues,
230 }
207 }
231− p.LoginURL, msgs = parseURL(o.LoginURL, "login", msgs)
208+ p.LoginURL, msgs = parseURL(o.Providers[0].LoginURL, "login", msgs)
232− p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs)
209+ p.RedeemURL, msgs = parseURL(o.Providers[0].RedeemURL, "redeem", msgs)
233− p.ProfileURL, msgs = parseURL(o.ProfileURL, "profile", msgs)
210+ p.ProfileURL, msgs = parseURL(o.Providers[0].ProfileURL, "profile", msgs)
234− p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
211+ p.ValidateURL, msgs = parseURL(o.Providers[0].ValidateURL, "validate", msgs)
235− p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs)
212+ p.ProtectedResource, msgs = parseURL(o.Providers[0].ProtectedResource, "resource", msgs)
236
213
237 // Make the OIDC options available to all providers that support it
214 // Make the OIDC options available to all providers that support it
238− p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
215+ p.AllowUnverifiedEmail = o.Providers[0].OIDCConfig.InsecureAllowUnverifiedEmail
239− p.EmailClaim = o.OIDCEmailClaim
216+ p.EmailClaim = o.Providers[0].OIDCConfig.EmailClaim
240− p.GroupsClaim = o.OIDCGroupsClaim
217+ p.GroupsClaim = o.Providers[0].OIDCConfig.GroupsClaim
241 p.Verifier = o.GetOIDCVerifier()
218 p.Verifier = o.GetOIDCVerifier()
242
219
243 // TODO (@NickMeves) - Remove This
220 // TODO (@NickMeves) - Remove This
244 // Backwards Compatibility for Deprecated UserIDClaim option
221 // Backwards Compatibility for Deprecated UserIDClaim option
245− if o.OIDCEmailClaim == providers.OIDCEmailClaim &&
222+ if o.Providers[0].OIDCConfig.EmailClaim == providers.OIDCEmailClaim &&
246− o.UserIDClaim != providers.OIDCEmailClaim {
223+ o.Providers[0].OIDCConfig.UserIDClaim != providers.OIDCEmailClaim {
247− p.EmailClaim = o.UserIDClaim
224+ p.EmailClaim = o.Providers[0].OIDCConfig.UserIDClaim
248 }
225 }
249
226
250− p.SetAllowedGroups(o.AllowedGroups)
227+ p.SetAllowedGroups(o.Providers[0].AllowedGroups)
251
228
252− provider := providers.New(o.ProviderType, p)
229+ provider := providers.New(o.Providers[0].Type, p)
253 if provider == nil {
230 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))
255 return msgs
232 return msgs
256 }
233 }
257 o.SetProvider(provider)
234 o.SetProvider(provider)
258
235
259 switch p := o.GetProvider().(type) {
236 switch p := o.GetProvider().(type) {
260 case *providers.AzureProvider:
237 case *providers.AzureProvider:
261− p.Configure(o.AzureTenant)
238+ p.Configure(o.Providers[0].AzureConfig.Tenant)
262 case *providers.GitHubProvider:
239 case *providers.GitHubProvider:
263− p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
240+ p.SetOrgTeam(o.Providers[0].GitHubConfig.Org, o.Providers[0].GitHubConfig.Team)
264− p.SetRepo(o.GitHubRepo, o.GitHubToken)
241+ p.SetRepo(o.Providers[0].GitHubConfig.Repo, o.Providers[0].GitHubConfig.Token)
265− p.SetUsers(o.GitHubUsers)
242+ p.SetUsers(o.Providers[0].GitHubConfig.Users)
266 case *providers.KeycloakProvider:
243 case *providers.KeycloakProvider:
267 // Backwards compatibility with `--keycloak-group` option
244 // Backwards compatibility with `--keycloak-group` option
268− if len(o.KeycloakGroups) > 0 {
245+ if len(o.Providers[0].KeycloakConfig.Groups) > 0 {
269− p.SetAllowedGroups(o.KeycloakGroups)
246+ p.SetAllowedGroups(o.Providers[0].KeycloakConfig.Groups)
270 }
247 }
271 case *providers.GoogleProvider:
248 case *providers.GoogleProvider:
272− if o.GoogleServiceAccountJSON != "" {
249+ if o.Providers[0].GoogleConfig.ServiceAccountJSON != "" {
273− file, err := os.Open(o.GoogleServiceAccountJSON)
250+ file, err := os.Open(o.Providers[0].GoogleConfig.ServiceAccountJSON)
274 if err != nil {
251 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)
276 } else {
253 } else {
277− groups := o.AllowedGroups
254+ groups := o.Providers[0].AllowedGroups
278 // Backwards compatibility with `--google-group` option
255 // Backwards compatibility with `--google-group` option
279− if len(o.GoogleGroups) > 0 {
256+ if len(o.Providers[0].GoogleConfig.Groups) > 0 {
280− groups = o.GoogleGroups
257+ groups = o.Providers[0].GoogleConfig.Groups
281 p.SetAllowedGroups(groups)
258 p.SetAllowedGroups(groups)
282 }
259 }
283− p.SetGroupRestriction(groups, o.GoogleAdminEmail, file)
260+ p.SetGroupRestriction(groups, o.Providers[0].GoogleConfig.AdminEmail, file)
284 }
261 }
285 }
262 }
286 case *providers.BitbucketProvider:
263 case *providers.BitbucketProvider:
287− p.SetTeam(o.BitbucketTeam)
264+ p.SetTeam(o.Providers[0].BitbucketConfig.Team)
288− p.SetRepository(o.BitbucketRepository)
265+ p.SetRepository(o.Providers[0].BitbucketConfig.Repository)
289 case *providers.OIDCProvider:
266 case *providers.OIDCProvider:
290 if p.Verifier == nil {
267 if p.Verifier == nil {
291 msgs = append(msgs, "oidc provider requires an oidc issuer URL")
268 msgs = append(msgs, "oidc provider requires an oidc issuer URL")
292 }
269 }
293 case *providers.GitLabProvider:
270 case *providers.GitLabProvider:
294− p.Groups = o.GitLabGroup
271+ p.Groups = o.Providers[0].GitLabConfig.Group
295− err := p.AddProjects(o.GitlabProjects)
272+ err := p.AddProjects(o.Providers[0].GitLabConfig.Projects)
296 if err != nil {
273 if err != nil {
297 msgs = append(msgs, "failed to setup gitlab project access level")
274 msgs = append(msgs, "failed to setup gitlab project access level")
298 }
275 }
@@ -308,7 +285,7 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
308 msgs = append(msgs, "failed to initialize oidc provider for gitlab.com")
285 msgs = append(msgs, "failed to initialize oidc provider for gitlab.com")
309 } else {
286 } else {
310 p.Verifier = provider.Verifier(&oidc.Config{
287 p.Verifier = provider.Verifier(&oidc.Config{
311− ClientID: o.ClientID,
288+ ClientID: o.Providers[0].ClientID,
312 })
289 })
313
290
314 p.LoginURL, msgs = parseURL(provider.Endpoint().AuthURL, "login", msgs)
291 p.LoginURL, msgs = parseURL(provider.Endpoint().AuthURL, "login", msgs)
@@ -316,31 +293,31 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
316 }
293 }
317 }
294 }
318 case *providers.LoginGovProvider:
295 case *providers.LoginGovProvider:
319− p.PubJWKURL, msgs = parseURL(o.PubJWKURL, "pubjwk", msgs)
296+ p.PubJWKURL, msgs = parseURL(o.Providers[0].LoginGovConfig.PubJWKURL, "pubjwk", msgs)
320
297
321 // JWT key can be supplied via env variable or file in the filesystem, but not both.
298 // JWT key can be supplied via env variable or file in the filesystem, but not both.
322 switch {
299 switch {
323− case o.JWTKey != "" && o.JWTKeyFile != "":
300+ case o.Providers[0].LoginGovConfig.JWTKey != "" && o.Providers[0].LoginGovConfig.JWTKeyFile != "":
324 msgs = append(msgs, "cannot set both jwt-key and jwt-key-file options")
301 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 == "":
326 msgs = append(msgs, "login.gov provider requires a private key for signing JWTs")
303 msgs = append(msgs, "login.gov provider requires a private key for signing JWTs")
327− case o.JWTKey != "":
304+ case o.Providers[0].LoginGovConfig.JWTKey != "":
328 // The JWT Key is in the commandline argument
305 // 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))
330 if err != nil {
307 if err != nil {
331 msgs = append(msgs, "could not parse RSA Private Key PEM")
308 msgs = append(msgs, "could not parse RSA Private Key PEM")
332 } else {
309 } else {
333 p.JWTKey = signKey
310 p.JWTKey = signKey
334 }
311 }
335− case o.JWTKeyFile != "":
312+ case o.Providers[0].LoginGovConfig.JWTKeyFile != "":
336 // The JWT key is in the filesystem
313 // The JWT key is in the filesystem
337− keyData, err := ioutil.ReadFile(o.JWTKeyFile)
314+ keyData, err := ioutil.ReadFile(o.Providers[0].LoginGovConfig.JWTKeyFile)
338 if err != nil {
315 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)
340 }
317 }
341 signKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyData)
318 signKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyData)
342 if err != nil {
319 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)
344 } else {
321 } else {
345 p.JWTKey = signKey
322 p.JWTKey = signKey
346 }
323 }
pkg/validation/options_test.go+23−18
@@ -17,6 +17,7 @@ const (
17 cookieSecret = "secretthirtytwobytes+abcdefghijk"
17 cookieSecret = "secretthirtytwobytes+abcdefghijk"
18 clientID = "bazquux"
18 clientID = "bazquux"
19 clientSecret = "xyzzyplugh"
19 clientSecret = "xyzzyplugh"
20+ providerID = "providerID"
20)
21)
21
22
22func testOptions() *options.Options {
23func testOptions() *options.Options {
@@ -27,8 +28,9 @@ func testOptions() *options.Options {
27 URI: "http://127.0.0.1:8080/",
28 URI: "http://127.0.0.1:8080/",
28 })
29 })
29 o.Cookie.Secret = cookieSecret
30 o.Cookie.Secret = cookieSecret
31+ o.Providers[0].ID = providerID
30− o.ClientID = clientID
32+ o.Providers[0].ClientID = clientID
31− o.ClientSecret = clientSecret
33+ o.Providers[0].ClientSecret = clientSecret
32 o.EmailDomains = []string{"*"}
34 o.EmailDomains = []string{"*"}
33 return o
35 return o
34}
36}
@@ -48,7 +50,8 @@ func TestNewOptions(t *testing.T) {
48
50
49 expected := errorMsg([]string{
51 expected := errorMsg([]string{
50 "missing setting: cookie-secret",
52 "missing setting: cookie-secret",
53+ "provider has empty id: ids are required for all providers",
51− "missing setting: client-id",
54+ "provider missing setting: client-id",
52 "missing setting: client-secret or client-secret-file"})
55 "missing setting: client-secret or client-secret-file"})
53 assert.Equal(t, expected, err.Error())
56 assert.Equal(t, expected, err.Error())
54}
57}
@@ -56,8 +59,9 @@ func TestNewOptions(t *testing.T) {
56func TestClientSecretFileOptionFails(t *testing.T) {
59func TestClientSecretFileOptionFails(t *testing.T) {
57 o := options.NewOptions()
60 o := options.NewOptions()
58 o.Cookie.Secret = cookieSecret
61 o.Cookie.Secret = cookieSecret
62+ o.Providers[0].ID = providerID
59− o.ClientID = clientID
63+ o.Providers[0].ClientID = clientID
60− o.ClientSecretFile = clientSecret
64+ o.Providers[0].ClientSecretFile = clientSecret
61 o.EmailDomains = []string{"*"}
65 o.EmailDomains = []string{"*"}
62 err := Validate(o)
66 err := Validate(o)
63 assert.NotEqual(t, nil, err)
67 assert.NotEqual(t, nil, err)
@@ -93,8 +97,9 @@ func TestClientSecretFileOption(t *testing.T) {
93
97
94 o := options.NewOptions()
98 o := options.NewOptions()
95 o.Cookie.Secret = cookieSecret
99 o.Cookie.Secret = cookieSecret
100+ o.Providers[0].ID = providerID
96− o.ClientID = clientID
101+ o.Providers[0].ClientID = clientID
97− o.ClientSecretFile = clientSecretFileName
102+ o.Providers[0].ClientSecretFile = clientSecretFileName
98 o.EmailDomains = []string{"*"}
103 o.EmailDomains = []string{"*"}
99 err = Validate(o)
104 err = Validate(o)
100 assert.Equal(t, nil, err)
105 assert.Equal(t, nil, err)
@@ -110,7 +115,7 @@ func TestClientSecretFileOption(t *testing.T) {
110
115
111func TestGoogleGroupOptions(t *testing.T) {
116func TestGoogleGroupOptions(t *testing.T) {
112 o := testOptions()
117 o := testOptions()
113− o.GoogleGroups = []string{"googlegroup"}
118+ o.Providers[0].GoogleConfig.Groups = []string{"googlegroup"}
114 err := Validate(o)
119 err := Validate(o)
115 assert.NotEqual(t, nil, err)
120 assert.NotEqual(t, nil, err)
116
121
@@ -122,9 +127,9 @@ func TestGoogleGroupOptions(t *testing.T) {
122
127
123func TestGoogleGroupInvalidFile(t *testing.T) {
128func TestGoogleGroupInvalidFile(t *testing.T) {
124 o := testOptions()
129 o := testOptions()
125− o.GoogleGroups = []string{"test_group"}
130+ o.Providers[0].GoogleConfig.Groups = []string{"test_group"}
126− o.GoogleAdminEmail = "admin@example.com"
131+ o.Providers[0].GoogleConfig.AdminEmail = "admin@example.com"
127− o.GoogleServiceAccountJSON = "file_doesnt_exist.json"
132+ o.Providers[0].GoogleConfig.ServiceAccountJSON = "file_doesnt_exist.json"
128 err := Validate(o)
133 err := Validate(o)
129 assert.NotEqual(t, nil, err)
134 assert.NotEqual(t, nil, err)
130
135
@@ -225,17 +230,17 @@ func TestValidateSignatureKeyUnsupportedAlgorithm(t *testing.T) {
225
230
226func TestSkipOIDCDiscovery(t *testing.T) {
231func TestSkipOIDCDiscovery(t *testing.T) {
227 o := testOptions()
232 o := testOptions()
228− o.ProviderType = "oidc"
233+ o.Providers[0].Type = "oidc"
229− o.OIDCIssuerURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/v2.0/"
234+ o.Providers[0].OIDCConfig.IssuerURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/v2.0/"
230− o.SkipOIDCDiscovery = true
235+ o.Providers[0].OIDCConfig.SkipDiscovery = true
231
236
232 err := Validate(o)
237 err := Validate(o)
233 assert.Equal(t, "invalid configuration:\n"+
238 assert.Equal(t, "invalid configuration:\n"+
234 " missing setting: login-url\n missing setting: redeem-url\n missing setting: oidc-jwks-url", err.Error())
239 " missing setting: login-url\n missing setting: redeem-url\n missing setting: oidc-jwks-url", err.Error())
235
240
236− o.LoginURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1_sign_in"
241+ o.Providers[0].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"
242+ o.Providers[0].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"
243+ o.Providers[0].OIDCConfig.JwksURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/discovery/v2.0/keys"
239
244
240 assert.Equal(t, nil, Validate(o))
245 assert.Equal(t, nil, Validate(o))
241}
246}
@@ -292,7 +297,7 @@ func TestProviderCAFilesError(t *testing.T) {
292 assert.NoError(t, os.Remove(file.Name()))
297 assert.NoError(t, os.Remove(file.Name()))
293
298
294 o := testOptions()
299 o := testOptions()
295− o.ProviderCAFiles = append(o.ProviderCAFiles, file.Name())
300+ o.Providers[0].CAFiles = append(o.Providers[0].CAFiles, file.Name())
296 err = Validate(o)
301 err = Validate(o)
297 assert.Error(t, err)
302 assert.Error(t, err)
298 assert.Contains(t, err.Error(), "unable to load provider CA file(s)")
303 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+})