git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge branch 'master' into feat/static-upstream
commit6d74a42e572e4049803a397ba36d029d593aa524 authorChristian Groschupp <christian@groschupp.org> dateTue 19 Nov 2019, 12:23 (7 years ago) parent9e4a7ee5c9a0f8
10 files changed+199−32
CHANGELOG.md+5−3 contrib/oauth2_proxy_autocomplete.sh+30 docs/5_endpoints.md+1 main.go+2 oauthproxy.go+21 oauthproxy_test.go+26 pkg/apis/options/sessions.go+2 pkg/sessions/redis/redis_store.go+28 providers/github.go+83−28 providers/github_test.go+1−1
CHANGELOG.md+5−3
@@ -6,17 +6,19 @@
66- [#259](https://github.com/pusher/oauth2_proxy/pull/259) Redirect to HTTPS (@jmickey)
77- [#273](https://github.com/pusher/oauth2_proxy/pull/273) Support Go 1.13 (@dio)
88- [#275](https://github.com/pusher/oauth2_proxy/pull/275) docker: build from debian buster (@syscll)
9−- [#258](https://github.com/pusher/oauth2_proxy/pull/258) Add IDToken for Azure provider
9+- [#258](https://github.com/pusher/oauth2_proxy/pull/258) Add IDToken for Azure provider (@leyshon)
1010 - This PR adds the IDToken into the session for the Azure provider allowing requests to a backend to be identified as a specific user. As a consequence, if you are using a cookie to store the session the cookie will now exceed the 4kb size limit and be split into multiple cookies. This can cause problems when using nginx as a proxy, resulting in no cookie being passed at all. Either increase the proxy_buffer_size in nginx or implement the redis session storage (see https://pusher.github.io/oauth2_proxy/configuration#redis-storage)
1111- [#286](https://github.com/pusher/oauth2_proxy/pull/286) Requests.go updated with useful error messages (@biotom)
12+- [#274](https://github.com/pusher/oauth2_proxy/pull/274) Supports many github teams with api pagination support (@toshi-miura, @apratina)
1213- [#302](https://github.com/pusher/oauth2_proxy/pull/302) Rewrite dist script (@syscll)
1314- [#304](https://github.com/pusher/oauth2_proxy/pull/304) Add new Logo! :tada: (@JoelSpeed)
15+- [#300](https://github.com/pusher/oauth2_proxy/pull/300) Added userinfo endpoint (@kbabuadze)
16+- [#309](https://github.com/pusher/oauth2_proxy/pull/309) Added support for custom CA when connecting to Redis cache (@lleszczu)
17+- [#248](https://github.com/pusher/oauth2_proxy/pull/248) Fix issue with X-Auth-Request-Redirect header being ignored (@webnard)
1418- [#265](https://github.com/pusher/oauth2_proxy/pull/265) Add upstream with static response (@cgroschupp)
1519
1620# v4.0.0
1721
18−- [#248](https://github.com/pusher/oauth2_proxy/pull/248) Fix issue with X-Auth-Request-Redirect header being ignored
19−
2022## Release Highlights
2123- Documentation is now on a [microsite](https://pusher.github.io/oauth2_proxy/)
2224- Health check logging can now be disabled for quieter logs
contrib/oauth2_proxy_autocomplete.shnew file+30
@@ -0,0 +1,30 @@
1+#
2+# Autocompletion for oauth2_proxy
3+#
4+# To install this, copy/move this file to /etc/bash.completion.d/
5+# or add a line to your ~/.bashrc | ~/.bash_profile that says ". /path/to/oauth2_proxy/contrib/oauth2_proxy_autocomplete.sh"
6+#
7+
8+_oauth2_proxy() {
9+ _oauth2_proxy_commands=$(oauth2_proxy -h 2>&1 | sed -n '/^\s*-/s/ \+/ /gp' | awk '{print $1}' | tr '\n' ' ')
10+ local cur prev
11+ COMPREPLY=()
12+ cur="${COMP_WORDS[COMP_CWORD]}"
13+ prev="${COMP_WORDS[COMP_CWORD-1]}"
14+ case "$prev" in
15+ -@(config|tls-cert-file|tls-key-file|authenticated-emails-file|htpasswd-file|custom-templates-dir|logging-filename|jwt-key-file))
16+ _filedir
17+ return 0
18+ ;;
19+ -provider)
20+ COMPREPLY=( $(compgen -W "google azure facebook github keycloak gitlab linkedin login.gov" -- ${cur}) )
21+ return 0
22+ ;;
23+ -@(http-address|https-address|redirect-url|upstream|basic-auth-password|skip-auth-regex|flush-interval|extra-jwt-issuers|email-domain|whitelist-domain|keycloak-group|azure-tenant|bitbucket-team|bitbucket-repository|github-org|github-team|gitlab-group|google-group|google-admin-email|google-service-account-json|client-id|client_secret|banner|footer|proxy-prefix|ping-path|cookie-name|cookie-secret|cookie-domain|cookie-path|cookie-expire|cookie-refresh|redist-sentinel-master-name|redist-sentinel-connection-urls|logging-max-size|logging-max-age|logging-max-backups|standard-logging-format|request-logging-format|exclude-logging-paths|auth-logging-format|oidc-issuer-url|oidc-jwks-url|login-url|redeem-url|profile-url|resource|validate-url|scope|approval-prompt|signature-key|acr-values|jwt-key|pubjwk-url))
24+ return 0
25+ ;;
26+ esac
27+ COMPREPLY=( $(compgen -W "${_oauth2_proxy_commands}" -- ${cur}) )
28+ return 0;
29+}
30+complete -F _oauth2_proxy oauth2_proxy
docs/5_endpoints.md+1
@@ -14,4 +14,5 @@ OAuth2 Proxy responds directly to the following endpoints. All other endpoints w
1414- /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies)
1515- /oauth2/start - a URL that will redirect to start the OAuth cycle
1616- /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url.
17+- /oauth2/userinfo - the URL is used to return user's email from the session in JSON format.
1718- /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](#nginx-auth-request)
main.go+2
@@ -91,6 +91,8 @@ func main() {
9191 flagSet.String("redis-connection-url", "", "URL of redis server for redis session storage (eg: redis://HOST[:PORT])")
9292 flagSet.Bool("redis-use-sentinel", false, "Connect to redis via sentinels. Must set --redis-sentinel-master-name and --redis-sentinel-connection-urls to use this feature")
9393 flagSet.String("redis-sentinel-master-name", "", "Redis sentinel master name. Used in conjunction with --redis-use-sentinel")
94+ flagSet.String("redis-ca-path", "", "Redis custom CA path")
95+ flagSet.Bool("redis-insecure-skip-tls-verify", false, "Use insecure TLS connection to redis")
9496 flagSet.Var(&redisSentinelConnectionURLs, "redis-sentinel-connection-urls", "List of Redis sentinel connection URLs (eg redis://HOST[:PORT]). Used in conjunction with --redis-use-sentinel")
9597
9698 flagSet.String("logging-filename", "", "File to log requests to, empty for stdout")
oauthproxy.go+21
@@ -4,6 +4,7 @@ import (
44 "context"
55 "crypto/tls"
66 b64 "encoding/base64"
7+ "encoding/json"
78 "errors"
89 "fmt"
910 "html/template"
@@ -76,6 +77,7 @@ type OAuthProxy struct {
7677 OAuthStartPath string
7778 OAuthCallbackPath string
7879 AuthOnlyPath string
80+ UserInfoPath string
7981
8082 redirectURL *url.URL // the url to receive requests at
8183 whitelistDomains []string
@@ -278,6 +280,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
278280 OAuthStartPath: fmt.Sprintf("%s/start", opts.ProxyPrefix),
279281 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
280282 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
283+ UserInfoPath: fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
281284
282285 ProxyPrefix: opts.ProxyPrefix,
283286 provider: opts.provider,
@@ -569,6 +572,8 @@ func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
569572 p.OAuthCallback(rw, req)
570573 case path == p.AuthOnlyPath:
571574 p.AuthenticateOnly(rw, req)
575+ case path == p.UserInfoPath:
576+ p.UserInfo(rw, req)
572577 default:
573578 p.Proxy(rw, req)
574579 }
@@ -597,6 +602,22 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
597602 }
598603}
599604
605+//UserInfo endpoint outputs session email in JSON format
606+func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
607+
608+ session, err := p.getAuthenticatedSession(rw, req)
609+ if err != nil {
610+ http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
611+ return
612+ }
613+ userInfo := struct {
614+ Email string `json:"email"`
615+ }{session.Email}
616+ rw.Header().Set("Content-Type", "application/json")
617+ rw.WriteHeader(http.StatusOK)
618+ json.NewEncoder(rw).Encode(userInfo)
619+}
620+
600621// SignOut sends a response to clear the authentication cookie
601622func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
602623 p.ClearSessionCookie(rw, req)
oauthproxy_test.go+26
@@ -775,6 +775,32 @@ func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
775775 }
776776}
777777
778+func NewUserInfoEndpointTest() *ProcessCookieTest {
779+ pcTest := NewProcessCookieTestWithDefaults()
780+ pcTest.req, _ = http.NewRequest("GET",
781+ pcTest.opts.ProxyPrefix+"/userinfo", nil)
782+ return pcTest
783+}
784+
785+func TestUserInfoEndpointAccepted(t *testing.T) {
786+ test := NewUserInfoEndpointTest()
787+ startSession := &sessions.SessionState{
788+ Email: "john.doe@example.com", AccessToken: "my_access_token"}
789+ test.SaveSession(startSession)
790+
791+ test.proxy.ServeHTTP(test.rw, test.req)
792+ assert.Equal(t, http.StatusOK, test.rw.Code)
793+ bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
794+ assert.Equal(t, "{\"email\":\"john.doe@example.com\"}\n", string(bodyBytes))
795+}
796+
797+func TestUserInfoEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
798+ test := NewUserInfoEndpointTest()
799+
800+ test.proxy.ServeHTTP(test.rw, test.req)
801+ assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
802+}
803+
778804func NewAuthOnlyEndpointTest(modifiers ...OptionsModifier) *ProcessCookieTest {
779805 pcTest := NewProcessCookieTestWithOptionsModifiers(modifiers...)
780806 pcTest.req, _ = http.NewRequest("GET",
pkg/apis/options/sessions.go+2
@@ -27,4 +27,6 @@ type RedisStoreOptions struct {
2727 UseSentinel bool `flag:"redis-use-sentinel" cfg:"redis_use_sentinel" env:"OAUTH2_PROXY_REDIS_USE_SENTINEL"`
2828 SentinelMasterName string `flag:"redis-sentinel-master-name" cfg:"redis_sentinel_master_name" env:"OAUTH2_PROXY_REDIS_SENTINEL_MASTER_NAME"`
2929 SentinelConnectionURLs []string `flag:"redis-sentinel-connection-urls" cfg:"redis_sentinel_connection_urls" env:"OAUTH2_PROXY_REDIS_SENTINEL_CONNECTION_URLS"`
30+ RedisCAPath string `flag:"redis-ca-path" cfg:"redis_ca_path" env:"OAUTH2_PROXY_REDIS_CA_PATH"`
31+ RedisInsecureTLS bool `flag:"redis-insecure-skip-tls-verify" cfg:"redis_insecure_skip_tls_verify" env:"OAUTH2_PROXY_REDIS_INSECURE_SKIP_TLS_VERIFY"`
3032}
pkg/sessions/redis/redis_store.go+28
@@ -4,10 +4,12 @@ import (
44 "crypto/aes"
55 "crypto/cipher"
66 "crypto/rand"
7+ "crypto/x509"
78 "encoding/base64"
89 "encoding/hex"
910 "fmt"
1011 "io"
12+ "io/ioutil"
1113 "net/http"
1214 "strings"
1315 "time"
@@ -17,6 +19,7 @@ import (
1719 "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
1820 "github.com/pusher/oauth2_proxy/pkg/cookies"
1921 "github.com/pusher/oauth2_proxy/pkg/encryption"
22+ "github.com/pusher/oauth2_proxy/pkg/logger"
2023)
2124
2225// TicketData is a structure representing the ticket used in server session storage
@@ -64,6 +67,31 @@ func newRedisClient(opts options.RedisStoreOptions) (*redis.Client, error) {
6467 return nil, fmt.Errorf("unable to parse redis url: %s", err)
6568 }
6669
70+ if opts.RedisInsecureTLS != false {
71+ opt.TLSConfig.InsecureSkipVerify = true
72+ }
73+
74+ if opts.RedisCAPath != "" {
75+ rootCAs, err := x509.SystemCertPool()
76+ if err != nil {
77+ logger.Printf("failed to load system cert pool for redis connection, falling back to empty cert pool")
78+ }
79+ if rootCAs == nil {
80+ rootCAs = x509.NewCertPool()
81+ }
82+ certs, err := ioutil.ReadFile(opts.RedisCAPath)
83+ if err != nil {
84+ return nil, fmt.Errorf("failed to load %q, %v", opts.RedisCAPath, err)
85+ }
86+
87+ // Append our cert to the system pool
88+ if ok := rootCAs.AppendCertsFromPEM(certs); !ok {
89+ logger.Printf("no certs appended, using system certs only")
90+ }
91+
92+ opt.TLSConfig.RootCAs = rootCAs
93+ }
94+
6795 client := redis.NewClient(opt)
6896 return client, nil
6997}
providers/github.go+83−28
@@ -7,6 +7,7 @@ import (
77 "net/http"
88 "net/url"
99 "path"
10+ "regexp"
1011 "strconv"
1112 "strings"
1213
@@ -75,8 +76,8 @@ func (p *GitHubProvider) hasOrg(accessToken string) (bool, error) {
7576 pn := 1
7677 for {
7778 params := url.Values{
78− "limit": {"200"},
79− "page": {strconv.Itoa(pn)},
79+ "per_page": {"100"},
80+ "page": {strconv.Itoa(pn)},
8081 }
8182
8283 endpoint := &url.URL{
@@ -139,36 +140,90 @@ func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) {
139140 } `json:"organization"`
140141 }
141142
142− params := url.Values{
143− "limit": {"200"},
143+ type teamsPage []struct {
144+ Name string `json:"name"`
145+ Slug string `json:"slug"`
146+ Org struct {
147+ Login string `json:"login"`
148+ } `json:"organization"`
144149 }
145150
146− endpoint := &url.URL{
147− Scheme: p.ValidateURL.Scheme,
148− Host: p.ValidateURL.Host,
149− Path: path.Join(p.ValidateURL.Path, "/user/teams"),
150− RawQuery: params.Encode(),
151− }
152− req, _ := http.NewRequest("GET", endpoint.String(), nil)
153− req.Header.Set("Accept", "application/vnd.github.v3+json")
154− req.Header.Set("Authorization", fmt.Sprintf("token %s", accessToken))
155− resp, err := http.DefaultClient.Do(req)
156− if err != nil {
157− return false, err
158− }
151+ pn := 1
152+ last := 0
153+ for {
154+ params := url.Values{
155+ "per_page": {"100"},
156+ "page": {strconv.Itoa(pn)},
157+ }
159158
160− body, err := ioutil.ReadAll(resp.Body)
161− resp.Body.Close()
162− if err != nil {
163− return false, err
164− }
165− if resp.StatusCode != 200 {
166− return false, fmt.Errorf(
167− "got %d from %q %s", resp.StatusCode, endpoint.String(), body)
168− }
159+ endpoint := &url.URL{
160+ Scheme: p.ValidateURL.Scheme,
161+ Host: p.ValidateURL.Host,
162+ Path: path.Join(p.ValidateURL.Path, "/user/teams"),
163+ RawQuery: params.Encode(),
164+ }
169165
170− if err := json.Unmarshal(body, &teams); err != nil {
171− return false, fmt.Errorf("%s unmarshaling %s", err, body)
166+ req, _ := http.NewRequest("GET", endpoint.String(), nil)
167+ req.Header.Set("Accept", "application/vnd.github.v3+json")
168+ req.Header.Set("Authorization", fmt.Sprintf("token %s", accessToken))
169+ resp, err := http.DefaultClient.Do(req)
170+ if err != nil {
171+ return false, err
172+ }
173+
174+ if last == 0 {
175+ // link header may not be obtained
176+ // When paging is not required and all data can be retrieved with a single call
177+
178+ // Conditions for obtaining the link header.
179+ // 1. When paging is required (Example: When the data size is 100 and the page size is 99 or less)
180+ // 2. When it exceeds the paging frame (Example: When there is only 10 records but the second page is called with a page size of 100)
181+
182+ // link header at not last page
183+ // <https://api.github.com/user/teams?page=1&per_page=100>; rel="prev", <https://api.github.com/user/teams?page=1&per_page=100>; rel="last", <https://api.github.com/user/teams?page=1&per_page=100>; rel="first"
184+ // link header at last page (doesn't exist last info)
185+ // <https://api.github.com/user/teams?page=3&per_page=10>; rel="prev", <https://api.github.com/user/teams?page=1&per_page=10>; rel="first"
186+
187+ link := resp.Header.Get("Link")
188+ rep1 := regexp.MustCompile(`(?s).*\<https://api.github.com/user/teams\?page=(.)&per_page=[0-9]+\>; rel="last".*`)
189+ i, converr := strconv.Atoi(rep1.ReplaceAllString(link, "$1"))
190+
191+ // If the last page cannot be taken from the link in the http header, the last variable remains zero
192+ if converr == nil {
193+ last = i
194+ }
195+ }
196+
197+ body, err := ioutil.ReadAll(resp.Body)
198+ if err != nil {
199+ resp.Body.Close()
200+ return false, err
201+ }
202+ resp.Body.Close()
203+
204+ if resp.StatusCode != 200 {
205+ return false, fmt.Errorf(
206+ "got %d from %q %s", resp.StatusCode, endpoint.String(), body)
207+ }
208+
209+ var tp teamsPage
210+ if err := json.Unmarshal(body, &tp); err != nil {
211+ return false, fmt.Errorf("%s unmarshaling %s", err, body)
212+ }
213+ if len(tp) == 0 {
214+ break
215+ }
216+
217+ teams = append(teams, tp...)
218+
219+ if pn == last {
220+ break
221+ }
222+ if last == 0 {
223+ break
224+ }
225+
226+ pn++
172227 }
173228
174229 var hasOrg bool
providers/github_test.go+1−1
@@ -32,7 +32,7 @@ func testGitHubBackend(payload []string) *httptest.Server {
3232 pathToQueryMap := map[string][]string{
3333 "/user": {""},
3434 "/user/emails": {""},
35− "/user/orgs": {"limit=200&page=1", "limit=200&page=2", "limit=200&page=3"},
35+ "/user/orgs": {"page=1&per_page=100", "page=2&per_page=100", "page=3&per_page=100"},
3636 }
3737
3838 return httptest.NewServer(http.HandlerFunc(