Multiple providers in alpha config (#947)
* Initial commit of multiple provider logic: 1. Created new provider options. 2. Created legacy provider options and conversion options. 3. Added Providers to alpha Options. 4. Started Validation migration of multiple providers 5. Tests. * fixed lint issues * additional lint fixes * Nits and alterations based on CR: manliy splitting large providers validation function and adding comments to provider options * fixed typo * removed weird : file * small CR changes * Removed GoogleGroups validation due to new allowed-groups (including tests). Added line in CHANGELOG * Update pkg/apis/options/providers.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/apis/options/providers.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/apis/options/providers.go Co-authored-by: Nick Meves <nick.meves@greenhouse.io> * Initial commit of multiple provider logic: 1. Created new provider options. 2. Created legacy provider options and conversion options. 3. Added Providers to alpha Options. 4. Started Validation migration of multiple providers 5. Tests. * fixed lint issues * additional lint fixes * Nits and alterations based on CR: manliy splitting large providers validation function and adding comments to provider options * small CR changes * auto generates alpha_config.md * rebase (mainly service alpha options related conflicts) * removed : * Nits and alterations based on CR: manliy splitting large providers validation function and adding comments to provider options * small CR changes * Removed GoogleGroups validation due to new allowed-groups (including tests). Added line in CHANGELOG * "cntd. rebase" * ran make generate again * last conflicts * removed duplicate client id validation * 1. Removed provider prefixes 2. altered optionsWithNilProvider logic 3. altered default provider logic 4. moved change in CHANELOG to 7.0.0 * fixed TestGoogleGroupOptions test * ran make generate * moved CHANGLOG line to 7.1.1 * moved changelog comment to 7.1.2 (additional rebase) Co-authored-by: Yana Segal <yana.segal@nielsen.com> Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> Co-authored-by: Nick Meves <nick.meves@greenhouse.io>
This commit is contained in:
		
							parent
							
								
									9d20b4e0e2
								
							
						
					
					
						commit
						42475c28f7
					
				|  | @ -8,6 +8,8 @@ | ||||||
| 
 | 
 | ||||||
| ## Changes since v7.1.2 | ## Changes since v7.1.2 | ||||||
| 
 | 
 | ||||||
|  | - [#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) | ||||||
|  | 
 | ||||||
| # V7.1.2 | # V7.1.2 | ||||||
| 
 | 
 | ||||||
| ## Release Highlights | ## Release Highlights | ||||||
|  |  | ||||||
|  | @ -1,10 +1,5 @@ | ||||||
| http_address="0.0.0.0:4180" | http_address="0.0.0.0:4180" | ||||||
| cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" | cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" | ||||||
| provider="oidc" |  | ||||||
| email_domains="example.com" | email_domains="example.com" | ||||||
| oidc_issuer_url="http://dex.localhost:4190/dex" |  | ||||||
| client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK" |  | ||||||
| client_id="oauth2-proxy" |  | ||||||
| cookie_secure="false" | cookie_secure="false" | ||||||
| 
 |  | ||||||
| redirect_url="http://localhost:4180/oauth2/callback" | redirect_url="http://localhost:4180/oauth2/callback" | ||||||
|  |  | ||||||
|  | @ -15,3 +15,9 @@ injectRequestHeaders: | ||||||
| - name: X-Forwarded-Preferred-Username | - name: X-Forwarded-Preferred-Username | ||||||
|   values: |   values: | ||||||
|   - claim: preferred_username |   - claim: preferred_username | ||||||
|  | providers: | ||||||
|  | - provider: oidc | ||||||
|  |   clientSecret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK | ||||||
|  |   clientID: oauth2-proxy | ||||||
|  |   oidcConfig: | ||||||
|  |     oidcIssuerURL: http://dex.localhost:4190/dex | ||||||
|  |  | ||||||
|  | @ -119,6 +119,28 @@ They may change between releases without notice. | ||||||
| | `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. | | | `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. | | ||||||
| | `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. | | | `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. | | ||||||
| | `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. | | | `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. | | ||||||
|  | | `providers` | _[Providers](#providers)_ | Providers is used to configure multiple providers. | | ||||||
|  | 
 | ||||||
|  | ### AzureOptions | ||||||
|  | 
 | ||||||
|  | (**Appears on:** [Provider](#provider)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | | Field | Type | Description | | ||||||
|  | | ----- | ---- | ----------- | | ||||||
|  | | `tenant` | _string_ | Tenant directs to a tenant-specific or common (tenant-independent) endpoint<br/>Default value is 'commmon' | | ||||||
|  | 
 | ||||||
|  | ### BitbucketOptions | ||||||
|  | 
 | ||||||
|  | (**Appears on:** [Provider](#provider)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | | Field | Type | Description | | ||||||
|  | | ----- | ---- | ----------- | | ||||||
|  | | `team` | _string_ | Team sets restrict logins to members of this team | | ||||||
|  | | `repository` | _string_ | Repository sets restrict logins to user with access to this repository | | ||||||
| 
 | 
 | ||||||
| ### ClaimSource | ### ClaimSource | ||||||
| 
 | 
 | ||||||
|  | @ -143,6 +165,43 @@ each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45 | ||||||
| Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". | Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | ### GitHubOptions | ||||||
|  | 
 | ||||||
|  | (**Appears on:** [Provider](#provider)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | | Field | Type | Description | | ||||||
|  | | ----- | ---- | ----------- | | ||||||
|  | | `org` | _string_ | Org sets restrict logins to members of this organisation | | ||||||
|  | | `team` | _string_ | Team sets restrict logins to members of this team | | ||||||
|  | | `repo` | _string_ | Repo sets restrict logins to collaborators of this repository | | ||||||
|  | | `token` | _string_ | Token is the token to use when verifying repository collaborators<br/>it must have push access to the repository | | ||||||
|  | | `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 | | ||||||
|  | 
 | ||||||
|  | ### GitLabOptions | ||||||
|  | 
 | ||||||
|  | (**Appears on:** [Provider](#provider)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | | Field | Type | Description | | ||||||
|  | | ----- | ---- | ----------- | | ||||||
|  | | `group` | _[]string_ | Group sets restrict logins to members of this group | | ||||||
|  | | `projects` | _[]string_ | Projects restricts logins to members of any of these projects | | ||||||
|  | 
 | ||||||
|  | ### GoogleOptions | ||||||
|  | 
 | ||||||
|  | (**Appears on:** [Provider](#provider)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | | Field | Type | Description | | ||||||
|  | | ----- | ---- | ----------- | | ||||||
|  | | `group` | _[]string_ | Groups sets restrict logins to members of this google group | | ||||||
|  | | `adminEmail` | _string_ | AdminEmail is the google admin to impersonate for api calls | | ||||||
|  | | `serviceAccountJson` | _string_ | ServiceAccountJSON is the path to the service account json credentials | | ||||||
|  | 
 | ||||||
| ### Header | ### Header | ||||||
| 
 | 
 | ||||||
| (**Appears on:** [AlphaOptions](#alphaoptions)) | (**Appears on:** [AlphaOptions](#alphaoptions)) | ||||||
|  | @ -172,6 +231,88 @@ make up the header value | ||||||
| | `prefix` | _string_ | Prefix is an optional prefix that will be prepended to the value of the<br/>claim if it is non-empty. | | | `prefix` | _string_ | Prefix is an optional prefix that will be prepended to the value of the<br/>claim if it is non-empty. | | ||||||
| | `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. | | | `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. | | ||||||
| 
 | 
 | ||||||
|  | ### KeycloakOptions | ||||||
|  | 
 | ||||||
|  | (**Appears on:** [Provider](#provider)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | | Field | Type | Description | | ||||||
|  | | ----- | ---- | ----------- | | ||||||
|  | | `groups` | _[]string_ | Group enables to restrict login to members of indicated group | | ||||||
|  | 
 | ||||||
|  | ### LoginGovOptions | ||||||
|  | 
 | ||||||
|  | (**Appears on:** [Provider](#provider)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | | Field | Type | Description | | ||||||
|  | | ----- | ---- | ----------- | | ||||||
|  | | `jwtKey` | _string_ | JWTKey is a private key in PEM format used to sign JWT, | | ||||||
|  | | `jwtKeyFile` | _string_ | JWTKeyFile is a path to the private key file in PEM format used to sign the JWT | | ||||||
|  | | `pubjwkURL` | _string_ | PubJWKURL is the JWK pubkey access endpoint | | ||||||
|  | 
 | ||||||
|  | ### OIDCOptions | ||||||
|  | 
 | ||||||
|  | (**Appears on:** [Provider](#provider)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | | Field | Type | Description | | ||||||
|  | | ----- | ---- | ----------- | | ||||||
|  | | `issuerURL` | _string_ | IssuerURL is the OpenID Connect issuer URL<br/>eg: https://accounts.google.com | | ||||||
|  | | `insecureAllowUnverifiedEmail` | _bool_ | InsecureAllowUnverifiedEmail prevents failures if an email address in an id_token is not verified<br/>default set to 'false' | | ||||||
|  | | `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' | | ||||||
|  | | `skipDiscovery` | _bool_ | SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints<br/>default set to 'false' | | ||||||
|  | | `jwksURL` | _string_ | JwksURL is the OpenID Connect JWKS URL<br/>eg: https://www.googleapis.com/oauth2/v3/certs | | ||||||
|  | | `emailClaim` | _string_ | EmailClaim indicates which claim contains the user email,<br/>default set to 'email' | | ||||||
|  | | `groupsClaim` | _string_ | GroupsClaim indicates which claim contains the user groups<br/>default set to 'groups' | | ||||||
|  | | `userIDClaim` | _string_ | UserIDClaim indicates which claim contains the user ID<br/>default set to 'email' | | ||||||
|  | 
 | ||||||
|  | ### Provider | ||||||
|  | 
 | ||||||
|  | (**Appears on:** [Providers](#providers)) | ||||||
|  | 
 | ||||||
|  | Provider holds all configuration for a single provider | ||||||
|  | 
 | ||||||
|  | | Field | Type | Description | | ||||||
|  | | ----- | ---- | ----------- | | ||||||
|  | | `clientID` | _string_ | ClientID is the OAuth Client ID that is defined in the provider<br/>This value is required for all providers. | | ||||||
|  | | `clientSecret` | _string_ | ClientSecret is the OAuth Client Secret that is defined in the provider<br/>This value is required for all providers. | | ||||||
|  | | `clientSecretFile` | _string_ | ClientSecretFile is the name of the file<br/>containing the OAuth Client Secret, it will be used if ClientSecret is not set. | | ||||||
|  | | `keycloakConfig` | _[KeycloakOptions](#keycloakoptions)_ | KeycloakConfig holds all configurations for Keycloak provider. | | ||||||
|  | | `azureConfig` | _[AzureOptions](#azureoptions)_ | AzureConfig holds all configurations for Azure provider. | | ||||||
|  | | `bitbucketConfig` | _[BitbucketOptions](#bitbucketoptions)_ | BitbucketConfig holds all configurations for Bitbucket provider. | | ||||||
|  | | `githubConfig` | _[GitHubOptions](#githuboptions)_ | GitHubConfig holds all configurations for GitHubC provider. | | ||||||
|  | | `gitlabConfig` | _[GitLabOptions](#gitlaboptions)_ | GitLabConfig holds all configurations for GitLab provider. | | ||||||
|  | | `googleConfig` | _[GoogleOptions](#googleoptions)_ | GoogleConfig holds all configurations for Google provider. | | ||||||
|  | | `oidcConfig` | _[OIDCOptions](#oidcoptions)_ | OIDCConfig holds all configurations for OIDC provider<br/>or providers utilize OIDC configurations. | | ||||||
|  | | `loginGovConfig` | _[LoginGovOptions](#logingovoptions)_ | LoginGovConfig holds all configurations for LoginGov provider. | | ||||||
|  | | `id` | _string_ | ID should be a unique identifier for the provider.<br/>This value is required for all providers. | | ||||||
|  | | `provider` | _string_ | Type is the OAuth provider<br/>must be set from the supported providers group,<br/>otherwise 'Google' is set as default | | ||||||
|  | | `name` | _string_ | Name is the providers display name<br/>if set, it will be shown to the users in the login page. | | ||||||
|  | | `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 | | ||||||
|  | | `loginURL` | _string_ | LoginURL is the authentication endpoint | | ||||||
|  | | `redeemURL` | _string_ | RedeemURL is the token redemption endpoint | | ||||||
|  | | `profileURL` | _string_ | ProfileURL is the profile access endpoint | | ||||||
|  | | `resource` | _string_ | ProtectedResource is the resource that is protected (Azure AD only) | | ||||||
|  | | `validateURL` | _string_ | ValidateURL is the access token validation endpoint | | ||||||
|  | | `scope` | _string_ | Scope is the OAuth scope specification | | ||||||
|  | | `prompt` | _string_ | Prompt is OIDC prompt | | ||||||
|  | | `approvalPrompt` | _string_ | ApprovalPrompt is the OAuth approval_prompt<br/>default is set to 'force' | | ||||||
|  | | `allowedGroups` | _[]string_ | AllowedGroups is a list of restrict logins to members of this group | | ||||||
|  | | `acrValues` | _string_ | AcrValues is a string of acr values | | ||||||
|  | 
 | ||||||
|  | ### Providers | ||||||
|  | 
 | ||||||
|  | #### ([[]Provider](#provider) alias) | ||||||
|  | 
 | ||||||
|  | (**Appears on:** [AlphaOptions](#alphaoptions)) | ||||||
|  | 
 | ||||||
|  | Providers is a collection of definitions for providers. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| ### SecretSource | ### SecretSource | ||||||
| 
 | 
 | ||||||
| (**Appears on:** [ClaimSource](#claimsource), [HeaderValue](#headervalue), [TLS](#tls)) | (**Appears on:** [ClaimSource](#claimsource), [HeaderValue](#headervalue), [TLS](#tls)) | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										1
									
								
								go.sum
								
								
								
								
							|  | @ -398,6 +398,7 @@ github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3 | ||||||
| github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= | github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= | ||||||
| github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= | github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= | ||||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
|  | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= | ||||||
| github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
| github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||||
|  |  | ||||||
							
								
								
									
										42
									
								
								main_test.go
								
								
								
								
							
							
						
						
									
										42
									
								
								main_test.go
								
								
								
								
							|  | @ -19,6 +19,8 @@ http_address="127.0.0.1:4180" | ||||||
| upstreams="http://httpbin" | upstreams="http://httpbin" | ||||||
| set_basic_auth="true" | set_basic_auth="true" | ||||||
| basic_auth_password="super-secret-password" | basic_auth_password="super-secret-password" | ||||||
|  | client_id="oauth2-proxy" | ||||||
|  | client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK" | ||||||
| ` | ` | ||||||
| 
 | 
 | ||||||
| 	const testAlphaConfig = ` | 	const testAlphaConfig = ` | ||||||
|  | @ -57,15 +59,23 @@ injectResponseHeaders: | ||||||
|       value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk |       value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk | ||||||
| server: | server: | ||||||
|   bindAddress: "127.0.0.1:4180" |   bindAddress: "127.0.0.1:4180" | ||||||
|  | providers: | ||||||
|  | - provider: google | ||||||
|  |   ID: google=oauth2-proxy | ||||||
|  |   clientSecret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK | ||||||
|  |   clientID: oauth2-proxy | ||||||
|  |   approvalPrompt: force | ||||||
|  |   azureConfig: | ||||||
|  |     tenant: common | ||||||
|  |   oidcConfig: | ||||||
|  |     groupsClaim: groups | ||||||
|  |     emailClaim: email | ||||||
|  |     userIDClaim: email | ||||||
| ` | ` | ||||||
| 
 | 
 | ||||||
| 	const testCoreConfig = ` | 	const testCoreConfig = ` | ||||||
| cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" | cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" | ||||||
| provider="oidc" |  | ||||||
| email_domains="example.com" | email_domains="example.com" | ||||||
| oidc_issuer_url="http://dex.localhost:4190/dex" |  | ||||||
| client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK" |  | ||||||
| client_id="oauth2-proxy" |  | ||||||
| cookie_secure="false" | cookie_secure="false" | ||||||
| 
 | 
 | ||||||
| redirect_url="http://localhost:4180/oauth2/callback" | redirect_url="http://localhost:4180/oauth2/callback" | ||||||
|  | @ -85,11 +95,7 @@ redirect_url="http://localhost:4180/oauth2/callback" | ||||||
| 		Expect(err).ToNot(HaveOccurred()) | 		Expect(err).ToNot(HaveOccurred()) | ||||||
| 
 | 
 | ||||||
| 		opts.Cookie.Secret = "OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" | 		opts.Cookie.Secret = "OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" | ||||||
| 		opts.ProviderType = "oidc" |  | ||||||
| 		opts.EmailDomains = []string{"example.com"} | 		opts.EmailDomains = []string{"example.com"} | ||||||
| 		opts.OIDCIssuerURL = "http://dex.localhost:4190/dex" |  | ||||||
| 		opts.ClientSecret = "b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK" |  | ||||||
| 		opts.ClientID = "oauth2-proxy" |  | ||||||
| 		opts.Cookie.Secure = false | 		opts.Cookie.Secure = false | ||||||
| 		opts.RawRedirectURL = "http://localhost:4180/oauth2/callback" | 		opts.RawRedirectURL = "http://localhost:4180/oauth2/callback" | ||||||
| 
 | 
 | ||||||
|  | @ -121,6 +127,24 @@ redirect_url="http://localhost:4180/oauth2/callback" | ||||||
| 
 | 
 | ||||||
| 		opts.InjectRequestHeaders = append([]options.Header{authHeader}, opts.InjectRequestHeaders...) | 		opts.InjectRequestHeaders = append([]options.Header{authHeader}, opts.InjectRequestHeaders...) | ||||||
| 		opts.InjectResponseHeaders = append(opts.InjectResponseHeaders, authHeader) | 		opts.InjectResponseHeaders = append(opts.InjectResponseHeaders, authHeader) | ||||||
|  | 
 | ||||||
|  | 		opts.Providers = options.Providers{ | ||||||
|  | 			{ | ||||||
|  | 				ID:           "google=oauth2-proxy", | ||||||
|  | 				Type:         "google", | ||||||
|  | 				ClientSecret: "b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK", | ||||||
|  | 				ClientID:     "oauth2-proxy", | ||||||
|  | 				AzureConfig: options.AzureOptions{ | ||||||
|  | 					Tenant: "common", | ||||||
|  | 				}, | ||||||
|  | 				OIDCConfig: options.OIDCOptions{ | ||||||
|  | 					GroupsClaim: "groups", | ||||||
|  | 					EmailClaim:  "email", | ||||||
|  | 					UserIDClaim: "email", | ||||||
|  | 				}, | ||||||
|  | 				ApprovalPrompt: "force", | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
| 		return opts | 		return opts | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -204,7 +228,7 @@ redirect_url="http://localhost:4180/oauth2/callback" | ||||||
| 			configContent:      testCoreConfig, | 			configContent:      testCoreConfig, | ||||||
| 			alphaConfigContent: testAlphaConfig + ":", | 			alphaConfigContent: testAlphaConfig + ":", | ||||||
| 			expectedOptions:    func() *options.Options { return nil }, | 			expectedOptions:    func() *options.Options { return nil }, | ||||||
| 			expectedErr:        errors.New("failed to load alpha options: error unmarshalling config: error converting YAML to JSON: yaml: line 36: did not find expected key"), | 			expectedErr:        errors.New("failed to load alpha options: error unmarshalling config: error converting YAML to JSON: yaml: line 48: did not find expected key"), | ||||||
| 		}), | 		}), | ||||||
| 		Entry("with alpha configuration and bad core configuration", loadConfigurationTableInput{ | 		Entry("with alpha configuration and bad core configuration", loadConfigurationTableInput{ | ||||||
| 			configContent:      testCoreConfig + "unknown_field=\"something\"", | 			configContent:      testCoreConfig + "unknown_field=\"something\"", | ||||||
|  |  | ||||||
|  | @ -122,7 +122,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr | ||||||
| 		Footer:           opts.Templates.Footer, | 		Footer:           opts.Templates.Footer, | ||||||
| 		Version:          VERSION, | 		Version:          VERSION, | ||||||
| 		Debug:            opts.Templates.Debug, | 		Debug:            opts.Templates.Debug, | ||||||
| 		ProviderName:     buildProviderName(opts.GetProvider(), opts.ProviderName), | 		ProviderName:     buildProviderName(opts.GetProvider(), opts.Providers[0].Name), | ||||||
| 		SignInMessage:    buildSignInMessage(opts), | 		SignInMessage:    buildSignInMessage(opts), | ||||||
| 		DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm, | 		DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm, | ||||||
| 	}) | 	}) | ||||||
|  | @ -136,7 +136,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if opts.SkipJwtBearerTokens { | 	if opts.SkipJwtBearerTokens { | ||||||
| 		logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL) | 		logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.Providers[0].OIDCConfig.IssuerURL) | ||||||
| 		for _, issuer := range opts.ExtraJwtIssuers { | 		for _, issuer := range opts.ExtraJwtIssuers { | ||||||
| 			logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer) | 			logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer) | ||||||
| 		} | 		} | ||||||
|  | @ -146,7 +146,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr | ||||||
| 		redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix) | 		redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.ClientID) | 	logger.Printf("OAuthProxy configured for %s Client ID: %s", opts.GetProvider().Data().ProviderName, opts.Providers[0].ClientID) | ||||||
| 	refresh := "disabled" | 	refresh := "disabled" | ||||||
| 	if opts.Cookie.Refresh != time.Duration(0) { | 	if opts.Cookie.Refresh != time.Duration(0) { | ||||||
| 		refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh) | 		refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh) | ||||||
|  |  | ||||||
|  | @ -981,7 +981,7 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi | ||||||
| 		ProviderData: &providers.ProviderData{}, | 		ProviderData: &providers.ProviderData{}, | ||||||
| 		ValidToken:   opts.providerValidateCookieResponse, | 		ValidToken:   opts.providerValidateCookieResponse, | ||||||
| 	} | 	} | ||||||
| 	pcTest.proxy.provider.(*TestProvider).SetAllowedGroups(pcTest.opts.AllowedGroups) | 	pcTest.proxy.provider.(*TestProvider).SetAllowedGroups(pcTest.opts.Providers[0].AllowedGroups) | ||||||
| 
 | 
 | ||||||
| 	pcTest.rw = httptest.NewRecorder() | 	pcTest.rw = httptest.NewRecorder() | ||||||
| 	pcTest.req, _ = http.NewRequest("GET", "/", strings.NewReader("")) | 	pcTest.req, _ = http.NewRequest("GET", "/", strings.NewReader("")) | ||||||
|  | @ -1322,7 +1322,7 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) { | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 	pcTest.opts.AllowedGroups = []string{"oauth_groups"} | 	pcTest.opts.Providers[0].AllowedGroups = []string{"oauth_groups"} | ||||||
| 	err := validation.Validate(pcTest.opts) | 	err := validation.Validate(pcTest.opts) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 
 | 
 | ||||||
|  | @ -2292,8 +2292,9 @@ func Test_noCacheHeaders(t *testing.T) { | ||||||
| func baseTestOptions() *options.Options { | func baseTestOptions() *options.Options { | ||||||
| 	opts := options.NewOptions() | 	opts := options.NewOptions() | ||||||
| 	opts.Cookie.Secret = rawCookieSecret | 	opts.Cookie.Secret = rawCookieSecret | ||||||
| 	opts.ClientID = clientID | 	opts.Providers[0].ID = "providerID" | ||||||
| 	opts.ClientSecret = clientSecret | 	opts.Providers[0].ClientID = clientID | ||||||
|  | 	opts.Providers[0].ClientSecret = clientSecret | ||||||
| 	opts.EmailDomains = []string{"*"} | 	opts.EmailDomains = []string{"*"} | ||||||
| 
 | 
 | ||||||
| 	// Default injected headers for legacy configuration
 | 	// Default injected headers for legacy configuration
 | ||||||
|  | @ -2786,7 +2787,7 @@ func TestProxyAllowedGroups(t *testing.T) { | ||||||
| 			t.Cleanup(upstreamServer.Close) | 			t.Cleanup(upstreamServer.Close) | ||||||
| 
 | 
 | ||||||
| 			test, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) { | 			test, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) { | ||||||
| 				opts.AllowedGroups = tt.allowedGroups | 				opts.Providers[0].AllowedGroups = tt.allowedGroups | ||||||
| 				opts.UpstreamServers = options.Upstreams{ | 				opts.UpstreamServers = options.Upstreams{ | ||||||
| 					{ | 					{ | ||||||
| 						ID:   upstreamServer.URL, | 						ID:   upstreamServer.URL, | ||||||
|  | @ -2915,7 +2916,7 @@ func TestAuthOnlyAllowedGroups(t *testing.T) { | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) { | 			test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) { | ||||||
| 				opts.AllowedGroups = tc.allowedGroups | 				opts.Providers[0].AllowedGroups = tc.allowedGroups | ||||||
| 			}) | 			}) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				t.Fatal(err) | 				t.Fatal(err) | ||||||
|  |  | ||||||
|  | @ -40,6 +40,9 @@ type AlphaOptions struct { | ||||||
| 	// This can be done by setting the BindAddress and the SecureBindAddress simultaneously.
 | 	// This can be done by setting the BindAddress and the SecureBindAddress simultaneously.
 | ||||||
| 	// To use the secure server you must configure a TLS certificate and key.
 | 	// To use the secure server you must configure a TLS certificate and key.
 | ||||||
| 	MetricsServer Server `json:"metricsServer,omitempty"` | 	MetricsServer Server `json:"metricsServer,omitempty"` | ||||||
|  | 
 | ||||||
|  | 	// Providers is used to configure multiple providers.
 | ||||||
|  | 	Providers Providers `json:"providers,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // MergeInto replaces alpha options in the Options struct with the values
 | // MergeInto replaces alpha options in the Options struct with the values
 | ||||||
|  | @ -50,6 +53,8 @@ func (a *AlphaOptions) MergeInto(opts *Options) { | ||||||
| 	opts.InjectResponseHeaders = a.InjectResponseHeaders | 	opts.InjectResponseHeaders = a.InjectResponseHeaders | ||||||
| 	opts.Server = a.Server | 	opts.Server = a.Server | ||||||
| 	opts.MetricsServer = a.MetricsServer | 	opts.MetricsServer = a.MetricsServer | ||||||
|  | 	opts.Providers = a.Providers | ||||||
|  | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ExtractFrom populates the fields in the AlphaOptions with the values from
 | // ExtractFrom populates the fields in the AlphaOptions with the values from
 | ||||||
|  | @ -60,4 +65,5 @@ func (a *AlphaOptions) ExtractFrom(opts *Options) { | ||||||
| 	a.InjectResponseHeaders = opts.InjectResponseHeaders | 	a.InjectResponseHeaders = opts.InjectResponseHeaders | ||||||
| 	a.Server = opts.Server | 	a.Server = opts.Server | ||||||
| 	a.MetricsServer = opts.MetricsServer | 	a.MetricsServer = opts.MetricsServer | ||||||
|  | 	a.Providers = opts.Providers | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import ( | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/v7/providers" | ||||||
| 	"github.com/spf13/pflag" | 	"github.com/spf13/pflag" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -21,6 +22,9 @@ type LegacyOptions struct { | ||||||
| 	// Legacy options for the server address and TLS
 | 	// Legacy options for the server address and TLS
 | ||||||
| 	LegacyServer LegacyServer `cfg:",squash"` | 	LegacyServer LegacyServer `cfg:",squash"` | ||||||
| 
 | 
 | ||||||
|  | 	// Legacy options for single provider
 | ||||||
|  | 	LegacyProvider LegacyProvider `cfg:",squash"` | ||||||
|  | 
 | ||||||
| 	Options Options `cfg:",squash"` | 	Options Options `cfg:",squash"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -43,6 +47,15 @@ func NewLegacyOptions() *LegacyOptions { | ||||||
| 			HTTPSAddress: ":443", | 			HTTPSAddress: ":443", | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		LegacyProvider: LegacyProvider{ | ||||||
|  | 			ProviderType:    "google", | ||||||
|  | 			AzureTenant:     "common", | ||||||
|  | 			ApprovalPrompt:  "force", | ||||||
|  | 			UserIDClaim:     "email", | ||||||
|  | 			OIDCEmailClaim:  "email", | ||||||
|  | 			OIDCGroupsClaim: "groups", | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		Options: *NewOptions(), | 		Options: *NewOptions(), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -53,6 +66,7 @@ func NewLegacyFlagSet() *pflag.FlagSet { | ||||||
| 	flagSet.AddFlagSet(legacyUpstreamsFlagSet()) | 	flagSet.AddFlagSet(legacyUpstreamsFlagSet()) | ||||||
| 	flagSet.AddFlagSet(legacyHeadersFlagSet()) | 	flagSet.AddFlagSet(legacyHeadersFlagSet()) | ||||||
| 	flagSet.AddFlagSet(legacyServerFlagset()) | 	flagSet.AddFlagSet(legacyServerFlagset()) | ||||||
|  | 	flagSet.AddFlagSet(legacyProviderFlagSet()) | ||||||
| 
 | 
 | ||||||
| 	return flagSet | 	return flagSet | ||||||
| } | } | ||||||
|  | @ -65,10 +79,17 @@ func (l *LegacyOptions) ToOptions() (*Options, error) { | ||||||
| 	l.Options.UpstreamServers = upstreams | 	l.Options.UpstreamServers = upstreams | ||||||
| 
 | 
 | ||||||
| 	l.Options.InjectRequestHeaders, l.Options.InjectResponseHeaders = l.LegacyHeaders.convert() | 	l.Options.InjectRequestHeaders, l.Options.InjectResponseHeaders = l.LegacyHeaders.convert() | ||||||
|  | 
 | ||||||
| 	l.Options.Server, l.Options.MetricsServer = l.LegacyServer.convert() | 	l.Options.Server, l.Options.MetricsServer = l.LegacyServer.convert() | ||||||
| 
 | 
 | ||||||
| 	l.Options.LegacyPreferEmailToUser = l.LegacyHeaders.PreferEmailToUser | 	l.Options.LegacyPreferEmailToUser = l.LegacyHeaders.PreferEmailToUser | ||||||
| 
 | 
 | ||||||
|  | 	providers, err := l.LegacyProvider.convert() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error converting provider: %v", err) | ||||||
|  | 	} | ||||||
|  | 	l.Options.Providers = providers | ||||||
|  | 
 | ||||||
| 	return &l.Options, nil | 	return &l.Options, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -443,6 +464,106 @@ func legacyServerFlagset() *pflag.FlagSet { | ||||||
| 	return flagSet | 	return flagSet | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | type LegacyProvider struct { | ||||||
|  | 	ClientID         string `flag:"client-id" cfg:"client_id"` | ||||||
|  | 	ClientSecret     string `flag:"client-secret" cfg:"client_secret"` | ||||||
|  | 	ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file"` | ||||||
|  | 
 | ||||||
|  | 	KeycloakGroups           []string `flag:"keycloak-group" cfg:"keycloak_groups"` | ||||||
|  | 	AzureTenant              string   `flag:"azure-tenant" cfg:"azure_tenant"` | ||||||
|  | 	BitbucketTeam            string   `flag:"bitbucket-team" cfg:"bitbucket_team"` | ||||||
|  | 	BitbucketRepository      string   `flag:"bitbucket-repository" cfg:"bitbucket_repository"` | ||||||
|  | 	GitHubOrg                string   `flag:"github-org" cfg:"github_org"` | ||||||
|  | 	GitHubTeam               string   `flag:"github-team" cfg:"github_team"` | ||||||
|  | 	GitHubRepo               string   `flag:"github-repo" cfg:"github_repo"` | ||||||
|  | 	GitHubToken              string   `flag:"github-token" cfg:"github_token"` | ||||||
|  | 	GitHubUsers              []string `flag:"github-user" cfg:"github_users"` | ||||||
|  | 	GitLabGroup              []string `flag:"gitlab-group" cfg:"gitlab_groups"` | ||||||
|  | 	GitLabProjects           []string `flag:"gitlab-project" cfg:"gitlab_projects"` | ||||||
|  | 	GoogleGroups             []string `flag:"google-group" cfg:"google_group"` | ||||||
|  | 	GoogleAdminEmail         string   `flag:"google-admin-email" cfg:"google_admin_email"` | ||||||
|  | 	GoogleServiceAccountJSON string   `flag:"google-service-account-json" cfg:"google_service_account_json"` | ||||||
|  | 
 | ||||||
|  | 	// These options allow for other providers besides Google, with
 | ||||||
|  | 	// potential overrides.
 | ||||||
|  | 	ProviderType                       string   `flag:"provider" cfg:"provider"` | ||||||
|  | 	ProviderName                       string   `flag:"provider-display-name" cfg:"provider_display_name"` | ||||||
|  | 	ProviderCAFiles                    []string `flag:"provider-ca-file" cfg:"provider_ca_files"` | ||||||
|  | 	OIDCIssuerURL                      string   `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"` | ||||||
|  | 	InsecureOIDCAllowUnverifiedEmail   bool     `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email"` | ||||||
|  | 	InsecureOIDCSkipIssuerVerification bool     `flag:"insecure-oidc-skip-issuer-verification" cfg:"insecure_oidc_skip_issuer_verification"` | ||||||
|  | 	SkipOIDCDiscovery                  bool     `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery"` | ||||||
|  | 	OIDCJwksURL                        string   `flag:"oidc-jwks-url" cfg:"oidc_jwks_url"` | ||||||
|  | 	OIDCEmailClaim                     string   `flag:"oidc-email-claim" cfg:"oidc_email_claim"` | ||||||
|  | 	OIDCGroupsClaim                    string   `flag:"oidc-groups-claim" cfg:"oidc_groups_claim"` | ||||||
|  | 	LoginURL                           string   `flag:"login-url" cfg:"login_url"` | ||||||
|  | 	RedeemURL                          string   `flag:"redeem-url" cfg:"redeem_url"` | ||||||
|  | 	ProfileURL                         string   `flag:"profile-url" cfg:"profile_url"` | ||||||
|  | 	ProtectedResource                  string   `flag:"resource" cfg:"resource"` | ||||||
|  | 	ValidateURL                        string   `flag:"validate-url" cfg:"validate_url"` | ||||||
|  | 	Scope                              string   `flag:"scope" cfg:"scope"` | ||||||
|  | 	Prompt                             string   `flag:"prompt" cfg:"prompt"` | ||||||
|  | 	ApprovalPrompt                     string   `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0
 | ||||||
|  | 	UserIDClaim                        string   `flag:"user-id-claim" cfg:"user_id_claim"` | ||||||
|  | 	AllowedGroups                      []string `flag:"allowed-group" cfg:"allowed_groups"` | ||||||
|  | 
 | ||||||
|  | 	AcrValues  string `flag:"acr-values" cfg:"acr_values"` | ||||||
|  | 	JWTKey     string `flag:"jwt-key" cfg:"jwt_key"` | ||||||
|  | 	JWTKeyFile string `flag:"jwt-key-file" cfg:"jwt_key_file"` | ||||||
|  | 	PubJWKURL  string `flag:"pubjwk-url" cfg:"pubjwk_url"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func legacyProviderFlagSet() *pflag.FlagSet { | ||||||
|  | 	flagSet := pflag.NewFlagSet("provider", pflag.ExitOnError) | ||||||
|  | 
 | ||||||
|  | 	flagSet.StringSlice("keycloak-group", []string{}, "restrict logins to members of these groups (may be given multiple times)") | ||||||
|  | 	flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.") | ||||||
|  | 	flagSet.String("bitbucket-team", "", "restrict logins to members of this team") | ||||||
|  | 	flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository") | ||||||
|  | 	flagSet.String("github-org", "", "restrict logins to members of this organisation") | ||||||
|  | 	flagSet.String("github-team", "", "restrict logins to members of this team") | ||||||
|  | 	flagSet.String("github-repo", "", "restrict logins to collaborators of this repository") | ||||||
|  | 	flagSet.String("github-token", "", "the token to use when verifying repository collaborators (must have push access to the repository)") | ||||||
|  | 	flagSet.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)") | ||||||
|  | 	flagSet.StringSlice("gitlab-group", []string{}, "restrict logins to members of this group (may be given multiple times)") | ||||||
|  | 	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") | ||||||
|  | 	flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).") | ||||||
|  | 	flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls") | ||||||
|  | 	flagSet.String("google-service-account-json", "", "the path to the service account json credentials") | ||||||
|  | 	flagSet.String("client-id", "", "the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"") | ||||||
|  | 	flagSet.String("client-secret", "", "the OAuth Client Secret") | ||||||
|  | 	flagSet.String("client-secret-file", "", "the file with OAuth Client Secret") | ||||||
|  | 
 | ||||||
|  | 	flagSet.String("provider", "google", "OAuth provider") | ||||||
|  | 	flagSet.String("provider-display-name", "", "Provider display name") | ||||||
|  | 	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.") | ||||||
|  | 	flagSet.String("oidc-issuer-url", "", "OpenID Connect issuer URL (ie: https://accounts.google.com)") | ||||||
|  | 	flagSet.Bool("insecure-oidc-allow-unverified-email", false, "Don't fail if an email address in an id_token is not verified") | ||||||
|  | 	flagSet.Bool("insecure-oidc-skip-issuer-verification", false, "Do not verify if issuer matches OIDC discovery URL") | ||||||
|  | 	flagSet.Bool("skip-oidc-discovery", false, "Skip OIDC discovery and use manually supplied Endpoints") | ||||||
|  | 	flagSet.String("oidc-jwks-url", "", "OpenID Connect JWKS URL (ie: https://www.googleapis.com/oauth2/v3/certs)") | ||||||
|  | 	flagSet.String("oidc-groups-claim", providers.OIDCGroupsClaim, "which OIDC claim contains the user groups") | ||||||
|  | 	flagSet.String("oidc-email-claim", providers.OIDCEmailClaim, "which OIDC claim contains the user's email") | ||||||
|  | 	flagSet.String("login-url", "", "Authentication endpoint") | ||||||
|  | 	flagSet.String("redeem-url", "", "Token redemption endpoint") | ||||||
|  | 	flagSet.String("profile-url", "", "Profile access endpoint") | ||||||
|  | 	flagSet.String("resource", "", "The resource that is protected (Azure AD only)") | ||||||
|  | 	flagSet.String("validate-url", "", "Access token validation endpoint") | ||||||
|  | 	flagSet.String("scope", "", "OAuth scope specification") | ||||||
|  | 	flagSet.String("prompt", "", "OIDC prompt") | ||||||
|  | 	flagSet.String("approval-prompt", "force", "OAuth approval_prompt") | ||||||
|  | 
 | ||||||
|  | 	flagSet.String("acr-values", "", "acr values string:  optional") | ||||||
|  | 	flagSet.String("jwt-key", "", "private key in PEM format used to sign JWT, so that you can say something like -jwt-key=\"${OAUTH2_PROXY_JWT_KEY}\": required by login.gov") | ||||||
|  | 	flagSet.String("jwt-key-file", "", "path to the private key file in PEM format used to sign the JWT so that you can say something like -jwt-key-file=/etc/ssl/private/jwt_signing_key.pem: required by login.gov") | ||||||
|  | 	flagSet.String("pubjwk-url", "", "JWK pubkey access endpoint: required by login.gov") | ||||||
|  | 
 | ||||||
|  | 	flagSet.String("user-id-claim", providers.OIDCEmailClaim, "(DEPRECATED for `oidc-email-claim`) which claim contains the user ID") | ||||||
|  | 	flagSet.StringSlice("allowed-group", []string{}, "restrict logins to members of this group (may be given multiple times)") | ||||||
|  | 
 | ||||||
|  | 	return flagSet | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (l LegacyServer) convert() (Server, Server) { | func (l LegacyServer) convert() (Server, Server) { | ||||||
| 	appServer := Server{ | 	appServer := Server{ | ||||||
| 		BindAddress:       l.HTTPAddress, | 		BindAddress:       l.HTTPAddress, | ||||||
|  | @ -482,3 +603,91 @@ func (l LegacyServer) convert() (Server, Server) { | ||||||
| 
 | 
 | ||||||
| 	return appServer, metricsServer | 	return appServer, metricsServer | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (l *LegacyProvider) convert() (Providers, error) { | ||||||
|  | 	providers := Providers{} | ||||||
|  | 
 | ||||||
|  | 	provider := Provider{ | ||||||
|  | 		ClientID:          l.ClientID, | ||||||
|  | 		ClientSecret:      l.ClientSecret, | ||||||
|  | 		ClientSecretFile:  l.ClientSecretFile, | ||||||
|  | 		Type:              l.ProviderType, | ||||||
|  | 		CAFiles:           l.ProviderCAFiles, | ||||||
|  | 		LoginURL:          l.LoginURL, | ||||||
|  | 		RedeemURL:         l.RedeemURL, | ||||||
|  | 		ProfileURL:        l.ProfileURL, | ||||||
|  | 		ProtectedResource: l.ProtectedResource, | ||||||
|  | 		ValidateURL:       l.ValidateURL, | ||||||
|  | 		Scope:             l.Scope, | ||||||
|  | 		Prompt:            l.Prompt, | ||||||
|  | 		ApprovalPrompt:    l.ApprovalPrompt, | ||||||
|  | 		AllowedGroups:     l.AllowedGroups, | ||||||
|  | 		AcrValues:         l.AcrValues, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// This part is out of the switch section for all providers that support OIDC
 | ||||||
|  | 	provider.OIDCConfig = OIDCOptions{ | ||||||
|  | 		IssuerURL:                      l.OIDCIssuerURL, | ||||||
|  | 		InsecureAllowUnverifiedEmail:   l.InsecureOIDCAllowUnverifiedEmail, | ||||||
|  | 		InsecureSkipIssuerVerification: l.InsecureOIDCSkipIssuerVerification, | ||||||
|  | 		SkipDiscovery:                  l.SkipOIDCDiscovery, | ||||||
|  | 		JwksURL:                        l.OIDCJwksURL, | ||||||
|  | 		UserIDClaim:                    l.UserIDClaim, | ||||||
|  | 		EmailClaim:                     l.OIDCEmailClaim, | ||||||
|  | 		GroupsClaim:                    l.OIDCGroupsClaim, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// This part is out of the switch section because azure has a default tenant
 | ||||||
|  | 	// that needs to be added from legacy options
 | ||||||
|  | 	provider.AzureConfig = AzureOptions{ | ||||||
|  | 		Tenant: l.AzureTenant, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch provider.Type { | ||||||
|  | 	case "github": | ||||||
|  | 		provider.GitHubConfig = GitHubOptions{ | ||||||
|  | 			Org:   l.GitHubOrg, | ||||||
|  | 			Team:  l.GitHubTeam, | ||||||
|  | 			Repo:  l.GitHubRepo, | ||||||
|  | 			Token: l.GitHubToken, | ||||||
|  | 			Users: l.GitHubUsers, | ||||||
|  | 		} | ||||||
|  | 	case "keycloak": | ||||||
|  | 		provider.KeycloakConfig = KeycloakOptions{ | ||||||
|  | 			Groups: l.KeycloakGroups, | ||||||
|  | 		} | ||||||
|  | 	case "gitlab": | ||||||
|  | 		provider.GitLabConfig = GitLabOptions{ | ||||||
|  | 			Group:    l.GitLabGroup, | ||||||
|  | 			Projects: l.GitLabProjects, | ||||||
|  | 		} | ||||||
|  | 	case "login.gov": | ||||||
|  | 		provider.LoginGovConfig = LoginGovOptions{ | ||||||
|  | 			JWTKey:     l.JWTKey, | ||||||
|  | 			JWTKeyFile: l.JWTKeyFile, | ||||||
|  | 			PubJWKURL:  l.PubJWKURL, | ||||||
|  | 		} | ||||||
|  | 	case "bitbucket": | ||||||
|  | 		provider.BitbucketConfig = BitbucketOptions{ | ||||||
|  | 			Team:       l.BitbucketTeam, | ||||||
|  | 			Repository: l.BitbucketRepository, | ||||||
|  | 		} | ||||||
|  | 	case "google": | ||||||
|  | 		provider.GoogleConfig = GoogleOptions{ | ||||||
|  | 			Groups:             l.GoogleGroups, | ||||||
|  | 			AdminEmail:         l.GoogleAdminEmail, | ||||||
|  | 			ServiceAccountJSON: l.GoogleServiceAccountJSON, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if l.ProviderName != "" { | ||||||
|  | 		provider.ID = l.ProviderName | ||||||
|  | 		provider.Name = l.ProviderName | ||||||
|  | 	} else { | ||||||
|  | 		provider.ID = l.ProviderType + "=" + l.ClientID | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	providers = append(providers, provider) | ||||||
|  | 
 | ||||||
|  | 	return providers, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ var _ = Describe("Legacy Options", func() { | ||||||
| 			legacyOpts.LegacyUpstreams.ProxyWebSockets = true | 			legacyOpts.LegacyUpstreams.ProxyWebSockets = true | ||||||
| 			legacyOpts.LegacyUpstreams.SSLUpstreamInsecureSkipVerify = true | 			legacyOpts.LegacyUpstreams.SSLUpstreamInsecureSkipVerify = true | ||||||
| 			legacyOpts.LegacyUpstreams.Upstreams = []string{"http://foo.bar/baz", "file:///var/lib/website#/bar", "static://204"} | 			legacyOpts.LegacyUpstreams.Upstreams = []string{"http://foo.bar/baz", "file:///var/lib/website#/bar", "static://204"} | ||||||
|  | 			legacyOpts.LegacyProvider.ClientID = "oauth-proxy" | ||||||
| 
 | 
 | ||||||
| 			truth := true | 			truth := true | ||||||
| 			staticCode := 204 | 			staticCode := 204 | ||||||
|  | @ -110,6 +111,9 @@ var _ = Describe("Legacy Options", func() { | ||||||
| 				BindAddress: "127.0.0.1:4180", | 				BindAddress: "127.0.0.1:4180", | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | 			opts.Providers[0].ClientID = "oauth-proxy" | ||||||
|  | 			opts.Providers[0].ID = "google=oauth-proxy" | ||||||
|  | 
 | ||||||
| 			converted, err := legacyOpts.ToOptions() | 			converted, err := legacyOpts.ToOptions() | ||||||
| 			Expect(err).ToNot(HaveOccurred()) | 			Expect(err).ToNot(HaveOccurred()) | ||||||
| 			Expect(converted).To(Equal(opts)) | 			Expect(converted).To(Equal(opts)) | ||||||
|  | @ -196,9 +200,9 @@ var _ = Describe("Legacy Options", func() { | ||||||
| 		invalidHTTPErrMsg := "could not parse upstream \":foo\": parse \":foo\": missing protocol scheme" | 		invalidHTTPErrMsg := "could not parse upstream \":foo\": parse \":foo\": missing protocol scheme" | ||||||
| 
 | 
 | ||||||
| 		DescribeTable("convertLegacyUpstreams", | 		DescribeTable("convertLegacyUpstreams", | ||||||
| 			func(o *convertUpstreamsTableInput) { | 			func(in *convertUpstreamsTableInput) { | ||||||
| 				legacyUpstreams := LegacyUpstreams{ | 				legacyUpstreams := LegacyUpstreams{ | ||||||
| 					Upstreams:                     o.upstreamStrings, | 					Upstreams:                     in.upstreamStrings, | ||||||
| 					SSLUpstreamInsecureSkipVerify: skipVerify, | 					SSLUpstreamInsecureSkipVerify: skipVerify, | ||||||
| 					PassHostHeader:                passHostHeader, | 					PassHostHeader:                passHostHeader, | ||||||
| 					ProxyWebSockets:               proxyWebSockets, | 					ProxyWebSockets:               proxyWebSockets, | ||||||
|  | @ -207,14 +211,14 @@ var _ = Describe("Legacy Options", func() { | ||||||
| 
 | 
 | ||||||
| 				upstreams, err := legacyUpstreams.convert() | 				upstreams, err := legacyUpstreams.convert() | ||||||
| 
 | 
 | ||||||
| 				if o.errMsg != "" { | 				if in.errMsg != "" { | ||||||
| 					Expect(err).To(HaveOccurred()) | 					Expect(err).To(HaveOccurred()) | ||||||
| 					Expect(err.Error()).To(Equal(o.errMsg)) | 					Expect(err.Error()).To(Equal(in.errMsg)) | ||||||
| 				} else { | 				} else { | ||||||
| 					Expect(err).ToNot(HaveOccurred()) | 					Expect(err).ToNot(HaveOccurred()) | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				Expect(upstreams).To(ConsistOf(o.expectedUpstreams)) | 				Expect(upstreams).To(ConsistOf(in.expectedUpstreams)) | ||||||
| 			}, | 			}, | ||||||
| 			Entry("with no upstreams", &convertUpstreamsTableInput{ | 			Entry("with no upstreams", &convertUpstreamsTableInput{ | ||||||
| 				upstreamStrings:   []string{}, | 				upstreamStrings:   []string{}, | ||||||
|  | @ -850,6 +854,87 @@ var _ = Describe("Legacy Options", func() { | ||||||
| 				}, | 				}, | ||||||
| 			}), | 			}), | ||||||
| 		) | 		) | ||||||
|  | 	}) | ||||||
| 
 | 
 | ||||||
|  | 	Context("Legacy Providers", func() { | ||||||
|  | 		type convertProvidersTableInput struct { | ||||||
|  | 			legacyProvider    LegacyProvider | ||||||
|  | 			expectedProviders Providers | ||||||
|  | 			errMsg            string | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Non defaults for these options
 | ||||||
|  | 		clientID := "abcd" | ||||||
|  | 
 | ||||||
|  | 		defaultProvider := Provider{ | ||||||
|  | 			ID:       "google=" + clientID, | ||||||
|  | 			ClientID: clientID, | ||||||
|  | 			Type:     "google", | ||||||
|  | 		} | ||||||
|  | 		defaultLegacyProvider := LegacyProvider{ | ||||||
|  | 			ClientID:     clientID, | ||||||
|  | 			ProviderType: "google", | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		displayNameProvider := Provider{ | ||||||
|  | 			ID:       "displayName", | ||||||
|  | 			Name:     "displayName", | ||||||
|  | 			ClientID: clientID, | ||||||
|  | 			Type:     "google", | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		displayNameLegacyProvider := LegacyProvider{ | ||||||
|  | 			ClientID:     clientID, | ||||||
|  | 			ProviderName: "displayName", | ||||||
|  | 			ProviderType: "google", | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		internalConfigProvider := Provider{ | ||||||
|  | 			ID:       "google=" + clientID, | ||||||
|  | 			ClientID: clientID, | ||||||
|  | 			Type:     "google", | ||||||
|  | 			GoogleConfig: GoogleOptions{ | ||||||
|  | 				AdminEmail:         "email@email.com", | ||||||
|  | 				ServiceAccountJSON: "test.json", | ||||||
|  | 				Groups:             []string{"1", "2"}, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		internalConfigLegacyProvider := LegacyProvider{ | ||||||
|  | 			ClientID:                 clientID, | ||||||
|  | 			ProviderType:             "google", | ||||||
|  | 			GoogleAdminEmail:         "email@email.com", | ||||||
|  | 			GoogleServiceAccountJSON: "test.json", | ||||||
|  | 			GoogleGroups:             []string{"1", "2"}, | ||||||
|  | 		} | ||||||
|  | 		DescribeTable("convertLegacyProviders", | ||||||
|  | 			func(in *convertProvidersTableInput) { | ||||||
|  | 				providers, err := in.legacyProvider.convert() | ||||||
|  | 
 | ||||||
|  | 				if in.errMsg != "" { | ||||||
|  | 					Expect(err).To(HaveOccurred()) | ||||||
|  | 					Expect(err.Error()).To(Equal(in.errMsg)) | ||||||
|  | 				} else { | ||||||
|  | 					Expect(err).ToNot(HaveOccurred()) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				Expect(providers).To(ConsistOf(in.expectedProviders)) | ||||||
|  | 			}, | ||||||
|  | 			Entry("with default provider", &convertProvidersTableInput{ | ||||||
|  | 				legacyProvider:    defaultLegacyProvider, | ||||||
|  | 				expectedProviders: Providers{defaultProvider}, | ||||||
|  | 				errMsg:            "", | ||||||
|  | 			}), | ||||||
|  | 			Entry("with provider display name", &convertProvidersTableInput{ | ||||||
|  | 				legacyProvider:    displayNameLegacyProvider, | ||||||
|  | 				expectedProviders: Providers{displayNameProvider}, | ||||||
|  | 				errMsg:            "", | ||||||
|  | 			}), | ||||||
|  | 			Entry("with internal provider config", &convertProvidersTableInput{ | ||||||
|  | 				legacyProvider:    internalConfigLegacyProvider, | ||||||
|  | 				expectedProviders: Providers{internalConfigProvider}, | ||||||
|  | 				errMsg:            "", | ||||||
|  | 			}), | ||||||
|  | 		) | ||||||
| 	}) | 	}) | ||||||
| }) | }) | ||||||
|  |  | ||||||
|  | @ -14,6 +14,49 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var _ = Describe("Load", func() { | var _ = Describe("Load", func() { | ||||||
|  | 	optionsWithNilProvider := NewOptions() | ||||||
|  | 	optionsWithNilProvider.Providers = nil | ||||||
|  | 
 | ||||||
|  | 	legacyOptionsWithNilProvider := &LegacyOptions{ | ||||||
|  | 		LegacyUpstreams: LegacyUpstreams{ | ||||||
|  | 			PassHostHeader:  true, | ||||||
|  | 			ProxyWebSockets: true, | ||||||
|  | 			FlushInterval:   DefaultUpstreamFlushInterval, | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		LegacyHeaders: LegacyHeaders{ | ||||||
|  | 			PassBasicAuth:        true, | ||||||
|  | 			PassUserHeaders:      true, | ||||||
|  | 			SkipAuthStripHeaders: true, | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		LegacyServer: LegacyServer{ | ||||||
|  | 			HTTPAddress:  "127.0.0.1:4180", | ||||||
|  | 			HTTPSAddress: ":443", | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		LegacyProvider: LegacyProvider{ | ||||||
|  | 			ProviderType:    "google", | ||||||
|  | 			AzureTenant:     "common", | ||||||
|  | 			ApprovalPrompt:  "force", | ||||||
|  | 			UserIDClaim:     "email", | ||||||
|  | 			OIDCEmailClaim:  "email", | ||||||
|  | 			OIDCGroupsClaim: "groups", | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		Options: Options{ | ||||||
|  | 			ProxyPrefix:        "/oauth2", | ||||||
|  | 			PingPath:           "/ping", | ||||||
|  | 			RealClientIPHeader: "X-Real-IP", | ||||||
|  | 			ForceHTTPS:         false, | ||||||
|  | 			Cookie:             cookieDefaults(), | ||||||
|  | 			Session:            sessionOptionsDefaults(), | ||||||
|  | 			Templates:          templatesDefaults(), | ||||||
|  | 			SkipAuthPreflight:  false, | ||||||
|  | 			Logging:            loggingDefaults(), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	Context("with a testOptions structure", func() { | 	Context("with a testOptions structure", func() { | ||||||
| 		type TestOptionSubStruct struct { | 		type TestOptionSubStruct struct { | ||||||
| 			StringSliceOption []string `flag:"string-slice-option" cfg:"string_slice_option"` | 			StringSliceOption []string `flag:"string-slice-option" cfg:"string_slice_option"` | ||||||
|  | @ -294,12 +337,12 @@ var _ = Describe("Load", func() { | ||||||
| 			Entry("with an empty Options struct, should return default values", &testOptionsTableInput{ | 			Entry("with an empty Options struct, should return default values", &testOptionsTableInput{ | ||||||
| 				flagSet:        NewFlagSet, | 				flagSet:        NewFlagSet, | ||||||
| 				input:          &Options{}, | 				input:          &Options{}, | ||||||
| 				expectedOutput: NewOptions(), | 				expectedOutput: optionsWithNilProvider, | ||||||
| 			}), | 			}), | ||||||
| 			Entry("with an empty LegacyOptions struct, should return default values", &testOptionsTableInput{ | 			Entry("with an empty LegacyOptions struct, should return default values", &testOptionsTableInput{ | ||||||
| 				flagSet:        NewLegacyFlagSet, | 				flagSet:        NewLegacyFlagSet, | ||||||
| 				input:          &LegacyOptions{}, | 				input:          &LegacyOptions{}, | ||||||
| 				expectedOutput: NewLegacyOptions(), | 				expectedOutput: legacyOptionsWithNilProvider, | ||||||
| 			}), | 			}), | ||||||
| 		) | 		) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | @ -27,29 +27,12 @@ type Options struct { | ||||||
| 	TrustedIPs         []string `flag:"trusted-ip" cfg:"trusted_ips"` | 	TrustedIPs         []string `flag:"trusted-ip" cfg:"trusted_ips"` | ||||||
| 	ForceHTTPS         bool     `flag:"force-https" cfg:"force_https"` | 	ForceHTTPS         bool     `flag:"force-https" cfg:"force_https"` | ||||||
| 	RawRedirectURL     string   `flag:"redirect-url" cfg:"redirect_url"` | 	RawRedirectURL     string   `flag:"redirect-url" cfg:"redirect_url"` | ||||||
| 	ClientID           string   `flag:"client-id" cfg:"client_id"` |  | ||||||
| 	ClientSecret       string   `flag:"client-secret" cfg:"client_secret"` |  | ||||||
| 	ClientSecretFile   string   `flag:"client-secret-file" cfg:"client_secret_file"` |  | ||||||
| 
 | 
 | ||||||
| 	AuthenticatedEmailsFile  string   `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"` | 	AuthenticatedEmailsFile string   `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"` | ||||||
| 	KeycloakGroups           []string `flag:"keycloak-group" cfg:"keycloak_groups"` | 	EmailDomains            []string `flag:"email-domain" cfg:"email_domains"` | ||||||
| 	AzureTenant              string   `flag:"azure-tenant" cfg:"azure_tenant"` | 	WhitelistDomains        []string `flag:"whitelist-domain" cfg:"whitelist_domains"` | ||||||
| 	BitbucketTeam            string   `flag:"bitbucket-team" cfg:"bitbucket_team"` | 	HtpasswdFile            string   `flag:"htpasswd-file" cfg:"htpasswd_file"` | ||||||
| 	BitbucketRepository      string   `flag:"bitbucket-repository" cfg:"bitbucket_repository"` | 	HtpasswdUserGroups      []string `flag:"htpasswd-user-group" cfg:"htpasswd_user_groups"` | ||||||
| 	EmailDomains             []string `flag:"email-domain" cfg:"email_domains"` |  | ||||||
| 	WhitelistDomains         []string `flag:"whitelist-domain" cfg:"whitelist_domains"` |  | ||||||
| 	GitHubOrg                string   `flag:"github-org" cfg:"github_org"` |  | ||||||
| 	GitHubTeam               string   `flag:"github-team" cfg:"github_team"` |  | ||||||
| 	GitHubRepo               string   `flag:"github-repo" cfg:"github_repo"` |  | ||||||
| 	GitHubToken              string   `flag:"github-token" cfg:"github_token"` |  | ||||||
| 	GitHubUsers              []string `flag:"github-user" cfg:"github_users"` |  | ||||||
| 	GitLabGroup              []string `flag:"gitlab-group" cfg:"gitlab_groups"` |  | ||||||
| 	GitlabProjects           []string `flag:"gitlab-project" cfg:"gitlab_projects"` |  | ||||||
| 	GoogleGroups             []string `flag:"google-group" cfg:"google_group"` |  | ||||||
| 	GoogleAdminEmail         string   `flag:"google-admin-email" cfg:"google_admin_email"` |  | ||||||
| 	GoogleServiceAccountJSON string   `flag:"google-service-account-json" cfg:"google_service_account_json"` |  | ||||||
| 	HtpasswdFile             string   `flag:"htpasswd-file" cfg:"htpasswd_file"` |  | ||||||
| 	HtpasswdUserGroups       []string `flag:"htpasswd-user-group" cfg:"htpasswd_user_groups"` |  | ||||||
| 
 | 
 | ||||||
| 	Cookie    Cookie         `cfg:",squash"` | 	Cookie    Cookie         `cfg:",squash"` | ||||||
| 	Session   SessionOptions `cfg:",squash"` | 	Session   SessionOptions `cfg:",squash"` | ||||||
|  | @ -66,6 +49,8 @@ type Options struct { | ||||||
| 	Server        Server `cfg:",internal"` | 	Server        Server `cfg:",internal"` | ||||||
| 	MetricsServer Server `cfg:",internal"` | 	MetricsServer Server `cfg:",internal"` | ||||||
| 
 | 
 | ||||||
|  | 	Providers Providers `cfg:",internal"` | ||||||
|  | 
 | ||||||
| 	SkipAuthRegex         []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"` | 	SkipAuthRegex         []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"` | ||||||
| 	SkipAuthRoutes        []string `flag:"skip-auth-route" cfg:"skip_auth_routes"` | 	SkipAuthRoutes        []string `flag:"skip-auth-route" cfg:"skip_auth_routes"` | ||||||
| 	SkipJwtBearerTokens   bool     `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"` | 	SkipJwtBearerTokens   bool     `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"` | ||||||
|  | @ -74,34 +59,7 @@ type Options struct { | ||||||
| 	SSLInsecureSkipVerify bool     `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"` | 	SSLInsecureSkipVerify bool     `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"` | ||||||
| 	SkipAuthPreflight     bool     `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"` | 	SkipAuthPreflight     bool     `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"` | ||||||
| 
 | 
 | ||||||
| 	// These options allow for other providers besides Google, with
 |  | ||||||
| 	// potential overrides.
 |  | ||||||
| 	ProviderType                       string   `flag:"provider" cfg:"provider"` |  | ||||||
| 	ProviderName                       string   `flag:"provider-display-name" cfg:"provider_display_name"` |  | ||||||
| 	ProviderCAFiles                    []string `flag:"provider-ca-file" cfg:"provider_ca_files"` |  | ||||||
| 	OIDCIssuerURL                      string   `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"` |  | ||||||
| 	InsecureOIDCAllowUnverifiedEmail   bool     `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email"` |  | ||||||
| 	InsecureOIDCSkipIssuerVerification bool     `flag:"insecure-oidc-skip-issuer-verification" cfg:"insecure_oidc_skip_issuer_verification"` |  | ||||||
| 	SkipOIDCDiscovery                  bool     `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery"` |  | ||||||
| 	OIDCJwksURL                        string   `flag:"oidc-jwks-url" cfg:"oidc_jwks_url"` |  | ||||||
| 	OIDCEmailClaim                     string   `flag:"oidc-email-claim" cfg:"oidc_email_claim"` |  | ||||||
| 	OIDCGroupsClaim                    string   `flag:"oidc-groups-claim" cfg:"oidc_groups_claim"` |  | ||||||
| 	LoginURL                           string   `flag:"login-url" cfg:"login_url"` |  | ||||||
| 	RedeemURL                          string   `flag:"redeem-url" cfg:"redeem_url"` |  | ||||||
| 	ProfileURL                         string   `flag:"profile-url" cfg:"profile_url"` |  | ||||||
| 	ProtectedResource                  string   `flag:"resource" cfg:"resource"` |  | ||||||
| 	ValidateURL                        string   `flag:"validate-url" cfg:"validate_url"` |  | ||||||
| 	Scope                              string   `flag:"scope" cfg:"scope"` |  | ||||||
| 	Prompt                             string   `flag:"prompt" cfg:"prompt"` |  | ||||||
| 	ApprovalPrompt                     string   `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0
 |  | ||||||
| 	UserIDClaim                        string   `flag:"user-id-claim" cfg:"user_id_claim"` |  | ||||||
| 	AllowedGroups                      []string `flag:"allowed-group" cfg:"allowed_groups"` |  | ||||||
| 
 |  | ||||||
| 	SignatureKey    string `flag:"signature-key" cfg:"signature_key"` | 	SignatureKey    string `flag:"signature-key" cfg:"signature_key"` | ||||||
| 	AcrValues       string `flag:"acr-values" cfg:"acr_values"` |  | ||||||
| 	JWTKey          string `flag:"jwt-key" cfg:"jwt_key"` |  | ||||||
| 	JWTKeyFile      string `flag:"jwt-key-file" cfg:"jwt_key_file"` |  | ||||||
| 	PubJWKURL       string `flag:"pubjwk-url" cfg:"pubjwk_url"` |  | ||||||
| 	GCPHealthChecks bool   `flag:"gcp-healthchecks" cfg:"gcp_healthchecks"` | 	GCPHealthChecks bool   `flag:"gcp-healthchecks" cfg:"gcp_healthchecks"` | ||||||
| 
 | 
 | ||||||
| 	// This is used for backwards compatibility for basic auth users
 | 	// This is used for backwards compatibility for basic auth users
 | ||||||
|  | @ -135,24 +93,16 @@ func (o *Options) SetRealClientIPParser(s ipapi.RealClientIPParser) { o.realClie | ||||||
| // NewOptions constructs a new Options with defaulted values
 | // NewOptions constructs a new Options with defaulted values
 | ||||||
| func NewOptions() *Options { | func NewOptions() *Options { | ||||||
| 	return &Options{ | 	return &Options{ | ||||||
| 		ProxyPrefix:                      "/oauth2", | 		ProxyPrefix:        "/oauth2", | ||||||
| 		ProviderType:                     "google", | 		Providers:          providerDefaults(), | ||||||
| 		PingPath:                         "/ping", | 		PingPath:           "/ping", | ||||||
| 		RealClientIPHeader:               "X-Real-IP", | 		RealClientIPHeader: "X-Real-IP", | ||||||
| 		ForceHTTPS:                       false, | 		ForceHTTPS:         false, | ||||||
| 		Cookie:                           cookieDefaults(), | 		Cookie:             cookieDefaults(), | ||||||
| 		Session:                          sessionOptionsDefaults(), | 		Session:            sessionOptionsDefaults(), | ||||||
| 		Templates:                        templatesDefaults(), | 		Templates:          templatesDefaults(), | ||||||
| 		AzureTenant:                      "common", | 		SkipAuthPreflight:  false, | ||||||
| 		SkipAuthPreflight:                false, | 		Logging:            loggingDefaults(), | ||||||
| 		Prompt:                           "", // Change to "login" when ApprovalPrompt officially deprecated
 |  | ||||||
| 		ApprovalPrompt:                   "force", |  | ||||||
| 		InsecureOIDCAllowUnverifiedEmail: false, |  | ||||||
| 		SkipOIDCDiscovery:                false, |  | ||||||
| 		Logging:                          loggingDefaults(), |  | ||||||
| 		UserIDClaim:                      providers.OIDCEmailClaim, // Deprecated: Use OIDCEmailClaim
 |  | ||||||
| 		OIDCEmailClaim:                   providers.OIDCEmailClaim, |  | ||||||
| 		OIDCGroupsClaim:                  providers.OIDCGroupsClaim, |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -175,23 +125,6 @@ func NewFlagSet() *pflag.FlagSet { | ||||||
| 
 | 
 | ||||||
| 	flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email") | 	flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email") | ||||||
| 	flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)") | 	flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)") | ||||||
| 	flagSet.StringSlice("keycloak-group", []string{}, "restrict logins to members of these groups (may be given multiple times)") |  | ||||||
| 	flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.") |  | ||||||
| 	flagSet.String("bitbucket-team", "", "restrict logins to members of this team") |  | ||||||
| 	flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository") |  | ||||||
| 	flagSet.String("github-org", "", "restrict logins to members of this organisation") |  | ||||||
| 	flagSet.String("github-team", "", "restrict logins to members of this team") |  | ||||||
| 	flagSet.String("github-repo", "", "restrict logins to collaborators of this repository") |  | ||||||
| 	flagSet.String("github-token", "", "the token to use when verifying repository collaborators (must have push access to the repository)") |  | ||||||
| 	flagSet.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)") |  | ||||||
| 	flagSet.StringSlice("gitlab-group", []string{}, "restrict logins to members of this group (may be given multiple times)") |  | ||||||
| 	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") |  | ||||||
| 	flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).") |  | ||||||
| 	flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls") |  | ||||||
| 	flagSet.String("google-service-account-json", "", "the path to the service account json credentials") |  | ||||||
| 	flagSet.String("client-id", "", "the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"") |  | ||||||
| 	flagSet.String("client-secret", "", "the OAuth Client Secret") |  | ||||||
| 	flagSet.String("client-secret-file", "", "the file with OAuth Client Secret") |  | ||||||
| 	flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)") | 	flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)") | ||||||
| 	flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -B\" for bcrypt encryption") | 	flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -B\" for bcrypt encryption") | ||||||
| 	flagSet.StringSlice("htpasswd-user-group", []string{}, "the groups to be set on sessions for htpasswd users (may be given multiple times)") | 	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 { | ||||||
| 	flagSet.Bool("redis-use-cluster", false, "Connect to redis cluster. Must set --redis-cluster-connection-urls to use this feature") | 	flagSet.Bool("redis-use-cluster", false, "Connect to redis cluster. Must set --redis-cluster-connection-urls to use this feature") | ||||||
| 	flagSet.StringSlice("redis-cluster-connection-urls", []string{}, "List of Redis cluster connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-cluster") | 	flagSet.StringSlice("redis-cluster-connection-urls", []string{}, "List of Redis cluster connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-cluster") | ||||||
| 
 | 
 | ||||||
| 	flagSet.String("provider", "google", "OAuth provider") |  | ||||||
| 	flagSet.String("provider-display-name", "", "Provider display name") |  | ||||||
| 	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.") |  | ||||||
| 	flagSet.String("oidc-issuer-url", "", "OpenID Connect issuer URL (ie: https://accounts.google.com)") |  | ||||||
| 	flagSet.Bool("insecure-oidc-allow-unverified-email", false, "Don't fail if an email address in an id_token is not verified") |  | ||||||
| 	flagSet.Bool("insecure-oidc-skip-issuer-verification", false, "Do not verify if issuer matches OIDC discovery URL") |  | ||||||
| 	flagSet.Bool("skip-oidc-discovery", false, "Skip OIDC discovery and use manually supplied Endpoints") |  | ||||||
| 	flagSet.String("oidc-jwks-url", "", "OpenID Connect JWKS URL (ie: https://www.googleapis.com/oauth2/v3/certs)") |  | ||||||
| 	flagSet.String("oidc-groups-claim", providers.OIDCGroupsClaim, "which OIDC claim contains the user groups") |  | ||||||
| 	flagSet.String("oidc-email-claim", providers.OIDCEmailClaim, "which OIDC claim contains the user's email") |  | ||||||
| 	flagSet.String("login-url", "", "Authentication endpoint") |  | ||||||
| 	flagSet.String("redeem-url", "", "Token redemption endpoint") |  | ||||||
| 	flagSet.String("profile-url", "", "Profile access endpoint") |  | ||||||
| 	flagSet.String("resource", "", "The resource that is protected (Azure AD only)") |  | ||||||
| 	flagSet.String("validate-url", "", "Access token validation endpoint") |  | ||||||
| 	flagSet.String("scope", "", "OAuth scope specification") |  | ||||||
| 	flagSet.String("prompt", "", "OIDC prompt") |  | ||||||
| 	flagSet.String("approval-prompt", "force", "OAuth approval_prompt") |  | ||||||
| 
 |  | ||||||
| 	flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)") | 	flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)") | ||||||
| 	flagSet.String("acr-values", "", "acr values string:  optional") |  | ||||||
| 	flagSet.String("jwt-key", "", "private key in PEM format used to sign JWT, so that you can say something like -jwt-key=\"${OAUTH2_PROXY_JWT_KEY}\": required by login.gov") |  | ||||||
| 	flagSet.String("jwt-key-file", "", "path to the private key file in PEM format used to sign the JWT so that you can say something like -jwt-key-file=/etc/ssl/private/jwt_signing_key.pem: required by login.gov") |  | ||||||
| 	flagSet.String("pubjwk-url", "", "JWK pubkey access endpoint: required by login.gov") |  | ||||||
| 	flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints") | 	flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints") | ||||||
| 
 | 
 | ||||||
| 	flagSet.String("user-id-claim", providers.OIDCEmailClaim, "(DEPRECATED for `oidc-email-claim`) which claim contains the user ID") |  | ||||||
| 	flagSet.StringSlice("allowed-group", []string{}, "restrict logins to members of this group (may be given multiple times)") |  | ||||||
| 
 |  | ||||||
| 	flagSet.AddFlagSet(cookieFlagSet()) | 	flagSet.AddFlagSet(cookieFlagSet()) | ||||||
| 	flagSet.AddFlagSet(loggingFlagSet()) | 	flagSet.AddFlagSet(loggingFlagSet()) | ||||||
| 	flagSet.AddFlagSet(templatesFlagSet()) | 	flagSet.AddFlagSet(templatesFlagSet()) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,180 @@ | ||||||
|  | package options | ||||||
|  | 
 | ||||||
|  | import "github.com/oauth2-proxy/oauth2-proxy/v7/providers" | ||||||
|  | 
 | ||||||
|  | // Providers is a collection of definitions for providers.
 | ||||||
|  | type Providers []Provider | ||||||
|  | 
 | ||||||
|  | // Provider holds all configuration for a single provider
 | ||||||
|  | type Provider struct { | ||||||
|  | 	// ClientID is the OAuth Client ID that is defined in the provider
 | ||||||
|  | 	// This value is required for all providers.
 | ||||||
|  | 	ClientID string `json:"clientID,omitempty"` | ||||||
|  | 	// ClientSecret is the OAuth Client Secret that is defined in the provider
 | ||||||
|  | 	// This value is required for all providers.
 | ||||||
|  | 	ClientSecret string `json:"clientSecret,omitempty"` | ||||||
|  | 	// ClientSecretFile is the name of the file
 | ||||||
|  | 	// containing the OAuth Client Secret, it will be used if ClientSecret is not set.
 | ||||||
|  | 	ClientSecretFile string `json:"clientSecretFile,omitempty"` | ||||||
|  | 
 | ||||||
|  | 	// KeycloakConfig holds all configurations for Keycloak provider.
 | ||||||
|  | 	KeycloakConfig KeycloakOptions `json:"keycloakConfig,omitempty"` | ||||||
|  | 	// AzureConfig holds all configurations for Azure provider.
 | ||||||
|  | 	AzureConfig AzureOptions `json:"azureConfig,omitempty"` | ||||||
|  | 	// BitbucketConfig holds all configurations for Bitbucket provider.
 | ||||||
|  | 	BitbucketConfig BitbucketOptions `json:"bitbucketConfig,omitempty"` | ||||||
|  | 	// GitHubConfig holds all configurations for GitHubC provider.
 | ||||||
|  | 	GitHubConfig GitHubOptions `json:"githubConfig,omitempty"` | ||||||
|  | 	// GitLabConfig holds all configurations for GitLab provider.
 | ||||||
|  | 	GitLabConfig GitLabOptions `json:"gitlabConfig,omitempty"` | ||||||
|  | 	// GoogleConfig holds all configurations for Google provider.
 | ||||||
|  | 	GoogleConfig GoogleOptions `json:"googleConfig,omitempty"` | ||||||
|  | 	// OIDCConfig holds all configurations for OIDC provider
 | ||||||
|  | 	// or providers utilize OIDC configurations.
 | ||||||
|  | 	OIDCConfig OIDCOptions `json:"oidcConfig,omitempty"` | ||||||
|  | 	// LoginGovConfig holds all configurations for LoginGov provider.
 | ||||||
|  | 	LoginGovConfig LoginGovOptions `json:"loginGovConfig,omitempty"` | ||||||
|  | 
 | ||||||
|  | 	// ID should be a unique identifier for the provider.
 | ||||||
|  | 	// This value is required for all providers.
 | ||||||
|  | 	ID string `json:"id,omitempty"` | ||||||
|  | 	// Type is the OAuth provider
 | ||||||
|  | 	// must be set from the supported providers group,
 | ||||||
|  | 	// otherwise 'Google' is set as default
 | ||||||
|  | 	Type string `json:"provider,omitempty"` | ||||||
|  | 	// Name is the providers display name
 | ||||||
|  | 	// if set, it will be shown to the users in the login page.
 | ||||||
|  | 	Name string `json:"name,omitempty"` | ||||||
|  | 	// CAFiles is a list of paths to CA certificates that should be used when connecting to the provider.
 | ||||||
|  | 	// If not specified, the default Go trust sources are used instead
 | ||||||
|  | 	CAFiles []string `json:"caFiles,omitempty"` | ||||||
|  | 
 | ||||||
|  | 	// LoginURL is the authentication endpoint
 | ||||||
|  | 	LoginURL string `json:"loginURL,omitempty"` | ||||||
|  | 	// RedeemURL is the token redemption endpoint
 | ||||||
|  | 	RedeemURL string `json:"redeemURL,omitempty"` | ||||||
|  | 	// ProfileURL is the profile access endpoint
 | ||||||
|  | 	ProfileURL string `json:"profileURL,omitempty"` | ||||||
|  | 	// ProtectedResource is the resource that is protected (Azure AD only)
 | ||||||
|  | 	ProtectedResource string `json:"resource,omitempty"` | ||||||
|  | 	// ValidateURL is the access token validation endpoint
 | ||||||
|  | 	ValidateURL string `json:"validateURL,omitempty"` | ||||||
|  | 	// Scope is the OAuth scope specification
 | ||||||
|  | 	Scope string `json:"scope,omitempty"` | ||||||
|  | 	// Prompt is OIDC prompt
 | ||||||
|  | 	Prompt string `json:"prompt,omitempty"` | ||||||
|  | 	// ApprovalPrompt is the OAuth approval_prompt
 | ||||||
|  | 	// default is set to 'force'
 | ||||||
|  | 	ApprovalPrompt string `json:"approvalPrompt,omitempty"` | ||||||
|  | 	// AllowedGroups is a list of restrict logins to members of this group
 | ||||||
|  | 	AllowedGroups []string `json:"allowedGroups,omitempty"` | ||||||
|  | 
 | ||||||
|  | 	// AcrValues is a string of acr values
 | ||||||
|  | 	AcrValues string `json:"acrValues,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type KeycloakOptions struct { | ||||||
|  | 	// Group enables to restrict login to members of indicated group
 | ||||||
|  | 	Groups []string `json:"groups,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type AzureOptions struct { | ||||||
|  | 	// Tenant directs to a tenant-specific or common (tenant-independent) endpoint
 | ||||||
|  | 	// Default value is 'commmon'
 | ||||||
|  | 	Tenant string `json:"tenant,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type BitbucketOptions struct { | ||||||
|  | 	// Team sets restrict logins to members of this team
 | ||||||
|  | 	Team string `json:"team,omitempty"` | ||||||
|  | 	// Repository sets restrict logins to user with access to this repository
 | ||||||
|  | 	Repository string `json:"repository,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type GitHubOptions struct { | ||||||
|  | 	// Org sets restrict logins to members of this organisation
 | ||||||
|  | 	Org string `json:"org,omitempty"` | ||||||
|  | 	// Team sets restrict logins to members of this team
 | ||||||
|  | 	Team string `json:"team,omitempty"` | ||||||
|  | 	// Repo sets restrict logins to collaborators of this repository
 | ||||||
|  | 	Repo string `json:"repo,omitempty"` | ||||||
|  | 	// Token is the token to use when verifying repository collaborators
 | ||||||
|  | 	// it must have push access to the repository
 | ||||||
|  | 	Token string `json:"token,omitempty"` | ||||||
|  | 	// Users allows users with these usernames to login
 | ||||||
|  | 	// even if they do not belong to the specified org and team or collaborators
 | ||||||
|  | 	Users []string `json:"users,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type GitLabOptions struct { | ||||||
|  | 	// Group sets restrict logins to members of this group
 | ||||||
|  | 	Group []string `json:"group,omitempty"` | ||||||
|  | 	// Projects restricts logins to members of any of these projects
 | ||||||
|  | 	Projects []string `json:"projects,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type GoogleOptions struct { | ||||||
|  | 	// Groups sets restrict logins to members of this google group
 | ||||||
|  | 	Groups []string `json:"group,omitempty"` | ||||||
|  | 	// AdminEmail is the google admin to impersonate for api calls
 | ||||||
|  | 	AdminEmail string `json:"adminEmail,omitempty"` | ||||||
|  | 	// ServiceAccountJSON is the path to the service account json credentials
 | ||||||
|  | 	ServiceAccountJSON string `json:"serviceAccountJson,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type OIDCOptions struct { | ||||||
|  | 	// IssuerURL is the OpenID Connect issuer URL
 | ||||||
|  | 	// eg: https://accounts.google.com
 | ||||||
|  | 	IssuerURL string `json:"issuerURL,omitempty"` | ||||||
|  | 	// InsecureAllowUnverifiedEmail prevents failures if an email address in an id_token is not verified
 | ||||||
|  | 	// default set to 'false'
 | ||||||
|  | 	InsecureAllowUnverifiedEmail bool `json:"insecureAllowUnverifiedEmail,omitempty"` | ||||||
|  | 	// InsecureSkipIssuerVerification skips verification of ID token issuers. When false, ID Token Issuers must match the OIDC discovery URL
 | ||||||
|  | 	// default set to 'false'
 | ||||||
|  | 	InsecureSkipIssuerVerification bool `json:"insecureSkipIssuerVerification,omitempty"` | ||||||
|  | 	// SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints
 | ||||||
|  | 	// default set to 'false'
 | ||||||
|  | 	SkipDiscovery bool `json:"skipDiscovery,omitempty"` | ||||||
|  | 	// JwksURL is the OpenID Connect JWKS URL
 | ||||||
|  | 	// eg: https://www.googleapis.com/oauth2/v3/certs
 | ||||||
|  | 	JwksURL string `json:"jwksURL,omitempty"` | ||||||
|  | 	// EmailClaim indicates which claim contains the user email,
 | ||||||
|  | 	// default set to 'email'
 | ||||||
|  | 	EmailClaim string `json:"emailClaim,omitempty"` | ||||||
|  | 	// GroupsClaim indicates which claim contains the user groups
 | ||||||
|  | 	// default set to 'groups'
 | ||||||
|  | 	GroupsClaim string `json:"groupsClaim,omitempty"` | ||||||
|  | 	// UserIDClaim indicates which claim contains the user ID
 | ||||||
|  | 	// default set to 'email'
 | ||||||
|  | 	UserIDClaim string `json:"userIDClaim,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type LoginGovOptions struct { | ||||||
|  | 	// JWTKey is a private key in PEM format used to sign JWT,
 | ||||||
|  | 	JWTKey string `json:"jwtKey,omitempty"` | ||||||
|  | 	// JWTKeyFile is a path to the private key file in PEM format used to sign the JWT
 | ||||||
|  | 	JWTKeyFile string `json:"jwtKeyFile,omitempty"` | ||||||
|  | 	// PubJWKURL is the JWK pubkey access endpoint
 | ||||||
|  | 	PubJWKURL string `json:"pubjwkURL,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func providerDefaults() Providers { | ||||||
|  | 	providers := Providers{ | ||||||
|  | 		{ | ||||||
|  | 			Type:           "google", | ||||||
|  | 			Prompt:         "", // Change to "login" when ApprovalPrompt officially deprecated
 | ||||||
|  | 			ApprovalPrompt: "force", | ||||||
|  | 			AzureConfig: AzureOptions{ | ||||||
|  | 				Tenant: "common", | ||||||
|  | 			}, | ||||||
|  | 			OIDCConfig: OIDCOptions{ | ||||||
|  | 				InsecureAllowUnverifiedEmail: false, | ||||||
|  | 				SkipDiscovery:                false, | ||||||
|  | 				UserIDClaim:                  providers.OIDCEmailClaim, // Deprecated: Use OIDCEmailClaim
 | ||||||
|  | 				EmailClaim:                   providers.OIDCEmailClaim, | ||||||
|  | 				GroupsClaim:                  providers.OIDCGroupsClaim, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	return providers | ||||||
|  | } | ||||||
|  | @ -29,6 +29,7 @@ func Validate(o *options.Options) error { | ||||||
| 	msgs = append(msgs, validateRedisSessionStore(o)...) | 	msgs = append(msgs, validateRedisSessionStore(o)...) | ||||||
| 	msgs = append(msgs, prefixValues("injectRequestHeaders: ", validateHeaders(o.InjectRequestHeaders)...)...) | 	msgs = append(msgs, prefixValues("injectRequestHeaders: ", validateHeaders(o.InjectRequestHeaders)...)...) | ||||||
| 	msgs = append(msgs, prefixValues("injectResponseHeaders: ", validateHeaders(o.InjectResponseHeaders)...)...) | 	msgs = append(msgs, prefixValues("injectResponseHeaders: ", validateHeaders(o.InjectResponseHeaders)...)...) | ||||||
|  | 	msgs = append(msgs, validateProviders(o)...) | ||||||
| 	msgs = configureLogger(o.Logging, msgs) | 	msgs = configureLogger(o.Logging, msgs) | ||||||
| 	msgs = parseSignatureKey(o, msgs) | 	msgs = parseSignatureKey(o, msgs) | ||||||
| 
 | 
 | ||||||
|  | @ -39,8 +40,8 @@ func Validate(o *options.Options) error { | ||||||
| 			TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, | 			TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, | ||||||
| 		} | 		} | ||||||
| 		http.DefaultClient = &http.Client{Transport: insecureTransport} | 		http.DefaultClient = &http.Client{Transport: insecureTransport} | ||||||
| 	} else if len(o.ProviderCAFiles) > 0 { | 	} else if len(o.Providers[0].CAFiles) > 0 { | ||||||
| 		pool, err := util.GetCertPool(o.ProviderCAFiles) | 		pool, err := util.GetCertPool(o.Providers[0].CAFiles) | ||||||
| 		if err == nil { | 		if err == nil { | ||||||
| 			transport := http.DefaultTransport.(*http.Transport).Clone() | 			transport := http.DefaultTransport.(*http.Transport).Clone() | ||||||
| 			transport.TLSClientConfig = &tls.Config{ | 			transport.TLSClientConfig = &tls.Config{ | ||||||
|  | @ -54,38 +55,23 @@ func Validate(o *options.Options) error { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if o.ClientID == "" { |  | ||||||
| 		msgs = append(msgs, "missing setting: client-id") |  | ||||||
| 	} |  | ||||||
| 	// login.gov uses a signed JWT to authenticate, not a client-secret
 |  | ||||||
| 	if o.ProviderType != "login.gov" { |  | ||||||
| 		if o.ClientSecret == "" && o.ClientSecretFile == "" { |  | ||||||
| 			msgs = append(msgs, "missing setting: client-secret or client-secret-file") |  | ||||||
| 		} |  | ||||||
| 		if o.ClientSecret == "" && o.ClientSecretFile != "" { |  | ||||||
| 			_, err := ioutil.ReadFile(o.ClientSecretFile) |  | ||||||
| 			if err != nil { |  | ||||||
| 				msgs = append(msgs, "could not read client secret file: "+o.ClientSecretFile) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" { | 	if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" { | ||||||
| 		msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required."+ | 		msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required."+ | ||||||
| 			"\n      use email-domain=* to authorize all email addresses") | 			"\n      use email-domain=* to authorize all email addresses") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if o.OIDCIssuerURL != "" { | 	if o.Providers[0].OIDCConfig.IssuerURL != "" { | ||||||
| 
 | 
 | ||||||
| 		ctx := context.Background() | 		ctx := context.Background() | ||||||
| 
 | 
 | ||||||
| 		if o.InsecureOIDCSkipIssuerVerification && !o.SkipOIDCDiscovery { | 		if o.Providers[0].OIDCConfig.InsecureSkipIssuerVerification && !o.Providers[0].OIDCConfig.SkipDiscovery { | ||||||
| 			// go-oidc doesn't let us pass bypass the issuer check this in the oidc.NewProvider call
 | 			// go-oidc doesn't let us pass bypass the issuer check this in the oidc.NewProvider call
 | ||||||
| 			// (which uses discovery to get the URLs), so we'll do a quick check ourselves and if
 | 			// (which uses discovery to get the URLs), so we'll do a quick check ourselves and if
 | ||||||
| 			// we get the URLs, we'll just use the non-discovery path.
 | 			// we get the URLs, we'll just use the non-discovery path.
 | ||||||
| 
 | 
 | ||||||
| 			logger.Printf("Performing OIDC Discovery...") | 			logger.Printf("Performing OIDC Discovery...") | ||||||
| 
 | 
 | ||||||
| 			requestURL := strings.TrimSuffix(o.OIDCIssuerURL, "/") + "/.well-known/openid-configuration" | 			requestURL := strings.TrimSuffix(o.Providers[0].OIDCConfig.IssuerURL, "/") + "/.well-known/openid-configuration" | ||||||
| 			body, err := requests.New(requestURL). | 			body, err := requests.New(requestURL). | ||||||
| 				WithContext(ctx). | 				WithContext(ctx). | ||||||
| 				Do(). | 				Do(). | ||||||
|  | @ -96,23 +82,23 @@ func Validate(o *options.Options) error { | ||||||
| 				// Prefer manually configured URLs. It's a bit unclear
 | 				// Prefer manually configured URLs. It's a bit unclear
 | ||||||
| 				// why you'd be doing discovery and also providing the URLs
 | 				// why you'd be doing discovery and also providing the URLs
 | ||||||
| 				// explicitly though...
 | 				// explicitly though...
 | ||||||
| 				if o.LoginURL == "" { | 				if o.Providers[0].LoginURL == "" { | ||||||
| 					o.LoginURL = body.Get("authorization_endpoint").MustString() | 					o.Providers[0].LoginURL = body.Get("authorization_endpoint").MustString() | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				if o.RedeemURL == "" { | 				if o.Providers[0].RedeemURL == "" { | ||||||
| 					o.RedeemURL = body.Get("token_endpoint").MustString() | 					o.Providers[0].RedeemURL = body.Get("token_endpoint").MustString() | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				if o.OIDCJwksURL == "" { | 				if o.Providers[0].OIDCConfig.JwksURL == "" { | ||||||
| 					o.OIDCJwksURL = body.Get("jwks_uri").MustString() | 					o.Providers[0].OIDCConfig.JwksURL = body.Get("jwks_uri").MustString() | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				if o.ProfileURL == "" { | 				if o.Providers[0].ProfileURL == "" { | ||||||
| 					o.ProfileURL = body.Get("userinfo_endpoint").MustString() | 					o.Providers[0].ProfileURL = body.Get("userinfo_endpoint").MustString() | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				o.SkipOIDCDiscovery = true | 				o.Providers[0].OIDCConfig.SkipDiscovery = true | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -120,42 +106,45 @@ func Validate(o *options.Options) error { | ||||||
| 		// instead of metadata discovery if we enable -skip-oidc-discovery.
 | 		// instead of metadata discovery if we enable -skip-oidc-discovery.
 | ||||||
| 		// In this case we need to make sure the required endpoints for
 | 		// In this case we need to make sure the required endpoints for
 | ||||||
| 		// the provider are configured.
 | 		// the provider are configured.
 | ||||||
| 		if o.SkipOIDCDiscovery { | 		if o.Providers[0].OIDCConfig.SkipDiscovery { | ||||||
| 			if o.LoginURL == "" { | 			if o.Providers[0].LoginURL == "" { | ||||||
| 				msgs = append(msgs, "missing setting: login-url") | 				msgs = append(msgs, "missing setting: login-url") | ||||||
| 			} | 			} | ||||||
| 			if o.RedeemURL == "" { | 			if o.Providers[0].RedeemURL == "" { | ||||||
| 				msgs = append(msgs, "missing setting: redeem-url") | 				msgs = append(msgs, "missing setting: redeem-url") | ||||||
| 			} | 			} | ||||||
| 			if o.OIDCJwksURL == "" { | 			if o.Providers[0].OIDCConfig.JwksURL == "" { | ||||||
| 				msgs = append(msgs, "missing setting: oidc-jwks-url") | 				msgs = append(msgs, "missing setting: oidc-jwks-url") | ||||||
| 			} | 			} | ||||||
| 			keySet := oidc.NewRemoteKeySet(ctx, o.OIDCJwksURL) | 			keySet := oidc.NewRemoteKeySet(ctx, o.Providers[0].OIDCConfig.JwksURL) | ||||||
| 			o.SetOIDCVerifier(oidc.NewVerifier(o.OIDCIssuerURL, keySet, &oidc.Config{ | 			o.SetOIDCVerifier(oidc.NewVerifier(o.Providers[0].OIDCConfig.IssuerURL, keySet, &oidc.Config{ | ||||||
| 				ClientID:        o.ClientID, | 				ClientID:        o.Providers[0].ClientID, | ||||||
| 				SkipIssuerCheck: o.InsecureOIDCSkipIssuerVerification, | 				SkipIssuerCheck: o.Providers[0].OIDCConfig.InsecureSkipIssuerVerification, | ||||||
| 			})) | 			})) | ||||||
| 		} else { | 		} else { | ||||||
| 			// Configure discoverable provider data.
 | 			// Configure discoverable provider data.
 | ||||||
| 			provider, err := oidc.NewProvider(ctx, o.OIDCIssuerURL) | 			provider, err := oidc.NewProvider(ctx, o.Providers[0].OIDCConfig.IssuerURL) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| 			o.SetOIDCVerifier(provider.Verifier(&oidc.Config{ | 			o.SetOIDCVerifier(provider.Verifier(&oidc.Config{ | ||||||
| 				ClientID:        o.ClientID, | 				ClientID:        o.Providers[0].ClientID, | ||||||
| 				SkipIssuerCheck: o.InsecureOIDCSkipIssuerVerification, | 				SkipIssuerCheck: o.Providers[0].OIDCConfig.InsecureSkipIssuerVerification, | ||||||
| 			})) | 			})) | ||||||
| 
 | 
 | ||||||
| 			o.LoginURL = provider.Endpoint().AuthURL | 			o.Providers[0].LoginURL = provider.Endpoint().AuthURL | ||||||
| 			o.RedeemURL = provider.Endpoint().TokenURL | 			o.Providers[0].RedeemURL = provider.Endpoint().TokenURL | ||||||
| 		} | 		} | ||||||
| 		if o.Scope == "" { | 		if o.Providers[0].Scope == "" { | ||||||
| 			o.Scope = "openid email profile" | 			o.Providers[0].Scope = "openid email profile" | ||||||
| 
 | 
 | ||||||
| 			if len(o.AllowedGroups) > 0 { | 			if len(o.Providers[0].AllowedGroups) > 0 { | ||||||
| 				o.Scope += " groups" | 				o.Providers[0].Scope += " groups" | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		if o.Providers[0].OIDCConfig.UserIDClaim == "" { | ||||||
|  | 			o.Providers[0].OIDCConfig.UserIDClaim = "email" | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if o.SkipJwtBearerTokens { | 	if o.SkipJwtBearerTokens { | ||||||
|  | @ -183,18 +172,6 @@ func Validate(o *options.Options) error { | ||||||
| 	msgs = append(msgs, validateUpstreams(o.UpstreamServers)...) | 	msgs = append(msgs, validateUpstreams(o.UpstreamServers)...) | ||||||
| 	msgs = parseProviderInfo(o, msgs) | 	msgs = parseProviderInfo(o, msgs) | ||||||
| 
 | 
 | ||||||
| 	if len(o.GoogleGroups) > 0 || o.GoogleAdminEmail != "" || o.GoogleServiceAccountJSON != "" { |  | ||||||
| 		if len(o.GoogleGroups) < 1 { |  | ||||||
| 			msgs = append(msgs, "missing setting: google-group") |  | ||||||
| 		} |  | ||||||
| 		if o.GoogleAdminEmail == "" { |  | ||||||
| 			msgs = append(msgs, "missing setting: google-admin-email") |  | ||||||
| 		} |  | ||||||
| 		if o.GoogleServiceAccountJSON == "" { |  | ||||||
| 			msgs = append(msgs, "missing setting: google-service-account-json") |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if o.ReverseProxy { | 	if o.ReverseProxy { | ||||||
| 		parser, err := ip.GetRealClientIPParser(o.RealClientIPHeader) | 		parser, err := ip.GetRealClientIPParser(o.RealClientIPHeader) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -220,79 +197,79 @@ func Validate(o *options.Options) error { | ||||||
| 
 | 
 | ||||||
| func parseProviderInfo(o *options.Options, msgs []string) []string { | func parseProviderInfo(o *options.Options, msgs []string) []string { | ||||||
| 	p := &providers.ProviderData{ | 	p := &providers.ProviderData{ | ||||||
| 		Scope:            o.Scope, | 		Scope:            o.Providers[0].Scope, | ||||||
| 		ClientID:         o.ClientID, | 		ClientID:         o.Providers[0].ClientID, | ||||||
| 		ClientSecret:     o.ClientSecret, | 		ClientSecret:     o.Providers[0].ClientSecret, | ||||||
| 		ClientSecretFile: o.ClientSecretFile, | 		ClientSecretFile: o.Providers[0].ClientSecretFile, | ||||||
| 		Prompt:           o.Prompt, | 		Prompt:           o.Providers[0].Prompt, | ||||||
| 		ApprovalPrompt:   o.ApprovalPrompt, | 		ApprovalPrompt:   o.Providers[0].ApprovalPrompt, | ||||||
| 		AcrValues:        o.AcrValues, | 		AcrValues:        o.Providers[0].AcrValues, | ||||||
| 	} | 	} | ||||||
| 	p.LoginURL, msgs = parseURL(o.LoginURL, "login", msgs) | 	p.LoginURL, msgs = parseURL(o.Providers[0].LoginURL, "login", msgs) | ||||||
| 	p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs) | 	p.RedeemURL, msgs = parseURL(o.Providers[0].RedeemURL, "redeem", msgs) | ||||||
| 	p.ProfileURL, msgs = parseURL(o.ProfileURL, "profile", msgs) | 	p.ProfileURL, msgs = parseURL(o.Providers[0].ProfileURL, "profile", msgs) | ||||||
| 	p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs) | 	p.ValidateURL, msgs = parseURL(o.Providers[0].ValidateURL, "validate", msgs) | ||||||
| 	p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs) | 	p.ProtectedResource, msgs = parseURL(o.Providers[0].ProtectedResource, "resource", msgs) | ||||||
| 
 | 
 | ||||||
| 	// Make the OIDC options available to all providers that support it
 | 	// Make the OIDC options available to all providers that support it
 | ||||||
| 	p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail | 	p.AllowUnverifiedEmail = o.Providers[0].OIDCConfig.InsecureAllowUnverifiedEmail | ||||||
| 	p.EmailClaim = o.OIDCEmailClaim | 	p.EmailClaim = o.Providers[0].OIDCConfig.EmailClaim | ||||||
| 	p.GroupsClaim = o.OIDCGroupsClaim | 	p.GroupsClaim = o.Providers[0].OIDCConfig.GroupsClaim | ||||||
| 	p.Verifier = o.GetOIDCVerifier() | 	p.Verifier = o.GetOIDCVerifier() | ||||||
| 
 | 
 | ||||||
| 	// TODO (@NickMeves) - Remove This
 | 	// TODO (@NickMeves) - Remove This
 | ||||||
| 	// Backwards Compatibility for Deprecated UserIDClaim option
 | 	// Backwards Compatibility for Deprecated UserIDClaim option
 | ||||||
| 	if o.OIDCEmailClaim == providers.OIDCEmailClaim && | 	if o.Providers[0].OIDCConfig.EmailClaim == providers.OIDCEmailClaim && | ||||||
| 		o.UserIDClaim != providers.OIDCEmailClaim { | 		o.Providers[0].OIDCConfig.UserIDClaim != providers.OIDCEmailClaim { | ||||||
| 		p.EmailClaim = o.UserIDClaim | 		p.EmailClaim = o.Providers[0].OIDCConfig.UserIDClaim | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	p.SetAllowedGroups(o.AllowedGroups) | 	p.SetAllowedGroups(o.Providers[0].AllowedGroups) | ||||||
| 
 | 
 | ||||||
| 	provider := providers.New(o.ProviderType, p) | 	provider := providers.New(o.Providers[0].Type, p) | ||||||
| 	if provider == nil { | 	if provider == nil { | ||||||
| 		msgs = append(msgs, fmt.Sprintf("invalid setting: provider '%s' is not available", o.ProviderType)) | 		msgs = append(msgs, fmt.Sprintf("invalid setting: provider '%s' is not available", o.Providers[0].Type)) | ||||||
| 		return msgs | 		return msgs | ||||||
| 	} | 	} | ||||||
| 	o.SetProvider(provider) | 	o.SetProvider(provider) | ||||||
| 
 | 
 | ||||||
| 	switch p := o.GetProvider().(type) { | 	switch p := o.GetProvider().(type) { | ||||||
| 	case *providers.AzureProvider: | 	case *providers.AzureProvider: | ||||||
| 		p.Configure(o.AzureTenant) | 		p.Configure(o.Providers[0].AzureConfig.Tenant) | ||||||
| 	case *providers.GitHubProvider: | 	case *providers.GitHubProvider: | ||||||
| 		p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam) | 		p.SetOrgTeam(o.Providers[0].GitHubConfig.Org, o.Providers[0].GitHubConfig.Team) | ||||||
| 		p.SetRepo(o.GitHubRepo, o.GitHubToken) | 		p.SetRepo(o.Providers[0].GitHubConfig.Repo, o.Providers[0].GitHubConfig.Token) | ||||||
| 		p.SetUsers(o.GitHubUsers) | 		p.SetUsers(o.Providers[0].GitHubConfig.Users) | ||||||
| 	case *providers.KeycloakProvider: | 	case *providers.KeycloakProvider: | ||||||
| 		// Backwards compatibility with `--keycloak-group` option
 | 		// Backwards compatibility with `--keycloak-group` option
 | ||||||
| 		if len(o.KeycloakGroups) > 0 { | 		if len(o.Providers[0].KeycloakConfig.Groups) > 0 { | ||||||
| 			p.SetAllowedGroups(o.KeycloakGroups) | 			p.SetAllowedGroups(o.Providers[0].KeycloakConfig.Groups) | ||||||
| 		} | 		} | ||||||
| 	case *providers.GoogleProvider: | 	case *providers.GoogleProvider: | ||||||
| 		if o.GoogleServiceAccountJSON != "" { | 		if o.Providers[0].GoogleConfig.ServiceAccountJSON != "" { | ||||||
| 			file, err := os.Open(o.GoogleServiceAccountJSON) | 			file, err := os.Open(o.Providers[0].GoogleConfig.ServiceAccountJSON) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				msgs = append(msgs, "invalid Google credentials file: "+o.GoogleServiceAccountJSON) | 				msgs = append(msgs, "invalid Google credentials file: "+o.Providers[0].GoogleConfig.ServiceAccountJSON) | ||||||
| 			} else { | 			} else { | ||||||
| 				groups := o.AllowedGroups | 				groups := o.Providers[0].AllowedGroups | ||||||
| 				// Backwards compatibility with `--google-group` option
 | 				// Backwards compatibility with `--google-group` option
 | ||||||
| 				if len(o.GoogleGroups) > 0 { | 				if len(o.Providers[0].GoogleConfig.Groups) > 0 { | ||||||
| 					groups = o.GoogleGroups | 					groups = o.Providers[0].GoogleConfig.Groups | ||||||
| 					p.SetAllowedGroups(groups) | 					p.SetAllowedGroups(groups) | ||||||
| 				} | 				} | ||||||
| 				p.SetGroupRestriction(groups, o.GoogleAdminEmail, file) | 				p.SetGroupRestriction(groups, o.Providers[0].GoogleConfig.AdminEmail, file) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	case *providers.BitbucketProvider: | 	case *providers.BitbucketProvider: | ||||||
| 		p.SetTeam(o.BitbucketTeam) | 		p.SetTeam(o.Providers[0].BitbucketConfig.Team) | ||||||
| 		p.SetRepository(o.BitbucketRepository) | 		p.SetRepository(o.Providers[0].BitbucketConfig.Repository) | ||||||
| 	case *providers.OIDCProvider: | 	case *providers.OIDCProvider: | ||||||
| 		if p.Verifier == nil { | 		if p.Verifier == nil { | ||||||
| 			msgs = append(msgs, "oidc provider requires an oidc issuer URL") | 			msgs = append(msgs, "oidc provider requires an oidc issuer URL") | ||||||
| 		} | 		} | ||||||
| 	case *providers.GitLabProvider: | 	case *providers.GitLabProvider: | ||||||
| 		p.Groups = o.GitLabGroup | 		p.Groups = o.Providers[0].GitLabConfig.Group | ||||||
| 		err := p.AddProjects(o.GitlabProjects) | 		err := p.AddProjects(o.Providers[0].GitLabConfig.Projects) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			msgs = append(msgs, "failed to setup gitlab project access level") | 			msgs = append(msgs, "failed to setup gitlab project access level") | ||||||
| 		} | 		} | ||||||
|  | @ -308,7 +285,7 @@ func parseProviderInfo(o *options.Options, msgs []string) []string { | ||||||
| 				msgs = append(msgs, "failed to initialize oidc provider for gitlab.com") | 				msgs = append(msgs, "failed to initialize oidc provider for gitlab.com") | ||||||
| 			} else { | 			} else { | ||||||
| 				p.Verifier = provider.Verifier(&oidc.Config{ | 				p.Verifier = provider.Verifier(&oidc.Config{ | ||||||
| 					ClientID: o.ClientID, | 					ClientID: o.Providers[0].ClientID, | ||||||
| 				}) | 				}) | ||||||
| 
 | 
 | ||||||
| 				p.LoginURL, msgs = parseURL(provider.Endpoint().AuthURL, "login", msgs) | 				p.LoginURL, msgs = parseURL(provider.Endpoint().AuthURL, "login", msgs) | ||||||
|  | @ -316,31 +293,31 @@ func parseProviderInfo(o *options.Options, msgs []string) []string { | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	case *providers.LoginGovProvider: | 	case *providers.LoginGovProvider: | ||||||
| 		p.PubJWKURL, msgs = parseURL(o.PubJWKURL, "pubjwk", msgs) | 		p.PubJWKURL, msgs = parseURL(o.Providers[0].LoginGovConfig.PubJWKURL, "pubjwk", msgs) | ||||||
| 
 | 
 | ||||||
| 		// JWT key can be supplied via env variable or file in the filesystem, but not both.
 | 		// JWT key can be supplied via env variable or file in the filesystem, but not both.
 | ||||||
| 		switch { | 		switch { | ||||||
| 		case o.JWTKey != "" && o.JWTKeyFile != "": | 		case o.Providers[0].LoginGovConfig.JWTKey != "" && o.Providers[0].LoginGovConfig.JWTKeyFile != "": | ||||||
| 			msgs = append(msgs, "cannot set both jwt-key and jwt-key-file options") | 			msgs = append(msgs, "cannot set both jwt-key and jwt-key-file options") | ||||||
| 		case o.JWTKey == "" && o.JWTKeyFile == "": | 		case o.Providers[0].LoginGovConfig.JWTKey == "" && o.Providers[0].LoginGovConfig.JWTKeyFile == "": | ||||||
| 			msgs = append(msgs, "login.gov provider requires a private key for signing JWTs") | 			msgs = append(msgs, "login.gov provider requires a private key for signing JWTs") | ||||||
| 		case o.JWTKey != "": | 		case o.Providers[0].LoginGovConfig.JWTKey != "": | ||||||
| 			// The JWT Key is in the commandline argument
 | 			// The JWT Key is in the commandline argument
 | ||||||
| 			signKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(o.JWTKey)) | 			signKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(o.Providers[0].LoginGovConfig.JWTKey)) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				msgs = append(msgs, "could not parse RSA Private Key PEM") | 				msgs = append(msgs, "could not parse RSA Private Key PEM") | ||||||
| 			} else { | 			} else { | ||||||
| 				p.JWTKey = signKey | 				p.JWTKey = signKey | ||||||
| 			} | 			} | ||||||
| 		case o.JWTKeyFile != "": | 		case o.Providers[0].LoginGovConfig.JWTKeyFile != "": | ||||||
| 			// The JWT key is in the filesystem
 | 			// The JWT key is in the filesystem
 | ||||||
| 			keyData, err := ioutil.ReadFile(o.JWTKeyFile) | 			keyData, err := ioutil.ReadFile(o.Providers[0].LoginGovConfig.JWTKeyFile) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				msgs = append(msgs, "could not read key file: "+o.JWTKeyFile) | 				msgs = append(msgs, "could not read key file: "+o.Providers[0].LoginGovConfig.JWTKeyFile) | ||||||
| 			} | 			} | ||||||
| 			signKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyData) | 			signKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyData) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				msgs = append(msgs, "could not parse private key from PEM file:"+o.JWTKeyFile) | 				msgs = append(msgs, "could not parse private key from PEM file:"+o.Providers[0].LoginGovConfig.JWTKeyFile) | ||||||
| 			} else { | 			} else { | ||||||
| 				p.JWTKey = signKey | 				p.JWTKey = signKey | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ const ( | ||||||
| 	cookieSecret = "secretthirtytwobytes+abcdefghijk" | 	cookieSecret = "secretthirtytwobytes+abcdefghijk" | ||||||
| 	clientID     = "bazquux" | 	clientID     = "bazquux" | ||||||
| 	clientSecret = "xyzzyplugh" | 	clientSecret = "xyzzyplugh" | ||||||
|  | 	providerID   = "providerID" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func testOptions() *options.Options { | func testOptions() *options.Options { | ||||||
|  | @ -27,8 +28,9 @@ func testOptions() *options.Options { | ||||||
| 		URI:  "http://127.0.0.1:8080/", | 		URI:  "http://127.0.0.1:8080/", | ||||||
| 	}) | 	}) | ||||||
| 	o.Cookie.Secret = cookieSecret | 	o.Cookie.Secret = cookieSecret | ||||||
| 	o.ClientID = clientID | 	o.Providers[0].ID = providerID | ||||||
| 	o.ClientSecret = clientSecret | 	o.Providers[0].ClientID = clientID | ||||||
|  | 	o.Providers[0].ClientSecret = clientSecret | ||||||
| 	o.EmailDomains = []string{"*"} | 	o.EmailDomains = []string{"*"} | ||||||
| 	return o | 	return o | ||||||
| } | } | ||||||
|  | @ -48,7 +50,8 @@ func TestNewOptions(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	expected := errorMsg([]string{ | 	expected := errorMsg([]string{ | ||||||
| 		"missing setting: cookie-secret", | 		"missing setting: cookie-secret", | ||||||
| 		"missing setting: client-id", | 		"provider has empty id: ids are required for all providers", | ||||||
|  | 		"provider missing setting: client-id", | ||||||
| 		"missing setting: client-secret or client-secret-file"}) | 		"missing setting: client-secret or client-secret-file"}) | ||||||
| 	assert.Equal(t, expected, err.Error()) | 	assert.Equal(t, expected, err.Error()) | ||||||
| } | } | ||||||
|  | @ -56,8 +59,9 @@ func TestNewOptions(t *testing.T) { | ||||||
| func TestClientSecretFileOptionFails(t *testing.T) { | func TestClientSecretFileOptionFails(t *testing.T) { | ||||||
| 	o := options.NewOptions() | 	o := options.NewOptions() | ||||||
| 	o.Cookie.Secret = cookieSecret | 	o.Cookie.Secret = cookieSecret | ||||||
| 	o.ClientID = clientID | 	o.Providers[0].ID = providerID | ||||||
| 	o.ClientSecretFile = clientSecret | 	o.Providers[0].ClientID = clientID | ||||||
|  | 	o.Providers[0].ClientSecretFile = clientSecret | ||||||
| 	o.EmailDomains = []string{"*"} | 	o.EmailDomains = []string{"*"} | ||||||
| 	err := Validate(o) | 	err := Validate(o) | ||||||
| 	assert.NotEqual(t, nil, err) | 	assert.NotEqual(t, nil, err) | ||||||
|  | @ -93,8 +97,9 @@ func TestClientSecretFileOption(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	o := options.NewOptions() | 	o := options.NewOptions() | ||||||
| 	o.Cookie.Secret = cookieSecret | 	o.Cookie.Secret = cookieSecret | ||||||
| 	o.ClientID = clientID | 	o.Providers[0].ID = providerID | ||||||
| 	o.ClientSecretFile = clientSecretFileName | 	o.Providers[0].ClientID = clientID | ||||||
|  | 	o.Providers[0].ClientSecretFile = clientSecretFileName | ||||||
| 	o.EmailDomains = []string{"*"} | 	o.EmailDomains = []string{"*"} | ||||||
| 	err = Validate(o) | 	err = Validate(o) | ||||||
| 	assert.Equal(t, nil, err) | 	assert.Equal(t, nil, err) | ||||||
|  | @ -110,7 +115,7 @@ func TestClientSecretFileOption(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| func TestGoogleGroupOptions(t *testing.T) { | func TestGoogleGroupOptions(t *testing.T) { | ||||||
| 	o := testOptions() | 	o := testOptions() | ||||||
| 	o.GoogleGroups = []string{"googlegroup"} | 	o.Providers[0].GoogleConfig.Groups = []string{"googlegroup"} | ||||||
| 	err := Validate(o) | 	err := Validate(o) | ||||||
| 	assert.NotEqual(t, nil, err) | 	assert.NotEqual(t, nil, err) | ||||||
| 
 | 
 | ||||||
|  | @ -122,9 +127,9 @@ func TestGoogleGroupOptions(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| func TestGoogleGroupInvalidFile(t *testing.T) { | func TestGoogleGroupInvalidFile(t *testing.T) { | ||||||
| 	o := testOptions() | 	o := testOptions() | ||||||
| 	o.GoogleGroups = []string{"test_group"} | 	o.Providers[0].GoogleConfig.Groups = []string{"test_group"} | ||||||
| 	o.GoogleAdminEmail = "admin@example.com" | 	o.Providers[0].GoogleConfig.AdminEmail = "admin@example.com" | ||||||
| 	o.GoogleServiceAccountJSON = "file_doesnt_exist.json" | 	o.Providers[0].GoogleConfig.ServiceAccountJSON = "file_doesnt_exist.json" | ||||||
| 	err := Validate(o) | 	err := Validate(o) | ||||||
| 	assert.NotEqual(t, nil, err) | 	assert.NotEqual(t, nil, err) | ||||||
| 
 | 
 | ||||||
|  | @ -225,17 +230,17 @@ func TestValidateSignatureKeyUnsupportedAlgorithm(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| func TestSkipOIDCDiscovery(t *testing.T) { | func TestSkipOIDCDiscovery(t *testing.T) { | ||||||
| 	o := testOptions() | 	o := testOptions() | ||||||
| 	o.ProviderType = "oidc" | 	o.Providers[0].Type = "oidc" | ||||||
| 	o.OIDCIssuerURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/v2.0/" | 	o.Providers[0].OIDCConfig.IssuerURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/v2.0/" | ||||||
| 	o.SkipOIDCDiscovery = true | 	o.Providers[0].OIDCConfig.SkipDiscovery = true | ||||||
| 
 | 
 | ||||||
| 	err := Validate(o) | 	err := Validate(o) | ||||||
| 	assert.Equal(t, "invalid configuration:\n"+ | 	assert.Equal(t, "invalid configuration:\n"+ | ||||||
| 		"  missing setting: login-url\n  missing setting: redeem-url\n  missing setting: oidc-jwks-url", err.Error()) | 		"  missing setting: login-url\n  missing setting: redeem-url\n  missing setting: oidc-jwks-url", err.Error()) | ||||||
| 
 | 
 | ||||||
| 	o.LoginURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1_sign_in" | 	o.Providers[0].LoginURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1_sign_in" | ||||||
| 	o.RedeemURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_sign_in" | 	o.Providers[0].RedeemURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_sign_in" | ||||||
| 	o.OIDCJwksURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/discovery/v2.0/keys" | 	o.Providers[0].OIDCConfig.JwksURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/discovery/v2.0/keys" | ||||||
| 
 | 
 | ||||||
| 	assert.Equal(t, nil, Validate(o)) | 	assert.Equal(t, nil, Validate(o)) | ||||||
| } | } | ||||||
|  | @ -292,7 +297,7 @@ func TestProviderCAFilesError(t *testing.T) { | ||||||
| 	assert.NoError(t, os.Remove(file.Name())) | 	assert.NoError(t, os.Remove(file.Name())) | ||||||
| 
 | 
 | ||||||
| 	o := testOptions() | 	o := testOptions() | ||||||
| 	o.ProviderCAFiles = append(o.ProviderCAFiles, file.Name()) | 	o.Providers[0].CAFiles = append(o.Providers[0].CAFiles, file.Name()) | ||||||
| 	err = Validate(o) | 	err = Validate(o) | ||||||
| 	assert.Error(t, err) | 	assert.Error(t, err) | ||||||
| 	assert.Contains(t, err.Error(), "unable to load provider CA file(s)") | 	assert.Contains(t, err.Error(), "unable to load provider CA file(s)") | ||||||
|  |  | ||||||
|  | @ -0,0 +1,83 @@ | ||||||
|  | package validation | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 
 | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // validateProviders is the initial validation migration for multiple providrers
 | ||||||
|  | // It currently includes only logic that can verify the providers one by one and does not break the valdation pipe
 | ||||||
|  | func validateProviders(o *options.Options) []string { | ||||||
|  | 	msgs := []string{} | ||||||
|  | 
 | ||||||
|  | 	// validate general multiple provider configuration
 | ||||||
|  | 	if len(o.Providers) == 0 { | ||||||
|  | 		msgs = append(msgs, "at least one provider has to be defined") | ||||||
|  | 	} | ||||||
|  | 	if o.SkipProviderButton && len(o.Providers) > 1 { | ||||||
|  | 		msgs = append(msgs, "SkipProviderButton and multiple providers are mutually exclusive") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	providerIDs := make(map[string]struct{}) | ||||||
|  | 
 | ||||||
|  | 	for _, provider := range o.Providers { | ||||||
|  | 		msgs = append(msgs, validateProvider(provider, providerIDs)...) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return msgs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func validateProvider(provider options.Provider, providerIDs map[string]struct{}) []string { | ||||||
|  | 	msgs := []string{} | ||||||
|  | 
 | ||||||
|  | 	if provider.ID == "" { | ||||||
|  | 		msgs = append(msgs, "provider has empty id: ids are required for all providers") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Ensure provider IDs are unique
 | ||||||
|  | 	if _, ok := providerIDs[provider.ID]; ok { | ||||||
|  | 		msgs = append(msgs, fmt.Sprintf("multiple providers found with id %s: provider ids must be unique", provider.ID)) | ||||||
|  | 	} | ||||||
|  | 	providerIDs[provider.ID] = struct{}{} | ||||||
|  | 
 | ||||||
|  | 	if provider.ClientID == "" { | ||||||
|  | 		msgs = append(msgs, "provider missing setting: client-id") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// login.gov uses a signed JWT to authenticate, not a client-secret
 | ||||||
|  | 	if provider.Type != "login.gov" { | ||||||
|  | 		if provider.ClientSecret == "" && provider.ClientSecretFile == "" { | ||||||
|  | 			msgs = append(msgs, "missing setting: client-secret or client-secret-file") | ||||||
|  | 		} | ||||||
|  | 		if provider.ClientSecret == "" && provider.ClientSecretFile != "" { | ||||||
|  | 			_, err := ioutil.ReadFile(provider.ClientSecretFile) | ||||||
|  | 			if err != nil { | ||||||
|  | 				msgs = append(msgs, "could not read client secret file: "+provider.ClientSecretFile) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	msgs = append(msgs, validateGoogleConfig(provider)...) | ||||||
|  | 
 | ||||||
|  | 	return msgs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func validateGoogleConfig(provider options.Provider) []string { | ||||||
|  | 	msgs := []string{} | ||||||
|  | 	if len(provider.GoogleConfig.Groups) > 0 || | ||||||
|  | 		provider.GoogleConfig.AdminEmail != "" || | ||||||
|  | 		provider.GoogleConfig.ServiceAccountJSON != "" { | ||||||
|  | 		if len(provider.GoogleConfig.Groups) < 1 { | ||||||
|  | 			msgs = append(msgs, "missing setting: google-group") | ||||||
|  | 		} | ||||||
|  | 		if provider.GoogleConfig.AdminEmail == "" { | ||||||
|  | 			msgs = append(msgs, "missing setting: google-admin-email") | ||||||
|  | 		} | ||||||
|  | 		if provider.GoogleConfig.ServiceAccountJSON == "" { | ||||||
|  | 			msgs = append(msgs, "missing setting: google-service-account-json") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return msgs | ||||||
|  | } | ||||||
|  | @ -0,0 +1,84 @@ | ||||||
|  | package validation | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" | ||||||
|  | 	. "github.com/onsi/ginkgo" | ||||||
|  | 	. "github.com/onsi/ginkgo/extensions/table" | ||||||
|  | 	. "github.com/onsi/gomega" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var _ = Describe("Providers", func() { | ||||||
|  | 	type validateProvidersTableInput struct { | ||||||
|  | 		options    *options.Options | ||||||
|  | 		errStrings []string | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	validProvider := options.Provider{ | ||||||
|  | 		ID:           "ProviderID", | ||||||
|  | 		ClientID:     "ClientID", | ||||||
|  | 		ClientSecret: "ClientSecret", | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	validLoginGovProvider := options.Provider{ | ||||||
|  | 		Type:         "login.gov", | ||||||
|  | 		ID:           "ProviderIDLoginGov", | ||||||
|  | 		ClientID:     "ClientID", | ||||||
|  | 		ClientSecret: "ClientSecret", | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	missingIDProvider := options.Provider{ | ||||||
|  | 		ClientID:     "ClientID", | ||||||
|  | 		ClientSecret: "ClientSecret", | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	missingProvider := "at least one provider has to be defined" | ||||||
|  | 	emptyIDMsg := "provider has empty id: ids are required for all providers" | ||||||
|  | 	duplicateProviderIDMsg := "multiple providers found with id ProviderID: provider ids must be unique" | ||||||
|  | 	skipButtonAndMultipleProvidersMsg := "SkipProviderButton and multiple providers are mutually exclusive" | ||||||
|  | 
 | ||||||
|  | 	DescribeTable("validateProviders", | ||||||
|  | 		func(o *validateProvidersTableInput) { | ||||||
|  | 			Expect(validateProviders(o.options)).To(ConsistOf(o.errStrings)) | ||||||
|  | 		}, | ||||||
|  | 		Entry("with no providers", &validateProvidersTableInput{ | ||||||
|  | 			options:    &options.Options{}, | ||||||
|  | 			errStrings: []string{missingProvider}, | ||||||
|  | 		}), | ||||||
|  | 		Entry("with valid providers", &validateProvidersTableInput{ | ||||||
|  | 			options: &options.Options{ | ||||||
|  | 				Providers: options.Providers{ | ||||||
|  | 					validProvider, | ||||||
|  | 					validLoginGovProvider, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			errStrings: []string{}, | ||||||
|  | 		}), | ||||||
|  | 		Entry("with an empty providerID", &validateProvidersTableInput{ | ||||||
|  | 			options: &options.Options{ | ||||||
|  | 				Providers: options.Providers{ | ||||||
|  | 					missingIDProvider, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			errStrings: []string{emptyIDMsg}, | ||||||
|  | 		}), | ||||||
|  | 		Entry("with same providerID", &validateProvidersTableInput{ | ||||||
|  | 			options: &options.Options{ | ||||||
|  | 				Providers: options.Providers{ | ||||||
|  | 					validProvider, | ||||||
|  | 					validProvider, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			errStrings: []string{duplicateProviderIDMsg}, | ||||||
|  | 		}), | ||||||
|  | 		Entry("with multiple providers and skip provider button", &validateProvidersTableInput{ | ||||||
|  | 			options: &options.Options{ | ||||||
|  | 				SkipProviderButton: true, | ||||||
|  | 				Providers: options.Providers{ | ||||||
|  | 					validProvider, | ||||||
|  | 					validLoginGovProvider, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			errStrings: []string{skipButtonAndMultipleProvidersMsg}, | ||||||
|  | 		}), | ||||||
|  | 	) | ||||||
|  | }) | ||||||
		Loading…
	
		Reference in New Issue