diff --git a/CHANGELOG.md b/CHANGELOG.md index cd42ac4e..e6791979 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,10 @@ ## Changes since v7.5.0 - [#2220](https://github.com/oauth2-proxy/oauth2-proxy/pull/2220) Added binary and docker release platforms (@kvanzuijlen) - [#2221](https://github.com/oauth2-proxy/oauth2-proxy/pull/2221) Backwards compatible fix for wrong environment variable name (OAUTH2_PROXY_GOOGLE_GROUPS) (@kvanzuijlen) -- [#1989](https://github.com/oauth2-proxy/oauth2-proxy/pull/1989) Fix default scope for keycloak-oidc provider +- [#1989](https://github.com/oauth2-proxy/oauth2-proxy/pull/1989) Fix default scope for keycloak-oidc provider (@tuunit) - [#2217](https://github.com/oauth2-proxy/oauth2-proxy/pull/2217) Upgrade alpine to version 3.18 (@polarctos) -- [#2229](https://github.com/oauth2-proxy/oauth2-proxy/pull/2229) bugfix: default scopes for OIDCProvider based providers +- [#2229](https://github.com/oauth2-proxy/oauth2-proxy/pull/2229) bugfix: default scopes for OIDCProvider based providers (@tuunit) +- [#2194](https://github.com/oauth2-proxy/oauth2-proxy/pull/2194) Fix Gitea token validation (@tuunit) # V7.5.0 diff --git a/contrib/local-environment/Makefile b/contrib/local-environment/Makefile index a04355cf..2d7adf24 100644 --- a/contrib/local-environment/Makefile +++ b/contrib/local-environment/Makefile @@ -1,34 +1,42 @@ .PHONY: up up: - docker-compose up -d + docker compose up -d .PHONY: % %: - docker-compose $* + docker compose $* .PHONY: alpha-config-up alpha-config-up: - docker-compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml up -d + docker compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml up -d .PHONY: alpha-config-% alpha-config-%: - docker-compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml $* + docker compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml $* .PHONY: nginx-up nginx-up: - docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml up -d + docker compose -f docker-compose.yaml -f docker-compose-nginx.yaml up -d .PHONY: nginx-% nginx-%: - docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml $* + docker compose -f docker-compose.yaml -f docker-compose-nginx.yaml $* .PHONY: keycloak-up keycloak-up: - docker-compose -f docker-compose-keycloak.yaml up -d + docker compose -f docker-compose-keycloak.yaml up -d .PHONY: keycloak-% keycloak-%: - docker-compose -f docker-compose-keycloak.yaml $* + docker compose -f docker-compose-keycloak.yaml $* + +.PHONY: gitea-up +gitea-up: + docker compose -f docker-compose-gitea.yaml up -d + +.PHONY: gitea-% +gitea-%: + docker compose -f docker-compose-gitea.yaml $* .PHONY: kubernetes-up kubernetes-up: @@ -41,8 +49,8 @@ kubernetes-down: .PHONY: traefik-up traefik-up: - docker-compose -f docker-compose.yaml -f docker-compose-traefik.yaml up -d + docker compose -f docker-compose.yaml -f docker-compose-traefik.yaml up -d .PHONY: traefik-% traefik-%: - docker-compose -f docker-compose.yaml -f docker-compose-traefik.yaml $* + docker compose -f docker-compose.yaml -f docker-compose-traefik.yaml $* diff --git a/contrib/local-environment/docker-compose-gitea.yaml b/contrib/local-environment/docker-compose-gitea.yaml new file mode 100644 index 00000000..c6168622 --- /dev/null +++ b/contrib/local-environment/docker-compose-gitea.yaml @@ -0,0 +1,65 @@ +# This docker-compose file can be used to bring up an example instance of oauth2-proxy +# for manual testing and exploration of features. +# Alongside OAuth2-Proxy, this file also starts Gitea to act as the identity provider, +# HTTPBin as an example upstream. +# +# This can either be created using docker-compose +# docker-compose -f docker-compose-gitea.yaml +# Or: +# make gitea- (eg. make gitea-up, make gitea-down) +# +# Access http://oauth2-proxy.localtest.me:4180 to initiate a login cycle using user=admin@example.com, password=password +# Access http://gitea.localtest.me:3000 with the same credentials to check out the settings +version: '3.0' +services: + oauth2-proxy: + container_name: oauth2-proxy + image: gitea-oauth #quay.io/oauth2-proxy/oauth2-proxy:v7.4.0 + command: --config /oauth2-proxy.cfg + hostname: oauth2-proxy + volumes: + - "./oauth2-proxy-gitea.cfg:/oauth2-proxy.cfg" + restart: unless-stopped + networks: + gitea: {} + httpbin: {} + oauth2-proxy: {} + depends_on: + - httpbin + - gitea + ports: + - 4180:4180/tcp + + httpbin: + container_name: httpbin + image: kennethreitz/httpbin:latest + hostname: httpbin + ports: + - 8080:80 + networks: + httpbin: + aliases: + - httpbin.localtest.me + + gitea: + image: gitea/gitea:latest + container_name: gitea + environment: + - USER_UID=1000 + - USER_GID=1000 + restart: always + networks: + gitea: + aliases: + - gitea.localtest.me + volumes: + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "3000:3000" + - "222:22" + +networks: + httpbin: {} + gitea: {} + oauth2-proxy: {} diff --git a/contrib/local-environment/oauth2-proxy-gitea.cfg b/contrib/local-environment/oauth2-proxy-gitea.cfg new file mode 100644 index 00000000..027a6c49 --- /dev/null +++ b/contrib/local-environment/oauth2-proxy-gitea.cfg @@ -0,0 +1,19 @@ +http_address="0.0.0.0:4180" +cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" +email_domains=["localhost"] +cookie_secure="false" +upstreams="http://httpbin" +cookie_domains=[".localtest.me"] # Required so cookie can be read on all subdomains. +whitelist_domains=[".localtest.me"] # Required to allow redirection back to original requested target. + +client_id="ef0c2b91-2e38-4fa8-908d-067a35dbb71c" +client_secret="gto_qdppomn2p26su5x46tyixj7bcny5m5er2s67xhrponq2qtp66f3a" +redirect_url="http://oauth2-proxy.localtest.me:4180/oauth2/callback" + +# gitea provider +provider="github" +provider_display_name="Gitea" +login_url="http://gitea.localtest.me:3000/login/oauth/authorize" +redeem_url="http://gitea.localtest.me:3000/login/oauth/access_token" +validate_url="http://gitea.localtest.me:3000/api/v1/user/emails" + diff --git a/docs/docs/configuration/auth.md b/docs/docs/configuration/auth.md index cfcad1d1..282745c6 100644 --- a/docs/docs/configuration/auth.md +++ b/docs/docs/configuration/auth.md @@ -12,6 +12,7 @@ Valid providers are : - [ADFS](#adfs-auth-provider) - [Facebook](#facebook-auth-provider) - [GitHub](#github-auth-provider) +- [Gitea](#gitea-auth-provider) - [Keycloak](#keycloak-auth-provider) - [GitLab](#gitlab-auth-provider) - [LinkedIn](#linkedin-auth-provider) @@ -21,7 +22,6 @@ Valid providers are : - [Nextcloud](#nextcloud-provider) - [DigitalOcean](#digitalocean-auth-provider) - [Bitbucket](#bitbucket-auth-provider) -- [Gitea](#gitea-auth-provider) The provider can be selected using the `provider` configuration value. @@ -177,6 +177,25 @@ If you are using GitHub enterprise, make sure you set the following to the appro -redeem-url="http(s):///login/oauth/access_token" -validate-url="http(s):///api/v3" +### Gitea Auth Provider + +1. Create a new application: `https://< your gitea host >/user/settings/applications` +2. Under `Redirect URI` enter the correct URL i.e. `https:///oauth2/callback` +3. Note the Client ID and Client Secret. +4. Pass the following options to the proxy: + +``` + --provider="github" + --redirect-url="https:///oauth2/callback" + --provider-display-name="Gitea" + --client-id="< client_id as generated by Gitea >" + --client-secret="< client_secret as generated by Gitea >" + --login-url="https://< your gitea host >/login/oauth/authorize" + --redeem-url="https://< your gitea host >/login/oauth/access_token" + --validate-url="https://< your gitea host >/api/v1/user/emails" +``` + + ### Keycloak Auth Provider :::note @@ -660,24 +679,6 @@ To use the provider, pass the following options: The default configuration allows everyone with Bitbucket account to authenticate. To restrict the access to the team members use additional configuration option: `--bitbucket-team=`. To restrict the access to only these users who has access to one selected repository use `--bitbucket-repository=`. -### Gitea Auth Provider - -1. Create a new application: `https://< your gitea host >/user/settings/applications` -2. Under `Redirect URI` enter the correct URL i.e. `https:///oauth2/callback` -3. Note the Client ID and Client Secret. -4. Pass the following options to the proxy: - -``` - --provider="github" - --redirect-url="https:///oauth2/callback" - --provider-display-name="Gitea" - --client-id="< client_id as generated by Gitea >" - --client-secret="< client_secret as generated by Gitea >" - --login-url="https://< your gitea host >/login/oauth/authorize" - --redeem-url="https://< your gitea host >/login/oauth/access_token" - --validate-url="https://< your gitea host >/api/v1" -``` - ## Email Authentication diff --git a/providers/gitea_test.go b/providers/gitea_test.go new file mode 100644 index 00000000..5ceb3c3e --- /dev/null +++ b/providers/gitea_test.go @@ -0,0 +1,98 @@ +package providers + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + "github.com/stretchr/testify/assert" +) + +func testGiteaProvider(hostname string, opts options.GitHubOptions) *GitHubProvider { + p := NewGitHubProvider( + &ProviderData{ + ProviderName: "Gitea", + LoginURL: &url.URL{}, + RedeemURL: &url.URL{}, + ProfileURL: &url.URL{}, + ValidateURL: &url.URL{Path: "/api/v1/user/emails"}, + Scope: ""}, + opts) + p.ProviderName = "Gitea" + + if hostname != "" { + updateURL(p.Data().LoginURL, hostname) + updateURL(p.Data().RedeemURL, hostname) + updateURL(p.Data().ProfileURL, hostname) + updateURL(p.Data().ValidateURL, hostname) + } + return p +} + +func testGiteaBackend(payloads map[string][]string) *httptest.Server { + pathToQueryMap := map[string][]string{ + "/api/v1/repos/oauth2-proxy/oauth2-proxy": {""}, + "/api/v1/repos/oauth2-proxy/oauth2-proxy/collaborators/mbland": {""}, + "/api/v1/user": {""}, + "/api/v1/user/emails": {""}, + "/api/v1/user/orgs": {"page=1&per_page=100", "page=2&per_page=100", "page=3&per_page=100"}, + } + + return httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + query, ok := pathToQueryMap[r.URL.Path] + validQuery := false + index := 0 + for i, q := range query { + if q == r.URL.RawQuery { + validQuery = true + index = i + } + } + payload := []string{} + if ok && validQuery { + payload, ok = payloads[r.URL.Path] + } + if !ok { + w.WriteHeader(404) + } else if !validQuery { + w.WriteHeader(404) + } else if payload[index] == "" { + w.WriteHeader(204) + } else { + w.WriteHeader(200) + w.Write([]byte(payload[index])) + } + })) +} + +func TestGiteaProvider_ValidateSessionWithBaseUrl(t *testing.T) { + b := testGiteaBackend(map[string][]string{}) + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGiteaProvider(bURL.Host, options.GitHubOptions{}) + + session := CreateAuthorizedSession() + + valid := p.ValidateSession(context.Background(), session) + assert.False(t, valid) +} + +func TestGiteaProvider_ValidateSessionWithUserEmails(t *testing.T) { + b := testGiteaBackend(map[string][]string{ + "/api/v1/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`}, + }) + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGiteaProvider(bURL.Host, options.GitHubOptions{}) + + session := CreateAuthorizedSession() + + valid := p.ValidateSession(context.Background(), session) + assert.True(t, valid) +} diff --git a/providers/github.go b/providers/github.go index ae4b2e83..6c6fc3db 100644 --- a/providers/github.go +++ b/providers/github.go @@ -89,6 +89,27 @@ func makeGitHubHeader(accessToken string) http.Header { return makeAuthorizationHeader(tokenTypeToken, accessToken, extraHeaders) } +func (p *GitHubProvider) makeGitHubAPIEndpoint(endpoint string, params *url.Values) *url.URL { + basePath := p.ValidateURL.Path + + re := regexp.MustCompile(`^/api/v\d+`) + match := re.FindString(p.ValidateURL.Path) + if match != "" { + basePath = match + } + + if params == nil { + params = &url.Values{} + } + + return &url.URL{ + Scheme: p.ValidateURL.Scheme, + Host: p.ValidateURL.Host, + Path: path.Join(basePath, endpoint), + RawQuery: params.Encode(), + } +} + // setOrgTeam adds GitHub org reading parameters to the OAuth2 scope func (p *GitHubProvider) setOrgTeam(org, team string) { p.Org = org @@ -141,12 +162,7 @@ func (p *GitHubProvider) hasOrg(ctx context.Context, accessToken string) (bool, "page": {strconv.Itoa(pn)}, } - endpoint := &url.URL{ - Scheme: p.ValidateURL.Scheme, - Host: p.ValidateURL.Host, - Path: path.Join(p.ValidateURL.Path, "/user/orgs"), - RawQuery: params.Encode(), - } + endpoint := p.makeGitHubAPIEndpoint("/user/orgs", ¶ms) var op orgsPage err := requests.New(endpoint.String()). @@ -206,12 +222,7 @@ func (p *GitHubProvider) hasOrgAndTeam(ctx context.Context, accessToken string) "page": {strconv.Itoa(pn)}, } - endpoint := &url.URL{ - Scheme: p.ValidateURL.Scheme, - Host: p.ValidateURL.Host, - Path: path.Join(p.ValidateURL.Path, "/user/teams"), - RawQuery: params.Encode(), - } + endpoint := p.makeGitHubAPIEndpoint("/user/teams", ¶ms) // bodyclose cannot detect that the body is being closed later in requests.Into, // so have to skip the linting for the next line. @@ -309,11 +320,7 @@ func (p *GitHubProvider) hasRepo(ctx context.Context, accessToken string) (bool, Private bool `json:"private"` } - endpoint := &url.URL{ - Scheme: p.ValidateURL.Scheme, - Host: p.ValidateURL.Host, - Path: path.Join(p.ValidateURL.Path, "/repos/", p.Repo), - } + endpoint := p.makeGitHubAPIEndpoint("/repos/"+p.Repo, nil) var repo repository err := requests.New(endpoint.String()). @@ -338,11 +345,7 @@ func (p *GitHubProvider) hasUser(ctx context.Context, accessToken string) (bool, Email string `json:"email"` } - endpoint := &url.URL{ - Scheme: p.ValidateURL.Scheme, - Host: p.ValidateURL.Host, - Path: path.Join(p.ValidateURL.Path, "/user"), - } + endpoint := p.makeGitHubAPIEndpoint("/user", nil) err := requests.New(endpoint.String()). WithContext(ctx). @@ -362,11 +365,7 @@ func (p *GitHubProvider) hasUser(ctx context.Context, accessToken string) (bool, func (p *GitHubProvider) isCollaborator(ctx context.Context, username, accessToken string) (bool, error) { //https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator - endpoint := &url.URL{ - Scheme: p.ValidateURL.Scheme, - Host: p.ValidateURL.Host, - Path: path.Join(p.ValidateURL.Path, "/repos/", p.Repo, "/collaborators/", username), - } + endpoint := p.makeGitHubAPIEndpoint("/repos/"+p.Repo+"/collaborators/"+username, nil) result := requests.New(endpoint.String()). WithContext(ctx). WithHeaders(makeGitHubHeader(accessToken)). @@ -426,11 +425,7 @@ func (p *GitHubProvider) getEmail(ctx context.Context, s *sessions.SessionState) } } - endpoint := &url.URL{ - Scheme: p.ValidateURL.Scheme, - Host: p.ValidateURL.Host, - Path: path.Join(p.ValidateURL.Path, "/user/emails"), - } + endpoint := p.makeGitHubAPIEndpoint("/user/emails", nil) err := requests.New(endpoint.String()). WithContext(ctx). WithHeaders(makeGitHubHeader(s.AccessToken)). @@ -459,11 +454,7 @@ func (p *GitHubProvider) getUser(ctx context.Context, s *sessions.SessionState) Email string `json:"email"` } - endpoint := &url.URL{ - Scheme: p.ValidateURL.Scheme, - Host: p.ValidateURL.Host, - Path: path.Join(p.ValidateURL.Path, "/user"), - } + endpoint := p.makeGitHubAPIEndpoint("/user", nil) err := requests.New(endpoint.String()). WithContext(ctx). diff --git a/providers/github_test.go b/providers/github_test.go index 28ab2baf..deb35aee 100644 --- a/providers/github_test.go +++ b/providers/github_test.go @@ -34,11 +34,15 @@ func testGitHubProvider(hostname string, opts options.GitHubOptions) *GitHubProv func testGitHubBackend(payloads map[string][]string) *httptest.Server { pathToQueryMap := map[string][]string{ - "/repos/oauth2-proxy/oauth2-proxy": {""}, + "/": {""}, + "/repos/oauth2-proxy/oauth2-proxy": {""}, "/repos/oauth2-proxy/oauth2-proxy/collaborators/mbland": {""}, "/user": {""}, "/user/emails": {""}, "/user/orgs": {"page=1&per_page=100", "page=2&per_page=100", "page=3&per_page=100"}, + // GitHub Enterprise Server API + "/api/v3": {""}, + "/api/v3/user/emails": {""}, } return httptest.NewServer(http.HandlerFunc( @@ -75,10 +79,10 @@ func TestNewGitHubProvider(t *testing.T) { // Test that defaults are set when calling for a new provider with nothing set providerData := NewGitHubProvider(&ProviderData{}, options.GitHubOptions{}).Data() g.Expect(providerData.ProviderName).To(Equal("GitHub")) - g.Expect(providerData.LoginURL.String()).To(Equal("https://github.com/login/oauth/authorize")) - g.Expect(providerData.RedeemURL.String()).To(Equal("https://github.com/login/oauth/access_token")) + g.Expect(providerData.LoginURL.String()).To(Equal(githubDefaultLoginURL.String())) + g.Expect(providerData.RedeemURL.String()).To(Equal(githubDefaultRedeemURL.String())) g.Expect(providerData.ProfileURL.String()).To(Equal("")) - g.Expect(providerData.ValidateURL.String()).To(Equal("https://api.github.com/")) + g.Expect(providerData.ValidateURL.String()).To(Equal(githubDefaultValidateURL.String())) g.Expect(providerData.Scope).To(Equal("user:email")) } @@ -440,3 +444,50 @@ func TestGitHubProvider_getEmailWithUsernameAndNoAccessToPrivateRepo(t *testing. assert.NoError(t, err) assert.Equal(t, "michael.bland@gsa.gov", session.Email) } + +func TestGitHubProvider_ValidateSessionWithBaseUrl(t *testing.T) { + b := testGitHubBackend(map[string][]string{ + "/": {`[]`}, + }) + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGitHubProvider(bURL.Host, options.GitHubOptions{}) + + session := CreateAuthorizedSession() + + valid := p.ValidateSession(context.Background(), session) + assert.True(t, valid) +} + +func TestGitHubProvider_ValidateSessionWithEnterpriseBaseUrl(t *testing.T) { + b := testGitHubBackend(map[string][]string{ + "/api/v3": {`[]`}, + }) + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGitHubProvider(bURL.Host, options.GitHubOptions{}) + p.ValidateURL.Path = "/api/v3" + + session := CreateAuthorizedSession() + + valid := p.ValidateSession(context.Background(), session) + assert.True(t, valid) +} + +func TestGitHubProvider_ValidateSessionWithUserEmails(t *testing.T) { + b := testGitHubBackend(map[string][]string{ + "/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`}, + }) + defer b.Close() + + bURL, _ := url.Parse(b.URL) + p := testGitHubProvider(bURL.Host, options.GitHubOptions{}) + p.ValidateURL.Path = "/user/emails" + + session := CreateAuthorizedSession() + + valid := p.ValidateSession(context.Background(), session) + assert.True(t, valid) +}