git.iko.soy/typeable/oauth2-proxy
on master help sign in
fix gitea token validation by allowing custom validation url and extracting the proper base api url for github cloud, github enterprise and gitea (#2194)
commit13af1b478633c449a99d7ae9051933a76a41d333 authorJan Larwig <jan@larwig.com> dateThu 14 Sep 2023, 11:09 (3 years ago) parent225dc92
8 files changed+306−72
CHANGELOG.md+3−2 contrib/local-environment/Makefile+18−10 contrib/local-environment/docker-compose-gitea.yaml+65 contrib/local-environment/oauth2-proxy-gitea.cfg+19 docs/docs/configuration/auth.md+20−19 providers/gitea_test.go+98 providers/github.go+28−37 providers/github_test.go+55−4
CHANGELOG.md+3−2
@@ -9,9 +9,10 @@
99## Changes since v7.5.0
1010- [#2220](https://github.com/oauth2-proxy/oauth2-proxy/pull/2220) Added binary and docker release platforms (@kvanzuijlen)
1111- [#2221](https://github.com/oauth2-proxy/oauth2-proxy/pull/2221) Backwards compatible fix for wrong environment variable name (OAUTH2_PROXY_GOOGLE_GROUPS) (@kvanzuijlen)
12−- [#1989](https://github.com/oauth2-proxy/oauth2-proxy/pull/1989) Fix default scope for keycloak-oidc provider
12+- [#1989](https://github.com/oauth2-proxy/oauth2-proxy/pull/1989) Fix default scope for keycloak-oidc provider (@tuunit)
1313- [#2217](https://github.com/oauth2-proxy/oauth2-proxy/pull/2217) Upgrade alpine to version 3.18 (@polarctos)
14−- [#2229](https://github.com/oauth2-proxy/oauth2-proxy/pull/2229) bugfix: default scopes for OIDCProvider based providers
14+- [#2229](https://github.com/oauth2-proxy/oauth2-proxy/pull/2229) bugfix: default scopes for OIDCProvider based providers (@tuunit)
15+- [#2194](https://github.com/oauth2-proxy/oauth2-proxy/pull/2194) Fix Gitea token validation (@tuunit)
1516
1617# V7.5.0
1718
contrib/local-environment/Makefile+18−10
@@ -1,34 +1,42 @@
11.PHONY: up
22up:
3− docker-compose up -d
3+ docker compose up -d
44
55.PHONY: %
66%:
7− docker-compose $*
7+ docker compose $*
88
99.PHONY: alpha-config-up
1010alpha-config-up:
11− docker-compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml up -d
11+ docker compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml up -d
1212
1313.PHONY: alpha-config-%
1414alpha-config-%:
15− docker-compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml $*
15+ docker compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml $*
1616
1717.PHONY: nginx-up
1818nginx-up:
19− docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml up -d
19+ docker compose -f docker-compose.yaml -f docker-compose-nginx.yaml up -d
2020
2121.PHONY: nginx-%
2222nginx-%:
23− docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml $*
23+ docker compose -f docker-compose.yaml -f docker-compose-nginx.yaml $*
2424
2525.PHONY: keycloak-up
2626keycloak-up:
27− docker-compose -f docker-compose-keycloak.yaml up -d
27+ docker compose -f docker-compose-keycloak.yaml up -d
2828
2929.PHONY: keycloak-%
3030keycloak-%:
31− docker-compose -f docker-compose-keycloak.yaml $*
31+ docker compose -f docker-compose-keycloak.yaml $*
32+
33+.PHONY: gitea-up
34+gitea-up:
35+ docker compose -f docker-compose-gitea.yaml up -d
36+
37+.PHONY: gitea-%
38+gitea-%:
39+ docker compose -f docker-compose-gitea.yaml $*
3240
3341.PHONY: kubernetes-up
3442kubernetes-up:
@@ -41,8 +49,8 @@ kubernetes-down:
4149
4250.PHONY: traefik-up
4351traefik-up:
44− docker-compose -f docker-compose.yaml -f docker-compose-traefik.yaml up -d
52+ docker compose -f docker-compose.yaml -f docker-compose-traefik.yaml up -d
4553
4654.PHONY: traefik-%
4755traefik-%:
48− docker-compose -f docker-compose.yaml -f docker-compose-traefik.yaml $*
56+ docker compose -f docker-compose.yaml -f docker-compose-traefik.yaml $*
contrib/local-environment/docker-compose-gitea.yamlnew file+65
@@ -0,0 +1,65 @@
1+# This docker-compose file can be used to bring up an example instance of oauth2-proxy
2+# for manual testing and exploration of features.
3+# Alongside OAuth2-Proxy, this file also starts Gitea to act as the identity provider,
4+# HTTPBin as an example upstream.
5+#
6+# This can either be created using docker-compose
7+# docker-compose -f docker-compose-gitea.yaml <command>
8+# Or:
9+# make gitea-<command> (eg. make gitea-up, make gitea-down)
10+#
11+# Access http://oauth2-proxy.localtest.me:4180 to initiate a login cycle using user=admin@example.com, password=password
12+# Access http://gitea.localtest.me:3000 with the same credentials to check out the settings
13+version: '3.0'
14+services:
15+ oauth2-proxy:
16+ container_name: oauth2-proxy
17+ image: gitea-oauth #quay.io/oauth2-proxy/oauth2-proxy:v7.4.0
18+ command: --config /oauth2-proxy.cfg
19+ hostname: oauth2-proxy
20+ volumes:
21+ - "./oauth2-proxy-gitea.cfg:/oauth2-proxy.cfg"
22+ restart: unless-stopped
23+ networks:
24+ gitea: {}
25+ httpbin: {}
26+ oauth2-proxy: {}
27+ depends_on:
28+ - httpbin
29+ - gitea
30+ ports:
31+ - 4180:4180/tcp
32+
33+ httpbin:
34+ container_name: httpbin
35+ image: kennethreitz/httpbin:latest
36+ hostname: httpbin
37+ ports:
38+ - 8080:80
39+ networks:
40+ httpbin:
41+ aliases:
42+ - httpbin.localtest.me
43+
44+ gitea:
45+ image: gitea/gitea:latest
46+ container_name: gitea
47+ environment:
48+ - USER_UID=1000
49+ - USER_GID=1000
50+ restart: always
51+ networks:
52+ gitea:
53+ aliases:
54+ - gitea.localtest.me
55+ volumes:
56+ - /etc/timezone:/etc/timezone:ro
57+ - /etc/localtime:/etc/localtime:ro
58+ ports:
59+ - "3000:3000"
60+ - "222:22"
61+
62+networks:
63+ httpbin: {}
64+ gitea: {}
65+ oauth2-proxy: {}
contrib/local-environment/oauth2-proxy-gitea.cfgnew file+19
@@ -0,0 +1,19 @@
1+http_address="0.0.0.0:4180"
2+cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
3+email_domains=["localhost"]
4+cookie_secure="false"
5+upstreams="http://httpbin"
6+cookie_domains=[".localtest.me"] # Required so cookie can be read on all subdomains.
7+whitelist_domains=[".localtest.me"] # Required to allow redirection back to original requested target.
8+
9+client_id="ef0c2b91-2e38-4fa8-908d-067a35dbb71c"
10+client_secret="gto_qdppomn2p26su5x46tyixj7bcny5m5er2s67xhrponq2qtp66f3a"
11+redirect_url="http://oauth2-proxy.localtest.me:4180/oauth2/callback"
12+
13+# gitea provider
14+provider="github"
15+provider_display_name="Gitea"
16+login_url="http://gitea.localtest.me:3000/login/oauth/authorize"
17+redeem_url="http://gitea.localtest.me:3000/login/oauth/access_token"
18+validate_url="http://gitea.localtest.me:3000/api/v1/user/emails"
19+
docs/docs/configuration/auth.md+20−19
@@ -12,6 +12,7 @@ Valid providers are :
1212- [ADFS](#adfs-auth-provider)
1313- [Facebook](#facebook-auth-provider)
1414- [GitHub](#github-auth-provider)
15+- [Gitea](#gitea-auth-provider)
1516- [Keycloak](#keycloak-auth-provider)
1617- [GitLab](#gitlab-auth-provider)
1718- [LinkedIn](#linkedin-auth-provider)
@@ -21,7 +22,6 @@ Valid providers are :
2122- [Nextcloud](#nextcloud-provider)
2223- [DigitalOcean](#digitalocean-auth-provider)
2324- [Bitbucket](#bitbucket-auth-provider)
24−- [Gitea](#gitea-auth-provider)
2525
2626The provider can be selected using the `provider` configuration value.
2727
@@ -177,6 +177,25 @@ If you are using GitHub enterprise, make sure you set the following to the appro
177177 -redeem-url="http(s)://<enterprise github host>/login/oauth/access_token"
178178 -validate-url="http(s)://<enterprise github host>/api/v3"
179179
180+### Gitea Auth Provider
181+
182+1. Create a new application: `https://< your gitea host >/user/settings/applications`
183+2. Under `Redirect URI` enter the correct URL i.e. `https://<proxied host>/oauth2/callback`
184+3. Note the Client ID and Client Secret.
185+4. Pass the following options to the proxy:
186+
187+```
188+ --provider="github"
189+ --redirect-url="https://<proxied host>/oauth2/callback"
190+ --provider-display-name="Gitea"
191+ --client-id="< client_id as generated by Gitea >"
192+ --client-secret="< client_secret as generated by Gitea >"
193+ --login-url="https://< your gitea host >/login/oauth/authorize"
194+ --redeem-url="https://< your gitea host >/login/oauth/access_token"
195+ --validate-url="https://< your gitea host >/api/v1/user/emails"
196+```
197+
198+
180199### Keycloak Auth Provider
181200
182201:::note
@@ -660,24 +679,6 @@ To use the provider, pass the following options:
660679The default configuration allows everyone with Bitbucket account to authenticate. To restrict the access to the team members use additional configuration option: `--bitbucket-team=<Team name>`. To restrict the access to only these users who has access to one selected repository use `--bitbucket-repository=<Repository name>`.
661680
662681
663−### Gitea Auth Provider
664−
665−1. Create a new application: `https://< your gitea host >/user/settings/applications`
666−2. Under `Redirect URI` enter the correct URL i.e. `https://<proxied host>/oauth2/callback`
667−3. Note the Client ID and Client Secret.
668−4. Pass the following options to the proxy:
669−
670−```
671− --provider="github"
672− --redirect-url="https://<proxied host>/oauth2/callback"
673− --provider-display-name="Gitea"
674− --client-id="< client_id as generated by Gitea >"
675− --client-secret="< client_secret as generated by Gitea >"
676− --login-url="https://< your gitea host >/login/oauth/authorize"
677− --redeem-url="https://< your gitea host >/login/oauth/access_token"
678− --validate-url="https://< your gitea host >/api/v1"
679−```
680−
681682
682683## Email Authentication
683684
providers/gitea_test.gonew file+98
@@ -0,0 +1,98 @@
1+package providers
2+
3+import (
4+ "context"
5+ "net/http"
6+ "net/http/httptest"
7+ "net/url"
8+ "testing"
9+
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
11+ "github.com/stretchr/testify/assert"
12+)
13+
14+func testGiteaProvider(hostname string, opts options.GitHubOptions) *GitHubProvider {
15+ p := NewGitHubProvider(
16+ &ProviderData{
17+ ProviderName: "Gitea",
18+ LoginURL: &url.URL{},
19+ RedeemURL: &url.URL{},
20+ ProfileURL: &url.URL{},
21+ ValidateURL: &url.URL{Path: "/api/v1/user/emails"},
22+ Scope: ""},
23+ opts)
24+ p.ProviderName = "Gitea"
25+
26+ if hostname != "" {
27+ updateURL(p.Data().LoginURL, hostname)
28+ updateURL(p.Data().RedeemURL, hostname)
29+ updateURL(p.Data().ProfileURL, hostname)
30+ updateURL(p.Data().ValidateURL, hostname)
31+ }
32+ return p
33+}
34+
35+func testGiteaBackend(payloads map[string][]string) *httptest.Server {
36+ pathToQueryMap := map[string][]string{
37+ "/api/v1/repos/oauth2-proxy/oauth2-proxy": {""},
38+ "/api/v1/repos/oauth2-proxy/oauth2-proxy/collaborators/mbland": {""},
39+ "/api/v1/user": {""},
40+ "/api/v1/user/emails": {""},
41+ "/api/v1/user/orgs": {"page=1&per_page=100", "page=2&per_page=100", "page=3&per_page=100"},
42+ }
43+
44+ return httptest.NewServer(http.HandlerFunc(
45+ func(w http.ResponseWriter, r *http.Request) {
46+ query, ok := pathToQueryMap[r.URL.Path]
47+ validQuery := false
48+ index := 0
49+ for i, q := range query {
50+ if q == r.URL.RawQuery {
51+ validQuery = true
52+ index = i
53+ }
54+ }
55+ payload := []string{}
56+ if ok && validQuery {
57+ payload, ok = payloads[r.URL.Path]
58+ }
59+ if !ok {
60+ w.WriteHeader(404)
61+ } else if !validQuery {
62+ w.WriteHeader(404)
63+ } else if payload[index] == "" {
64+ w.WriteHeader(204)
65+ } else {
66+ w.WriteHeader(200)
67+ w.Write([]byte(payload[index]))
68+ }
69+ }))
70+}
71+
72+func TestGiteaProvider_ValidateSessionWithBaseUrl(t *testing.T) {
73+ b := testGiteaBackend(map[string][]string{})
74+ defer b.Close()
75+
76+ bURL, _ := url.Parse(b.URL)
77+ p := testGiteaProvider(bURL.Host, options.GitHubOptions{})
78+
79+ session := CreateAuthorizedSession()
80+
81+ valid := p.ValidateSession(context.Background(), session)
82+ assert.False(t, valid)
83+}
84+
85+func TestGiteaProvider_ValidateSessionWithUserEmails(t *testing.T) {
86+ b := testGiteaBackend(map[string][]string{
87+ "/api/v1/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`},
88+ })
89+ defer b.Close()
90+
91+ bURL, _ := url.Parse(b.URL)
92+ p := testGiteaProvider(bURL.Host, options.GitHubOptions{})
93+
94+ session := CreateAuthorizedSession()
95+
96+ valid := p.ValidateSession(context.Background(), session)
97+ assert.True(t, valid)
98+}
providers/github.go+28−37
@@ -89,6 +89,27 @@ func makeGitHubHeader(accessToken string) http.Header {
8989 return makeAuthorizationHeader(tokenTypeToken, accessToken, extraHeaders)
9090}
9191
92+func (p *GitHubProvider) makeGitHubAPIEndpoint(endpoint string, params *url.Values) *url.URL {
93+ basePath := p.ValidateURL.Path
94+
95+ re := regexp.MustCompile(`^/api/v\d+`)
96+ match := re.FindString(p.ValidateURL.Path)
97+ if match != "" {
98+ basePath = match
99+ }
100+
101+ if params == nil {
102+ params = &url.Values{}
103+ }
104+
105+ return &url.URL{
106+ Scheme: p.ValidateURL.Scheme,
107+ Host: p.ValidateURL.Host,
108+ Path: path.Join(basePath, endpoint),
109+ RawQuery: params.Encode(),
110+ }
111+}
112+
92113// setOrgTeam adds GitHub org reading parameters to the OAuth2 scope
93114func (p *GitHubProvider) setOrgTeam(org, team string) {
94115 p.Org = org
@@ -141,12 +162,7 @@ func (p *GitHubProvider) hasOrg(ctx context.Context, accessToken string) (bool,
141162 "page": {strconv.Itoa(pn)},
142163 }
143164
144− endpoint := &url.URL{
145− Scheme: p.ValidateURL.Scheme,
146− Host: p.ValidateURL.Host,
147− Path: path.Join(p.ValidateURL.Path, "/user/orgs"),
148− RawQuery: params.Encode(),
149− }
165+ endpoint := p.makeGitHubAPIEndpoint("/user/orgs", &params)
150166
151167 var op orgsPage
152168 err := requests.New(endpoint.String()).
@@ -206,12 +222,7 @@ func (p *GitHubProvider) hasOrgAndTeam(ctx context.Context, accessToken string)
206222 "page": {strconv.Itoa(pn)},
207223 }
208224
209− endpoint := &url.URL{
210− Scheme: p.ValidateURL.Scheme,
211− Host: p.ValidateURL.Host,
212− Path: path.Join(p.ValidateURL.Path, "/user/teams"),
213− RawQuery: params.Encode(),
214− }
225+ endpoint := p.makeGitHubAPIEndpoint("/user/teams", &params)
215226
216227 // bodyclose cannot detect that the body is being closed later in requests.Into,
217228 // so have to skip the linting for the next line.
@@ -309,11 +320,7 @@ func (p *GitHubProvider) hasRepo(ctx context.Context, accessToken string) (bool,
309320 Private bool `json:"private"`
310321 }
311322
312− endpoint := &url.URL{
313− Scheme: p.ValidateURL.Scheme,
314− Host: p.ValidateURL.Host,
315− Path: path.Join(p.ValidateURL.Path, "/repos/", p.Repo),
316− }
323+ endpoint := p.makeGitHubAPIEndpoint("/repos/"+p.Repo, nil)
317324
318325 var repo repository
319326 err := requests.New(endpoint.String()).
@@ -338,11 +345,7 @@ func (p *GitHubProvider) hasUser(ctx context.Context, accessToken string) (bool,
338345 Email string `json:"email"`
339346 }
340347
341− endpoint := &url.URL{
342− Scheme: p.ValidateURL.Scheme,
343− Host: p.ValidateURL.Host,
344− Path: path.Join(p.ValidateURL.Path, "/user"),
345− }
348+ endpoint := p.makeGitHubAPIEndpoint("/user", nil)
346349
347350 err := requests.New(endpoint.String()).
348351 WithContext(ctx).
@@ -362,11 +365,7 @@ func (p *GitHubProvider) hasUser(ctx context.Context, accessToken string) (bool,
362365func (p *GitHubProvider) isCollaborator(ctx context.Context, username, accessToken string) (bool, error) {
363366 //https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator
364367
365− endpoint := &url.URL{
366− Scheme: p.ValidateURL.Scheme,
367− Host: p.ValidateURL.Host,
368− Path: path.Join(p.ValidateURL.Path, "/repos/", p.Repo, "/collaborators/", username),
369− }
368+ endpoint := p.makeGitHubAPIEndpoint("/repos/"+p.Repo+"/collaborators/"+username, nil)
370369 result := requests.New(endpoint.String()).
371370 WithContext(ctx).
372371 WithHeaders(makeGitHubHeader(accessToken)).
@@ -426,11 +425,7 @@ func (p *GitHubProvider) getEmail(ctx context.Context, s *sessions.SessionState)
426425 }
427426 }
428427
429− endpoint := &url.URL{
430− Scheme: p.ValidateURL.Scheme,
431− Host: p.ValidateURL.Host,
432− Path: path.Join(p.ValidateURL.Path, "/user/emails"),
433− }
428+ endpoint := p.makeGitHubAPIEndpoint("/user/emails", nil)
434429 err := requests.New(endpoint.String()).
435430 WithContext(ctx).
436431 WithHeaders(makeGitHubHeader(s.AccessToken)).
@@ -459,11 +454,7 @@ func (p *GitHubProvider) getUser(ctx context.Context, s *sessions.SessionState)
459454 Email string `json:"email"`
460455 }
461456
462− endpoint := &url.URL{
463− Scheme: p.ValidateURL.Scheme,
464− Host: p.ValidateURL.Host,
465− Path: path.Join(p.ValidateURL.Path, "/user"),
466− }
457+ endpoint := p.makeGitHubAPIEndpoint("/user", nil)
467458
468459 err := requests.New(endpoint.String()).
469460 WithContext(ctx).
providers/github_test.go+55−4
@@ -34,11 +34,15 @@ func testGitHubProvider(hostname string, opts options.GitHubOptions) *GitHubProv
3434
3535func testGitHubBackend(payloads map[string][]string) *httptest.Server {
3636 pathToQueryMap := map[string][]string{
37− "/repos/oauth2-proxy/oauth2-proxy": {""},
37+ "/": {""},
38+ "/repos/oauth2-proxy/oauth2-proxy": {""},
3839 "/repos/oauth2-proxy/oauth2-proxy/collaborators/mbland": {""},
3940 "/user": {""},
4041 "/user/emails": {""},
4142 "/user/orgs": {"page=1&per_page=100", "page=2&per_page=100", "page=3&per_page=100"},
43+ // GitHub Enterprise Server API
44+ "/api/v3": {""},
45+ "/api/v3/user/emails": {""},
4246 }
4347
4448 return httptest.NewServer(http.HandlerFunc(
@@ -75,10 +79,10 @@ func TestNewGitHubProvider(t *testing.T) {
7579 // Test that defaults are set when calling for a new provider with nothing set
7680 providerData := NewGitHubProvider(&ProviderData{}, options.GitHubOptions{}).Data()
7781 g.Expect(providerData.ProviderName).To(Equal("GitHub"))
78− g.Expect(providerData.LoginURL.String()).To(Equal("https://github.com/login/oauth/authorize"))
79− g.Expect(providerData.RedeemURL.String()).To(Equal("https://github.com/login/oauth/access_token"))
82+ g.Expect(providerData.LoginURL.String()).To(Equal(githubDefaultLoginURL.String()))
83+ g.Expect(providerData.RedeemURL.String()).To(Equal(githubDefaultRedeemURL.String()))
8084 g.Expect(providerData.ProfileURL.String()).To(Equal(""))
81− g.Expect(providerData.ValidateURL.String()).To(Equal("https://api.github.com/"))
85+ g.Expect(providerData.ValidateURL.String()).To(Equal(githubDefaultValidateURL.String()))
8286 g.Expect(providerData.Scope).To(Equal("user:email"))
8387}
8488
@@ -440,3 +444,50 @@ func TestGitHubProvider_getEmailWithUsernameAndNoAccessToPrivateRepo(t *testing.
440444 assert.NoError(t, err)
441445 assert.Equal(t, "michael.bland@gsa.gov", session.Email)
442446}
447+
448+func TestGitHubProvider_ValidateSessionWithBaseUrl(t *testing.T) {
449+ b := testGitHubBackend(map[string][]string{
450+ "/": {`[]`},
451+ })
452+ defer b.Close()
453+
454+ bURL, _ := url.Parse(b.URL)
455+ p := testGitHubProvider(bURL.Host, options.GitHubOptions{})
456+
457+ session := CreateAuthorizedSession()
458+
459+ valid := p.ValidateSession(context.Background(), session)
460+ assert.True(t, valid)
461+}
462+
463+func TestGitHubProvider_ValidateSessionWithEnterpriseBaseUrl(t *testing.T) {
464+ b := testGitHubBackend(map[string][]string{
465+ "/api/v3": {`[]`},
466+ })
467+ defer b.Close()
468+
469+ bURL, _ := url.Parse(b.URL)
470+ p := testGitHubProvider(bURL.Host, options.GitHubOptions{})
471+ p.ValidateURL.Path = "/api/v3"
472+
473+ session := CreateAuthorizedSession()
474+
475+ valid := p.ValidateSession(context.Background(), session)
476+ assert.True(t, valid)
477+}
478+
479+func TestGitHubProvider_ValidateSessionWithUserEmails(t *testing.T) {
480+ b := testGitHubBackend(map[string][]string{
481+ "/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`},
482+ })
483+ defer b.Close()
484+
485+ bURL, _ := url.Parse(b.URL)
486+ p := testGitHubProvider(bURL.Host, options.GitHubOptions{})
487+ p.ValidateURL.Path = "/user/emails"
488+
489+ session := CreateAuthorizedSession()
490+
491+ valid := p.ValidateSession(context.Background(), session)
492+ assert.True(t, valid)
493+}