git.iko.soy/typeable/oauth2-proxy
on master help sign in
Move provider initialisation into providers package
commitd162b018a837b57075e2f8dd486041ccc4be9e5f authorJoel Speed <joel.speed@hotmail.co.uk> dateTue 15 Feb 2022, 11:18 (5 years ago) parent95dd274
28 files changed+786−211
docs/docs/configuration/alpha_config.md+12−1 pkg/apis/options/legacy_options.go+1−1 pkg/apis/options/providers.go+51−1 providers/adfs.go+3−8 providers/adfs_test.go+6−6 providers/azure.go+10−15 providers/azure_test.go+11−10 providers/bitbucket.go+16−6 providers/bitbucket_test.go+10−11 providers/github.go+15−8 providers/github_test.go+81−35 providers/gitlab.go+12−4 providers/gitlab_test.go+22−14 providers/google.go+20−3 providers/google_test.go+22−12 providers/keycloak.go+6−2 providers/keycloak_oidc.go+9−4 providers/keycloak_oidc_test.go+9−6 providers/keycloak_test.go+6−3 providers/logingov.go+45−2 providers/logingov_test.go+57−12 providers/nextcloud.go+3−1 providers/oidc.go+3−2 providers/oidc_test.go+7−4 providers/provider_data.go+4−7 providers/provider_default_test.go+1−1 providers/providers.go+251−32 providers/providers_test.go+93
docs/docs/configuration/alpha_config.md+12−1
@@ -305,7 +305,7 @@ Provider holds all configuration for a single provider
305305| `oidcConfig` | _[OIDCOptions](#oidcoptions)_ | OIDCConfig holds all configurations for OIDC provider<br/>or providers utilize OIDC configurations. |
306306| `loginGovConfig` | _[LoginGovOptions](#logingovoptions)_ | LoginGovConfig holds all configurations for LoginGov provider. |
307307| `id` | _string_ | ID should be a unique identifier for the provider.<br/>This value is required for all providers. |
308−| `provider` | _string_ | Type is the OAuth provider<br/>must be set from the supported providers group,<br/>otherwise 'Google' is set as default |
308+| `provider` | _[ProviderType](#providertype)_ | Type is the OAuth provider<br/>must be set from the supported providers group,<br/>otherwise 'Google' is set as default |
309309| `name` | _string_ | Name is the providers display name<br/>if set, it will be shown to the users in the login page. |
310310| `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 |
311311| `loginURL` | _string_ | LoginURL is the authentication endpoint |
@@ -319,6 +319,17 @@ Provider holds all configuration for a single provider
319319| `allowedGroups` | _[]string_ | AllowedGroups is a list of restrict logins to members of this group |
320320| `acrValues` | _string_ | AcrValues is a string of acr values |
321321
322+### ProviderType
323+#### (`string` alias)
324+
325+(**Appears on:** [Provider](#provider))
326+
327+ProviderType is used to enumerate the different provider type options
328+Valid options are: adfs, azure, bitbucket, digitalocean facebook, github,
329+gitlab, google, keycloak, keycloak-oidc, linkedin, login.gov, nextcloud
330+and oidc.
331+
332+
322333### Providers
323334
324335#### ([[]Provider](#provider) alias)
pkg/apis/options/legacy_options.go+1−1
@@ -624,7 +624,7 @@ func (l *LegacyProvider) convert() (Providers, error) {
624624 ClientID: l.ClientID,
625625 ClientSecret: l.ClientSecret,
626626 ClientSecretFile: l.ClientSecretFile,
627− Type: l.ProviderType,
627+ Type: ProviderType(l.ProviderType),
628628 CAFiles: l.ProviderCAFiles,
629629 LoginURL: l.LoginURL,
630630 RedeemURL: l.RedeemURL,
pkg/apis/options/providers.go+51−1
@@ -52,7 +52,7 @@ type Provider struct {
5252 // Type is the OAuth provider
5353 // must be set from the supported providers group,
5454 // otherwise 'Google' is set as default
55− Type string `json:"provider,omitempty"`
55+ Type ProviderType `json:"provider,omitempty"`
5656 // Name is the providers display name
5757 // if set, it will be shown to the users in the login page.
5858 Name string `json:"name,omitempty"`
@@ -84,6 +84,56 @@ type Provider struct {
8484 AcrValues string `json:"acrValues,omitempty"`
8585}
8686
87+// ProviderType is used to enumerate the different provider type options
88+// Valid options are: adfs, azure, bitbucket, digitalocean facebook, github,
89+// gitlab, google, keycloak, keycloak-oidc, linkedin, login.gov, nextcloud
90+// and oidc.
91+type ProviderType string
92+
93+const (
94+ // ADFSProvider is the provider type for ADFS
95+ ADFSProvider ProviderType = "adfs"
96+
97+ // AzureProvider is the provider type for Azure
98+ AzureProvider ProviderType = "azure"
99+
100+ // BitbucketProvider is the provider type for Bitbucket
101+ BitbucketProvider ProviderType = "bitbucket"
102+
103+ // DigitalOceanProvider is the provider type for DigitalOcean
104+ DigitalOceanProvider ProviderType = "digitalocean"
105+
106+ // FacebookProvider is the provider type for Facebook
107+ FacebookProvider ProviderType = "facebook"
108+
109+ // GitHubProvider is the provider type for GitHub
110+ GitHubProvider ProviderType = "github"
111+
112+ // GitLabProvider is the provider type for GitLab
113+ GitLabProvider ProviderType = "gitlab"
114+
115+ // GoogleProvider is the provider type for GoogleProvider
116+ GoogleProvider ProviderType = "google"
117+
118+ // KeycloakProvider is the provider type for Keycloak
119+ KeycloakProvider ProviderType = "keycloak"
120+
121+ // KeycloakOIDCProvider is the provider type for Keycloak OIDC
122+ KeycloakOIDCProvider ProviderType = "keycloak-oidc"
123+
124+ // LinkedInProvider is the provider type for LinkedIn
125+ LinkedInProvider ProviderType = "linkedin"
126+
127+ // LoginGovProvider is the provider type for LoginGov
128+ LoginGovProvider ProviderType = "login.gov"
129+
130+ // NextCloudProvider is the provider type for NextCloud
131+ NextCloudProvider ProviderType = "nextcloud"
132+
133+ // OIDCProvider is the provider type for OIDC
134+ OIDCProvider ProviderType = "oidc"
135+)
136+
87137type KeycloakOptions struct {
88138 // Group enables to restrict login to members of indicated group
89139 Groups []string `json:"groups,omitempty"`
providers/adfs.go+3−8
@@ -6,6 +6,7 @@ import (
66 "net/url"
77 "strings"
88
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
910 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1011)
1112
@@ -24,12 +25,11 @@ var _ Provider = (*ADFSProvider)(nil)
2425const (
2526 adfsProviderName = "ADFS"
2627 adfsDefaultScope = "openid email profile"
27− adfsSkipScope = false
2828 adfsUPNClaim = "upn"
2929)
3030
3131// NewADFSProvider initiates a new ADFSProvider
32−func NewADFSProvider(p *ProviderData) *ADFSProvider {
32+func NewADFSProvider(p *ProviderData, opts options.ADFSOptions) *ADFSProvider {
3333 p.setProviderDefaults(providerDefaults{
3434 name: adfsProviderName,
3535 scope: adfsDefaultScope,
@@ -53,17 +53,12 @@ func NewADFSProvider(p *ProviderData) *ADFSProvider {
5353
5454 return &ADFSProvider{
5555 OIDCProvider: oidcProvider,
56− skipScope: adfsSkipScope,
56+ skipScope: opts.SkipScope,
5757 oidcEnrichFunc: oidcProvider.EnrichSession,
5858 oidcRefreshFunc: oidcProvider.RefreshSession,
5959 }
6060}
6161
62−// Configure defaults the ADFSProvider configuration options
63−func (p *ADFSProvider) Configure(skipScope bool) {
64− p.skipScope = skipScope
65−}
66−
6762// GetLoginURL Override to double encode the state parameter. If not query params are lost
6863// More info here: https://docs.microsoft.com/en-us/powerapps/maker/portals/configure/configure-saml2-settings
6964func (p *ADFSProvider) GetLoginURL(redirectURI, state, nonce string) string {
providers/adfs_test.go+6−6
@@ -13,6 +13,7 @@ import (
1313
1414 "github.com/coreos/go-oidc/v3/oidc"
1515 "github.com/golang-jwt/jwt"
16+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1617 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1718 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
1819 . "github.com/onsi/ginkgo"
@@ -61,8 +62,8 @@ func testADFSProvider(hostname string) *ADFSProvider {
6162 ValidateURL: &url.URL{},
6263 Scope: "",
6364 Verifier: o,
64− EmailClaim: OIDCEmailClaim,
65− })
65+ EmailClaim: options.OIDCEmailClaim,
66+ }, options.ADFSOptions{})
6667
6768 if hostname != "" {
6869 updateURL(p.Data().LoginURL, hostname)
@@ -133,7 +134,7 @@ var _ = Describe("ADFS Provider Tests", func() {
133134
134135 Context("New Provider Init", func() {
135136 It("uses defaults", func() {
136− providerData := NewADFSProvider(&ProviderData{}).Data()
137+ providerData := NewADFSProvider(&ProviderData{}, options.ADFSOptions{}).Data()
137138 Expect(providerData.ProviderName).To(Equal("ADFS"))
138139 Expect(providerData.Scope).To(Equal("openid email profile"))
139140 })
@@ -165,8 +166,7 @@ var _ = Describe("ADFS Provider Tests", func() {
165166 p := NewADFSProvider(&ProviderData{
166167 ProtectedResource: resource,
167168 Scope: "",
168− })
169− p.skipScope = true
169+ }, options.ADFSOptions{SkipScope: true})
170170
171171 result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "")
172172 Expect(result).NotTo(ContainSubstring("scope="))
@@ -186,7 +186,7 @@ var _ = Describe("ADFS Provider Tests", func() {
186186 p := NewADFSProvider(&ProviderData{
187187 ProtectedResource: resource,
188188 Scope: in.scope,
189− })
189+ }, options.ADFSOptions{})
190190
191191 Expect(p.Data().Scope).To(Equal(in.expectedScope))
192192 result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "")
providers/azure.go+10−15
@@ -10,6 +10,7 @@ import (
1010 "time"
1111
1212 "github.com/bitly/go-simplejson"
13+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1314 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1415 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1516 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
@@ -62,7 +63,7 @@ var (
6263)
6364
6465// NewAzureProvider initiates a new AzureProvider
65−func NewAzureProvider(p *ProviderData) *AzureProvider {
66+func NewAzureProvider(p *ProviderData, opts options.AzureOptions) *AzureProvider {
6667 p.setProviderDefaults(providerDefaults{
6768 name: azureProviderName,
6869 loginURL: azureDefaultLoginURL,
@@ -80,23 +81,17 @@ func NewAzureProvider(p *ProviderData) *AzureProvider {
8081 }
8182 p.getAuthorizationHeaderFunc = makeAzureHeader
8283
83− return &AzureProvider{
84− ProviderData: p,
85− Tenant: "common",
84+ tenant := "common"
85+ if opts.Tenant != "" {
86+ tenant = opts.Tenant
87+ overrideTenantURL(p.LoginURL, azureDefaultLoginURL, tenant, "authorize")
88+ overrideTenantURL(p.RedeemURL, azureDefaultRedeemURL, tenant, "token")
8689 }
87−}
8890
89−// Configure defaults the AzureProvider configuration options
90−func (p *AzureProvider) Configure(tenant string) {
91− if tenant == "" || tenant == "common" {
92− // tenant is empty or default, remain on the default "common" tenant
93− return
91+ return &AzureProvider{
92+ ProviderData: p,
93+ Tenant: tenant,
9494 }
95−
96− // Specific tennant specified, override the Login and RedeemURLs
97− p.Tenant = tenant
98− overrideTenantURL(p.LoginURL, azureDefaultLoginURL, tenant, "authorize")
99− overrideTenantURL(p.RedeemURL, azureDefaultRedeemURL, tenant, "token")
10095}
10196
10297func overrideTenantURL(current, defaultURL *url.URL, tenant, path string) {
providers/azure_test.go+11−10
@@ -15,6 +15,7 @@ import (
1515
1616 "github.com/coreos/go-oidc/v3/oidc"
1717 "github.com/golang-jwt/jwt"
18+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1819 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1920 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
2021
@@ -39,7 +40,7 @@ type azureOAuthPayload struct {
3940 IDToken string `json:"id_token,omitempty"`
4041}
4142
42−func testAzureProvider(hostname string) *AzureProvider {
43+func testAzureProvider(hostname string, opts options.AzureOptions) *AzureProvider {
4344 verificationOptions := &internaloidc.IDTokenVerificationOptions{
4445 AudienceClaims: []string{"aud"},
4546 ClientID: "cd6d4fae-f6a6-4a34-8454-2c6b598e9532",
@@ -64,7 +65,7 @@ func testAzureProvider(hostname string) *AzureProvider {
6465 SkipExpiryCheck: true,
6566 },
6667 ), verificationOptions),
67− })
68+ }, opts)
6869
6970 if hostname != "" {
7071 updateURL(p.Data().LoginURL, hostname)
@@ -80,7 +81,7 @@ func TestNewAzureProvider(t *testing.T) {
8081 g := NewWithT(t)
8182
8283 // Test that defaults are set when calling for a new provider with nothing set
83− providerData := NewAzureProvider(&ProviderData{}).Data()
84+ providerData := NewAzureProvider(&ProviderData{}, options.AzureOptions{}).Data()
8485 g.Expect(providerData.ProviderName).To(Equal("Azure"))
8586 g.Expect(providerData.LoginURL.String()).To(Equal("https://login.microsoftonline.com/common/oauth2/authorize"))
8687 g.Expect(providerData.RedeemURL.String()).To(Equal("https://login.microsoftonline.com/common/oauth2/token"))
@@ -111,7 +112,8 @@ func TestAzureProviderOverrides(t *testing.T) {
111112 ProtectedResource: &url.URL{
112113 Scheme: "https",
113114 Host: "example.com"},
114− Scope: "profile"})
115+ Scope: "profile"},
116+ options.AzureOptions{})
115117 assert.NotEqual(t, nil, p)
116118 assert.Equal(t, "Azure", p.Data().ProviderName)
117119 assert.Equal(t, "https://example.com/oauth/auth",
@@ -128,8 +130,7 @@ func TestAzureProviderOverrides(t *testing.T) {
128130}
129131
130132func TestAzureSetTenant(t *testing.T) {
131− p := testAzureProvider("")
132− p.Configure("example")
133+ p := testAzureProvider("", options.AzureOptions{Tenant: "example"})
133134 assert.Equal(t, "Azure", p.Data().ProviderName)
134135 assert.Equal(t, "example", p.Tenant)
135136 assert.Equal(t, "https://login.microsoftonline.com/example/oauth2/authorize",
@@ -230,7 +231,7 @@ func TestAzureProviderEnrichSession(t *testing.T) {
230231 bURL, _ := url.Parse(b.URL)
231232 host = bURL.Host
232233 }
233− p := testAzureProvider(host)
234+ p := testAzureProvider(host, options.AzureOptions{})
234235 session := CreateAuthorizedSession()
235236 session.Email = testCase.Email
236237 err := p.EnrichSession(context.Background(), session)
@@ -323,7 +324,7 @@ func TestAzureProviderRedeem(t *testing.T) {
323324 defer b.Close()
324325
325326 bURL, _ := url.Parse(b.URL)
326− p := testAzureProvider(bURL.Host)
327+ p := testAzureProvider(bURL.Host, options.AzureOptions{})
327328 p.Data().RedeemURL.Path = "/common/oauth2/token"
328329 s, err := p.Redeem(context.Background(), "https://localhost", "1234")
329330 if testCase.InjectRedeemURLError {
@@ -345,7 +346,7 @@ func TestAzureProviderRedeem(t *testing.T) {
345346}
346347
347348func TestAzureProviderProtectedResourceConfigured(t *testing.T) {
348− p := testAzureProvider("")
349+ p := testAzureProvider("", options.AzureOptions{})
349350 p.ProtectedResource, _ = url.Parse("http://my.resource.test")
350351 result := p.GetLoginURL("https://my.test.app/oauth", "", "")
351352 assert.Contains(t, result, "resource="+url.QueryEscape("http://my.resource.test"))
@@ -381,7 +382,7 @@ func TestAzureProviderRefresh(t *testing.T) {
381382 b := testAzureBackend(string(payloadBytes), newAccessToken, refreshToken)
382383 defer b.Close()
383384 bURL, _ := url.Parse(b.URL)
384− p := testAzureProvider(bURL.Host)
385+ p := testAzureProvider(bURL.Host, options.AzureOptions{})
385386
386387 expires := time.Now().Add(time.Duration(-1) * time.Hour)
387388 session := &sessions.SessionState{AccessToken: "some_access_token", RefreshToken: refreshToken, IDToken: "some_id_token", ExpiresOn: &expires}
providers/bitbucket.go+16−6
@@ -5,6 +5,7 @@ import (
55 "net/url"
66 "strings"
77
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
89 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
910 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1011 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
@@ -53,7 +54,7 @@ var (
5354)
5455
5556// NewBitbucketProvider initiates a new BitbucketProvider
56−func NewBitbucketProvider(p *ProviderData) *BitbucketProvider {
57+func NewBitbucketProvider(p *ProviderData, opts options.BitbucketOptions) *BitbucketProvider {
5758 p.setProviderDefaults(providerDefaults{
5859 name: bitbucketProviderName,
5960 loginURL: bitbucketDefaultLoginURL,
@@ -62,19 +63,28 @@ func NewBitbucketProvider(p *ProviderData) *BitbucketProvider {
6263 validateURL: bitbucketDefaultValidateURL,
6364 scope: bitbucketDefaultScope,
6465 })
65− return &BitbucketProvider{ProviderData: p}
66+
67+ provider := &BitbucketProvider{ProviderData: p}
68+
69+ if opts.Team != "" {
70+ provider.setTeam(opts.Team)
71+ }
72+ if opts.Repository != "" {
73+ provider.setRepository(opts.Repository)
74+ }
75+ return provider
6676}
6777
68−// SetTeam defines the Bitbucket team the user must be part of
69−func (p *BitbucketProvider) SetTeam(team string) {
78+// setTeam defines the Bitbucket team the user must be part of
79+func (p *BitbucketProvider) setTeam(team string) {
7080 p.Team = team
7181 if !strings.Contains(p.Scope, "team") {
7282 p.Scope += " team"
7383 }
7484}
7585
76−// SetRepository defines the repository the user must have access to
77−func (p *BitbucketProvider) SetRepository(repository string) {
86+// setRepository defines the repository the user must have access to
87+func (p *BitbucketProvider) setRepository(repository string) {
7888 p.Repository = repository
7989 if !strings.Contains(p.Scope, "repository") {
8090 p.Scope += " repository"
providers/bitbucket_test.go+10−11
@@ -8,6 +8,7 @@ import (
88 "net/url"
99 "testing"
1010
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1112 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1213 . "github.com/onsi/gomega"
1314 "github.com/stretchr/testify/assert"
@@ -21,15 +22,12 @@ func testBitbucketProvider(hostname, team string, repository string) *BitbucketP
2122 RedeemURL: &url.URL{},
2223 ProfileURL: &url.URL{},
2324 ValidateURL: &url.URL{},
24− Scope: ""})
25−
26− if team != "" {
27− p.SetTeam(team)
28− }
29−
30− if repository != "" {
31− p.SetRepository(repository)
32− }
25+ Scope: ""},
26+ options.BitbucketOptions{
27+ Team: team,
28+ Repository: repository,
29+ },
30+ )
3331
3432 if hostname != "" {
3533 updateURL(p.Data().LoginURL, hostname)
@@ -65,7 +63,7 @@ func TestNewBitbucketProvider(t *testing.T) {
6563 g := NewWithT(t)
6664
6765 // Test that defaults are set when calling for a new provider with nothing set
68− providerData := NewBitbucketProvider(&ProviderData{}).Data()
66+ providerData := NewBitbucketProvider(&ProviderData{}, options.BitbucketOptions{}).Data()
6967 g.Expect(providerData.ProviderName).To(Equal("Bitbucket"))
7068 g.Expect(providerData.LoginURL.String()).To(Equal("https://bitbucket.org/site/oauth2/authorize"))
7169 g.Expect(providerData.RedeemURL.String()).To(Equal("https://bitbucket.org/site/oauth2/access_token"))
@@ -101,7 +99,8 @@ func TestBitbucketProviderOverrides(t *testing.T) {
10199 Scheme: "https",
102100 Host: "example.com",
103101 Path: "/api/v3/user"},
104− Scope: "profile"})
102+ Scope: "profile"},
103+ options.BitbucketOptions{})
105104 assert.NotEqual(t, nil, p)
106105 assert.Equal(t, "Bitbucket", p.Data().ProviderName)
107106 assert.Equal(t, "https://example.com/oauth/auth",
providers/github.go+15−8
@@ -11,6 +11,7 @@ import (
1111 "strconv"
1212 "strings"
1313
14+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1415 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1516 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1617 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
@@ -62,7 +63,7 @@ var (
6263)
6364
6465// NewGitHubProvider initiates a new GitHubProvider
65−func NewGitHubProvider(p *ProviderData) *GitHubProvider {
66+func NewGitHubProvider(p *ProviderData, opts options.GitHubOptions) *GitHubProvider {
6667 p.setProviderDefaults(providerDefaults{
6768 name: githubProviderName,
6869 loginURL: githubDefaultLoginURL,
@@ -71,7 +72,13 @@ func NewGitHubProvider(p *ProviderData) *GitHubProvider {
7172 validateURL: githubDefaultValidateURL,
7273 scope: githubDefaultScope,
7374 })
74− return &GitHubProvider{ProviderData: p}
75+
76+ provider := &GitHubProvider{ProviderData: p}
77+
78+ provider.setOrgTeam(opts.Org, opts.Team)
79+ provider.setRepo(opts.Repo, opts.Token)
80+ provider.setUsers(opts.Users)
81+ return provider
7582}
7683
7784func makeGitHubHeader(accessToken string) http.Header {
@@ -82,8 +89,8 @@ func makeGitHubHeader(accessToken string) http.Header {
8289 return makeAuthorizationHeader(tokenTypeToken, accessToken, extraHeaders)
8390}
8491
85−// SetOrgTeam adds GitHub org reading parameters to the OAuth2 scope
86−func (p *GitHubProvider) SetOrgTeam(org, team string) {
92+// setOrgTeam adds GitHub org reading parameters to the OAuth2 scope
93+func (p *GitHubProvider) setOrgTeam(org, team string) {
8794 p.Org = org
8895 p.Team = team
8996 if org != "" || team != "" {
@@ -91,14 +98,14 @@ func (p *GitHubProvider) SetOrgTeam(org, team string) {
9198 }
9299}
93100
94−// SetRepo configures the target repository and optional token to use
95−func (p *GitHubProvider) SetRepo(repo, token string) {
101+// setRepo configures the target repository and optional token to use
102+func (p *GitHubProvider) setRepo(repo, token string) {
96103 p.Repo = repo
97104 p.Token = token
98105}
99106
100−// SetUsers configures allowed usernames
101−func (p *GitHubProvider) SetUsers(users []string) {
107+// setUsers configures allowed usernames
108+func (p *GitHubProvider) setUsers(users []string) {
102109 p.Users = users
103110}
104111
providers/github_test.go+81−35
@@ -7,12 +7,13 @@ import (
77 "net/url"
88 "testing"
99
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1011 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1112 . "github.com/onsi/gomega"
1213 "github.com/stretchr/testify/assert"
1314)
1415
15−func testGitHubProvider(hostname string) *GitHubProvider {
16+func testGitHubProvider(hostname string, opts options.GitHubOptions) *GitHubProvider {
1617 p := NewGitHubProvider(
1718 &ProviderData{
1819 ProviderName: "",
@@ -20,7 +21,8 @@ func testGitHubProvider(hostname string) *GitHubProvider {
2021 RedeemURL: &url.URL{},
2122 ProfileURL: &url.URL{},
2223 ValidateURL: &url.URL{},
23− Scope: ""})
24+ Scope: ""},
25+ opts)
2426 if hostname != "" {
2527 updateURL(p.Data().LoginURL, hostname)
2628 updateURL(p.Data().RedeemURL, hostname)
@@ -71,7 +73,7 @@ func TestNewGitHubProvider(t *testing.T) {
7173 g := NewWithT(t)
7274
7375 // Test that defaults are set when calling for a new provider with nothing set
74− providerData := NewGitHubProvider(&ProviderData{}).Data()
76+ providerData := NewGitHubProvider(&ProviderData{}, options.GitHubOptions{}).Data()
7577 g.Expect(providerData.ProviderName).To(Equal("GitHub"))
7678 g.Expect(providerData.LoginURL.String()).To(Equal("https://github.com/login/oauth/authorize"))
7779 g.Expect(providerData.RedeemURL.String()).To(Equal("https://github.com/login/oauth/access_token"))
@@ -95,7 +97,8 @@ func TestGitHubProviderOverrides(t *testing.T) {
9597 Scheme: "https",
9698 Host: "api.example.com",
9799 Path: "/"},
98− Scope: "profile"})
100+ Scope: "profile"},
101+ options.GitHubOptions{})
99102 assert.NotEqual(t, nil, p)
100103 assert.Equal(t, "GitHub", p.Data().ProviderName)
101104 assert.Equal(t, "https://example.com/login/oauth/authorize",
@@ -114,7 +117,7 @@ func TestGitHubProvider_getEmail(t *testing.T) {
114117 defer b.Close()
115118
116119 bURL, _ := url.Parse(b.URL)
117− p := testGitHubProvider(bURL.Host)
120+ p := testGitHubProvider(bURL.Host, options.GitHubOptions{})
118121
119122 session := CreateAuthorizedSession()
120123 err := p.getEmail(context.Background(), session)
@@ -129,7 +132,7 @@ func TestGitHubProvider_getEmailNotVerified(t *testing.T) {
129132 defer b.Close()
130133
131134 bURL, _ := url.Parse(b.URL)
132− p := testGitHubProvider(bURL.Host)
135+ p := testGitHubProvider(bURL.Host, options.GitHubOptions{})
133136
134137 session := CreateAuthorizedSession()
135138 err := p.getEmail(context.Background(), session)
@@ -149,8 +152,11 @@ func TestGitHubProvider_getEmailWithOrg(t *testing.T) {
149152 defer b.Close()
150153
151154 bURL, _ := url.Parse(b.URL)
152− p := testGitHubProvider(bURL.Host)
153− p.Org = "testorg1"
155+ p := testGitHubProvider(bURL.Host,
156+ options.GitHubOptions{
157+ Org: "testorg1",
158+ },
159+ )
154160
155161 session := CreateAuthorizedSession()
156162 err := p.getEmail(context.Background(), session)
@@ -166,8 +172,12 @@ func TestGitHubProvider_getEmailWithWriteAccessToPublicRepo(t *testing.T) {
166172 defer b.Close()
167173
168174 bURL, _ := url.Parse(b.URL)
169− p := testGitHubProvider(bURL.Host)
170− p.SetRepo("oauth2-proxy/oauth2-proxy", "")
175+ p := testGitHubProvider(bURL.Host,
176+ options.GitHubOptions{
177+ Repo: "oauth2-proxy/oauth2-proxy",
178+ Token: "token",
179+ },
180+ )
171181
172182 session := CreateAuthorizedSession()
173183 err := p.getEmail(context.Background(), session)
@@ -183,8 +193,12 @@ func TestGitHubProvider_getEmailWithReadOnlyAccessToPrivateRepo(t *testing.T) {
183193 defer b.Close()
184194
185195 bURL, _ := url.Parse(b.URL)
186− p := testGitHubProvider(bURL.Host)
187− p.SetRepo("oauth2-proxy/oauth2-proxy", "")
196+ p := testGitHubProvider(bURL.Host,
197+ options.GitHubOptions{
198+ Repo: "oauth2-proxy/oauth2-proxy",
199+ Token: "token",
200+ },
201+ )
188202
189203 session := CreateAuthorizedSession()
190204 err := p.getEmail(context.Background(), session)
@@ -200,8 +214,12 @@ func TestGitHubProvider_getEmailWithWriteAccessToPrivateRepo(t *testing.T) {
200214 defer b.Close()
201215
202216 bURL, _ := url.Parse(b.URL)
203− p := testGitHubProvider(bURL.Host)
204− p.SetRepo("oauth2-proxy/oauth2-proxy", "")
217+ p := testGitHubProvider(bURL.Host,
218+ options.GitHubOptions{
219+ Repo: "oauth2-proxy/oauth2-proxy",
220+ Token: "token",
221+ },
222+ )
205223
206224 session := CreateAuthorizedSession()
207225 err := p.getEmail(context.Background(), session)
@@ -216,8 +234,11 @@ func TestGitHubProvider_getEmailWithNoAccessToPrivateRepo(t *testing.T) {
216234 defer b.Close()
217235
218236 bURL, _ := url.Parse(b.URL)
219− p := testGitHubProvider(bURL.Host)
220− p.SetRepo("oauth2-proxy/oauth2-proxy", "")
237+ p := testGitHubProvider(bURL.Host,
238+ options.GitHubOptions{
239+ Repo: "oauth2-proxy/oauth2-proxy",
240+ },
241+ )
221242
222243 session := CreateAuthorizedSession()
223244 err := p.getEmail(context.Background(), session)
@@ -232,8 +253,12 @@ func TestGitHubProvider_getEmailWithToken(t *testing.T) {
232253 defer b.Close()
233254
234255 bURL, _ := url.Parse(b.URL)
235− p := testGitHubProvider(bURL.Host)
236− p.SetRepo("oauth2-proxy/oauth2-proxy", "token")
256+ p := testGitHubProvider(bURL.Host,
257+ options.GitHubOptions{
258+ Repo: "oauth2-proxy/oauth2-proxy",
259+ Token: "token",
260+ },
261+ )
237262
238263 session := CreateAuthorizedSession()
239264 err := p.getEmail(context.Background(), session)
@@ -248,7 +273,7 @@ func TestGitHubProvider_getEmailFailedRequest(t *testing.T) {
248273 defer b.Close()
249274
250275 bURL, _ := url.Parse(b.URL)
251− p := testGitHubProvider(bURL.Host)
276+ p := testGitHubProvider(bURL.Host, options.GitHubOptions{})
252277
253278 // We'll trigger a request failure by using an unexpected access
254279 // token. Alternatively, we could allow the parsing of the payload as
@@ -266,7 +291,7 @@ func TestGitHubProvider_getEmailNotPresentInPayload(t *testing.T) {
266291 defer b.Close()
267292
268293 bURL, _ := url.Parse(b.URL)
269− p := testGitHubProvider(bURL.Host)
294+ p := testGitHubProvider(bURL.Host, options.GitHubOptions{})
270295
271296 session := CreateAuthorizedSession()
272297 err := p.getEmail(context.Background(), session)
@@ -281,7 +306,7 @@ func TestGitHubProvider_getUser(t *testing.T) {
281306 defer b.Close()
282307
283308 bURL, _ := url.Parse(b.URL)
284− p := testGitHubProvider(bURL.Host)
309+ p := testGitHubProvider(bURL.Host, options.GitHubOptions{})
285310
286311 session := CreateAuthorizedSession()
287312 err := p.getUser(context.Background(), session)
@@ -297,8 +322,12 @@ func TestGitHubProvider_getUserWithRepoAndToken(t *testing.T) {
297322 defer b.Close()
298323
299324 bURL, _ := url.Parse(b.URL)
300− p := testGitHubProvider(bURL.Host)
301− p.SetRepo("oauth2-proxy/oauth2-proxy", "token")
325+ p := testGitHubProvider(bURL.Host,
326+ options.GitHubOptions{
327+ Repo: "oauth2-proxy/oauth2-proxy",
328+ Token: "token",
329+ },
330+ )
302331
303332 session := CreateAuthorizedSession()
304333 err := p.getUser(context.Background(), session)
@@ -311,8 +340,12 @@ func TestGitHubProvider_getUserWithRepoAndTokenWithoutPushAccess(t *testing.T) {
311340 defer b.Close()
312341
313342 bURL, _ := url.Parse(b.URL)
314− p := testGitHubProvider(bURL.Host)
315− p.SetRepo("oauth2-proxy/oauth2-proxy", "token")
343+ p := testGitHubProvider(bURL.Host,
344+ options.GitHubOptions{
345+ Repo: "oauth2-proxy/oauth2-proxy",
346+ Token: "token",
347+ },
348+ )
316349
317350 session := CreateAuthorizedSession()
318351 err := p.getUser(context.Background(), session)
@@ -328,8 +361,11 @@ func TestGitHubProvider_getEmailWithUsername(t *testing.T) {
328361 defer b.Close()
329362
330363 bURL, _ := url.Parse(b.URL)
331− p := testGitHubProvider(bURL.Host)
332− p.SetUsers([]string{"mbland", "octocat"})
364+ p := testGitHubProvider(bURL.Host,
365+ options.GitHubOptions{
366+ Users: []string{"mbland", "octocat"},
367+ },
368+ )
333369
334370 session := CreateAuthorizedSession()
335371 err := p.getEmail(context.Background(), session)
@@ -345,8 +381,11 @@ func TestGitHubProvider_getEmailWithNotAllowedUsername(t *testing.T) {
345381 defer b.Close()
346382
347383 bURL, _ := url.Parse(b.URL)
348− p := testGitHubProvider(bURL.Host)
349− p.SetUsers([]string{"octocat"})
384+ p := testGitHubProvider(bURL.Host,
385+ options.GitHubOptions{
386+ Users: []string{"octocat"},
387+ },
388+ )
350389
351390 session := CreateAuthorizedSession()
352391 err := p.getEmail(context.Background(), session)
@@ -366,9 +405,12 @@ func TestGitHubProvider_getEmailWithUsernameAndNotBelongToOrg(t *testing.T) {
366405 defer b.Close()
367406
368407 bURL, _ := url.Parse(b.URL)
369− p := testGitHubProvider(bURL.Host)
370− p.SetOrgTeam("not_belong_to", "")
371− p.SetUsers([]string{"mbland"})
408+ p := testGitHubProvider(bURL.Host,
409+ options.GitHubOptions{
410+ Org: "not_belog_to",
411+ Users: []string{"mbland"},
412+ },
413+ )
372414
373415 session := CreateAuthorizedSession()
374416 err := p.getEmail(context.Background(), session)
@@ -385,9 +427,13 @@ func TestGitHubProvider_getEmailWithUsernameAndNoAccessToPrivateRepo(t *testing.
385427 defer b.Close()
386428
387429 bURL, _ := url.Parse(b.URL)
388− p := testGitHubProvider(bURL.Host)
389− p.SetRepo("oauth2-proxy/oauth2-proxy", "")
390− p.SetUsers([]string{"mbland"})
430+ p := testGitHubProvider(bURL.Host,
431+ options.GitHubOptions{
432+ Repo: "oauth2-proxy/oauth2-proxy",
433+ Token: "token",
434+ Users: []string{"mbland"},
435+ },
436+ )
391437
392438 session := CreateAuthorizedSession()
393439 err := p.getEmail(context.Background(), session)
providers/gitlab.go+12−4
@@ -7,6 +7,7 @@ import (
77 "strconv"
88 "strings"
99
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1011 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1112 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1213 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
@@ -30,7 +31,7 @@ type GitLabProvider struct {
3031var _ Provider = (*GitLabProvider)(nil)
3132
3233// NewGitLabProvider initiates a new GitLabProvider
33−func NewGitLabProvider(p *ProviderData) *GitLabProvider {
34+func NewGitLabProvider(p *ProviderData, opts options.GitLabOptions) (*GitLabProvider, error) {
3435 p.ProviderName = gitlabProviderName
3536 if p.Scope == "" {
3637 p.Scope = gitlabDefaultScope
@@ -41,15 +42,22 @@ func NewGitLabProvider(p *ProviderData) *GitLabProvider {
4142 SkipNonce: false,
4243 }
4344
44− return &GitLabProvider{
45+ provider := &GitLabProvider{
4546 OIDCProvider: oidcProvider,
4647 oidcRefreshFunc: oidcProvider.RefreshSession,
4748 }
49+ provider.setAllowedGroups(opts.Group)
50+
51+ if err := provider.setAllowedProjects(opts.Projects); err != nil {
52+ return nil, fmt.Errorf("could not configure allowed projects: %v", err)
53+ }
54+
55+ return provider, nil
4856}
4957
50−// SetAllowedProjects adds Gitlab projects to the AllowedGroups list
58+// setAllowedProjects adds Gitlab projects to the AllowedGroups list
5159// and tracks them to do a project API lookup during `EnrichSession`.
52−func (p *GitLabProvider) SetAllowedProjects(projects []string) error {
60+func (p *GitLabProvider) setAllowedProjects(projects []string) error {
5361 for _, project := range projects {
5462 gp, err := newGitlabProject(project)
5563 if err != nil {
providers/gitlab_test.go+22−14
@@ -7,21 +7,26 @@ import (
77 "net/http/httptest"
88 "net/url"
99
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1011 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1112 . "github.com/onsi/ginkgo"
1213 . "github.com/onsi/ginkgo/extensions/table"
1314 . "github.com/onsi/gomega"
1415)
1516
16−func testGitLabProvider(hostname string) *GitLabProvider {
17− p := NewGitLabProvider(
17+func testGitLabProvider(hostname, scope string, opts options.GitLabOptions) (*GitLabProvider, error) {
18+ p, err := NewGitLabProvider(
1819 &ProviderData{
1920 ProviderName: "",
2021 LoginURL: &url.URL{},
2122 RedeemURL: &url.URL{},
2223 ProfileURL: &url.URL{},
2324 ValidateURL: &url.URL{},
24− Scope: ""})
25+ Scope: scope},
26+ opts)
27+ if err != nil {
28+ return nil, err
29+ }
2530 if hostname != "" {
2631 updateURL(p.Data().LoginURL, hostname)
2732 updateURL(p.Data().RedeemURL, hostname)
@@ -29,7 +34,7 @@ func testGitLabProvider(hostname string) *GitLabProvider {
2934 updateURL(p.Data().ValidateURL, hostname)
3035 }
3136
32− return p
37+ return p, err
3338}
3439
3540func testGitLabBackend() *httptest.Server {
@@ -157,7 +162,8 @@ var _ = Describe("Gitlab Provider Tests", func() {
157162 bURL, err := url.Parse(b.URL)
158163 Expect(err).To(BeNil())
159164
160− p = testGitLabProvider(bURL.Host)
165+ p, err = testGitLabProvider(bURL.Host, "", options.GitLabOptions{})
166+ Expect(err).ToNot(HaveOccurred())
161167 })
162168
163169 AfterEach(func() {
@@ -219,21 +225,23 @@ var _ = Describe("Gitlab Provider Tests", func() {
219225
220226 DescribeTable("should return expected results",
221227 func(in entitiesTableInput) {
222− p.AllowUnverifiedEmail = true
223− if in.scope != "" {
224− p.Scope = in.scope
225− }
226− session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
227−
228− p.SetAllowedGroups(in.allowedGroups)
228+ bURL, err := url.Parse(b.URL)
229+ Expect(err).To(BeNil())
229230
230− err := p.SetAllowedProjects(in.allowedProjects)
231+ p, err := testGitLabProvider(bURL.Host, in.scope, options.GitLabOptions{
232+ Group: in.allowedGroups,
233+ Projects: in.allowedProjects,
234+ })
231235 if in.expectedError == nil {
232236 Expect(err).To(BeNil())
233237 } else {
234238 Expect(err).To(MatchError(in.expectedError))
235239 return
236240 }
241+
242+ p.AllowUnverifiedEmail = true
243+ session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
244+
237245 Expect(p.Scope).To(Equal(in.expectedScope))
238246
239247 err = p.EnrichSession(context.Background(), session)
@@ -302,7 +310,7 @@ var _ = Describe("Gitlab Provider Tests", func() {
302310 }),
303311 Entry("invalid project format", entitiesTableInput{
304312 allowedProjects: []string{"my_group/my_invalid_project=123"},
305− expectedError: errors.New("invalid gitlab project access level specified (my_group/my_invalid_project)"),
313+ expectedError: errors.New("could not configure allowed projects: invalid gitlab project access level specified (my_group/my_invalid_project)"),
306314 expectedScope: "openid email read_api",
307315 }),
308316 )
providers/google.go+20−3
@@ -10,9 +10,11 @@ import (
1010 "io"
1111 "io/ioutil"
1212 "net/url"
13+ "os"
1314 "strings"
1415 "time"
1516
17+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1618 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1719 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1820 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
@@ -79,7 +81,7 @@ var (
7981)
8082
8183// NewGoogleProvider initiates a new GoogleProvider
82−func NewGoogleProvider(p *ProviderData) *GoogleProvider {
84+func NewGoogleProvider(p *ProviderData, opts options.GoogleOptions) (*GoogleProvider, error) {
8385 p.setProviderDefaults(providerDefaults{
8486 name: googleProviderName,
8587 loginURL: googleDefaultLoginURL,
@@ -88,7 +90,7 @@ func NewGoogleProvider(p *ProviderData) *GoogleProvider {
8890 validateURL: googleDefaultValidateURL,
8991 scope: googleDefaultScope,
9092 })
91− return &GoogleProvider{
93+ provider := &GoogleProvider{
9294 ProviderData: p,
9395 // Set a default groupValidator to just always return valid (true), it will
9496 // be overwritten if we configured a Google group restriction.
@@ -96,6 +98,21 @@ func NewGoogleProvider(p *ProviderData) *GoogleProvider {
9698 return true
9799 },
98100 }
101+
102+ if opts.ServiceAccountJSON != "" {
103+ file, err := os.Open(opts.ServiceAccountJSON)
104+ if err != nil {
105+ return nil, fmt.Errorf("invalid Google credentials file: %s", opts.ServiceAccountJSON)
106+ }
107+
108+ // Backwards compatibility with `--google-group` option
109+ if len(opts.Groups) > 0 {
110+ provider.setAllowedGroups(opts.Groups)
111+ }
112+ provider.setGroupRestriction(opts.Groups, opts.AdminEmail, file)
113+ }
114+
115+ return provider, nil
99116}
100117
101118func claimsFromIDToken(idToken string) (*claims, error) {
@@ -195,7 +212,7 @@ func (p *GoogleProvider) EnrichSession(_ context.Context, s *sessions.SessionSta
195212// account credentials.
196213//
197214// TODO (@NickMeves) - Unit Test this OR refactor away from groupValidator func
198−func (p *GoogleProvider) SetGroupRestriction(groups []string, adminEmail string, credentialsReader io.Reader) {
215+func (p *GoogleProvider) setGroupRestriction(groups []string, adminEmail string, credentialsReader io.Reader) {
199216 adminService := getAdminService(adminEmail, credentialsReader)
200217 p.groupValidator = func(s *sessions.SessionState) bool {
201218 // Reset our saved Groups in case membership changed
providers/google_test.go+22−12
@@ -10,6 +10,7 @@ import (
1010 "net/url"
1111 "testing"
1212
13+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1314 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1415 . "github.com/onsi/gomega"
1516 "github.com/stretchr/testify/assert"
@@ -25,22 +26,29 @@ func newRedeemServer(body []byte) (*url.URL, *httptest.Server) {
2526 return u, s
2627}
2728
28−func newGoogleProvider() *GoogleProvider {
29− return NewGoogleProvider(
29+func newGoogleProvider(t *testing.T) *GoogleProvider {
30+ g := NewWithT(t)
31+ p, err := NewGoogleProvider(
3032 &ProviderData{
3133 ProviderName: "",
3234 LoginURL: &url.URL{},
3335 RedeemURL: &url.URL{},
3436 ProfileURL: &url.URL{},
3537 ValidateURL: &url.URL{},
36− Scope: ""})
38+ Scope: ""},
39+ options.GoogleOptions{})
40+ g.Expect(err).ToNot(HaveOccurred())
41+ return p
3742}
3843
3944func TestNewGoogleProvider(t *testing.T) {
4045 g := NewWithT(t)
4146
4247 // Test that defaults are set when calling for a new provider with nothing set
43− providerData := NewGoogleProvider(&ProviderData{}).Data()
48+ provider, err := NewGoogleProvider(&ProviderData{}, options.GoogleOptions{})
49+ g.Expect(err).ToNot(HaveOccurred())
50+ providerData := provider.Data()
51+
4452 g.Expect(providerData.ProviderName).To(Equal("Google"))
4553 g.Expect(providerData.LoginURL.String()).To(Equal("https://accounts.google.com/o/oauth2/auth?access_type=offline"))
4654 g.Expect(providerData.RedeemURL.String()).To(Equal("https://www.googleapis.com/oauth2/v3/token"))
@@ -50,7 +58,7 @@ func TestNewGoogleProvider(t *testing.T) {
5058}
5159
5260func TestGoogleProviderOverrides(t *testing.T) {
53− p := NewGoogleProvider(
61+ p, err := NewGoogleProvider(
5462 &ProviderData{
5563 LoginURL: &url.URL{
5664 Scheme: "https",
@@ -68,7 +76,9 @@ func TestGoogleProviderOverrides(t *testing.T) {
6876 Scheme: "https",
6977 Host: "example.com",
7078 Path: "/oauth/tokeninfo"},
71− Scope: "profile"})
79+ Scope: "profile"},
80+ options.GoogleOptions{})
81+ assert.NoError(t, err)
7282 assert.NotEqual(t, nil, p)
7383 assert.Equal(t, "Google", p.Data().ProviderName)
7484 assert.Equal(t, "https://example.com/oauth/auth",
@@ -90,7 +100,7 @@ type redeemResponse struct {
90100}
91101
92102func TestGoogleProviderGetEmailAddress(t *testing.T) {
93− p := newGoogleProvider()
103+ p := newGoogleProvider(t)
94104 body, err := json.Marshal(redeemResponse{
95105 AccessToken: "a1234",
96106 ExpiresIn: 10,
@@ -147,7 +157,7 @@ func TestGoogleProviderGroupValidator(t *testing.T) {
147157 for name, tc := range testCases {
148158 t.Run(name, func(t *testing.T) {
149159 g := NewWithT(t)
150− p := newGoogleProvider()
160+ p := newGoogleProvider(t)
151161 if tc.validatorFunc != nil {
152162 p.groupValidator = tc.validatorFunc
153163 }
@@ -158,7 +168,7 @@ func TestGoogleProviderGroupValidator(t *testing.T) {
158168
159169//
160170func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) {
161− p := newGoogleProvider()
171+ p := newGoogleProvider(t)
162172 body, err := json.Marshal(redeemResponse{
163173 AccessToken: "a1234",
164174 IDToken: "ignored prefix." + `{"email": "michael.bland@gsa.gov"}`,
@@ -176,7 +186,7 @@ func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) {
176186}
177187
178188func TestGoogleProviderRedeemFailsNoCLientSecret(t *testing.T) {
179− p := newGoogleProvider()
189+ p := newGoogleProvider(t)
180190 p.ProviderData.ClientSecretFile = "srvnoerre"
181191
182192 session, err := p.Redeem(context.Background(), "http://redirect/", "code1234")
@@ -188,7 +198,7 @@ func TestGoogleProviderRedeemFailsNoCLientSecret(t *testing.T) {
188198}
189199
190200func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) {
191− p := newGoogleProvider()
201+ p := newGoogleProvider(t)
192202
193203 body, err := json.Marshal(redeemResponse{
194204 AccessToken: "a1234",
@@ -208,7 +218,7 @@ func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) {
208218}
209219
210220func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
211− p := newGoogleProvider()
221+ p := newGoogleProvider(t)
212222 body, err := json.Marshal(redeemResponse{
213223 AccessToken: "a1234",
214224 IDToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"not_email": "missing"}`)),
providers/keycloak.go+6−2
@@ -5,6 +5,7 @@ import (
55 "fmt"
66 "net/url"
77
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
89 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
910 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1011 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
@@ -48,7 +49,7 @@ var (
4849)
4950
5051// NewKeycloakProvider creates a KeyCloakProvider using the passed ProviderData
51−func NewKeycloakProvider(p *ProviderData) *KeycloakProvider {
52+func NewKeycloakProvider(p *ProviderData, opts options.KeycloakOptions) *KeycloakProvider {
5253 p.setProviderDefaults(providerDefaults{
5354 name: keycloakProviderName,
5455 loginURL: keycloakDefaultLoginURL,
@@ -57,7 +58,10 @@ func NewKeycloakProvider(p *ProviderData) *KeycloakProvider {
5758 validateURL: keycloakDefaultValidateURL,
5859 scope: keycloakDefaultScope,
5960 })
60− return &KeycloakProvider{ProviderData: p}
61+
62+ provider := &KeycloakProvider{ProviderData: p}
63+ provider.setAllowedGroups(opts.Groups)
64+ return provider
6165}
6266
6367// EnrichSession uses the Keycloak userinfo endpoint to populate the session's
providers/keycloak_oidc.go+9−4
@@ -4,6 +4,7 @@ import (
44 "context"
55 "fmt"
66
7+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
78 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
89)
910
@@ -15,21 +16,25 @@ type KeycloakOIDCProvider struct {
1516}
1617
1718// NewKeycloakOIDCProvider makes a KeycloakOIDCProvider using the ProviderData
18−func NewKeycloakOIDCProvider(p *ProviderData) *KeycloakOIDCProvider {
19+func NewKeycloakOIDCProvider(p *ProviderData, opts options.KeycloakOptions) *KeycloakOIDCProvider {
1920 p.ProviderName = keycloakOIDCProviderName
20− return &KeycloakOIDCProvider{
21+
22+ provider := &KeycloakOIDCProvider{
2123 OIDCProvider: &OIDCProvider{
2224 ProviderData: p,
2325 },
2426 }
27+
28+ provider.addAllowedRoles(opts.Roles)
29+ return provider
2530}
2631
2732var _ Provider = (*KeycloakOIDCProvider)(nil)
2833
29−// AddAllowedRoles sets Keycloak roles that are authorized.
34+// addAllowedRoles sets Keycloak roles that are authorized.
3035// Assumes `SetAllowedGroups` is already called on groups and appends to that
3136// with `role:` prefixed roles.
32−func (p *KeycloakOIDCProvider) AddAllowedRoles(roles []string) {
37+func (p *KeycloakOIDCProvider) addAllowedRoles(roles []string) {
3338 if p.AllowedGroups == nil {
3439 p.AllowedGroups = make(map[string]struct{})
3540 }
providers/keycloak_oidc_test.go+9−6
@@ -9,6 +9,7 @@ import (
99
1010 "github.com/coreos/go-oidc/v3/oidc"
1111
12+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1213 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1314 . "github.com/onsi/ginkgo"
1415 . "github.com/onsi/gomega"
@@ -39,11 +40,11 @@ func getAccessToken() string {
3940
4041func newTestKeycloakOIDCSetup() (*httptest.Server, *KeycloakOIDCProvider) {
4142 redeemURL, server := newOIDCServer([]byte(fmt.Sprintf(`{"email": "new@thing.com", "expires_in": 300, "access_token": "%v"}`, getAccessToken())))
42− provider := newKeycloakOIDCProvider(redeemURL)
43+ provider := newKeycloakOIDCProvider(redeemURL, options.KeycloakOptions{})
4344 return server, provider
4445}
4546
46−func newKeycloakOIDCProvider(serverURL *url.URL) *KeycloakOIDCProvider {
47+func newKeycloakOIDCProvider(serverURL *url.URL, opts options.KeycloakOptions) *KeycloakOIDCProvider {
4748 verificationOptions := &internaloidc.IDTokenVerificationOptions{
4849 AudienceClaims: []string{defaultAudienceClaim},
4950 ClientID: mockClientID,
@@ -66,7 +67,8 @@ func newKeycloakOIDCProvider(serverURL *url.URL) *KeycloakOIDCProvider {
6667 Scheme: "https",
6768 Host: "keycloak-oidc.com",
6869 Path: "/api/v3/user"},
69− Scope: "openid email profile"})
70+ Scope: "openid email profile"},
71+ opts)
7072
7173 if serverURL != nil {
7274 p.RedeemURL.Scheme = serverURL.Scheme
@@ -88,7 +90,7 @@ func newKeycloakOIDCProvider(serverURL *url.URL) *KeycloakOIDCProvider {
8890var _ = Describe("Keycloak OIDC Provider Tests", func() {
8991 Context("New Provider Init", func() {
9092 It("creates new keycloak oidc provider with expected defaults", func() {
91− p := newKeycloakOIDCProvider(nil)
93+ p := newKeycloakOIDCProvider(nil, options.KeycloakOptions{})
9294 providerData := p.Data()
9395 Expect(providerData.ProviderName).To(Equal(keycloakOIDCProviderName))
9496 Expect(providerData.LoginURL.String()).To(Equal("https://keycloak-oidc.com/oauth/auth"))
@@ -101,8 +103,9 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
101103
102104 Context("Allowed Roles", func() {
103105 It("should prefix allowed roles and add them to groups", func() {
104− p := newKeycloakOIDCProvider(nil)
105− p.AddAllowedRoles([]string{"admin", "editor"})
106+ p := newKeycloakOIDCProvider(nil, options.KeycloakOptions{
107+ Roles: []string{"admin", "editor"},
108+ })
106109 Expect(p.AllowedGroups).To(HaveKey("role:admin"))
107110 Expect(p.AllowedGroups).To(HaveKey("role:editor"))
108111 })
providers/keycloak_test.go+6−3
@@ -8,6 +8,7 @@ import (
88 "net/http/httptest"
99 "net/url"
1010
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1112 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1213 . "github.com/onsi/ginkgo"
1314 . "github.com/onsi/ginkgo/extensions/table"
@@ -27,7 +28,8 @@ func testKeycloakProvider(backend *httptest.Server) (*KeycloakProvider, error) {
2728 RedeemURL: &url.URL{},
2829 ProfileURL: &url.URL{},
2930 ValidateURL: &url.URL{},
30− Scope: ""})
31+ Scope: ""},
32+ options.KeycloakOptions{})
3133
3234 if backend != nil {
3335 bURL, err := url.Parse(backend.URL)
@@ -48,7 +50,7 @@ func testKeycloakProvider(backend *httptest.Server) (*KeycloakProvider, error) {
4850var _ = Describe("Keycloak Provider Tests", func() {
4951 Context("New Provider Init", func() {
5052 It("uses defaults", func() {
51− providerData := NewKeycloakProvider(&ProviderData{}).Data()
53+ providerData := NewKeycloakProvider(&ProviderData{}, options.KeycloakOptions{}).Data()
5254 Expect(providerData.ProviderName).To(Equal("Keycloak"))
5355 Expect(providerData.LoginURL.String()).To(Equal("https://keycloak.org/oauth/authorize"))
5456 Expect(providerData.RedeemURL.String()).To(Equal("https://keycloak.org/oauth/token"))
@@ -76,7 +78,8 @@ var _ = Describe("Keycloak Provider Tests", func() {
7678 Scheme: "https",
7779 Host: "example.com",
7880 Path: "/api/v3/user"},
79− Scope: "profile"})
81+ Scope: "profile"},
82+ options.KeycloakOptions{})
8083 providerData := p.Data()
8184
8285 Expect(providerData.ProviderName).To(Equal("Keycloak"))
providers/logingov.go+45−2
@@ -5,12 +5,15 @@ import (
55 "context"
66 "crypto/rand"
77 "crypto/rsa"
8+ "errors"
89 "fmt"
10+ "io/ioutil"
911 "math/big"
1012 "net/url"
1113 "time"
1214
1315 "github.com/golang-jwt/jwt"
16+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1417 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1518 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
1619 "gopkg.in/square/go-jose.v2"
@@ -78,7 +81,7 @@ var (
7881)
7982
8083// NewLoginGovProvider initiates a new LoginGovProvider
81−func NewLoginGovProvider(p *ProviderData) *LoginGovProvider {
84+func NewLoginGovProvider(p *ProviderData, opts options.LoginGovOptions) (*LoginGovProvider, error) {
8285 p.setProviderDefaults(providerDefaults{
8386 name: loginGovProviderName,
8487 loginURL: loginGovDefaultLoginURL,
@@ -87,10 +90,50 @@ func NewLoginGovProvider(p *ProviderData) *LoginGovProvider {
8790 validateURL: loginGovDefaultProfileURL,
8891 scope: loginGovDefaultScope,
8992 })
90− return &LoginGovProvider{
93+ provider := &LoginGovProvider{
9194 ProviderData: p,
9295 Nonce: randSeq(32),
9396 }
97+
98+ if err := provider.configure(opts); err != nil {
99+ return nil, fmt.Errorf("could not configure login.gov provider: %v", err)
100+ }
101+ return provider, nil
102+}
103+
104+func (p *LoginGovProvider) configure(opts options.LoginGovOptions) error {
105+ pubJWKURL, err := url.Parse(opts.PubJWKURL)
106+ if err != nil {
107+ return fmt.Errorf("could not parse Public JWK URL: %v", err)
108+ }
109+ p.PubJWKURL = pubJWKURL
110+
111+ // JWT key can be supplied via env variable or file in the filesystem, but not both.
112+ switch {
113+ case opts.JWTKey != "" && opts.JWTKeyFile != "":
114+ return errors.New("cannot set both jwt-key and jwt-key-file options")
115+ case opts.JWTKey == "" && opts.JWTKeyFile == "":
116+ return errors.New("login.gov provider requires a private key for signing JWTs")
117+ case opts.JWTKey != "":
118+ // The JWT Key is in the commandline argument
119+ signKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(opts.JWTKey))
120+ if err != nil {
121+ return fmt.Errorf("could not parse RSA Private Key PEM: %v", err)
122+ }
123+ p.JWTKey = signKey
124+ case opts.JWTKeyFile != "":
125+ // The JWT key is in the filesystem
126+ keyData, err := ioutil.ReadFile(opts.JWTKeyFile)
127+ if err != nil {
128+ return fmt.Errorf("could not read key file: %v", opts.JWTKeyFile)
129+ }
130+ signKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyData)
131+ if err != nil {
132+ return fmt.Errorf("could not parse private key from PEM file: %v", opts.JWTKeyFile)
133+ }
134+ p.JWTKey = signKey
135+ }
136+ return nil
94137}
95138
96139type loginGovCustomClaims struct {
providers/logingov_test.go+57−12
@@ -1,11 +1,14 @@
11package providers
22
33import (
4+ "bytes"
45 "context"
56 "crypto"
67 "crypto/rand"
78 "crypto/rsa"
9+ "crypto/x509"
810 "encoding/json"
11+ "encoding/pem"
912 "net/http"
1013 "net/http/httptest"
1114 "net/url"
@@ -13,6 +16,7 @@ import (
1316 "time"
1417
1518 "github.com/golang-jwt/jwt"
19+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1620 . "github.com/onsi/gomega"
1721 "github.com/stretchr/testify/assert"
1822 "gopkg.in/square/go-jose.v2"
@@ -32,12 +36,35 @@ func newLoginGovServer(body []byte) (*url.URL, *httptest.Server) {
3236 return u, s
3337}
3438
35−func newLoginGovProvider() (l *LoginGovProvider, serverKey *MyKeyData, err error) {
39+func newPrivateKeyBytes() ([]byte, error) {
40+ privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
41+ if err != nil {
42+ return nil, err
43+ }
44+
45+ keyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
46+ if err != nil {
47+ return nil, err
48+ }
49+
50+ privateKeyBlock := &pem.Block{
51+ Type: "RSA PRIVATE KEY",
52+ Bytes: keyBytes,
53+ }
54+ b := &bytes.Buffer{}
55+ if err := pem.Encode(b, privateKeyBlock); err != nil {
56+ return nil, err
57+ }
58+
59+ return b.Bytes(), nil
60+}
61+
62+func newLoginGovProvider() (*LoginGovProvider, *MyKeyData, error) {
3663 key, err := rsa.GenerateKey(rand.Reader, 2048)
3764 if err != nil {
38− return
65+ return nil, nil, err
3966 }
40− serverKey = &MyKeyData{
67+ serverKey := &MyKeyData{
4168 PubKey: key.Public(),
4269 PrivKey: key,
4370 PubJWK: jose.JSONWebKey{
@@ -48,29 +75,40 @@ func newLoginGovProvider() (l *LoginGovProvider, serverKey *MyKeyData, err error
4875 },
4976 }
5077
51− privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
78+ privKey, err := newPrivateKeyBytes()
5279 if err != nil {
53− return
80+ return nil, nil, err
5481 }
5582
56− l = NewLoginGovProvider(
83+ l, err := NewLoginGovProvider(
5784 &ProviderData{
5885 ProviderName: "",
5986 LoginURL: &url.URL{},
6087 RedeemURL: &url.URL{},
6188 ProfileURL: &url.URL{},
6289 ValidateURL: &url.URL{},
63− Scope: ""})
64− l.JWTKey = privateKey
90+ Scope: ""},
91+ options.LoginGovOptions{
92+ JWTKey: string(privKey),
93+ },
94+ )
6595 l.Nonce = "fakenonce"
66− return
96+ return l, serverKey, err
6797}
6898
6999func TestNewLoginGovProvider(t *testing.T) {
70100 g := NewWithT(t)
71101
102+ privKey, err := newPrivateKeyBytes()
103+ g.Expect(err).ToNot(HaveOccurred())
104+
72105 // Test that defaults are set when calling for a new provider with nothing set
73− providerData := NewLoginGovProvider(&ProviderData{}).Data()
106+ provider, err := NewLoginGovProvider(&ProviderData{}, options.LoginGovOptions{
107+ JWTKey: string(privKey),
108+ })
109+ g.Expect(err).ToNot(HaveOccurred())
110+
111+ providerData := provider.Data()
74112 g.Expect(providerData.ProviderName).To(Equal("login.gov"))
75113 g.Expect(providerData.LoginURL.String()).To(Equal("https://secure.login.gov/openid_connect/authorize"))
76114 g.Expect(providerData.RedeemURL.String()).To(Equal("https://secure.login.gov/api/openid_connect/token"))
@@ -80,7 +118,10 @@ func TestNewLoginGovProvider(t *testing.T) {
80118}
81119
82120func TestLoginGovProviderOverrides(t *testing.T) {
83− p := NewLoginGovProvider(
121+ privKey, err := newPrivateKeyBytes()
122+ assert.NoError(t, err)
123+
124+ p, err := NewLoginGovProvider(
84125 &ProviderData{
85126 LoginURL: &url.URL{
86127 Scheme: "https",
@@ -94,7 +135,11 @@ func TestLoginGovProviderOverrides(t *testing.T) {
94135 Scheme: "https",
95136 Host: "example.com",
96137 Path: "/oauth/profile"},
97− Scope: "profile"})
138+ Scope: "profile"},
139+ options.LoginGovOptions{
140+ JWTKey: string(privKey),
141+ })
142+ assert.NoError(t, err)
98143 assert.NotEqual(t, nil, p)
99144 assert.Equal(t, "login.gov", p.Data().ProviderName)
100145 assert.Equal(t, "https://example.com/oauth/auth",
providers/nextcloud.go+3−1
@@ -1,5 +1,7 @@
11package providers
22
3+import "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
4+
35// NextcloudProvider represents an Nextcloud based Identity Provider
46type NextcloudProvider struct {
57 *ProviderData
@@ -13,7 +15,7 @@ const nextCloudProviderName = "Nextcloud"
1315func NewNextcloudProvider(p *ProviderData) *NextcloudProvider {
1416 p.ProviderName = nextCloudProviderName
1517 p.getAuthorizationHeaderFunc = makeOIDCHeader
16− if p.EmailClaim == OIDCEmailClaim {
18+ if p.EmailClaim == options.OIDCEmailClaim {
1719 // This implies the email claim has not been overridden, we should set a default
1820 // for this provider
1921 p.EmailClaim = "ocs.data.email"
providers/oidc.go+3−2
@@ -7,6 +7,7 @@ import (
77 "net/url"
88 "time"
99
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1011 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1112 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1213 "golang.org/x/oauth2"
@@ -20,13 +21,13 @@ type OIDCProvider struct {
2021}
2122
2223// NewOIDCProvider initiates a new OIDCProvider
23−func NewOIDCProvider(p *ProviderData) *OIDCProvider {
24+func NewOIDCProvider(p *ProviderData, opts options.OIDCOptions) *OIDCProvider {
2425 p.ProviderName = "OpenID Connect"
2526 p.getAuthorizationHeaderFunc = makeOIDCHeader
2627
2728 return &OIDCProvider{
2829 ProviderData: p,
29− SkipNonce: true,
30+ SkipNonce: opts.InsecureSkipNonce,
3031 }
3132}
3233
providers/oidc_test.go+7−4
@@ -11,6 +11,7 @@ import (
1111 "testing"
1212
1313 "github.com/coreos/go-oidc/v3/oidc"
14+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1415 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1516 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
1617 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
@@ -25,7 +26,7 @@ type redeemTokenResponse struct {
2526 IDToken string `json:"id_token,omitempty"`
2627}
2728
28−func newOIDCProvider(serverURL *url.URL) *OIDCProvider {
29+func newOIDCProvider(serverURL *url.URL, skipNonce bool) *OIDCProvider {
2930 verificationOptions := &internaloidc.IDTokenVerificationOptions{
3031 AudienceClaims: []string{"aud"},
3132 ClientID: "https://test.myapp.com",
@@ -61,7 +62,9 @@ func newOIDCProvider(serverURL *url.URL) *OIDCProvider {
6162 ), verificationOptions),
6263 }
6364
64− p := NewOIDCProvider(providerData)
65+ p := NewOIDCProvider(providerData, options.OIDCOptions{
66+ InsecureSkipNonce: skipNonce,
67+ })
6568
6669 return p
6770}
@@ -77,7 +80,7 @@ func newOIDCServer(body []byte) (*url.URL, *httptest.Server) {
7780
7881func newTestOIDCSetup(body []byte) (*httptest.Server, *OIDCProvider) {
7982 redeemURL, server := newOIDCServer(body)
80− provider := newOIDCProvider(redeemURL)
83+ provider := newOIDCProvider(redeemURL, false)
8184 return server, provider
8285}
8386
@@ -86,7 +89,7 @@ func TestOIDCProviderGetLoginURL(t *testing.T) {
8689 Scheme: "https",
8790 Host: "oauth2proxy.oidctest",
8891 }
89− provider := newOIDCProvider(serverURL)
92+ provider := newOIDCProvider(serverURL, true)
9093
9194 n, err := encryption.Nonce()
9295 assert.NoError(t, err)
providers/provider_data.go+4−7
@@ -10,6 +10,7 @@ import (
1010 "strings"
1111
1212 "github.com/coreos/go-oidc/v3/oidc"
13+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1314 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1415 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1516 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
@@ -18,14 +19,10 @@ import (
1819)
1920
2021const (
21− OIDCEmailClaim = "email"
22− OIDCGroupsClaim = "groups"
2322 // This is not exported as it's not currently user configurable
2423 oidcUserClaim = "sub"
2524)
2625
27−var OIDCAudienceClaims = []string{"aud"}
28−
2926// ProviderData contains information required to configure all implementations
3027// of OAuth2 providers
3128type ProviderData struct {
@@ -76,9 +73,9 @@ func (p *ProviderData) GetClientSecret() (clientSecret string, err error) {
7673 return string(fileClientSecret), nil
7774}
7875
79−// SetAllowedGroups organizes a group list into the AllowedGroups map
76+// setAllowedGroups organizes a group list into the AllowedGroups map
8077// to be consumed by Authorize implementations
81−func (p *ProviderData) SetAllowedGroups(groups []string) {
78+func (p *ProviderData) setAllowedGroups(groups []string) {
8279 p.AllowedGroups = make(map[string]struct{}, len(groups))
8380 for _, group := range groups {
8481 p.AllowedGroups[group] = struct{}{}
@@ -172,7 +169,7 @@ func (p *ProviderData) buildSessionFromClaims(rawIDToken, accessToken string) (*
172169
173170 // `email_verified` must be present and explicitly set to `false` to be
174171 // considered unverified.
175− verifyEmail := (p.EmailClaim == OIDCEmailClaim) && !p.AllowUnverifiedEmail
172+ verifyEmail := (p.EmailClaim == options.OIDCEmailClaim) && !p.AllowUnverifiedEmail
176173
177174 var verified bool
178175 exists, err := extractor.GetClaimInto("email_verified", &verified)
providers/provider_default_test.go+1−1
@@ -107,7 +107,7 @@ func TestProviderDataAuthorize(t *testing.T) {
107107 Groups: tc.groups,
108108 }
109109 p := &ProviderData{}
110− p.SetAllowedGroups(tc.allowedGroups)
110+ p.setAllowedGroups(tc.allowedGroups)
111111
112112 authorized, err := p.Authorize(context.Background(), session)
113113 g.Expect(err).ToNot(HaveOccurred())
providers/providers.go+251−32
@@ -2,8 +2,18 @@ package providers
22
33import (
44 "context"
5+ "errors"
6+ "fmt"
7+ "net/url"
8+ "strings"
59
10+ "github.com/coreos/go-oidc/v3/oidc"
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
612 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
13+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
14+ internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
15+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
16+ k8serrors "k8s.io/apimachinery/pkg/util/errors"
717)
818
919// Provider represents an upstream identity provider implementation
@@ -20,38 +30,247 @@ type Provider interface {
2030 CreateSessionFromToken(ctx context.Context, token string) (*sessions.SessionState, error)
2131}
2232
23−// New provides a new Provider based on the configured provider string
24−func New(provider string, p *ProviderData) Provider {
25− switch provider {
26− case "linkedin":
27− return NewLinkedInProvider(p)
28− case "facebook":
29− return NewFacebookProvider(p)
30− case "github":
31− return NewGitHubProvider(p)
32− case "keycloak":
33− return NewKeycloakProvider(p)
34− case "keycloak-oidc":
35− return NewKeycloakOIDCProvider(p)
36− case "azure":
37− return NewAzureProvider(p)
38− case "adfs":
39− return NewADFSProvider(p)
40− case "gitlab":
41− return NewGitLabProvider(p)
42− case "oidc":
43− return NewOIDCProvider(p)
44− case "login.gov":
45− return NewLoginGovProvider(p)
46− case "bitbucket":
47− return NewBitbucketProvider(p)
48− case "nextcloud":
49− return NewNextcloudProvider(p)
50− case "digitalocean":
51− return NewDigitalOceanProvider(p)
52− case "google":
53− return NewGoogleProvider(p)
33+func NewProvider(providerConfig options.Provider) (Provider, error) {
34+ providerData, err := newProviderDataFromConfig(providerConfig)
35+ if err != nil {
36+ return nil, fmt.Errorf("could not create provider data: %v", err)
37+ }
38+ switch providerConfig.Type {
39+ case options.ADFSProvider:
40+ return NewADFSProvider(providerData, providerConfig.ADFSConfig), nil
41+ case options.AzureProvider:
42+ return NewAzureProvider(providerData, providerConfig.AzureConfig), nil
43+ case options.BitbucketProvider:
44+ return NewBitbucketProvider(providerData, providerConfig.BitbucketConfig), nil
45+ case options.DigitalOceanProvider:
46+ return NewDigitalOceanProvider(providerData), nil
47+ case options.FacebookProvider:
48+ return NewFacebookProvider(providerData), nil
49+ case options.GitHubProvider:
50+ return NewGitHubProvider(providerData, providerConfig.GitHubConfig), nil
51+ case options.GitLabProvider:
52+ return NewGitLabProvider(providerData, providerConfig.GitLabConfig)
53+ case options.GoogleProvider:
54+ return NewGoogleProvider(providerData, providerConfig.GoogleConfig)
55+ case options.KeycloakProvider:
56+ return NewKeycloakProvider(providerData, providerConfig.KeycloakConfig), nil
57+ case options.KeycloakOIDCProvider:
58+ return NewKeycloakOIDCProvider(providerData, providerConfig.KeycloakConfig), nil
59+ case options.LinkedInProvider:
60+ return NewLinkedInProvider(providerData), nil
61+ case options.LoginGovProvider:
62+ return NewLoginGovProvider(providerData, providerConfig.LoginGovConfig)
63+ case options.NextCloudProvider:
64+ return NewNextcloudProvider(providerData), nil
65+ case options.OIDCProvider:
66+ return NewOIDCProvider(providerData, providerConfig.OIDCConfig), nil
67+ default:
68+ return nil, fmt.Errorf("unknown provider type %q", providerConfig.Type)
69+ }
70+}
71+
72+func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData, error) {
73+ p := &ProviderData{
74+ Scope: providerConfig.Scope,
75+ ClientID: providerConfig.ClientID,
76+ ClientSecret: providerConfig.ClientSecret,
77+ ClientSecretFile: providerConfig.ClientSecretFile,
78+ Prompt: providerConfig.Prompt,
79+ ApprovalPrompt: providerConfig.ApprovalPrompt,
80+ AcrValues: providerConfig.AcrValues,
81+ }
82+
83+ needsVerifier, err := providerRequiresOIDCProviderVerifier(providerConfig.Type)
84+ if err != nil {
85+ return nil, err
86+ }
87+
88+ if needsVerifier {
89+ oidcProvider, verifier, err := newOIDCProviderVerifier(providerConfig)
90+ if err != nil {
91+ return nil, fmt.Errorf("error setting OIDC configuration: %v", err)
92+ }
93+
94+ p.Verifier = verifier
95+ if oidcProvider != nil {
96+ // Use the discovered values rather than any specified values
97+ providerConfig.LoginURL = oidcProvider.Endpoint().AuthURL
98+ providerConfig.RedeemURL = oidcProvider.Endpoint().TokenURL
99+ }
100+ }
101+
102+ errs := []error{}
103+ for name, u := range map[string]struct {
104+ dst *url.URL
105+ raw string
106+ }{
107+ "login": {dst: p.LoginURL, raw: providerConfig.LoginURL},
108+ "redeem": {dst: p.RedeemURL, raw: providerConfig.RedeemURL},
109+ "profile": {dst: p.ProfileURL, raw: providerConfig.ProfileURL},
110+ "validate": {dst: p.ValidateURL, raw: providerConfig.ValidateURL},
111+ "resource": {dst: p.ProtectedResource, raw: providerConfig.ProtectedResource},
112+ } {
113+ var err error
114+ u.dst, err = url.Parse(u.raw)
115+ if err != nil {
116+ errs = append(errs, fmt.Errorf("could not parse %s URL: %v", name, err))
117+ }
118+ }
119+ if len(errs) > 0 {
120+ return nil, k8serrors.NewAggregate(errs)
121+ }
122+
123+ // Make the OIDC options available to all providers that support it
124+ p.AllowUnverifiedEmail = providerConfig.OIDCConfig.InsecureAllowUnverifiedEmail
125+ p.EmailClaim = providerConfig.OIDCConfig.EmailClaim
126+ p.GroupsClaim = providerConfig.OIDCConfig.GroupsClaim
127+
128+ // TODO (@NickMeves) - Remove This
129+ // Backwards Compatibility for Deprecated UserIDClaim option
130+ if providerConfig.OIDCConfig.EmailClaim == options.OIDCEmailClaim &&
131+ providerConfig.OIDCConfig.UserIDClaim != options.OIDCEmailClaim {
132+ p.EmailClaim = providerConfig.OIDCConfig.UserIDClaim
133+ }
134+
135+ if providerConfig.Scope == "" {
136+ providerConfig.Scope = "openid email profile"
137+
138+ if len(providerConfig.AllowedGroups) > 0 {
139+ providerConfig.Scope += " groups"
140+ }
141+ }
142+ if providerConfig.OIDCConfig.UserIDClaim == "" {
143+ providerConfig.OIDCConfig.UserIDClaim = "email"
144+ }
145+
146+ p.setAllowedGroups(providerConfig.AllowedGroups)
147+
148+ return p, nil
149+}
150+
151+func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bool, error) {
152+ switch providerType {
153+ case options.BitbucketProvider, options.DigitalOceanProvider, options.FacebookProvider, options.GitHubProvider,
154+ options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider, options.NextCloudProvider:
155+ return false, nil
156+ case options.ADFSProvider, options.AzureProvider, options.GitLabProvider, options.KeycloakOIDCProvider, options.OIDCProvider:
157+ return true, nil
54158 default:
55− return nil
159+ return false, fmt.Errorf("unknown provider type: %s", providerType)
160+ }
161+}
162+
163+func newOIDCProviderVerifier(providerConfig options.Provider) (*oidc.Provider, *internaloidc.IDTokenVerifier, error) {
164+ // If the issuer isn't set, default it for platforms where it makes sense
165+ if providerConfig.OIDCConfig.IssuerURL == "" {
166+ switch providerConfig.Type {
167+ case "gitlab":
168+ providerConfig.OIDCConfig.IssuerURL = "https://gitlab.com"
169+ case "oidc":
170+ return nil, nil, errors.New("missing required setting: OIDC Issuer URL cannot be empty")
171+ }
172+ }
173+
174+ switch {
175+ case providerConfig.OIDCConfig.InsecureSkipIssuerVerification && !providerConfig.OIDCConfig.SkipDiscovery:
176+ verifier, err := newInsecureSkipIssuerVerificationOIDCVerifier(providerConfig)
177+ return nil, verifier, err
178+ case providerConfig.OIDCConfig.SkipDiscovery:
179+ verifier, err := newSkipDiscoveryOIDCVerifier(providerConfig)
180+ return nil, verifier, err
181+ default:
182+ return newDiscoveryOIDCProviderVerifier(providerConfig)
183+ }
184+}
185+
186+func newDiscoveryOIDCProviderVerifier(providerConfig options.Provider) (*oidc.Provider, *internaloidc.IDTokenVerifier, error) {
187+ // Configure discoverable provider data.
188+ provider, err := oidc.NewProvider(context.TODO(), providerConfig.OIDCConfig.IssuerURL)
189+ if err != nil {
190+ return nil, nil, err
191+ }
192+ verificationOptions := &internaloidc.IDTokenVerificationOptions{
193+ AudienceClaims: providerConfig.OIDCConfig.AudienceClaims,
194+ ClientID: providerConfig.ClientID,
195+ ExtraAudiences: providerConfig.OIDCConfig.ExtraAudiences,
196+ }
197+ verifier := internaloidc.NewVerifier(provider.Verifier(&oidc.Config{
198+ ClientID: providerConfig.ClientID,
199+ SkipIssuerCheck: providerConfig.OIDCConfig.InsecureSkipIssuerVerification,
200+ SkipClientIDCheck: true, // client id check is done within oauth2-proxy: IDTokenVerifier.Verify
201+ }), verificationOptions)
202+
203+ return provider, verifier, nil
204+}
205+
206+func newInsecureSkipIssuerVerificationOIDCVerifier(providerConfig options.Provider) (*internaloidc.IDTokenVerifier, error) {
207+ // go-oidc doesn't let us pass bypass the issuer check this in the oidc.NewProvider call
208+ // (which uses discovery to get the URLs), so we'll do a quick check ourselves and if
209+ // we get the URLs, we'll just use the non-discovery path.
210+
211+ logger.Printf("Performing OIDC Discovery...")
212+
213+ requestURL := strings.TrimSuffix(providerConfig.OIDCConfig.IssuerURL, "/") + "/.well-known/openid-configuration"
214+ body, err := requests.New(requestURL).
215+ Do().
216+ UnmarshalJSON()
217+ if err != nil {
218+ return nil, fmt.Errorf("failed to discover OIDC configuration: %v", err)
219+ }
220+
221+ // Prefer manually configured URLs. It's a bit unclear
222+ // why you'd be doing discovery and also providing the URLs
223+ // explicitly though...
224+ if providerConfig.LoginURL == "" {
225+ providerConfig.LoginURL = body.Get("authorization_endpoint").MustString()
226+ }
227+
228+ if providerConfig.RedeemURL == "" {
229+ providerConfig.RedeemURL = body.Get("token_endpoint").MustString()
230+ }
231+
232+ if providerConfig.OIDCConfig.JwksURL == "" {
233+ providerConfig.OIDCConfig.JwksURL = body.Get("jwks_uri").MustString()
234+ }
235+
236+ if providerConfig.ProfileURL == "" {
237+ providerConfig.ProfileURL = body.Get("userinfo_endpoint").MustString()
238+ }
239+
240+ // Now we have performed the discovery, construct the verifier manually
241+ return newSkipDiscoveryOIDCVerifier(providerConfig)
242+}
243+
244+func newSkipDiscoveryOIDCVerifier(providerConfig options.Provider) (*internaloidc.IDTokenVerifier, error) {
245+ var errs []error
246+
247+ // Construct a manual IDTokenVerifier from issuer URL & JWKS URI
248+ // instead of metadata discovery if we enable -skip-oidc-discovery.
249+ // In this case we need to make sure the required endpoints for
250+ // the provider are configured.
251+ if providerConfig.LoginURL == "" {
252+ errs = append(errs, errors.New("missing required setting: login-url"))
253+ }
254+ if providerConfig.RedeemURL == "" {
255+ errs = append(errs, errors.New("missing required setting: redeem-url"))
256+ }
257+ if providerConfig.OIDCConfig.JwksURL == "" {
258+ errs = append(errs, errors.New("missing required setting: oidc-jwks-url"))
259+ }
260+ if len(errs) > 0 {
261+ return nil, k8serrors.NewAggregate(errs)
262+ }
263+
264+ keySet := oidc.NewRemoteKeySet(context.TODO(), providerConfig.OIDCConfig.JwksURL)
265+ verificationOptions := &internaloidc.IDTokenVerificationOptions{
266+ AudienceClaims: providerConfig.OIDCConfig.AudienceClaims,
267+ ClientID: providerConfig.ClientID,
268+ ExtraAudiences: providerConfig.OIDCConfig.ExtraAudiences,
56269 }
270+ verifier := internaloidc.NewVerifier(oidc.NewVerifier(providerConfig.OIDCConfig.IssuerURL, keySet, &oidc.Config{
271+ ClientID: providerConfig.ClientID,
272+ SkipIssuerCheck: providerConfig.OIDCConfig.InsecureSkipIssuerVerification,
273+ SkipClientIDCheck: true, // client id check is done within oauth2-proxy: IDTokenVerifier.Verify
274+ }), verificationOptions)
275+ return verifier, nil
57276}
providers/providers_test.gonew file+93
@@ -0,0 +1,93 @@
1+package providers
2+
3+import (
4+ "io/ioutil"
5+ "os"
6+ "testing"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
9+ . "github.com/onsi/gomega"
10+)
11+
12+const (
13+ clientID = "bazquux"
14+ clientSecret = "xyzzyplugh"
15+ providerID = "providerID"
16+)
17+
18+func TestClientSecretFileOptionFails(t *testing.T) {
19+ g := NewWithT(t)
20+
21+ providerConfig := options.Provider{
22+ ID: providerID,
23+ Type: "google",
24+ ClientID: clientID,
25+ ClientSecretFile: clientSecret,
26+ }
27+
28+ p, err := newProviderDataFromConfig(providerConfig)
29+ g.Expect(err).ToNot(HaveOccurred())
30+ g.Expect(p.ClientSecretFile).To(Equal(clientSecret))
31+ g.Expect(p.ClientSecret).To(BeEmpty())
32+
33+ s, err := p.GetClientSecret()
34+ g.Expect(err).To(HaveOccurred())
35+ g.Expect(s).To(BeEmpty())
36+}
37+
38+func TestClientSecretFileOption(t *testing.T) {
39+ g := NewWithT(t)
40+
41+ f, err := ioutil.TempFile("", "client_secret_temp_file_")
42+ g.Expect(err).ToNot(HaveOccurred())
43+
44+ clientSecretFileName := f.Name()
45+
46+ defer func() {
47+ g.Expect(f.Close()).To(Succeed())
48+ g.Expect(os.Remove(clientSecretFileName)).To(Succeed())
49+ }()
50+
51+ _, err = f.WriteString("testcase")
52+ g.Expect(err).ToNot(HaveOccurred())
53+
54+ providerConfig := options.Provider{
55+ ID: providerID,
56+ Type: "google",
57+ ClientID: clientID,
58+ ClientSecretFile: clientSecretFileName,
59+ }
60+
61+ p, err := newProviderDataFromConfig(providerConfig)
62+ g.Expect(err).ToNot(HaveOccurred())
63+ g.Expect(p.ClientSecretFile).To(Equal(clientSecretFileName))
64+ g.Expect(p.ClientSecret).To(BeEmpty())
65+
66+ s, err := p.GetClientSecret()
67+ g.Expect(err).ToNot(HaveOccurred())
68+ g.Expect(s).To(Equal("testcase"))
69+}
70+
71+func TestSkipOIDCDiscovery(t *testing.T) {
72+ g := NewWithT(t)
73+ providerConfig := options.Provider{
74+ ID: providerID,
75+ Type: "oidc",
76+ ClientID: clientID,
77+ ClientSecretFile: clientSecret,
78+ OIDCConfig: options.OIDCOptions{
79+ IssuerURL: "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/v2.0/",
80+ SkipDiscovery: true,
81+ },
82+ }
83+
84+ _, err := newProviderDataFromConfig(providerConfig)
85+ g.Expect(err).To(MatchError("error setting OIDC configuration: [missing required setting: login-url, missing required setting: redeem-url, missing required setting: oidc-jwks-url]"))
86+
87+ providerConfig.LoginURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1_sign_in"
88+ providerConfig.RedeemURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_sign_in"
89+ providerConfig.OIDCConfig.JwksURL = "https://login.microsoftonline.com/fabrikamb2c.onmicrosoft.com/discovery/v2.0/keys"
90+
91+ _, err = newProviderDataFromConfig(providerConfig)
92+ g.Expect(err).ToNot(HaveOccurred())
93+}