1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-02-03 13:21:51 +02:00

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:
yanasega 2021-04-03 19:06:30 +03:00 committed by GitHub
parent 9d20b4e0e2
commit 42475c28f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1013 additions and 264 deletions

View File

@ -8,6 +8,8 @@
## 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
## Release Highlights

View File

@ -1,10 +1,5 @@
http_address="0.0.0.0:4180"
cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
provider="oidc"
email_domains="example.com"
oidc_issuer_url="http://dex.localhost:4190/dex"
client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
client_id="oauth2-proxy"
cookie_secure="false"
redirect_url="http://localhost:4180/oauth2/callback"

View File

@ -15,3 +15,9 @@ injectRequestHeaders:
- name: X-Forwarded-Preferred-Username
values:
- claim: preferred_username
providers:
- provider: oidc
clientSecret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
clientID: oauth2-proxy
oidcConfig:
oidcIssuerURL: http://dex.localhost:4190/dex

View File

@ -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. |
| `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. |
| `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
@ -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".
### 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
(**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. |
| `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
(**Appears on:** [ClaimSource](#claimsource), [HeaderValue](#headervalue), [TLS](#tls))

1
go.sum
View File

@ -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/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.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

View File

@ -19,6 +19,8 @@ http_address="127.0.0.1:4180"
upstreams="http://httpbin"
set_basic_auth="true"
basic_auth_password="super-secret-password"
client_id="oauth2-proxy"
client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
`
const testAlphaConfig = `
@ -57,15 +59,23 @@ injectResponseHeaders:
value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk
server:
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 = `
cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
provider="oidc"
email_domains="example.com"
oidc_issuer_url="http://dex.localhost:4190/dex"
client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
client_id="oauth2-proxy"
cookie_secure="false"
redirect_url="http://localhost:4180/oauth2/callback"
@ -85,11 +95,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
Expect(err).ToNot(HaveOccurred())
opts.Cookie.Secret = "OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
opts.ProviderType = "oidc"
opts.EmailDomains = []string{"example.com"}
opts.OIDCIssuerURL = "http://dex.localhost:4190/dex"
opts.ClientSecret = "b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
opts.ClientID = "oauth2-proxy"
opts.Cookie.Secure = false
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.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
}
@ -204,7 +228,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
configContent: testCoreConfig,
alphaConfigContent: testAlphaConfig + ":",
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{
configContent: testCoreConfig + "unknown_field=\"something\"",

View File

@ -122,7 +122,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
Footer: opts.Templates.Footer,
Version: VERSION,
Debug: opts.Templates.Debug,
ProviderName: buildProviderName(opts.GetProvider(), opts.ProviderName),
ProviderName: buildProviderName(opts.GetProvider(), opts.Providers[0].Name),
SignInMessage: buildSignInMessage(opts),
DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm,
})
@ -136,7 +136,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
}
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 {
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)
}
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"
if opts.Cookie.Refresh != time.Duration(0) {
refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh)

View File

