git.iko.soy/typeable/oauth2-proxy
on master help sign in
Move provider initialisation into providers package
d162b01Joel Speed
docs/docs/configuration/alpha_config.md+12−1
@@ -305,7 +305,7 @@ Provider holds all configuration for a single provider
305| `oidcConfig` | _[OIDCOptions](#oidcoptions)_ | OIDCConfig holds all configurations for OIDC provider<br/>or providers utilize OIDC configurations. |
305| `oidcConfig` | _[OIDCOptions](#oidcoptions)_ | OIDCConfig holds all configurations for OIDC provider<br/>or providers utilize OIDC configurations. |
306| `loginGovConfig` | _[LoginGovOptions](#logingovoptions)_ | LoginGovConfig holds all configurations for LoginGov provider. |
306| `loginGovConfig` | _[LoginGovOptions](#logingovoptions)_ | LoginGovConfig holds all configurations for LoginGov provider. |
307| `id` | _string_ | ID should be a unique identifier for the provider.<br/>This value is required for all providers. |
307| `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 |
309| `name` | _string_ | Name is the providers display name<br/>if set, it will be shown to the users in the login page. |
309| `name` | _string_ | Name is the providers display name<br/>if set, it will be shown to the users in the login page. |
310| `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 |
310| `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 |
311| `loginURL` | _string_ | LoginURL is the authentication endpoint |
311| `loginURL` | _string_ | LoginURL is the authentication endpoint |
@@ -319,6 +319,17 @@ Provider holds all configuration for a single provider
319| `allowedGroups` | _[]string_ | AllowedGroups is a list of restrict logins to members of this group |
319| `allowedGroups` | _[]string_ | AllowedGroups is a list of restrict logins to members of this group |
320| `acrValues` | _string_ | AcrValues is a string of acr values |
320| `acrValues` | _string_ | AcrValues is a string of acr values |
321
321
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+
322### Providers
333### Providers
323
334
324#### ([[]Provider](#provider) alias)
335#### ([[]Provider](#provider) alias)
pkg/apis/options/legacy_options.go+1−1
@@ -624,7 +624,7 @@ func (l *LegacyProvider) convert() (Providers, error) {
624 ClientID: l.ClientID,
624 ClientID: l.ClientID,
625 ClientSecret: l.ClientSecret,
625 ClientSecret: l.ClientSecret,
626 ClientSecretFile: l.ClientSecretFile,
626 ClientSecretFile: l.ClientSecretFile,
627− Type: l.ProviderType,
627+ Type: ProviderType(l.ProviderType),
628 CAFiles: l.ProviderCAFiles,
628 CAFiles: l.ProviderCAFiles,
629 LoginURL: l.LoginURL,
629 LoginURL: l.LoginURL,
630 RedeemURL: l.RedeemURL,
630 RedeemURL: l.RedeemURL,
pkg/apis/options/providers.go+51−1
@@ -52,7 +52,7 @@ type Provider struct {
52 // Type is the OAuth provider
52 // Type is the OAuth provider
53 // must be set from the supported providers group,
53 // must be set from the supported providers group,
54 // otherwise 'Google' is set as default
54 // otherwise 'Google' is set as default
55− Type string `json:"provider,omitempty"`
55+ Type ProviderType `json:"provider,omitempty"`
56 // Name is the providers display name
56 // Name is the providers display name
57 // if set, it will be shown to the users in the login page.
57 // if set, it will be shown to the users in the login page.
58 Name string `json:"name,omitempty"`
58 Name string `json:"name,omitempty"`
@@ -84,6 +84,56 @@ type Provider struct {
84 AcrValues string `json:"acrValues,omitempty"`
84 AcrValues string `json:"acrValues,omitempty"`
85}
85}
86
86
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+
87type KeycloakOptions struct {
137type KeycloakOptions struct {
88 // Group enables to restrict login to members of indicated group
138 // Group enables to restrict login to members of indicated group
89 Groups []string `json:"groups,omitempty"`
139 Groups []string `json:"groups,omitempty"`
providers/adfs.go+3−8
@@ -6,6 +6,7 @@ import (
6 "net/url"
6 "net/url"
7 "strings"
7 "strings"
8
8
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
9 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
10)
11)
11
12
@@ -24,12 +25,11 @@ var _ Provider = (*ADFSProvider)(nil)
24const (
25const (
25 adfsProviderName = "ADFS"
26 adfsProviderName = "ADFS"
26 adfsDefaultScope = "openid email profile"
27 adfsDefaultScope = "openid email profile"
27− adfsSkipScope = false
28 adfsUPNClaim = "upn"
28 adfsUPNClaim = "upn"
29)
29)
30
30
31// NewADFSProvider initiates a new ADFSProvider
31// NewADFSProvider initiates a new ADFSProvider
32−func NewADFSProvider(p *ProviderData) *ADFSProvider {
32+func NewADFSProvider(p *ProviderData, opts options.ADFSOptions) *ADFSProvider {
33 p.setProviderDefaults(providerDefaults{
33 p.setProviderDefaults(providerDefaults{
34 name: adfsProviderName,
34 name: adfsProviderName,
35 scope: adfsDefaultScope,
35 scope: adfsDefaultScope,
@@ -53,17 +53,12 @@ func NewADFSProvider(p *ProviderData) *ADFSProvider {
53
53
54 return &ADFSProvider{
54 return &ADFSProvider{
55 OIDCProvider: oidcProvider,
55 OIDCProvider: oidcProvider,
56− skipScope: adfsSkipScope,
56+ skipScope: opts.SkipScope,
57 oidcEnrichFunc: oidcProvider.EnrichSession,
57 oidcEnrichFunc: oidcProvider.EnrichSession,
58 oidcRefreshFunc: oidcProvider.RefreshSession,
58 oidcRefreshFunc: oidcProvider.RefreshSession,
59 }
59 }
60}
60}
61
61
62−// Configure defaults the ADFSProvider configuration options
63−func (p *ADFSProvider) Configure(skipScope bool) {
64− p.skipScope = skipScope
65−}
66−
67// GetLoginURL Override to double encode the state parameter. If not query params are lost
62// GetLoginURL Override to double encode the state parameter. If not query params are lost
68// More info here: https://docs.microsoft.com/en-us/powerapps/maker/portals/configure/configure-saml2-settings
63// More info here: https://docs.microsoft.com/en-us/powerapps/maker/portals/configure/configure-saml2-settings
69func (p *ADFSProvider) GetLoginURL(redirectURI, state, nonce string) string {
64func (p *ADFSProvider) GetLoginURL(redirectURI, state, nonce string) string {
providers/adfs_test.go+6−6
@@ -13,6 +13,7 @@ import (
13
13
14 "github.com/coreos/go-oidc/v3/oidc"
14 "github.com/coreos/go-oidc/v3/oidc"
15 "github.com/golang-jwt/jwt"
15 "github.com/golang-jwt/jwt"
16+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
16 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
17 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
17 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
18 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
18 . "github.com/onsi/ginkgo"
19 . "github.com/onsi/ginkgo"
@@ -61,8 +62,8 @@ func testADFSProvider(hostname string) *ADFSProvider {
61 ValidateURL: &url.URL{},
62 ValidateURL: &url.URL{},
62 Scope: "",
63 Scope: "",
63 Verifier: o,
64 Verifier: o,
64− EmailClaim: OIDCEmailClaim,
65+ EmailClaim: options.OIDCEmailClaim,
65− })
66+ }, options.ADFSOptions{})
66
67
67 if hostname != "" {
68 if hostname != "" {
68 updateURL(p.Data().LoginURL, hostname)
69 updateURL(p.Data().LoginURL, hostname)
@@ -133,7 +134,7 @@ var _ = Describe("ADFS Provider Tests", func() {
133
134
134 Context("New Provider Init", func() {
135 Context("New Provider Init", func() {
135 It("uses defaults", func() {
136 It("uses defaults", func() {
136− providerData := NewADFSProvider(&ProviderData{}).Data()
137+ providerData := NewADFSProvider(&ProviderData{}, options.ADFSOptions{}).Data()
137 Expect(providerData.ProviderName).To(Equal("ADFS"))
138 Expect(providerData.ProviderName).To(Equal("ADFS"))
138 Expect(providerData.Scope).To(Equal("openid email profile"))
139 Expect(providerData.Scope).To(Equal("openid email profile"))
139 })
140 })
@@ -165,8 +166,7 @@ var _ = Describe("ADFS Provider Tests", func() {
165 p := NewADFSProvider(&ProviderData{
166 p := NewADFSProvider(&ProviderData{
166 ProtectedResource: resource,
167 ProtectedResource: resource,
167 Scope: "",
168 Scope: "",
168− })
169+ }, options.ADFSOptions{SkipScope: true})
169− p.skipScope = true
170
170
171 result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "")
171 result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "")
172 Expect(result).NotTo(ContainSubstring("scope="))
172 Expect(result).NotTo(ContainSubstring("scope="))
@@ -186,7 +186,7 @@ var _ = Describe("ADFS Provider Tests", func() {
186 p := NewADFSProvider(&ProviderData{
186 p := NewADFSProvider(&ProviderData{
187 ProtectedResource: resource,
187 ProtectedResource: resource,
188 Scope: in.scope,
188 Scope: in.scope,
189− })
189+ }, options.ADFSOptions{})
190
190
191 Expect(p.Data().Scope).To(Equal(in.expectedScope))
191 Expect(p.Data().Scope).To(Equal(in.expectedScope))
192 result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "")
192 result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "")
providers/azure.go+10−15
@@ -10,6 +10,7 @@ import (
10 "time"
10 "time"
11
11
12 "github.com/bitly/go-simplejson"
12 "github.com/bitly/go-simplejson"
13+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
13 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
16 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
@@ -62,7 +63,7 @@ var (
62)
63)
63
64
64// NewAzureProvider initiates a new AzureProvider
65// NewAzureProvider initiates a new AzureProvider
65−func NewAzureProvider(p *ProviderData) *AzureProvider {
66+func NewAzureProvider(p *ProviderData, opts options.AzureOptions) *AzureProvider {
66 p.setProviderDefaults(providerDefaults{
67 p.setProviderDefaults(providerDefaults{
67 name: azureProviderName,
68 name: azureProviderName,
68 loginURL: azureDefaultLoginURL,
69 loginURL: azureDefaultLoginURL,
@@ -80,23 +81,17 @@ func NewAzureProvider(p *ProviderData) *AzureProvider {
80 }
81 }
81 p.getAuthorizationHeaderFunc = makeAzureHeader
82 p.getAuthorizationHeaderFunc = makeAzureHeader
82
83
83− return &AzureProvider{
84+ tenant := "common"
84− ProviderData: p,
85+ if opts.Tenant != "" {
85− Tenant: "common",
86+ tenant = opts.Tenant
87+ overrideTenantURL(p.LoginURL, azureDefaultLoginURL, tenant, "authorize")
88+ overrideTenantURL(p.RedeemURL, azureDefaultRedeemURL, tenant, "token")
86 }
89 }
87−}
88
90
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,
94 }
94 }
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")
100}
95}
101
96
102func overrideTenantURL(current, defaultURL *url.URL, tenant, path string) {
97func overrideTenantURL(current, defaultURL *url.URL, tenant, path string) {
providers/azure_test.go+11−10
@@ -15,6 +15,7 @@ import (
15
15
16 "github.com/coreos/go-oidc/v3/oidc"
16 "github.com/coreos/go-oidc/v3/oidc"
17 "github.com/golang-jwt/jwt"
17 "github.com/golang-jwt/jwt"
18+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
18 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
19 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
19 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
20 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
20
21
@@ -39,7 +40,7 @@ type azureOAuthPayload struct {
39 IDToken string `json:"id_token,omitempty"`
40 IDToken string `json:"id_token,omitempty"`
40}
41}
41
42
42−func testAzureProvider(hostname string) *AzureProvider {
43+func testAzureProvider(hostname string, opts options.AzureOptions) *AzureProvider {
43 verificationOptions := &internaloidc.IDTokenVerificationOptions{
44 verificationOptions := &internaloidc.IDTokenVerificationOptions{
44 AudienceClaims: []string{"aud"},
45 AudienceClaims: []string{"aud"},
45 ClientID: "cd6d4fae-f6a6-4a34-8454-2c6b598e9532",
46 ClientID: "cd6d4fae-f6a6-4a34-8454-2c6b598e9532",
@@ -64,7 +65,7 @@ func testAzureProvider(hostname string) *AzureProvider {
64 SkipExpiryCheck: true,
65 SkipExpiryCheck: true,
65 },
66 },
66 ), verificationOptions),
67 ), verificationOptions),
67− })
68+ }, opts)
68
69
69 if hostname != "" {
70 if hostname != "" {
70 updateURL(p.Data().LoginURL, hostname)
71 updateURL(p.Data().LoginURL, hostname)
@@ -80,7 +81,7 @@ func TestNewAzureProvider(t *testing.T) {
80 g := NewWithT(t)
81 g := NewWithT(t)
81
82
82 // Test that defaults are set when calling for a new provider with nothing set
83 // 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()
84 g.Expect(providerData.ProviderName).To(Equal("Azure"))
85 g.Expect(providerData.ProviderName).To(Equal("Azure"))
85 g.Expect(providerData.LoginURL.String()).To(Equal("https://login.microsoftonline.com/common/oauth2/authorize"))
86 g.Expect(providerData.LoginURL.String()).To(Equal("https://login.microsoftonline.com/common/oauth2/authorize"))
86 g.Expect(providerData.RedeemURL.String()).To(Equal("https://login.microsoftonline.com/common/oauth2/token"))
87 g.Expect(providerData.RedeemURL.String()).To(Equal("https://login.microsoftonline.com/common/oauth2/token"))
@@ -111,7 +112,8 @@ func TestAzureProviderOverrides(t *testing.T) {
111 ProtectedResource: &url.URL{
112 ProtectedResource: &url.URL{
112 Scheme: "https",
113 Scheme: "https",
113 Host: "example.com"},
114 Host: "example.com"},
114− Scope: "profile"})
115+ Scope: "profile"},
116+ options.AzureOptions{})
115 assert.NotEqual(t, nil, p)
117 assert.NotEqual(t, nil, p)
116 assert.Equal(t, "Azure", p.Data().ProviderName)
118 assert.Equal(t, "Azure", p.Data().ProviderName)
117 assert.Equal(t, "https://example.com/oauth/auth",
119 assert.Equal(t, "https://example.com/oauth/auth",
@@ -128,8 +130,7 @@ func TestAzureProviderOverrides(t *testing.T) {
128}
130}
129
131
130func TestAzureSetTenant(t *testing.T) {
132func TestAzureSetTenant(t *testing.T) {
131− p := testAzureProvider("")
133+ p := testAzureProvider("", options.AzureOptions{Tenant: "example"})
132− p.Configure("example")
133 assert.Equal(t, "Azure", p.Data().ProviderName)
134 assert.Equal(t, "Azure", p.Data().ProviderName)
134 assert.Equal(t, "example", p.Tenant)
135 assert.Equal(t, "example", p.Tenant)
135 assert.Equal(t, "https://login.microsoftonline.com/example/oauth2/authorize",
136 assert.Equal(t, "https://login.microsoftonline.com/example/oauth2/authorize",
@@ -230,7 +231,7 @@ func TestAzureProviderEnrichSession(t *testing.T) {
230 bURL, _ := url.Parse(b.URL)
231 bURL, _ := url.Parse(b.URL)
231 host = bURL.Host
232 host = bURL.Host
232 }
233 }
233− p := testAzureProvider(host)
234+ p := testAzureProvider(host, options.AzureOptions{})
234 session := CreateAuthorizedSession()
235 session := CreateAuthorizedSession()
235 session.Email = testCase.Email
236 session.Email = testCase.Email
236 err := p.EnrichSession(context.Background(), session)
237 err := p.EnrichSession(context.Background(), session)
@@ -323,7 +324,7 @@ func TestAzureProviderRedeem(t *testing.T) {
323 defer b.Close()
324 defer b.Close()
324
325
325 bURL, _ := url.Parse(b.URL)
326 bURL, _ := url.Parse(b.URL)
326− p := testAzureProvider(bURL.Host)
327+ p := testAzureProvider(bURL.Host, options.AzureOptions{})
327 p.Data().RedeemURL.Path = "/common/oauth2/token"
328 p.Data().RedeemURL.Path = "/common/oauth2/token"
328 s, err := p.Redeem(context.Background(), "https://localhost", "1234")
329 s, err := p.Redeem(context.Background(), "https://localhost", "1234")
329 if testCase.InjectRedeemURLError {
330 if testCase.InjectRedeemURLError {
@@ -345,7 +346,7 @@ func TestAzureProviderRedeem(t *testing.T) {
345}
346}
346
347
347func TestAzureProviderProtectedResourceConfigured(t *testing.T) {
348func TestAzureProviderProtectedResourceConfigured(t *testing.T) {
348− p := testAzureProvider("")
349+ p := testAzureProvider("", options.AzureOptions{})
349 p.ProtectedResource, _ = url.Parse("http://my.resource.test")
350 p.ProtectedResource, _ = url.Parse("http://my.resource.test")
350 result := p.GetLoginURL("https://my.test.app/oauth", "", "")
351 result := p.GetLoginURL("https://my.test.app/oauth", "", "")
351 assert.Contains(t, result, "resource="+url.QueryEscape("http://my.resource.test"))
352 assert.Contains(t, result, "resource="+url.QueryEscape("http://my.resource.test"))
@@ -381,7 +382,7 @@ func TestAzureProviderRefresh(t *testing.T) {
381 b := testAzureBackend(string(payloadBytes), newAccessToken, refreshToken)
382 b := testAzureBackend(string(payloadBytes), newAccessToken, refreshToken)
382 defer b.Close()
383 defer b.Close()
383 bURL, _ := url.Parse(b.URL)
384 bURL, _ := url.Parse(b.URL)
384− p := testAzureProvider(bURL.Host)
385+ p := testAzureProvider(bURL.Host, options.AzureOptions{})
385
386
386 expires := time.Now().Add(time.Duration(-1) * time.Hour)
387 expires := time.Now().Add(time.Duration(-1) * time.Hour)
387 session := &sessions.SessionState{AccessToken: "some_access_token", RefreshToken: refreshToken, IDToken: "some_id_token", ExpiresOn: &expires}
388 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 (
5 "net/url"
5 "net/url"
6 "strings"
6 "strings"
7
7
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
8 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
9 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
9 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
@@ -53,7 +54,7 @@ var (
53)
54)
54
55
55// NewBitbucketProvider initiates a new BitbucketProvider
56// NewBitbucketProvider initiates a new BitbucketProvider
56−func NewBitbucketProvider(p *ProviderData) *BitbucketProvider {
57+func NewBitbucketProvider(p *ProviderData, opts options.BitbucketOptions) *BitbucketProvider {
57 p.setProviderDefaults(providerDefaults{
58 p.setProviderDefaults(providerDefaults{
58 name: bitbucketProviderName,
59 name: bitbucketProviderName,
59 loginURL: bitbucketDefaultLoginURL,
60 loginURL: bitbucketDefaultLoginURL,
@@ -62,19 +63,28 @@ func NewBitbucketProvider(p *ProviderData) *BitbucketProvider {
62 validateURL: bitbucketDefaultValidateURL,
63 validateURL: bitbucketDefaultValidateURL,
63 scope: bitbucketDefaultScope,
64 scope: bitbucketDefaultScope,
64 })
65 })
66+
65− return &BitbucketProvider{ProviderData: p}
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
66}
76}
67
77
68−// SetTeam defines the Bitbucket team the user must be part of
78+// setTeam defines the Bitbucket team the user must be part of
69−func (p *BitbucketProvider) SetTeam(team string) {
79+func (p *BitbucketProvider) setTeam(team string) {
70 p.Team = team
80 p.Team = team
71 if !strings.Contains(p.Scope, "team") {
81 if !strings.Contains(p.Scope, "team") {
72 p.Scope += " team"
82 p.Scope += " team"
73 }
83 }
74}
84}
75
85
76−// SetRepository defines the repository the user must have access to
86+// setRepository defines the repository the user must have access to
77−func (p *BitbucketProvider) SetRepository(repository string) {
87+func (p *BitbucketProvider) setRepository(repository string) {
78 p.Repository = repository
88 p.Repository = repository
79 if !strings.Contains(p.Scope, "repository") {
89 if !strings.Contains(p.Scope, "repository") {
80 p.Scope += " repository"
90 p.Scope += " repository"
providers/bitbucket_test.go+10−11
@@ -8,6 +8,7 @@ import (
8 "net/url"
8 "net/url"
9 "testing"
9 "testing"
10
10
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12 . "github.com/onsi/gomega"
13 . "github.com/onsi/gomega"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/assert"
@@ -21,15 +22,12 @@ func testBitbucketProvider(hostname, team string, repository string) *BitbucketP
21 RedeemURL: &url.URL{},
22 RedeemURL: &url.URL{},
22 ProfileURL: &url.URL{},
23 ProfileURL: &url.URL{},
23 ValidateURL: &url.URL{},
24 ValidateURL: &url.URL{},
24− Scope: ""})
25+ Scope: ""},
25−
26+ options.BitbucketOptions{
26− if team != "" {
27+ Team: team,
27− p.SetTeam(team)
28− }
29−
30− if repository != "" {
28+ Repository: repository,
31− p.SetRepository(repository)
29+ },
32− }
30+ )
33
31
34 if hostname != "" {
32 if hostname != "" {
35 updateURL(p.Data().LoginURL, hostname)
33 updateURL(p.Data().LoginURL, hostname)
@@ -65,7 +63,7 @@ func TestNewBitbucketProvider(t *testing.T) {
65 g := NewWithT(t)
63 g := NewWithT(t)
66
64
67 // Test that defaults are set when calling for a new provider with nothing set
65 // 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()
69 g.Expect(providerData.ProviderName).To(Equal("Bitbucket"))
67 g.Expect(providerData.ProviderName).To(Equal("Bitbucket"))
70 g.Expect(providerData.LoginURL.String()).To(Equal("https://bitbucket.org/site/oauth2/authorize"))
68 g.Expect(providerData.LoginURL.String()).To(Equal("https://bitbucket.org/site/oauth2/authorize"))
71 g.Expect(providerData.RedeemURL.String()).To(Equal("https://bitbucket.org/site/oauth2/access_token"))
69 g.Expect(providerData.RedeemURL.String()).To(Equal("https://bitbucket.org/site/oauth2/access_token"))
@@ -101,7 +99,8 @@ func TestBitbucketProviderOverrides(t *testing.T) {
101 Scheme: "https",
99 Scheme: "https",
102 Host: "example.com",
100 Host: "example.com",
103 Path: "/api/v3/user"},
101 Path: "/api/v3/user"},
104− Scope: "profile"})
102+ Scope: "profile"},
103+ options.BitbucketOptions{})
105 assert.NotEqual(t, nil, p)
104 assert.NotEqual(t, nil, p)
106 assert.Equal(t, "Bitbucket", p.Data().ProviderName)
105 assert.Equal(t, "Bitbucket", p.Data().ProviderName)
107 assert.Equal(t, "https://example.com/oauth/auth",
106 assert.Equal(t, "https://example.com/oauth/auth",
providers/github.go+15−8
@@ -11,6 +11,7 @@ import (
11 "strconv"
11 "strconv"
12 "strings"
12 "strings"
13
13
14+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
16 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
16 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
17 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
@@ -62,7 +63,7 @@ var (
62)
63)
63
64
64// NewGitHubProvider initiates a new GitHubProvider
65// NewGitHubProvider initiates a new GitHubProvider
65−func NewGitHubProvider(p *ProviderData) *GitHubProvider {
66+func NewGitHubProvider(p *ProviderData, opts options.GitHubOptions) *GitHubProvider {
66 p.setProviderDefaults(providerDefaults{
67 p.setProviderDefaults(providerDefaults{
67 name: githubProviderName,
68 name: githubProviderName,
68 loginURL: githubDefaultLoginURL,
69 loginURL: githubDefaultLoginURL,
@@ -71,7 +72,13 @@ func NewGitHubProvider(p *ProviderData) *GitHubProvider {
71 validateURL: githubDefaultValidateURL,
72 validateURL: githubDefaultValidateURL,
72 scope: githubDefaultScope,
73 scope: githubDefaultScope,
73 })
74 })
75+
74− return &GitHubProvider{ProviderData: p}
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
75}
82}
76
83
77func makeGitHubHeader(accessToken string) http.Header {
84func makeGitHubHeader(accessToken string) http.Header {
@@ -82,8 +89,8 @@ func makeGitHubHeader(accessToken string) http.Header {
82 return makeAuthorizationHeader(tokenTypeToken, accessToken, extraHeaders)
89 return makeAuthorizationHeader(tokenTypeToken, accessToken, extraHeaders)
83}
90}
84
91
85−// SetOrgTeam adds GitHub org reading parameters to the OAuth2 scope
92+// setOrgTeam adds GitHub org reading parameters to the OAuth2 scope
86−func (p *GitHubProvider) SetOrgTeam(org, team string) {
93+func (p *GitHubProvider) setOrgTeam(org, team string) {
87 p.Org = org
94 p.Org = org
88 p.Team = team
95 p.Team = team
89 if org != "" || team != "" {
96 if org != "" || team != "" {
@@ -91,14 +98,14 @@ func (p *GitHubProvider) SetOrgTeam(org, team string) {
91 }
98 }
92}
99}
93
100
94−// SetRepo configures the target repository and optional token to use
101+// setRepo configures the target repository and optional token to use
95−func (p *GitHubProvider) SetRepo(repo, token string) {
102+func (p *GitHubProvider) setRepo(repo, token string) {
96 p.Repo = repo
103 p.Repo = repo
97 p.Token = token
104 p.Token = token
98}
105}
99
106
100−// SetUsers configures allowed usernames
107+// setUsers configures allowed usernames
101−func (p *GitHubProvider) SetUsers(users []string) {
108+func (p *GitHubProvider) setUsers(users []string) {
102 p.Users = users
109 p.Users = users
103}
110}
104
111
providers/github_test.go+81−35
@@ -7,12 +7,13 @@ import (
7 "net/url"
7 "net/url"
8 "testing"
8 "testing"
9
9
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11 . "github.com/onsi/gomega"
12 . "github.com/onsi/gomega"
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/assert"
13)
14)
14
15
15−func testGitHubProvider(hostname string) *GitHubProvider {
16+func testGitHubProvider(hostname string, opts options.GitHubOptions) *GitHubProvider {
16 p := NewGitHubProvider(
17 p := NewGitHubProvider(
17 &ProviderData{
18 &ProviderData{
18 ProviderName: "",
19 ProviderName: "",
@@ -20,7 +21,8 @@ func testGitHubProvider(hostname string) *GitHubProvider {
20 RedeemURL: &url.URL{},
21 RedeemURL: &url.URL{},
21 ProfileURL: &url.URL{},
22 ProfileURL: &url.URL{},
22 ValidateURL: &url.URL{},
23 ValidateURL: &url.URL{},
23− Scope: ""})
24+ Scope: ""},
25+ opts)
24 if hostname != "" {
26 if hostname != "" {
25 updateURL(p.Data().LoginURL, hostname)
27 updateURL(p.Data().LoginURL, hostname)
26 updateURL(p.Data().RedeemURL, hostname)
28 updateURL(p.Data().RedeemURL, hostname)
@@ -71,7 +73,7 @@ func TestNewGitHubProvider(t *testing.T) {
71 g := NewWithT(t)
73 g := NewWithT(t)
72
74
73 // Test that defaults are set when calling for a new provider with nothing set
75 // 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()
75 g.Expect(providerData.ProviderName).To(Equal("GitHub"))
77 g.Expect(providerData.ProviderName).To(Equal("GitHub"))
76 g.Expect(providerData.LoginURL.String()).To(Equal("https://github.com/login/oauth/authorize"))
78 g.Expect(providerData.LoginURL.String()).To(Equal("https://github.com/login/oauth/authorize"))
77 g.Expect(providerData.RedeemURL.String()).To(Equal("https://github.com/login/oauth/access_token"))
79 g.Expect(providerData.RedeemURL.String()).To(Equal("https://github.com/login/oauth/access_token"))
@@ -95,7 +97,8 @@ func TestGitHubProviderOverrides(t *testing.T) {
95 Scheme: "https",
97 Scheme: "https",
96 Host: "api.example.com",
98 Host: "api.example.com",
97 Path: "/"},
99 Path: "/"},
98− Scope: "profile"})
100+ Scope: "profile"},
101+ options.GitHubOptions{})
99 assert.NotEqual(t, nil, p)
102 assert.NotEqual(t, nil, p)
100 assert.Equal(t, "GitHub", p.Data().ProviderName)
103 assert.Equal(t, "GitHub", p.Data().ProviderName)
101 assert.Equal(t, "https://example.com/login/oauth/authorize",
104 assert.Equal(t, "https://example.com/login/oauth/authorize",
@@ -114,7 +117,7 @@ func TestGitHubProvider_getEmail(t *testing.T) {
114 defer b.Close()
117 defer b.Close()
115
118
116 bURL, _ := url.Parse(b.URL)
119 bURL, _ := url.Parse(b.URL)
117− p := testGitHubProvider(bURL.Host)
120+ p := testGitHubProvider(bURL.Host, options.GitHubOptions{})
118
121
119 session := CreateAuthorizedSession()
122 session := CreateAuthorizedSession()
120 err := p.getEmail(context.Background(), session)
123 err := p.getEmail(context.Background(), session)
@@ -129,7 +132,7 @@ func TestGitHubProvider_getEmailNotVerified(t *testing.T) {
129 defer b.Close()
132 defer b.Close()
130
133
131 bURL, _ := url.Parse(b.URL)
134 bURL, _ := url.Parse(b.URL)
132− p := testGitHubProvider(bURL.Host)
135+ p := testGitHubProvider(bURL.Host, options.GitHubOptions{})
133
136
134 session := CreateAuthorizedSession()
137 session := CreateAuthorizedSession()
135 err := p.getEmail(context.Background(), session)
138 err := p.getEmail(context.Background(), session)
@@ -149,8 +152,11 @@ func TestGitHubProvider_getEmailWithOrg(t *testing.T) {
149 defer b.Close()
152 defer b.Close()
150
153
151 bURL, _ := url.Parse(b.URL)
154 bURL, _ := url.Parse(b.URL)
152− p := testGitHubProvider(bURL.Host)
155+ p := testGitHubProvider(bURL.Host,
156+ options.GitHubOptions{
153− p.Org = "testorg1"
157+ Org: "testorg1",
158+ },
159+ )
154
160
155 session := CreateAuthorizedSession()
161 session := CreateAuthorizedSession()
156 err := p.getEmail(context.Background(), session)
162 err := p.getEmail(context.Background(), session)
@@ -166,8 +172,12 @@ func TestGitHubProvider_getEmailWithWriteAccessToPublicRepo(t *testing.T) {
166 defer b.Close()
172 defer b.Close()
167
173
168 bURL, _ := url.Parse(b.URL)
174 bURL, _ := url.Parse(b.URL)
169− p := testGitHubProvider(bURL.Host)
175+ p := testGitHubProvider(bURL.Host,
176+ options.GitHubOptions{
170− p.SetRepo("oauth2-proxy/oauth2-proxy", "")
177+ Repo: "oauth2-proxy/oauth2-proxy",
178+ Token: "token",
179+ },
180+ )
171
181
172 session := CreateAuthorizedSession()
182 session := CreateAuthorizedSession()
173 err := p.getEmail(context.Background(), session)
183 err := p.getEmail(context.Background(), session)
@@ -183,8 +193,12 @@ func TestGitHubProvider_getEmailWithReadOnlyAccessToPrivateRepo(t *testing.T) {
183 defer b.Close()
193 defer b.Close()
184
194
185 bURL, _ := url.Parse(b.URL)
195 bURL, _ := url.Parse(b.URL)
186− p := testGitHubProvider(bURL.Host)
196+ p := testGitHubProvider(bURL.Host,
197+ options.GitHubOptions{
187− p.SetRepo("oauth2-proxy/oauth2-proxy", "")
198+ Repo: "oauth2-proxy/oauth2-proxy",
199+ Token: "token",
200+ },
201+ )
188
202
189 session := CreateAuthorizedSession()
203 session := CreateAuthorizedSession()
190 err := p.getEmail(context.Background(), session)
204 err := p.getEmail(context.Background(), session)
@@ -200,8 +214,12 @@ func TestGitHubProvider_getEmailWithWriteAccessToPrivateRepo(t *testing.T) {
200 defer b.Close()
214 defer b.Close()
201
215
202 bURL, _ := url.Parse(b.URL)
216 bURL, _ := url.Parse(b.URL)
203− p := testGitHubProvider(bURL.Host)
217+ p := testGitHubProvider(bURL.Host,
218+ options.GitHubOptions{
204− p.SetRepo("oauth2-proxy/oauth2-proxy", "")
219+ Repo: "oauth2-proxy/oauth2-proxy",
220+ Token: "token",
221+ },
222+ )
205
223
206 session := CreateAuthorizedSession()
224 session := CreateAuthorizedSession()
207 err := p.getEmail(context.Background(), session)
225 err := p.getEmail(context.Background(), session)
@@ -216,8 +234,11 @@ func TestGitHubProvider_getEmailWithNoAccessToPrivateRepo(t *testing.T) {
216 defer b.Close()
234 defer b.Close()
217
235
218 bURL, _ := url.Parse(b.URL)
236 bURL, _ := url.Parse(b.URL)
219− p := testGitHubProvider(bURL.Host)
237+ p := testGitHubProvider(bURL.Host,
238+ options.GitHubOptions{
220− p.SetRepo("oauth2-proxy/oauth2-proxy", "")
239+ Repo: "oauth2-proxy/oauth2-proxy",
240+ },
241+ )
221
242
222 session := CreateAuthorizedSession()
243 session := CreateAuthorizedSession()
223 err := p.getEmail(context.Background(), session)
244 err := p.getEmail(context.Background(), session)
@@ -232,8 +253,12 @@ func TestGitHubProvider_getEmailWithToken(t *testing.T) {
232 defer b.Close()
253 defer b.Close()
233
254
234 bURL, _ := url.Parse(b.URL)
255 bURL, _ := url.Parse(b.URL)
235− p := testGitHubProvider(bURL.Host)
256+ p := testGitHubProvider(bURL.Host,
257+ options.GitHubOptions{
236− p.SetRepo("oauth2-proxy/oauth2-proxy", "token")
258+ Repo: "oauth2-proxy/oauth2-proxy",
259+ Token: "token",
260+ },
261+ )
237
262
238 session := CreateAuthorizedSession()
263 session := CreateAuthorizedSession()
239 err := p.getEmail(context.Background(), session)
264 err := p.getEmail(context.Background(), session)
@@ -248,7 +273,7 @@ func TestGitHubProvider_getEmailFailedRequest(t *testing.T) {
248 defer b.Close()
273 defer b.Close()
249
274
250 bURL, _ := url.Parse(b.URL)
275 bURL, _ := url.Parse(b.URL)
251− p := testGitHubProvider(bURL.Host)
276+ p := testGitHubProvider(bURL.Host, options.GitHubOptions{})
252
277
253 // We'll trigger a request failure by using an unexpected access
278 // We'll trigger a request failure by using an unexpected access
254 // token. Alternatively, we could allow the parsing of the payload as
279 // token. Alternatively, we could allow the parsing of the payload as
@@ -266,7 +291,7 @@ func TestGitHubProvider_getEmailNotPresentInPayload(t *testing.T) {
266 defer b.Close()
291 defer b.Close()
267
292
268 bURL, _ := url.Parse(b.URL)
293 bURL, _ := url.Parse(b.URL)
269− p := testGitHubProvider(bURL.Host)
294+ p := testGitHubProvider(bURL.Host, options.GitHubOptions{})
270
295
271 session := CreateAuthorizedSession()
296 session := CreateAuthorizedSession()
272 err := p.getEmail(context.Background(), session)
297 err := p.getEmail(context.Background(), session)
@@ -281,7 +306,7 @@ func TestGitHubProvider_getUser(t *testing.T) {
281 defer b.Close()
306 defer b.Close()
282
307
283 bURL, _ := url.Parse(b.URL)
308 bURL, _ := url.Parse(b.URL)
284− p := testGitHubProvider(bURL.Host)
309+ p := testGitHubProvider(bURL.Host, options.GitHubOptions{})
285
310
286 session := CreateAuthorizedSession()
311 session := CreateAuthorizedSession()
287 err := p.getUser(context.Background(), session)
312 err := p.getUser(context.Background(), session)
@@ -297,8 +322,12 @@ func TestGitHubProvider_getUserWithRepoAndToken(t *testing.T) {
297 defer b.Close()
322 defer b.Close()
298
323
299 bURL, _ := url.Parse(b.URL)
324 bURL, _ := url.Parse(b.URL)
300− p := testGitHubProvider(bURL.Host)
325+ p := testGitHubProvider(bURL.Host,
326+ options.GitHubOptions{
301− p.SetRepo("oauth2-proxy/oauth2-proxy", "token")
327+ Repo: "oauth2-proxy/oauth2-proxy",
328+ Token: "token",
329+ },
330+ )
302
331
303 session := CreateAuthorizedSession()
332 session := CreateAuthorizedSession()
304 err := p.getUser(context.Background(), session)
333 err := p.getUser(context.Background(), session)
@@ -311,8 +340,12 @@ func TestGitHubProvider_getUserWithRepoAndTokenWithoutPushAccess(t *testing.T) {
311 defer b.Close()
340 defer b.Close()
312
341
313 bURL, _ := url.Parse(b.URL)
342 bURL, _ := url.Parse(b.URL)
314− p := testGitHubProvider(bURL.Host)
343+ p := testGitHubProvider(bURL.Host,
344+ options.GitHubOptions{
315− p.SetRepo("oauth2-proxy/oauth2-proxy", "token")
345+ Repo: "oauth2-proxy/oauth2-proxy",
346+ Token: "token",
347+ },
348+ )
316
349
317 session := CreateAuthorizedSession()
350 session := CreateAuthorizedSession()
318 err := p.getUser(context.Background(), session)
351 err := p.getUser(context.Background(), session)
@@ -328,8 +361,11 @@ func TestGitHubProvider_getEmailWithUsername(t *testing.T) {
328 defer b.Close()
361 defer b.Close()
329
362
330 bURL, _ := url.Parse(b.URL)
363 bURL, _ := url.Parse(b.URL)
331− p := testGitHubProvider(bURL.Host)
364+ p := testGitHubProvider(bURL.Host,
365+ options.GitHubOptions{
332− p.SetUsers([]string{"mbland", "octocat"})
366+ Users: []string{"mbland", "octocat"},
367+ },
368+ )
333
369
334 session := CreateAuthorizedSession()
370 session := CreateAuthorizedSession()
335 err := p.getEmail(context.Background(), session)
371 err := p.getEmail(context.Background(), session)
@@ -345,8 +381,11 @@ func TestGitHubProvider_getEmailWithNotAllowedUsername(t *testing.T) {
345 defer b.Close()
381 defer b.Close()
346
382
347 bURL, _ := url.Parse(b.URL)
383 bURL, _ := url.Parse(b.URL)
348− p := testGitHubProvider(bURL.Host)
384+ p := testGitHubProvider(bURL.Host,
385+ options.GitHubOptions{
349− p.SetUsers([]string{"octocat"})
386+ Users: []string{"octocat"},
387+ },
388+ )
350
389
351 session := CreateAuthorizedSession()
390 session := CreateAuthorizedSession()
352 err := p.getEmail(context.Background(), session)
391 err := p.getEmail(context.Background(), session)
@@ -366,9 +405,12 @@ func TestGitHubProvider_getEmailWithUsernameAndNotBelongToOrg(t *testing.T) {
366 defer b.Close()
405 defer b.Close()
367
406
368 bURL, _ := url.Parse(b.URL)
407 bURL, _ := url.Parse(b.URL)
369− p := testGitHubProvider(bURL.Host)
408+ p := testGitHubProvider(bURL.Host,
370− p.SetOrgTeam("not_belong_to", "")
409+ options.GitHubOptions{
410+ Org: "not_belog_to",
371− p.SetUsers([]string{"mbland"})
411+ Users: []string{"mbland"},
412+ },
413+ )
372
414
373 session := CreateAuthorizedSession()
415 session := CreateAuthorizedSession()
374 err := p.getEmail(context.Background(), session)
416 err := p.getEmail(context.Background(), session)
@@ -385,9 +427,13 @@ func TestGitHubProvider_getEmailWithUsernameAndNoAccessToPrivateRepo(t *testing.
385 defer b.Close()
427 defer b.Close()
386
428
387 bURL, _ := url.Parse(b.URL)
429 bURL, _ := url.Parse(b.URL)
388− p := testGitHubProvider(bURL.Host)
430+ p := testGitHubProvider(bURL.Host,
431+ options.GitHubOptions{
389− p.SetRepo("oauth2-proxy/oauth2-proxy", "")
432+ Repo: "oauth2-proxy/oauth2-proxy",
433+ Token: "token",
390− p.SetUsers([]string{"mbland"})
434+ Users: []string{"mbland"},
435+ },
436+ )
391
437
392 session := CreateAuthorizedSession()
438 session := CreateAuthorizedSession()
393 err := p.getEmail(context.Background(), session)
439 err := p.getEmail(context.Background(), session)
providers/gitlab.go+12−4
@@ -7,6 +7,7 @@ import (
7 "strconv"
7 "strconv"
8 "strings"
8 "strings"
9
9
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
13 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
@@ -30,7 +31,7 @@ type GitLabProvider struct {
30var _ Provider = (*GitLabProvider)(nil)
31var _ Provider = (*GitLabProvider)(nil)
31
32
32// NewGitLabProvider initiates a new GitLabProvider
33// NewGitLabProvider initiates a new GitLabProvider
33−func NewGitLabProvider(p *ProviderData) *GitLabProvider {
34+func NewGitLabProvider(p *ProviderData, opts options.GitLabOptions) (*GitLabProvider, error) {
34 p.ProviderName = gitlabProviderName
35 p.ProviderName = gitlabProviderName
35 if p.Scope == "" {
36 if p.Scope == "" {
36 p.Scope = gitlabDefaultScope
37 p.Scope = gitlabDefaultScope
@@ -41,15 +42,22 @@ func NewGitLabProvider(p *ProviderData) *GitLabProvider {
41 SkipNonce: false,
42 SkipNonce: false,
42 }
43 }
43
44
44− return &GitLabProvider{
45+ provider := &GitLabProvider{
45 OIDCProvider: oidcProvider,
46 OIDCProvider: oidcProvider,
46 oidcRefreshFunc: oidcProvider.RefreshSession,
47 oidcRefreshFunc: oidcProvider.RefreshSession,
47 }
48 }
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
48}
56}
49
57
50−// SetAllowedProjects adds Gitlab projects to the AllowedGroups list
58+// setAllowedProjects adds Gitlab projects to the AllowedGroups list
51// and tracks them to do a project API lookup during `EnrichSession`.
59// 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 {
53 for _, project := range projects {
61 for _, project := range projects {
54 gp, err := newGitlabProject(project)
62 gp, err := newGitlabProject(project)
55 if err != nil {
63 if err != nil {
providers/gitlab_test.go+22−14
@@ -7,21 +7,26 @@ import (
7 "net/http/httptest"
7 "net/http/httptest"
8 "net/url"
8 "net/url"
9
9
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11 . "github.com/onsi/ginkgo"
12 . "github.com/onsi/ginkgo"
12 . "github.com/onsi/ginkgo/extensions/table"
13 . "github.com/onsi/ginkgo/extensions/table"
13 . "github.com/onsi/gomega"
14 . "github.com/onsi/gomega"
14)
15)
15
16
16−func testGitLabProvider(hostname string) *GitLabProvider {
17+func testGitLabProvider(hostname, scope string, opts options.GitLabOptions) (*GitLabProvider, error) {
17− p := NewGitLabProvider(
18+ p, err := NewGitLabProvider(
18 &ProviderData{
19 &ProviderData{
19 ProviderName: "",
20 ProviderName: "",
20 LoginURL: &url.URL{},
21 LoginURL: &url.URL{},
21 RedeemURL: &url.URL{},
22 RedeemURL: &url.URL{},
22 ProfileURL: &url.URL{},
23 ProfileURL: &url.URL{},
23 ValidateURL: &url.URL{},
24 ValidateURL: &url.URL{},
24− Scope: ""})
25+ Scope: scope},
26+ opts)
27+ if err != nil {
28+ return nil, err
29+ }
25 if hostname != "" {
30 if hostname != "" {
26 updateURL(p.Data().LoginURL, hostname)
31 updateURL(p.Data().LoginURL, hostname)
27 updateURL(p.Data().RedeemURL, hostname)
32 updateURL(p.Data().RedeemURL, hostname)
@@ -29,7 +34,7 @@ func testGitLabProvider(hostname string) *GitLabProvider {
29 updateURL(p.Data().ValidateURL, hostname)
34 updateURL(p.Data().ValidateURL, hostname)
30 }
35 }
31
36
32− return p
37+ return p, err
33}
38}
34
39
35func testGitLabBackend() *httptest.Server {
40func testGitLabBackend() *httptest.Server {
@@ -157,7 +162,8 @@ var _ = Describe("Gitlab Provider Tests", func() {
157 bURL, err := url.Parse(b.URL)
162 bURL, err := url.Parse(b.URL)
158 Expect(err).To(BeNil())
163 Expect(err).To(BeNil())
159
164
160− p = testGitLabProvider(bURL.Host)
165+ p, err = testGitLabProvider(bURL.Host, "", options.GitLabOptions{})
166+ Expect(err).ToNot(HaveOccurred())
161 })
167 })
162
168
163 AfterEach(func() {
169 AfterEach(func() {
@@ -219,21 +225,23 @@ var _ = Describe("Gitlab Provider Tests", func() {
219
225
220 DescribeTable("should return expected results",
226 DescribeTable("should return expected results",
221 func(in entitiesTableInput) {
227 func(in entitiesTableInput) {
222− p.AllowUnverifiedEmail = true
228+ bURL, err := url.Parse(b.URL)
223− if in.scope != "" {
229+ Expect(err).To(BeNil())
224− p.Scope = in.scope
225− }
226− session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
227−
228− p.SetAllowedGroups(in.allowedGroups)
229
230
231+ p, err := testGitLabProvider(bURL.Host, in.scope, options.GitLabOptions{
232+ Group: in.allowedGroups,
230− err := p.SetAllowedProjects(in.allowedProjects)
233+ Projects: in.allowedProjects,
234+ })
231 if in.expectedError == nil {
235 if in.expectedError == nil {
232 Expect(err).To(BeNil())
236 Expect(err).To(BeNil())
233 } else {
237 } else {
234 Expect(err).To(MatchError(in.expectedError))
238 Expect(err).To(MatchError(in.expectedError))
235 return
239 return
236 }
240 }
241+
242+ p.AllowUnverifiedEmail = true
243+ session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
244+
237 Expect(p.Scope).To(Equal(in.expectedScope))
245 Expect(p.Scope).To(Equal(in.expectedScope))
238
246
239 err = p.EnrichSession(context.Background(), session)
247 err = p.EnrichSession(context.Background(), session)
@@ -302,7 +310,7 @@ var _ = Describe("Gitlab Provider Tests", func() {
302 }),
310 }),
303 Entry("invalid project format", entitiesTableInput{
311 Entry("invalid project format", entitiesTableInput{
304 allowedProjects: []string{"my_group/my_invalid_project=123"},
312 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)"),
306 expectedScope: "openid email read_api",
314 expectedScope: "openid email read_api",
307 }),
315 }),
308 )
316 )
providers/google.go+20−3
@@ -10,9 +10,11 @@ import (
10 "io"
10 "io"
11 "io/ioutil"
11 "io/ioutil"
12 "net/url"
12 "net/url"
13+ "os"
13 "strings"
14 "strings"
14 "time"
15 "time"
15
16
17+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
16 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
18 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
17 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
19 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
18 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
20 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
@@ -79,7 +81,7 @@ var (
79)
81)
80
82
81// NewGoogleProvider initiates a new GoogleProvider
83// NewGoogleProvider initiates a new GoogleProvider
82−func NewGoogleProvider(p *ProviderData) *GoogleProvider {
84+func NewGoogleProvider(p *ProviderData, opts options.GoogleOptions) (*GoogleProvider, error) {
83 p.setProviderDefaults(providerDefaults{
85 p.setProviderDefaults(providerDefaults{
84 name: googleProviderName,
86 name: googleProviderName,
85 loginURL: googleDefaultLoginURL,
87 loginURL: googleDefaultLoginURL,
@@ -88,7 +90,7 @@ func NewGoogleProvider(p *ProviderData) *GoogleProvider {
88 validateURL: googleDefaultValidateURL,
90 validateURL: googleDefaultValidateURL,
89 scope: googleDefaultScope,
91 scope: googleDefaultScope,
90 })
92 })
91− return &GoogleProvider{
93+ provider := &GoogleProvider{
92 ProviderData: p,
94 ProviderData: p,
93 // Set a default groupValidator to just always return valid (true), it will
95 // Set a default groupValidator to just always return valid (true), it will
94 // be overwritten if we configured a Google group restriction.
96 // be overwritten if we configured a Google group restriction.
@@ -96,6 +98,21 @@ func NewGoogleProvider(p *ProviderData) *GoogleProvider {
96 return true
98 return true
97 },
99 },
98 }
100 }
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
99}
116}
100
117
101func claimsFromIDToken(idToken string) (*claims, error) {
118func claimsFromIDToken(idToken string) (*claims, error) {
@@ -195,7 +212,7 @@ func (p *GoogleProvider) EnrichSession(_ context.Context, s *sessions.SessionSta
195// account credentials.
212// account credentials.
196//
213//
197// TODO (@NickMeves) - Unit Test this OR refactor away from groupValidator func
214// 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) {
199 adminService := getAdminService(adminEmail, credentialsReader)
216 adminService := getAdminService(adminEmail, credentialsReader)
200 p.groupValidator = func(s *sessions.SessionState) bool {
217 p.groupValidator = func(s *sessions.SessionState) bool {
201 // Reset our saved Groups in case membership changed
218 // Reset our saved Groups in case membership changed
providers/google_test.go+22−12
@@ -10,6 +10,7 @@ import (
10 "net/url"
10 "net/url"
11 "testing"
11 "testing"
12
12
13+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
13 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
14 . "github.com/onsi/gomega"
15 . "github.com/onsi/gomega"
15 "github.com/stretchr/testify/assert"
16 "github.com/stretchr/testify/assert"
@@ -25,22 +26,29 @@ func newRedeemServer(body []byte) (*url.URL, *httptest.Server) {
25 return u, s
26 return u, s
26}
27}
27
28
28−func newGoogleProvider() *GoogleProvider {
29+func newGoogleProvider(t *testing.T) *GoogleProvider {
30+ g := NewWithT(t)
29− return NewGoogleProvider(
31+ p, err := NewGoogleProvider(
30 &ProviderData{
32 &ProviderData{
31 ProviderName: "",
33 ProviderName: "",
32 LoginURL: &url.URL{},
34 LoginURL: &url.URL{},
33 RedeemURL: &url.URL{},
35 RedeemURL: &url.URL{},
34 ProfileURL: &url.URL{},
36 ProfileURL: &url.URL{},
35 ValidateURL: &url.URL{},
37 ValidateURL: &url.URL{},
36− Scope: ""})
38+ Scope: ""},
39+ options.GoogleOptions{})
40+ g.Expect(err).ToNot(HaveOccurred())
41+ return p
37}
42}
38
43
39func TestNewGoogleProvider(t *testing.T) {
44func TestNewGoogleProvider(t *testing.T) {
40 g := NewWithT(t)
45 g := NewWithT(t)
41
46
42 // Test that defaults are set when calling for a new provider with nothing set
47 // 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+
44 g.Expect(providerData.ProviderName).To(Equal("Google"))
52 g.Expect(providerData.ProviderName).To(Equal("Google"))
45 g.Expect(providerData.LoginURL.String()).To(Equal("https://accounts.google.com/o/oauth2/auth?access_type=offline"))
53 g.Expect(providerData.LoginURL.String()).To(Equal("https://accounts.google.com/o/oauth2/auth?access_type=offline"))
46 g.Expect(providerData.RedeemURL.String()).To(Equal("https://www.googleapis.com/oauth2/v3/token"))
54 g.Expect(providerData.RedeemURL.String()).To(Equal("https://www.googleapis.com/oauth2/v3/token"))
@@ -50,7 +58,7 @@ func TestNewGoogleProvider(t *testing.T) {
50}
58}
51
59
52func TestGoogleProviderOverrides(t *testing.T) {
60func TestGoogleProviderOverrides(t *testing.T) {
53− p := NewGoogleProvider(
61+ p, err := NewGoogleProvider(
54 &ProviderData{
62 &ProviderData{
55 LoginURL: &url.URL{
63 LoginURL: &url.URL{
56 Scheme: "https",
64 Scheme: "https",
@@ -68,7 +76,9 @@ func TestGoogleProviderOverrides(t *testing.T) {
68 Scheme: "https",
76 Scheme: "https",
69 Host: "example.com",
77 Host: "example.com",
70 Path: "/oauth/tokeninfo"},
78 Path: "/oauth/tokeninfo"},
71− Scope: "profile"})
79+ Scope: "profile"},
80+ options.GoogleOptions{})
81+ assert.NoError(t, err)
72 assert.NotEqual(t, nil, p)
82 assert.NotEqual(t, nil, p)
73 assert.Equal(t, "Google", p.Data().ProviderName)
83 assert.Equal(t, "Google", p.Data().ProviderName)
74 assert.Equal(t, "https://example.com/oauth/auth",
84 assert.Equal(t, "https://example.com/oauth/auth",
@@ -90,7 +100,7 @@ type redeemResponse struct {
90}
100}
91
101
92func TestGoogleProviderGetEmailAddress(t *testing.T) {
102func TestGoogleProviderGetEmailAddress(t *testing.T) {
93− p := newGoogleProvider()
103+ p := newGoogleProvider(t)
94 body, err := json.Marshal(redeemResponse{
104 body, err := json.Marshal(redeemResponse{
95 AccessToken: "a1234",
105 AccessToken: "a1234",
96 ExpiresIn: 10,
106 ExpiresIn: 10,
@@ -147,7 +157,7 @@ func TestGoogleProviderGroupValidator(t *testing.T) {
147 for name, tc := range testCases {
157 for name, tc := range testCases {
148 t.Run(name, func(t *testing.T) {
158 t.Run(name, func(t *testing.T) {
149 g := NewWithT(t)
159 g := NewWithT(t)
150− p := newGoogleProvider()
160+ p := newGoogleProvider(t)
151 if tc.validatorFunc != nil {
161 if tc.validatorFunc != nil {
152 p.groupValidator = tc.validatorFunc
162 p.groupValidator = tc.validatorFunc
153 }
163 }
@@ -158,7 +168,7 @@ func TestGoogleProviderGroupValidator(t *testing.T) {
158
168
159//
169//
160func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) {
170func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) {
161− p := newGoogleProvider()
171+ p := newGoogleProvider(t)
162 body, err := json.Marshal(redeemResponse{
172 body, err := json.Marshal(redeemResponse{
163 AccessToken: "a1234",
173 AccessToken: "a1234",
164 IDToken: "ignored prefix." + `{"email": "michael.bland@gsa.gov"}`,
174 IDToken: "ignored prefix." + `{"email": "michael.bland@gsa.gov"}`,
@@ -176,7 +186,7 @@ func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) {
176}
186}
177
187
178func TestGoogleProviderRedeemFailsNoCLientSecret(t *testing.T) {
188func TestGoogleProviderRedeemFailsNoCLientSecret(t *testing.T) {
179− p := newGoogleProvider()
189+ p := newGoogleProvider(t)
180 p.ProviderData.ClientSecretFile = "srvnoerre"
190 p.ProviderData.ClientSecretFile = "srvnoerre"
181
191
182 session, err := p.Redeem(context.Background(), "http://redirect/", "code1234")
192 session, err := p.Redeem(context.Background(), "http://redirect/", "code1234")
@@ -188,7 +198,7 @@ func TestGoogleProviderRedeemFailsNoCLientSecret(t *testing.T) {
188}
198}
189
199
190func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) {
200func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) {
191− p := newGoogleProvider()
201+ p := newGoogleProvider(t)
192
202
193 body, err := json.Marshal(redeemResponse{
203 body, err := json.Marshal(redeemResponse{
194 AccessToken: "a1234",
204 AccessToken: "a1234",
@@ -208,7 +218,7 @@ func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) {
208}
218}
209
219
210func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
220func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
211− p := newGoogleProvider()
221+ p := newGoogleProvider(t)
212 body, err := json.Marshal(redeemResponse{
222 body, err := json.Marshal(redeemResponse{
213 AccessToken: "a1234",
223 AccessToken: "a1234",
214 IDToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"not_email": "missing"}`)),
224 IDToken: "ignored prefix." + base64.URLEncoding.EncodeToString([]byte(`{"not_email": "missing"}`)),
providers/keycloak.go+6−2
@@ -5,6 +5,7 @@ import (
5 "fmt"
5 "fmt"
6 "net/url"
6 "net/url"
7
7
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
8 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
9 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
9 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
@@ -48,7 +49,7 @@ var (
48)
49)
49
50
50// NewKeycloakProvider creates a KeyCloakProvider using the passed ProviderData
51// NewKeycloakProvider creates a KeyCloakProvider using the passed ProviderData
51−func NewKeycloakProvider(p *ProviderData) *KeycloakProvider {
52+func NewKeycloakProvider(p *ProviderData, opts options.KeycloakOptions) *KeycloakProvider {
52 p.setProviderDefaults(providerDefaults{
53 p.setProviderDefaults(providerDefaults{
53 name: keycloakProviderName,
54 name: keycloakProviderName,
54 loginURL: keycloakDefaultLoginURL,
55 loginURL: keycloakDefaultLoginURL,
@@ -57,7 +58,10 @@ func NewKeycloakProvider(p *ProviderData) *KeycloakProvider {
57 validateURL: keycloakDefaultValidateURL,
58 validateURL: keycloakDefaultValidateURL,
58 scope: keycloakDefaultScope,
59 scope: keycloakDefaultScope,
59 })
60 })
61+
60− return &KeycloakProvider{ProviderData: p}
62+ provider := &KeycloakProvider{ProviderData: p}
63+ provider.setAllowedGroups(opts.Groups)
64+ return provider
61}
65}
62
66
63// EnrichSession uses the Keycloak userinfo endpoint to populate the session's
67// EnrichSession uses the Keycloak userinfo endpoint to populate the session's
providers/keycloak_oidc.go+9−4
@@ -4,6 +4,7 @@ import (
4 "context"
4 "context"
5 "fmt"
5 "fmt"
6
6
7+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
7 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
8 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
8)
9)
9
10
@@ -15,21 +16,25 @@ type KeycloakOIDCProvider struct {
15}
16}
16
17
17// NewKeycloakOIDCProvider makes a KeycloakOIDCProvider using the ProviderData
18// NewKeycloakOIDCProvider makes a KeycloakOIDCProvider using the ProviderData
18−func NewKeycloakOIDCProvider(p *ProviderData) *KeycloakOIDCProvider {
19+func NewKeycloakOIDCProvider(p *ProviderData, opts options.KeycloakOptions) *KeycloakOIDCProvider {
19 p.ProviderName = keycloakOIDCProviderName
20 p.ProviderName = keycloakOIDCProviderName
21+
20− return &KeycloakOIDCProvider{
22+ provider := &KeycloakOIDCProvider{
21 OIDCProvider: &OIDCProvider{
23 OIDCProvider: &OIDCProvider{
22 ProviderData: p,
24 ProviderData: p,
23 },
25 },
24 }
26 }
27+
28+ provider.addAllowedRoles(opts.Roles)
29+ return provider
25}
30}
26
31
27var _ Provider = (*KeycloakOIDCProvider)(nil)
32var _ Provider = (*KeycloakOIDCProvider)(nil)
28
33
29−// AddAllowedRoles sets Keycloak roles that are authorized.
34+// addAllowedRoles sets Keycloak roles that are authorized.
30// Assumes `SetAllowedGroups` is already called on groups and appends to that
35// Assumes `SetAllowedGroups` is already called on groups and appends to that
31// with `role:` prefixed roles.
36// with `role:` prefixed roles.
32−func (p *KeycloakOIDCProvider) AddAllowedRoles(roles []string) {
37+func (p *KeycloakOIDCProvider) addAllowedRoles(roles []string) {
33 if p.AllowedGroups == nil {
38 if p.AllowedGroups == nil {
34 p.AllowedGroups = make(map[string]struct{})
39 p.AllowedGroups = make(map[string]struct{})
35 }
40 }
providers/keycloak_oidc_test.go+9−6
@@ -9,6 +9,7 @@ import (
9
9
10 "github.com/coreos/go-oidc/v3/oidc"
10 "github.com/coreos/go-oidc/v3/oidc"
11
11
12+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
13 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
13 . "github.com/onsi/ginkgo"
14 . "github.com/onsi/ginkgo"
14 . "github.com/onsi/gomega"
15 . "github.com/onsi/gomega"
@@ -39,11 +40,11 @@ func getAccessToken() string {
39
40
40func newTestKeycloakOIDCSetup() (*httptest.Server, *KeycloakOIDCProvider) {
41func newTestKeycloakOIDCSetup() (*httptest.Server, *KeycloakOIDCProvider) {
41 redeemURL, server := newOIDCServer([]byte(fmt.Sprintf(`{"email": "new@thing.com", "expires_in": 300, "access_token": "%v"}`, getAccessToken())))
42 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{})
43 return server, provider
44 return server, provider
44}
45}
45
46
46−func newKeycloakOIDCProvider(serverURL *url.URL) *KeycloakOIDCProvider {
47+func newKeycloakOIDCProvider(serverURL *url.URL, opts options.KeycloakOptions) *KeycloakOIDCProvider {
47 verificationOptions := &internaloidc.IDTokenVerificationOptions{
48 verificationOptions := &internaloidc.IDTokenVerificationOptions{
48 AudienceClaims: []string{defaultAudienceClaim},
49 AudienceClaims: []string{defaultAudienceClaim},
49 ClientID: mockClientID,
50 ClientID: mockClientID,
@@ -66,7 +67,8 @@ func newKeycloakOIDCProvider(serverURL *url.URL) *KeycloakOIDCProvider {
66 Scheme: "https",
67 Scheme: "https",
67 Host: "keycloak-oidc.com",
68 Host: "keycloak-oidc.com",
68 Path: "/api/v3/user"},
69 Path: "/api/v3/user"},
69− Scope: "openid email profile"})
70+ Scope: "openid email profile"},
71+ opts)
70
72
71 if serverURL != nil {
73 if serverURL != nil {
72 p.RedeemURL.Scheme = serverURL.Scheme
74 p.RedeemURL.Scheme = serverURL.Scheme
@@ -88,7 +90,7 @@ func newKeycloakOIDCProvider(serverURL *url.URL) *KeycloakOIDCProvider {
88var _ = Describe("Keycloak OIDC Provider Tests", func() {
90var _ = Describe("Keycloak OIDC Provider Tests", func() {
89 Context("New Provider Init", func() {
91 Context("New Provider Init", func() {
90 It("creates new keycloak oidc provider with expected defaults", func() {
92 It("creates new keycloak oidc provider with expected defaults", func() {
91− p := newKeycloakOIDCProvider(nil)
93+ p := newKeycloakOIDCProvider(nil, options.KeycloakOptions{})
92 providerData := p.Data()
94 providerData := p.Data()
93 Expect(providerData.ProviderName).To(Equal(keycloakOIDCProviderName))
95 Expect(providerData.ProviderName).To(Equal(keycloakOIDCProviderName))
94 Expect(providerData.LoginURL.String()).To(Equal("https://keycloak-oidc.com/oauth/auth"))
96 Expect(providerData.LoginURL.String()).To(Equal("https://keycloak-oidc.com/oauth/auth"))
@@ -101,8 +103,9 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
101
103
102 Context("Allowed Roles", func() {
104 Context("Allowed Roles", func() {
103 It("should prefix allowed roles and add them to groups", func() {
105 It("should prefix allowed roles and add them to groups", func() {
104− p := newKeycloakOIDCProvider(nil)
106+ p := newKeycloakOIDCProvider(nil, options.KeycloakOptions{
105− p.AddAllowedRoles([]string{"admin", "editor"})
107+ Roles: []string{"admin", "editor"},
108+ })
106 Expect(p.AllowedGroups).To(HaveKey("role:admin"))
109 Expect(p.AllowedGroups).To(HaveKey("role:admin"))
107 Expect(p.AllowedGroups).To(HaveKey("role:editor"))
110 Expect(p.AllowedGroups).To(HaveKey("role:editor"))
108 })
111 })
providers/keycloak_test.go+6−3
@@ -8,6 +8,7 @@ import (
8 "net/http/httptest"
8 "net/http/httptest"
9 "net/url"
9 "net/url"
10
10
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12 . "github.com/onsi/ginkgo"
13 . "github.com/onsi/ginkgo"
13 . "github.com/onsi/ginkgo/extensions/table"
14 . "github.com/onsi/ginkgo/extensions/table"
@@ -27,7 +28,8 @@ func testKeycloakProvider(backend *httptest.Server) (*KeycloakProvider, error) {
27 RedeemURL: &url.URL{},
28 RedeemURL: &url.URL{},
28 ProfileURL: &url.URL{},
29 ProfileURL: &url.URL{},
29 ValidateURL: &url.URL{},
30 ValidateURL: &url.URL{},
30− Scope: ""})
31+ Scope: ""},
32+ options.KeycloakOptions{})
31
33
32 if backend != nil {
34 if backend != nil {
33 bURL, err := url.Parse(backend.URL)
35 bURL, err := url.Parse(backend.URL)
@@ -48,7 +50,7 @@ func testKeycloakProvider(backend *httptest.Server) (*KeycloakProvider, error) {
48var _ = Describe("Keycloak Provider Tests", func() {
50var _ = Describe("Keycloak Provider Tests", func() {
49 Context("New Provider Init", func() {
51 Context("New Provider Init", func() {
50 It("uses defaults", func() {
52 It("uses defaults", func() {
51− providerData := NewKeycloakProvider(&ProviderData{}).Data()
53+ providerData := NewKeycloakProvider(&ProviderData{}, options.KeycloakOptions{}).Data()
52 Expect(providerData.ProviderName).To(Equal("Keycloak"))
54 Expect(providerData.ProviderName).To(Equal("Keycloak"))
53 Expect(providerData.LoginURL.String()).To(Equal("https://keycloak.org/oauth/authorize"))
55 Expect(providerData.LoginURL.String()).To(Equal("https://keycloak.org/oauth/authorize"))
54 Expect(providerData.RedeemURL.String()).To(Equal("https://keycloak.org/oauth/token"))
56 Expect(providerData.RedeemURL.String()).To(Equal("https://keycloak.org/oauth/token"))
@@ -76,7 +78,8 @@ var _ = Describe("Keycloak Provider Tests", func() {
76 Scheme: "https",
78 Scheme: "https",
77 Host: "example.com",
79 Host: "example.com",
78 Path: "/api/v3/user"},
80 Path: "/api/v3/user"},
79− Scope: "profile"})
81+ Scope: "profile"},
82+ options.KeycloakOptions{})
80 providerData := p.Data()
83 providerData := p.Data()
81
84
82 Expect(providerData.ProviderName).To(Equal("Keycloak"))
85 Expect(providerData.ProviderName).To(Equal("Keycloak"))
providers/logingov.go+45−2
@@ -5,12 +5,15 @@ import (
5 "context"
5 "context"
6 "crypto/rand"
6 "crypto/rand"
7 "crypto/rsa"
7 "crypto/rsa"
8+ "errors"
8 "fmt"
9 "fmt"
10+ "io/ioutil"
9 "math/big"
11 "math/big"
10 "net/url"
12 "net/url"
11 "time"
13 "time"
12
14
13 "github.com/golang-jwt/jwt"
15 "github.com/golang-jwt/jwt"
16+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
17 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
18 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
16 "gopkg.in/square/go-jose.v2"
19 "gopkg.in/square/go-jose.v2"
@@ -78,7 +81,7 @@ var (
78)
81)
79
82
80// NewLoginGovProvider initiates a new LoginGovProvider
83// NewLoginGovProvider initiates a new LoginGovProvider
81−func NewLoginGovProvider(p *ProviderData) *LoginGovProvider {
84+func NewLoginGovProvider(p *ProviderData, opts options.LoginGovOptions) (*LoginGovProvider, error) {
82 p.setProviderDefaults(providerDefaults{
85 p.setProviderDefaults(providerDefaults{
83 name: loginGovProviderName,
86 name: loginGovProviderName,
84 loginURL: loginGovDefaultLoginURL,
87 loginURL: loginGovDefaultLoginURL,
@@ -87,10 +90,50 @@ func NewLoginGovProvider(p *ProviderData) *LoginGovProvider {
87 validateURL: loginGovDefaultProfileURL,
90 validateURL: loginGovDefaultProfileURL,
88 scope: loginGovDefaultScope,
91 scope: loginGovDefaultScope,
89 })
92 })
90− return &LoginGovProvider{
93+ provider := &LoginGovProvider{
91 ProviderData: p,
94 ProviderData: p,
92 Nonce: randSeq(32),
95 Nonce: randSeq(32),
93 }
96 }
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
94}
137}
95
138
96type loginGovCustomClaims struct {
139type loginGovCustomClaims struct {
providers/logingov_test.go+57−12
@@ -1,11 +1,14 @@
1package providers
1package providers
2
2
3import (
3import (
4+ "bytes"
4 "context"
5 "context"
5 "crypto"
6 "crypto"
6 "crypto/rand"
7 "crypto/rand"
7 "crypto/rsa"
8 "crypto/rsa"
9+ "crypto/x509"
8 "encoding/json"
10 "encoding/json"
11+ "encoding/pem"
9 "net/http"
12 "net/http"
10 "net/http/httptest"
13 "net/http/httptest"
11 "net/url"
14 "net/url"
@@ -13,6 +16,7 @@ import (
13 "time"
16 "time"
14
17
15 "github.com/golang-jwt/jwt"
18 "github.com/golang-jwt/jwt"
19+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
16 . "github.com/onsi/gomega"
20 . "github.com/onsi/gomega"
17 "github.com/stretchr/testify/assert"
21 "github.com/stretchr/testify/assert"
18 "gopkg.in/square/go-jose.v2"
22 "gopkg.in/square/go-jose.v2"
@@ -32,12 +36,35 @@ func newLoginGovServer(body []byte) (*url.URL, *httptest.Server) {
32 return u, s
36 return u, s
33}
37}
34
38
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+
35−func newLoginGovProvider() (l *LoginGovProvider, serverKey *MyKeyData, err error) {
62+func newLoginGovProvider() (*LoginGovProvider, *MyKeyData, error) {
36 key, err := rsa.GenerateKey(rand.Reader, 2048)
63 key, err := rsa.GenerateKey(rand.Reader, 2048)
37 if err != nil {
64 if err != nil {
38− return
65+ return nil, nil, err
39 }
66 }
40− serverKey = &MyKeyData{
67+ serverKey := &MyKeyData{
41 PubKey: key.Public(),
68 PubKey: key.Public(),
42 PrivKey: key,
69 PrivKey: key,
43 PubJWK: jose.JSONWebKey{
70 PubJWK: jose.JSONWebKey{
@@ -48,29 +75,40 @@ func newLoginGovProvider() (l *LoginGovProvider, serverKey *MyKeyData, err error
48 },
75 },
49 }
76 }
50
77
51− privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
78+ privKey, err := newPrivateKeyBytes()
52 if err != nil {
79 if err != nil {
53− return
80+ return nil, nil, err
54 }
81 }
55
82
56− l = NewLoginGovProvider(
83+ l, err := NewLoginGovProvider(
57 &ProviderData{
84 &ProviderData{
58 ProviderName: "",
85 ProviderName: "",
59 LoginURL: &url.URL{},
86 LoginURL: &url.URL{},
60 RedeemURL: &url.URL{},
87 RedeemURL: &url.URL{},
61 ProfileURL: &url.URL{},
88 ProfileURL: &url.URL{},
62 ValidateURL: &url.URL{},
89 ValidateURL: &url.URL{},
63− Scope: ""})
90+ Scope: ""},
64− l.JWTKey = privateKey
91+ options.LoginGovOptions{
92+ JWTKey: string(privKey),
93+ },
94+ )
65 l.Nonce = "fakenonce"
95 l.Nonce = "fakenonce"
66− return
96+ return l, serverKey, err
67}
97}
68
98
69func TestNewLoginGovProvider(t *testing.T) {
99func TestNewLoginGovProvider(t *testing.T) {
70 g := NewWithT(t)
100 g := NewWithT(t)
71
101
102+ privKey, err := newPrivateKeyBytes()
103+ g.Expect(err).ToNot(HaveOccurred())
104+
72 // Test that defaults are set when calling for a new provider with nothing set
105 // 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()
74 g.Expect(providerData.ProviderName).To(Equal("login.gov"))
112 g.Expect(providerData.ProviderName).To(Equal("login.gov"))
75 g.Expect(providerData.LoginURL.String()).To(Equal("https://secure.login.gov/openid_connect/authorize"))
113 g.Expect(providerData.LoginURL.String()).To(Equal("https://secure.login.gov/openid_connect/authorize"))
76 g.Expect(providerData.RedeemURL.String()).To(Equal("https://secure.login.gov/api/openid_connect/token"))
114 g.Expect(providerData.RedeemURL.String()).To(Equal("https://secure.login.gov/api/openid_connect/token"))
@@ -80,7 +118,10 @@ func TestNewLoginGovProvider(t *testing.T) {
80}
118}
81
119
82func TestLoginGovProviderOverrides(t *testing.T) {
120func TestLoginGovProviderOverrides(t *testing.T) {
121+ privKey, err := newPrivateKeyBytes()
122+ assert.NoError(t, err)
123+
83− p := NewLoginGovProvider(
124+ p, err := NewLoginGovProvider(
84 &ProviderData{
125 &ProviderData{
85 LoginURL: &url.URL{
126 LoginURL: &url.URL{
86 Scheme: "https",
127 Scheme: "https",
@@ -94,7 +135,11 @@ func TestLoginGovProviderOverrides(t *testing.T) {
94 Scheme: "https",
135 Scheme: "https",
95 Host: "example.com",
136 Host: "example.com",
96 Path: "/oauth/profile"},
137 Path: "/oauth/profile"},
97− Scope: "profile"})
138+ Scope: "profile"},
139+ options.LoginGovOptions{
140+ JWTKey: string(privKey),
141+ })
142+ assert.NoError(t, err)
98 assert.NotEqual(t, nil, p)
143 assert.NotEqual(t, nil, p)
99 assert.Equal(t, "login.gov", p.Data().ProviderName)
144 assert.Equal(t, "login.gov", p.Data().ProviderName)
100 assert.Equal(t, "https://example.com/oauth/auth",
145 assert.Equal(t, "https://example.com/oauth/auth",
providers/nextcloud.go+3−1
@@ -1,5 +1,7 @@
1package providers
1package providers
2
2
3+import "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
4+
3// NextcloudProvider represents an Nextcloud based Identity Provider
5// NextcloudProvider represents an Nextcloud based Identity Provider
4type NextcloudProvider struct {
6type NextcloudProvider struct {
5 *ProviderData
7 *ProviderData
@@ -13,7 +15,7 @@ const nextCloudProviderName = "Nextcloud"
13func NewNextcloudProvider(p *ProviderData) *NextcloudProvider {
15func NewNextcloudProvider(p *ProviderData) *NextcloudProvider {
14 p.ProviderName = nextCloudProviderName
16 p.ProviderName = nextCloudProviderName
15 p.getAuthorizationHeaderFunc = makeOIDCHeader
17 p.getAuthorizationHeaderFunc = makeOIDCHeader
16− if p.EmailClaim == OIDCEmailClaim {
18+ if p.EmailClaim == options.OIDCEmailClaim {
17 // This implies the email claim has not been overridden, we should set a default
19 // This implies the email claim has not been overridden, we should set a default
18 // for this provider
20 // for this provider
19 p.EmailClaim = "ocs.data.email"
21 p.EmailClaim = "ocs.data.email"
providers/oidc.go+3−2
@@ -7,6 +7,7 @@ import (
7 "net/url"
7 "net/url"
8 "time"
8 "time"
9
9
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
12 "golang.org/x/oauth2"
13 "golang.org/x/oauth2"
@@ -20,13 +21,13 @@ type OIDCProvider struct {
20}
21}
21
22
22// NewOIDCProvider initiates a new OIDCProvider
23// NewOIDCProvider initiates a new OIDCProvider
23−func NewOIDCProvider(p *ProviderData) *OIDCProvider {
24+func NewOIDCProvider(p *ProviderData, opts options.OIDCOptions) *OIDCProvider {
24 p.ProviderName = "OpenID Connect"
25 p.ProviderName = "OpenID Connect"
25 p.getAuthorizationHeaderFunc = makeOIDCHeader
26 p.getAuthorizationHeaderFunc = makeOIDCHeader
26
27
27 return &OIDCProvider{
28 return &OIDCProvider{
28 ProviderData: p,
29 ProviderData: p,
29− SkipNonce: true,
30+ SkipNonce: opts.InsecureSkipNonce,
30 }
31 }
31}
32}
32
33
providers/oidc_test.go+7−4
@@ -11,6 +11,7 @@ import (
11 "testing"
11 "testing"
12
12
13 "github.com/coreos/go-oidc/v3/oidc"
13 "github.com/coreos/go-oidc/v3/oidc"
14+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
16 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
16 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
17 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
@@ -25,7 +26,7 @@ type redeemTokenResponse struct {
25 IDToken string `json:"id_token,omitempty"`
26 IDToken string `json:"id_token,omitempty"`
26}
27}
27
28
28−func newOIDCProvider(serverURL *url.URL) *OIDCProvider {
29+func newOIDCProvider(serverURL *url.URL, skipNonce bool) *OIDCProvider {
29 verificationOptions := &internaloidc.IDTokenVerificationOptions{
30 verificationOptions := &internaloidc.IDTokenVerificationOptions{
30 AudienceClaims: []string{"aud"},
31 AudienceClaims: []string{"aud"},
31 ClientID: "https://test.myapp.com",
32 ClientID: "https://test.myapp.com",
@@ -61,7 +62,9 @@ func newOIDCProvider(serverURL *url.URL) *OIDCProvider {
61 ), verificationOptions),
62 ), verificationOptions),
62 }
63 }
63
64
64− p := NewOIDCProvider(providerData)
65+ p := NewOIDCProvider(providerData, options.OIDCOptions{
66+ InsecureSkipNonce: skipNonce,
67+ })
65
68
66 return p
69 return p
67}
70}
@@ -77,7 +80,7 @@ func newOIDCServer(body []byte) (*url.URL, *httptest.Server) {
77
80
78func newTestOIDCSetup(body []byte) (*httptest.Server, *OIDCProvider) {
81func newTestOIDCSetup(body []byte) (*httptest.Server, *OIDCProvider) {
79 redeemURL, server := newOIDCServer(body)
82 redeemURL, server := newOIDCServer(body)
80− provider := newOIDCProvider(redeemURL)
83+ provider := newOIDCProvider(redeemURL, false)
81 return server, provider
84 return server, provider
82}
85}
83
86
@@ -86,7 +89,7 @@ func TestOIDCProviderGetLoginURL(t *testing.T) {
86 Scheme: "https",
89 Scheme: "https",
87 Host: "oauth2proxy.oidctest",
90 Host: "oauth2proxy.oidctest",
88 }
91 }
89− provider := newOIDCProvider(serverURL)
92+ provider := newOIDCProvider(serverURL, true)
90
93
91 n, err := encryption.Nonce()
94 n, err := encryption.Nonce()
92 assert.NoError(t, err)
95 assert.NoError(t, err)
providers/provider_data.go+4−7
@@ -10,6 +10,7 @@ import (
10 "strings"
10 "strings"
11
11
12 "github.com/coreos/go-oidc/v3/oidc"
12 "github.com/coreos/go-oidc/v3/oidc"
13+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
13 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
15 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
16 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
@@ -18,14 +19,10 @@ import (
18)
19)
19
20
20const (
21const (
21− OIDCEmailClaim = "email"
22− OIDCGroupsClaim = "groups"
23 // This is not exported as it's not currently user configurable
22 // This is not exported as it's not currently user configurable
24 oidcUserClaim = "sub"
23 oidcUserClaim = "sub"
25)
24)
26
25
27−var OIDCAudienceClaims = []string{"aud"}
28−
29// ProviderData contains information required to configure all implementations
26// ProviderData contains information required to configure all implementations
30// of OAuth2 providers
27// of OAuth2 providers
31type ProviderData struct {
28type ProviderData struct {
@@ -76,9 +73,9 @@ func (p *ProviderData) GetClientSecret() (clientSecret string, err error) {
76 return string(fileClientSecret), nil
73 return string(fileClientSecret), nil
77}
74}
78
75
79−// SetAllowedGroups organizes a group list into the AllowedGroups map
76+// setAllowedGroups organizes a group list into the AllowedGroups map
80// to be consumed by Authorize implementations
77// to be consumed by Authorize implementations
81−func (p *ProviderData) SetAllowedGroups(groups []string) {
78+func (p *ProviderData) setAllowedGroups(groups []string) {
82 p.AllowedGroups = make(map[string]struct{}, len(groups))
79 p.AllowedGroups = make(map[string]struct{}, len(groups))
83 for _, group := range groups {
80 for _, group := range groups {
84 p.AllowedGroups[group] = struct{}{}
81 p.AllowedGroups[group] = struct{}{}
@@ -172,7 +169,7 @@ func (p *ProviderData) buildSessionFromClaims(rawIDToken, accessToken string) (*
172
169
173 // `email_verified` must be present and explicitly set to `false` to be
170 // `email_verified` must be present and explicitly set to `false` to be
174 // considered unverified.
171 // considered unverified.
175− verifyEmail := (p.EmailClaim == OIDCEmailClaim) && !p.AllowUnverifiedEmail
172+ verifyEmail := (p.EmailClaim == options.OIDCEmailClaim) && !p.AllowUnverifiedEmail
176
173
177 var verified bool
174 var verified bool
178 exists, err := extractor.GetClaimInto("email_verified", &verified)
175 exists, err := extractor.GetClaimInto("email_verified", &verified)
providers/provider_default_test.go+1−1
@@ -107,7 +107,7 @@ func TestProviderDataAuthorize(t *testing.T) {
107 Groups: tc.groups,
107 Groups: tc.groups,
108 }
108 }
109 p := &ProviderData{}
109 p := &ProviderData{}
110− p.SetAllowedGroups(tc.allowedGroups)
110+ p.setAllowedGroups(tc.allowedGroups)
111
111
112 authorized, err := p.Authorize(context.Background(), session)
112 authorized, err := p.Authorize(context.Background(), session)
113 g.Expect(err).ToNot(HaveOccurred())
113 g.Expect(err).ToNot(HaveOccurred())
providers/providers.go+251−32
@@ -2,8 +2,18 @@ package providers
2
2
3import (
3import (
4 "context"
4 "context"
5+ "errors"
6+ "fmt"
7+ "net/url"
8+ "strings"
5
9
10+ "github.com/coreos/go-oidc/v3/oidc"
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
6 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12 "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"
7)
17)
8
18
9// Provider represents an upstream identity provider implementation
19// Provider represents an upstream identity provider implementation
@@ -20,38 +30,247 @@ type Provider interface {
20 CreateSessionFromToken(ctx context.Context, token string) (*sessions.SessionState, error)
30 CreateSessionFromToken(ctx context.Context, token string) (*sessions.SessionState, error)
21}
31}
22
32
23−// New provides a new Provider based on the configured provider string
33+func NewProvider(providerConfig options.Provider) (Provider, error) {
24−func New(provider string, p *ProviderData) Provider {
34+ providerData, err := newProviderDataFromConfig(providerConfig)
25− switch provider {
35+ if err != nil {
26− case "linkedin":
36+ return nil, fmt.Errorf("could not create provider data: %v", err)
27− return NewLinkedInProvider(p)
37+ }
28− case "facebook":
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:
29− return NewFacebookProvider(p)
48+ return NewFacebookProvider(providerData), nil
30− case "github":
49+ case options.GitHubProvider:
31− return NewGitHubProvider(p)
50+ return NewGitHubProvider(providerData, providerConfig.GitHubConfig), nil
32− case "keycloak":
51+ case options.GitLabProvider:
52+ return NewGitLabProvider(providerData, providerConfig.GitLabConfig)
53+ case options.GoogleProvider:
54+ return NewGoogleProvider(providerData, providerConfig.GoogleConfig)
55+ case options.KeycloakProvider:
33− return NewKeycloakProvider(p)
56+ return NewKeycloakProvider(providerData, providerConfig.KeycloakConfig), nil
34− case "keycloak-oidc":
57+ case options.KeycloakOIDCProvider:
35− return NewKeycloakOIDCProvider(p)
58+ return NewKeycloakOIDCProvider(providerData, providerConfig.KeycloakConfig), nil
36− case "azure":
59+ case options.LinkedInProvider:
37− return NewAzureProvider(p)
60+ return NewLinkedInProvider(providerData), nil
38− case "adfs":
61+ case options.LoginGovProvider:
39− return NewADFSProvider(p)
62+ return NewLoginGovProvider(providerData, providerConfig.LoginGovConfig)
40− case "gitlab":
63+ case options.NextCloudProvider:
41− return NewGitLabProvider(p)
64+ return NewNextcloudProvider(providerData), nil
42− case "oidc":
65+ case options.OIDCProvider:
43− return NewOIDCProvider(p)
66+ return NewOIDCProvider(providerData, providerConfig.OIDCConfig), nil
44− case "login.gov":
67+ default:
45− return NewLoginGovProvider(p)
68+ return nil, fmt.Errorf("unknown provider type %q", providerConfig.Type)
46− case "bitbucket":
69+ }
47− return NewBitbucketProvider(p)
70+}
48− case "nextcloud":
71+
49− return NewNextcloudProvider(p)
72+func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData, error) {
50− case "digitalocean":
73+ p := &ProviderData{
51− return NewDigitalOceanProvider(p)
74+ Scope: providerConfig.Scope,
52− case "google":
75+ ClientID: providerConfig.ClientID,
53− return NewGoogleProvider(p)
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
54 default:
158 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,
56 }
269 }
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
57}
276}
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+}