@ -981,7 +981,7 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi
ProviderData: &providers.ProviderData{},
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.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)
assert.NoError(t, err)
@ -2292,8 +2292,9 @@ func Test_noCacheHeaders(t *testing.T) {
func baseTestOptions() *options.Options {
opts := options.NewOptions()
opts.Cookie.Secret = rawCookieSecret
opts.ClientID = clientID
opts.ClientSecret = clientSecret
opts.Providers[0].ID = "providerID"
opts.Providers[0].ClientID = clientID
opts.Providers[0].ClientSecret = clientSecret
opts.EmailDomains = []string{"*"}
// Default injected headers for legacy configuration
@ -2786,7 +2787,7 @@ func TestProxyAllowedGroups(t *testing.T) {
t.Cleanup(upstreamServer.Close)
test, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
opts.AllowedGroups = tt.allowedGroups
opts.Providers[0].AllowedGroups = tt.allowedGroups
opts.UpstreamServers = options.Upstreams{
{
ID: upstreamServer.URL,
@ -2915,7 +2916,7 @@ func TestAuthOnlyAllowedGroups(t *testing.T) {
}
test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {
opts.AllowedGroups = tc.allowedGroups
opts.Providers[0].AllowedGroups = tc.allowedGroups
})
if err != nil {
t.Fatal(err)

View File

@ -40,6 +40,9 @@ type AlphaOptions struct {
// 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.
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
@ -50,6 +53,8 @@ func (a *AlphaOptions) MergeInto(opts *Options) {
opts.InjectResponseHeaders = a.InjectResponseHeaders
opts.Server = a.Server
opts.MetricsServer = a.MetricsServer
opts.Providers = a.Providers
}
// 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.Server = opts.Server
a.MetricsServer = opts.MetricsServer
a.Providers = opts.Providers
}

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
"github.com/oauth2-proxy/oauth2-proxy/v7/providers"
"github.com/spf13/pflag"
)
@ -21,6 +22,9 @@ type LegacyOptions struct {
// Legacy options for the server address and TLS
LegacyServer LegacyServer `cfg:",squash"`
// Legacy options for single provider
LegacyProvider LegacyProvider `cfg:",squash"`
Options Options `cfg:",squash"`
}
@ -43,6 +47,15 @@ func NewLegacyOptions() *LegacyOptions {
HTTPSAddress: ":443",
},
LegacyProvider: LegacyProvider{
ProviderType: "google",
AzureTenant: "common",
ApprovalPrompt: "force",
UserIDClaim: "email",
OIDCEmailClaim: "email",
OIDCGroupsClaim: "groups",
},
Options: *NewOptions(),
}
}
@ -53,6 +66,7 @@ func NewLegacyFlagSet() *pflag.FlagSet {
flagSet.AddFlagSet(legacyUpstreamsFlagSet())
flagSet.AddFlagSet(legacyHeadersFlagSet())
flagSet.AddFlagSet(legacyServerFlagset())
flagSet.AddFlagSet(legacyProviderFlagSet())
return flagSet
}
@ -65,10 +79,17 @@ func (l *LegacyOptions) ToOptions() (*Options, error) {
l.Options.UpstreamServers = upstreams
l.Options.InjectRequestHeaders, l.Options.InjectResponseHeaders = l.LegacyHeaders.convert()
l.Options.Server, l.Options.MetricsServer = l.LegacyServer.convert()
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
}
@ -443,6 +464,106 @@ func legacyServerFlagset() *pflag.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) {
appServer := Server{
BindAddress: l.HTTPAddress,
@ -482,3 +603,91 @@ func (l LegacyServer) convert() (Server, Server) {
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
}

View File

@ -22,6 +22,7 @@ var _ = Describe("Legacy Options", func() {
legacyOpts.LegacyUpstreams.ProxyWebSockets = true
legacyOpts.LegacyUpstreams.SSLUpstreamInsecureSkipVerify = true
legacyOpts.LegacyUpstreams.Upstreams = []string{"http://foo.bar/baz", "file:///var/lib/website#/bar", "static://204"}
legacyOpts.LegacyProvider.ClientID = "oauth-proxy"
truth := true
staticCode := 204
@ -110,6 +111,9 @@ var _ = Describe("Legacy Options", func() {
BindAddress: "127.0.0.1:4180",
}
opts.Providers[0].ClientID = "oauth-proxy"
opts.Providers[0].ID = "google=oauth-proxy"
converted, err := legacyOpts.ToOptions()
Expect(err).ToNot(HaveOccurred())
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"
DescribeTable("convertLegacyUpstreams",
func(o *convertUpstreamsTableInput) {
func(in *convertUpstreamsTableInput) {
legacyUpstreams := LegacyUpstreams{
Upstreams: o.upstreamStrings,
Upstreams: in.upstreamStrings,
SSLUpstreamInsecureSkipVerify: skipVerify,
PassHostHeader: passHostHeader,
ProxyWebSockets: proxyWebSockets,
@ -207,14 +211,14 @@ var _ = Describe("Legacy Options", func() {
upstreams, err := legacyUpstreams.convert()
if o.errMsg != "" {
if in.errMsg != "" {
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal(o.errMsg))
Expect(err.Error()).To(Equal(in.errMsg))
} else {
Expect(err).ToNot(HaveOccurred())
}
Expect(upstreams).To(ConsistOf(o.expectedUpstreams))
Expect(upstreams).To(ConsistOf(in.expectedUpstreams))
},
Entry("with no upstreams", &convertUpstreamsTableInput{
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: "",
}),
)
})
})

View File

@ -14,6 +14,49 @@ import (
)
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() {
type TestOptionSubStruct struct {
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{
flagSet: NewFlagSet,
input: &Options{},
expectedOutput: NewOptions(),
expectedOutput: optionsWithNilProvider,
}),
Entry("with an empty LegacyOptions struct, should return default values", &testOptionsTableInput{
flagSet: NewLegacyFlagSet,
input: &LegacyOptions{},
expectedOutput: NewLegacyOptions(),
expectedOutput: legacyOptionsWithNilProvider,
}),
)
})

View File

@ -27,27 +27,10 @@ type Options struct {
TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
ForceHTTPS bool `flag:"force-https" cfg:"force_https"`
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"`
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"`
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"`
@ -66,6 +49,8 @@ type Options struct {
Server Server `cfg:",internal"`
MetricsServer Server `cfg:",internal"`
Providers Providers `cfg:",internal"`
SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
SkipAuthRoutes []string `flag:"skip-auth-route" cfg:"skip_auth_routes"`
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"`
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"`
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"`
// This is used for backwards compatibility for basic auth users
@ -136,23 +94,15 @@ func (o *Options) SetRealClientIPParser(s ipapi.RealClientIPParser) { o.realClie
func NewOptions() *Options {
return &Options{
ProxyPrefix: "/oauth2",
ProviderType: "google",
Providers: providerDefaults(),
PingPath: "/ping",
RealClientIPHeader: "X-Real-IP",
ForceHTTPS: false,
Cookie: cookieDefaults(),
Session: sessionOptionsDefaults(),
Templates: templatesDefaults(),
AzureTenant: "common",
SkipAuthPreflight: false,
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("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("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)")
@ -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.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("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.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(loggingFlagSet())
flagSet.AddFlagSet(templatesFlagSet())

View File

@ -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
}

View File

@ -29,6 +29,7 @@ func Validate(o *options.Options) error {
msgs = append(msgs, validateRedisSessionStore(o)...)
msgs = append(msgs, prefixValues("injectRequestHeaders: ", validateHeaders(o.InjectRequestHeaders)...)...)
msgs = append(msgs, prefixValues("injectResponseHeaders: ", validateHeaders(o.InjectResponseHeaders)...)...)
msgs = append(msgs, validateProviders(o)...)
msgs = configureLogger(o.Logging, msgs)
msgs = parseSignatureKey(o, msgs)
@ -39,8 +40,8 @@ func Validate(o *options.Options) error {
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
http.DefaultClient = &http.Client{Transport: insecureTransport}
} else if len(o.ProviderCAFiles) > 0 {
pool, err := util.GetCertPool(o.ProviderCAFiles)
} else if len(o.Providers[0].CAFiles) > 0 {
pool, err := util.GetCertPool(o.Providers[0].CAFiles)
if err == nil {
transport := http.DefaultTransport.(*http.Transport).Clone()
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 == "" {
msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required."+
"\n use email-domain=* to authorize all email addresses")
}
if o.OIDCIssuerURL != "" {
if o.Providers[0].OIDCConfig.IssuerURL != "" {
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
// (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.
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).
WithContext(ctx).
Do().
@ -96,23 +82,23 @@ func Validate(o *options.Options) error {
// Prefer manually configured URLs. It's a bit unclear
// why you'd be doing discovery and also providing the URLs
// explicitly though...
if o.LoginURL == "" {
o.LoginURL = body.Get("authorization_endpoint").MustString()
if o.Providers[0].LoginURL == "" {
o.Providers[0].LoginURL = body.Get("authorization_endpoint").MustString()
}
if o.RedeemURL == "" {
o.RedeemURL = body.Get("token_endpoint").MustString()
if o.Providers[0].RedeemURL == "" {
o.Providers[0].RedeemURL = body.Get("token_endpoint").MustString()
}
if o.OIDCJwksURL == "" {
o.OIDCJwksURL = body.Get("jwks_uri").MustString()
if o.Providers[0].OIDCConfig.JwksURL == "" {
o.Providers[0].OIDCConfig.JwksURL = body.Get("jwks_uri").MustString()
}
if o.ProfileURL == "" {
o.ProfileURL = body.Get("userinfo_endpoint").MustString()
if o.Providers[0].ProfileURL == "" {
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.
// In this case we need to make sure the required endpoints for
// the provider are configured.
if o.SkipOIDCDiscovery {
if o.LoginURL == "" {
if o.Providers[0].OIDCConfig.SkipDiscovery {
if o.Providers[0].LoginURL == "" {
msgs = append(msgs, "missing setting: login-url")
}
if o.RedeemURL == "" {
if o.Providers[0].RedeemURL == "" {
msgs = append(msgs, "missing setting: redeem-url")
}
if o.OIDCJwksURL == "" {
if o.Providers[0].OIDCConfig.JwksURL == "" {
msgs = append(msgs, "missing setting: oidc-jwks-url")
}
keySet := oidc.NewRemoteKeySet(ctx, o.OIDCJwksURL)
o.SetOIDCVerifier(oidc.NewVerifier(o.OIDCIssuerURL, keySet, &oidc.Config{
ClientID: o.ClientID,
SkipIssuerCheck: o.InsecureOIDCSkipIssuerVerification,
keySet := oidc.NewRemoteKeySet(ctx, o.Providers[0].OIDCConfig.JwksURL)
o.SetOIDCVerifier(oidc.NewVerifier(o.Providers[0].OIDCConfig.IssuerURL, keySet, &oidc.Config{
ClientID: o.Providers[0].ClientID,
SkipIssuerCheck: o.Providers[0].OIDCConfig.InsecureSkipIssuerVerification,
}))
} else {
// Configure discoverable provider data.
provider, err := oidc.NewProvider(ctx, o.OIDCIssuerURL)
provider, err := oidc.NewProvider(ctx, o.Providers[0].OIDCConfig.IssuerURL)
if err != nil {
return err
}
o.SetOIDCVerifier(provider.Verifier(&oidc.Config{
ClientID: o.ClientID,
SkipIssuerCheck: o.InsecureOIDCSkipIssuerVerification,
ClientID: o.Providers[0].ClientID,
SkipIssuerCheck: o.Providers[0].OIDCConfig.InsecureSkipIssuerVerification,
}))
o.LoginURL = provider.Endpoint().AuthURL
o.RedeemURL = provider.Endpoint().TokenURL
o.Providers[0].LoginURL = provider.Endpoint().AuthURL
o.Providers[0].RedeemURL = provider.Endpoint().TokenURL
}
if o.Scope == "" {
o.Scope = "openid email profile"
if o.Providers[0].Scope == "" {
o.Providers[0].Scope = "openid email profile"
if len(o.AllowedGroups) > 0 {
o.Scope += " groups"
if len(o.Providers[0].AllowedGroups) > 0 {
o.Providers[0].Scope += " groups"
}
}
if o.Providers[0].OIDCConfig.UserIDClaim == "" {
o.Providers[0].OIDCConfig.UserIDClaim = "email"
}
}
if o.SkipJwtBearerTokens {
@ -183,18 +172,6 @@ func Validate(o *options.Options) error {
msgs = append(msgs, validateUpstreams(o.UpstreamServers)...)
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 {
parser, err := ip.GetRealClientIPParser(o.RealClientIPHeader)
if err != nil {
@ -220,79 +197,79 @@ func Validate(o *options.Options) error {
func parseProviderInfo(o *options.Options, msgs []string) []string {
p := &providers.ProviderData{
Scope: o.Scope,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ClientSecretFile: o.ClientSecretFile,
Prompt: o.Prompt,
ApprovalPrompt: o.ApprovalPrompt,
AcrValues: o.AcrValues,
Scope: o.Providers[0].Scope,
ClientID: o.Providers[0].ClientID,
ClientSecret: o.Providers[0].ClientSecret,
ClientSecretFile: o.Providers[0].ClientSecretFile,
Prompt: o.Providers[0].Prompt,
ApprovalPrompt: o.Providers[0].ApprovalPrompt,
AcrValues: o.Providers[0].AcrValues,
}
p.LoginURL, msgs = parseURL(o.LoginURL, "login", msgs)
p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs)
p.ProfileURL, msgs = parseURL(o.ProfileURL, "profile", msgs)
p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs)
p.LoginURL, msgs = parseURL(o.Providers[0].LoginURL, "login", msgs)
p.RedeemURL, msgs = parseURL(o.Providers[0].RedeemURL, "redeem", msgs)
p.ProfileURL, msgs = parseURL(o.Providers[0].ProfileURL, "profile", msgs)
p.ValidateURL, msgs = parseURL(o.Providers[0].ValidateURL, "validate", msgs)
p.ProtectedResource, msgs = parseURL(o.Providers[0].ProtectedResource, "resource", msgs)
// Make the OIDC options available to all providers that support it
p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
p.EmailClaim = o.OIDCEmailClaim
p.GroupsClaim = o.OIDCGroupsClaim
p.AllowUnverifiedEmail = o.Providers[0].OIDCConfig.InsecureAllowUnverifiedEmail
p.EmailClaim = o.Providers[0].OIDCConfig.EmailClaim
p.GroupsClaim = o.Providers[0].OIDCConfig.GroupsClaim
p.Verifier = o.GetOIDCVerifier()
// TODO (@NickMeves) - Remove This
// Backwards Compatibility for Deprecated UserIDClaim option
if o.OIDCEmailClaim == providers.OIDCEmailClaim &&
o.UserIDClaim != providers.OIDCEmailClaim {
p.EmailClaim = o.UserIDClaim
if o.Providers[0].OIDCConfig.EmailClaim == providers.OIDCEmailClaim &&
o.Providers[0].OIDCConfig.UserIDClaim != providers.OIDCEmailClaim {
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 {
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
}
o.SetProvider(provider)
switch p := o.GetProvider().(type) {
case *providers.AzureProvider:
p.Configure(o.AzureTenant)
p.Configure(o.Providers[0].AzureConfig.Tenant)
case *providers.GitHubProvider:
p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
p.SetRepo(o.GitHubRepo, o.GitHubToken)
p.SetUsers(o.GitHubUsers)
p.SetOrgTeam(o.Providers[0].GitHubConfig.Org, o.Providers[0].GitHubConfig.Team)
p.SetRepo(o.Providers[0].GitHubConfig.Repo, o.Providers[0].GitHubConfig.Token)
p.SetUsers(o.Providers[0].GitHubConfig.Users)
case *providers.KeycloakProvider:
// Backwards compatibility with `--keycloak-group` option
if len(o.KeycloakGroups) > 0 {
p.SetAllowedGroups(o.KeycloakGroups)
if len(o.Providers[0].KeycloakConfig.Groups) > 0 {
p.SetAllowedGroups(o.Providers[0].KeycloakConfig.Groups)
}
case *providers.GoogleProvider:
if o.GoogleServiceAccountJSON != "" {
file, err := os.Open(o.GoogleServiceAccountJSON)
if o.Providers[0].GoogleConfig.ServiceAccountJSON != "" {
file, err := os.Open(o.Providers[0].GoogleConfig.ServiceAccountJSON)
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 {
groups := o.AllowedGroups
groups := o.Providers[0].AllowedGroups
// Backwards compatibility with `--google-group` option
if len(o.GoogleGroups) > 0 {
groups = o.GoogleGroups
if len(o.Providers[0].GoogleConfig.Groups) > 0 {
groups = o.Providers[0].GoogleConfig.Groups
p.SetAllowedGroups(groups)
}
p.SetGroupRestriction(groups, o.GoogleAdminEmail, file)
p.SetGroupRestriction(groups, o.Providers[0].GoogleConfig.AdminEmail, file)
}
}
case *providers.BitbucketProvider:
p.SetTeam(o.BitbucketTeam)
p.SetRepository(o.BitbucketRepository)
p.SetTeam(o.Providers[0].BitbucketConfig.Team)
p.SetRepository(o.Providers[0].BitbucketConfig.Repository)
case *providers.OIDCProvider:
if p.Verifier == nil {
msgs = append(msgs, "oidc provider requires an oidc issuer URL")
}
case *providers.GitLabProvider:
p.Groups = o.GitLabGroup
err := p.AddProjects(o.GitlabProjects)
p.Groups = o.Providers[0].GitLabConfig.Group
err := p.AddProjects(o.Providers[0].GitLabConfig.Projects)
if err != nil {
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")
} else {
p.Verifier = provider.Verifier(&oidc.Config{
ClientID: o.ClientID,
ClientID: o.Providers[0].ClientID,
})
p.LoginURL, msgs = parseURL(provider.Endpoint().AuthURL, "login", msgs)
@ -316,31 +293,31 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
}
}
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.
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")
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")
case o.JWTKey != "":
case o.Providers[0].LoginGovConfig.JWTKey != "":
// 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 {
msgs = append(msgs, "could not parse RSA Private Key PEM")
} else {
p.JWTKey = signKey
}
case o.JWTKeyFile != "":
case o.Providers[0].LoginGovConfig.JWTKeyFile != "":
// 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 {
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)
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 {
p.JWTKey = signKey
}

View File

@ -17,6 +17,7 @@ const (
cookieSecret = "secretthirtytwobytes+abcdefghijk"
clientID = "bazquux"
clientSecret = "xyzzyplugh"
providerID = "providerID"
)
func testOptions() *options.Options {
@ -27,8 +28,9 @@ func testOptions() *options.Options {
URI: "http://127.0.0.1:8080/",
})
o.Cookie.Secret = cookieSecret
o.ClientID = clientID
o.ClientSecret = clientSecret
o.Providers[0].ID = providerID
o.Providers[0].ClientID = clientID
o.Providers[0].ClientSecret = clientSecret
o.EmailDomains = []string{"*"}
return o
}
@ -48,7 +50,8 @@ func TestNewOptions(t *testing.T) {
expected := errorMsg([]string{
"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"})
assert.Equal(t, expected, err.Error())
}
@ -56,8 +59,9 @@ func TestNewOptions(t *testing.T) {
func TestClientSecretFileOptionFails(t *testing.T) {
o := options.NewOptions()
o.Cookie.Secret = cookieSecret
o.ClientID = clientID
o.ClientSecretFile = clientSecret
o.Providers[0].ID = providerID
o.Providers[0].ClientID = clientID
o.Providers[0].ClientSecretFile = clientSecret
o.EmailDomains = []string{"*"}
err := Validate(o)
assert.NotEqual(t, nil, err)
@ -93,8 +97,9 @@ func TestClientSecretFileOption(t *testing.T) {
o := options.NewOptions()
o.Cookie.Secret = cookieSecret
o.ClientID = clientID
o.ClientSecretFile = clientSecretFileName
o.Providers[0].ID = providerID
o.Providers[0].ClientID = clientID
o.Providers[0].ClientSecretFile = clientSecretFileName
o.EmailDomains = []string{"*"}
err = Validate(o)
assert.Equal(t, nil, err)
@ -110,7 +115,7 @@ func TestClientSecretFileOption(t *testing.T) {
func TestGoogleGroupOptions(t *testing.T) {
o := testOptions()
o.GoogleGroups = []string{"googlegroup"}
o.Providers[0].GoogleConfig.Groups = []string{"googlegroup"}
err := Validate(o)
assert.NotEqual(t, nil, err)
@ -122,9 +127,9 @@ func TestGoogleGroupOptions(t *testing.T) {
func TestGoogleGroupInvalidFile(t *testing.T) {
o := testOptions()
o.GoogleGroups = []string{"test_group"}
o.GoogleAdminEmail = "admin@example.com"
o.GoogleServiceAccountJSON = "file_doesnt_exist.json"
o.Providers[0].GoogleConfig.Groups = []string{"test_group"}
o.Providers[0].GoogleConfig.AdminEmail = "admin@example.com"
o.Providers[0].GoogleConfig.ServiceAccountJSON = "file_doesnt_exist.json"
err := Validate(o)
assert.NotEqual(t, nil, err)
@ -225,17 +230,17 @@ func TestValidateSignatureKeyUnsupportedAlgorithm(t *testing.T) {
func TestSkipOIDCDiscovery(t *testing.T) {
o := testOptions()
o.ProviderType = "oidc"
o.OIDCIssuerURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/v2.0/"
o.SkipOIDCDiscovery = true
o.Providers[0].Type = "oidc"
o.Providers[0].OIDCConfig.IssuerURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/v2.0/"
o.Providers[0].OIDCConfig.SkipDiscovery = true
err := Validate(o)
assert.Equal(t, "invalid configuration:\n"+
" 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.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].LoginURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/authorize?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.Providers[0].OIDCConfig.JwksURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/discovery/v2.0/keys"
assert.Equal(t, nil, Validate(o))
}
@ -292,7 +297,7 @@ func TestProviderCAFilesError(t *testing.T) {
assert.NoError(t, os.Remove(file.Name()))
o := testOptions()
o.ProviderCAFiles = append(o.ProviderCAFiles, file.Name())
o.Providers[0].CAFiles = append(o.Providers[0].CAFiles, file.Name())
err = Validate(o)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unable to load provider CA file(s)")

View File

@ -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
}

View File

@ -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},
}),
)
})