git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge branch 'master' into refactor
commit630db3769bb5672f7dfbda52417d17f8ef198be2 authorJoel Speed <Joel.speed@hotmail.co.uk> dateMon 15 Jul 2019, 11:30 (7 years ago) parent63666904bc0a91
18 files changed+689−184
.golangci.yml+13 .travis.yml+1−2 CHANGELOG.md+12 Dockerfile+2−1 Dockerfile.arm64+2−1 Dockerfile.armv6+2−1 Makefile+1−11 configure+2−2 docs/configuration/configuration.md+6 http_test.go+15−12 main.go+12−1 oauthproxy.go+227−75 oauthproxy_test.go+230−27 options.go+112−41 providers/google.go+10−6 providers/google_test.go+37 providers/internal_util.go+1−1 providers/oidc.go+4−3
.golangci.ymlnew file+13
@@ -0,0 +1,13 @@
1+run:
2+ deadline: 120s
3+linters:
4+ enable:
5+ - govet
6+ - golint
7+ - ineffassign
8+ - goconst
9+ - deadcode
10+ - gofmt
11+ - goimports
12+ enable-all: false
13+ disable-all: true
.travis.yml+1−2
@@ -6,8 +6,7 @@ install:
66 - wget -O dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64
77 - chmod +x dep
88 - mv dep $GOPATH/bin/dep
9− - go get github.com/alecthomas/gometalinter
10− - gometalinter --install
9+ - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $GOPATH/bin v1.17.1
1110script:
1211 - ./configure && make test
1312sudo: false
CHANGELOG.md+12
@@ -15,6 +15,11 @@
1515## Changes since v3.2.0
1616
1717- [#187](https://github.com/pusher/oauth2_proxy/pull/187) Move root packages to pkg folder (@JoelSpeed)
18+- [#65](https://github.com/pusher/oauth2_proxy/pull/65) Improvements to authenticate requests with a JWT bearer token in the `Authorization` header via
19+ the `-skip-jwt-bearer-token` options.
20+ - Additional verifiers can be configured via the `-extra-jwt-issuers` flag if the JWT issuers is either an OpenID provider or has a JWKS URL
21+ (e.g. `https://example.com/.well-known/jwks.json`).
22+- [#180](https://github.com/pusher/outh2_proxy/pull/180) Minor refactor of core proxying path (@aeijdenberg).
1823- [#175](https://github.com/pusher/outh2_proxy/pull/175) Bump go-oidc to v2.0.0 (@aeijdenberg).
1924 - Includes fix for potential signature checking issue when OIDC discovery is skipped.
2025- [#155](https://github.com/pusher/outh2_proxy/pull/155) Add RedisSessionStore implementation (@brianv0, @JoelSpeed)
@@ -55,6 +60,13 @@
5560
5661- [#111](https://github.com/pusher/oauth2_proxy/pull/111) Add option for telling where to find a login.gov JWT key file (@timothy-spencer)
5762- [#170](https://github.com/pusher/oauth2_proxy/pull/170) Restore binary tarball contents to be compatible with bitlys original tarballs (@zeha)
63+- [#185](https://github.com/pusher/oauth2_proxy/pull/185) Fix an unsupported protocol scheme error during token validation when using the Azure provider (@jonas)
64+- [#141](https://github.com/pusher/oauth2_proxy/pull/141) Check google group membership based on email address (@bchess)
65+ - Google Group membership is additionally checked via email address, allowing users outside a GSuite domain to be authorized.
66+- [#195](https://github.com/pusher/outh2_proxy/pull/195) Add `-banner` flag for overriding the banner line that is displayed (@steakunderscore)
67+- [#198](https://github.com/pusher/outh2_proxy/pull/198) Switch from gometalinter to golangci-lint (@steakunderscore)
68+- [#159](https://github.com/pusher/oauth2_proxy/pull/159) Add option to skip the OIDC provider verified email check: `--insecure-oidc-allow-unverified-email`
69+- [#210](https://github.com/pusher/oauth2_proxy/pull/210) Update base image from Alpine 3.9 to 3.10 (@steakunderscore)
5870
5971# v3.2.0
6072
Dockerfile+2−1
@@ -3,6 +3,7 @@ FROM golang:1.12-stretch AS builder
33# Download tools
44RUN wget -O $GOPATH/bin/dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64
55RUN chmod +x $GOPATH/bin/dep
6+RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
67
78# Copy sources
89WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
@@ -20,7 +21,7 @@ RUN dep ensure --vendor-only
2021RUN ./configure && make build && touch jwt_signing_key.pem
2122
2223# Copy binary to alpine
23−FROM alpine:3.9
24+FROM alpine:3.10
2425COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
2526COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy
2627COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem
Dockerfile.arm64+2−1
@@ -3,6 +3,7 @@ FROM golang:1.12-stretch AS builder
33# Download tools
44RUN wget -O $GOPATH/bin/dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64
55RUN chmod +x $GOPATH/bin/dep
6+RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
67
78# Copy sources
89WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
@@ -20,7 +21,7 @@ RUN dep ensure --vendor-only
2021RUN ./configure && GOARCH=arm64 make build && touch jwt_signing_key.pem
2122
2223# Copy binary to alpine
23−FROM arm64v8/alpine:3.9
24+FROM arm64v8/alpine:3.10
2425COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
2526COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy
2627COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem
Dockerfile.armv6+2−1
@@ -3,6 +3,7 @@ FROM golang:1.12-stretch AS builder
33# Download tools
44RUN wget -O $GOPATH/bin/dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64
55RUN chmod +x $GOPATH/bin/dep
6+RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
67
78# Copy sources
89WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
@@ -20,7 +21,7 @@ RUN dep ensure --vendor-only
2021RUN ./configure && GOARCH=arm GOARM=6 make build && touch jwt_signing_key.pem
2122
2223# Copy binary to alpine
23−FROM arm32v6/alpine:3.9
24+FROM arm32v6/alpine:3.10
2425COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
2526COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy
2627COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem
Makefile+1−11
@@ -17,17 +17,7 @@ distclean: clean
1717
1818.PHONY: lint
1919lint:
20− $(GOMETALINTER) --vendor --disable-all \
21− --enable=vet \
22− --enable=vetshadow \
23− --enable=golint \
24− --enable=ineffassign \
25− --enable=goconst \
26− --enable=deadcode \
27− --enable=gofmt \
28− --enable=goimports \
29− --deadline=120s \
30− --tests ./...
20+ $(GOLANGCILINT) run
3121
3222.PHONY: dep
3323dep:
configure+2−2
@@ -126,7 +126,7 @@ check_for go
126126check_go_version
127127check_go_env
128128check_for dep
129−check_for gometalinter
129+check_for golangci-lint
130130
131131echo
132132
@@ -135,7 +135,7 @@ cat <<- EOF > .env
135135 GO := "${tools[go]}"
136136 GO_VERSION := ${tools[go_version]}
137137 DEP := "${tools[dep]}"
138− GOMETALINTER := "${tools[gometalinter]}"
138+ GOLANGCILINT := "${tools[golangci-lint]}"
139139EOF
140140
141141echo "Environment configuration written to .env"
docs/configuration/configuration.md+6
@@ -41,7 +41,9 @@ Usage of oauth2_proxy:
4141 -custom-templates-dir string: path to custom html templates
4242 -display-htpasswd-form: display username / password login form if an htpasswd file is provided (default true)
4343 -email-domain value: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email
44+ -extra-jwt-issuers: if -skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)
4445 -flush-interval: period between flushing response buffers when streaming responses (default "1s")
46+ -banner string: custom banner string. Use "-" to disable default banner.
4547 -footer string: custom footer string. Use "-" to disable default footer.
4648 -gcp-healthchecks: will enable /liveness_check, /readiness_check, and / (with the proper user-agent) endpoints that will make it work well with GCP App Engine and GKE Ingresses (default false)
4749 -github-org string: restrict logins to members of this organisation
@@ -61,6 +63,7 @@ Usage of oauth2_proxy:
6163 -jwt-key string: private key in PEM format used to sign JWT, so that you can say something like -jwt-key="${OAUTH2_PROXY_JWT_KEY}": required by login.gov
6264 -jwt-key-file string: path to the private key file in PEM format used to sign the JWT so that you can say something like -jwt-key-file=/etc/ssl/private/jwt_signing_key.pem: required by login.gov
6365 -login-url string: Authentication endpoint
66+ -insecure-oidc-allow-unverified-email: don't fail if an email address in an id_token is not verified
6467 -oidc-issuer-url: the OpenID Connect issuer URL. ie: "https://accounts.google.com"
6568 -oidc-jwks-url string: OIDC JWKS URI for token verification; required if OIDC discovery is disabled
6669 -pass-access-token: pass OAuth access_token to upstream via X-Forwarded-Access-Token header
@@ -89,6 +92,7 @@ Usage of oauth2_proxy:
8992 -signature-key string: GAP-Signature request signature key (algorithm:secretkey)
9093 -skip-auth-preflight: will skip authentication for OPTIONS requests
9194 -skip-auth-regex value: bypass authentication for requests path's that match (may be given multiple times)
95+ -skip-jwt-bearer-tokens: will skip requests that have verified JWT bearer tokens
9296 -skip-oidc-discovery: bypass OIDC endpoint discovery. login-url, redeem-url and oidc-jwks-url must be configured in this case
9397 -skip-provider-button: will skip sign-in-page to directly reach the next step: oauth/start
9498 -ssl-insecure-skip-verify: skip validation of certificates presented when using HTTPS
@@ -310,3 +314,5 @@ nginx.ingress.kubernetes.io/configuration-snippet: |
310314 end
311315 }
312316```
317+
318+You have to substitute *name* with the actual cookie name you configured via --cookie-name parameter. If you don't set a custom cookie name the variable should be "$upstream_cookie__oauth2_proxy_1" instead of "$upstream_cookie_name_1" and the new cookie-name should be "_oauth2_proxy_1=" instead of "name_1=".
http_test.go+15−12
@@ -8,6 +8,9 @@ import (
88 "github.com/stretchr/testify/assert"
99)
1010
11+const localhost = "127.0.0.1"
12+const host = "test-server"
13+
1114func TestGCPHealthcheckLiveness(t *testing.T) {
1215 handler := func(w http.ResponseWriter, req *http.Request) {
1316 w.Write([]byte("test"))
@@ -16,8 +19,8 @@ func TestGCPHealthcheckLiveness(t *testing.T) {
1619 h := gcpHealthcheck(http.HandlerFunc(handler))
1720 rw := httptest.NewRecorder()
1821 r, _ := http.NewRequest("GET", "/liveness_check", nil)
19− r.RemoteAddr = "127.0.0.1"
20− r.Host = "test-server"
22+ r.RemoteAddr = localhost
23+ r.Host = host
2124 h.ServeHTTP(rw, r)
2225
2326 assert.Equal(t, 200, rw.Code)
@@ -32,8 +35,8 @@ func TestGCPHealthcheckReadiness(t *testing.T) {
3235 h := gcpHealthcheck(http.HandlerFunc(handler))
3336 rw := httptest.NewRecorder()
3437 r, _ := http.NewRequest("GET", "/readiness_check", nil)
35− r.RemoteAddr = "127.0.0.1"
36− r.Host = "test-server"
38+ r.RemoteAddr = localhost
39+ r.Host = host
3740 h.ServeHTTP(rw, r)
3841
3942 assert.Equal(t, 200, rw.Code)
@@ -48,8 +51,8 @@ func TestGCPHealthcheckNotHealthcheck(t *testing.T) {
4851 h := gcpHealthcheck(http.HandlerFunc(handler))
4952 rw := httptest.NewRecorder()
5053 r, _ := http.NewRequest("GET", "/not_any_check", nil)
51− r.RemoteAddr = "127.0.0.1"
52− r.Host = "test-server"
54+ r.RemoteAddr = localhost
55+ r.Host = host
5356 h.ServeHTTP(rw, r)
5457
5558 assert.Equal(t, "test", rw.Body.String())
@@ -63,8 +66,8 @@ func TestGCPHealthcheckIngress(t *testing.T) {
6366 h := gcpHealthcheck(http.HandlerFunc(handler))
6467 rw := httptest.NewRecorder()
6568 r, _ := http.NewRequest("GET", "/", nil)
66− r.RemoteAddr = "127.0.0.1"
67− r.Host = "test-server"
69+ r.RemoteAddr = localhost
70+ r.Host = host
6871 r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
6972 h.ServeHTTP(rw, r)
7073
@@ -80,8 +83,8 @@ func TestGCPHealthcheckNotIngress(t *testing.T) {
8083 h := gcpHealthcheck(http.HandlerFunc(handler))
8184 rw := httptest.NewRecorder()
8285 r, _ := http.NewRequest("GET", "/foo", nil)
83− r.RemoteAddr = "127.0.0.1"
84− r.Host = "test-server"
86+ r.RemoteAddr = localhost
87+ r.Host = host
8588 r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
8689 h.ServeHTTP(rw, r)
8790
@@ -96,8 +99,8 @@ func TestGCPHealthcheckNotIngressPut(t *testing.T) {
9699 h := gcpHealthcheck(http.HandlerFunc(handler))
97100 rw := httptest.NewRecorder()
98101 r, _ := http.NewRequest("PUT", "/", nil)
99− r.RemoteAddr = "127.0.0.1"
100− r.Host = "test-server"
102+ r.RemoteAddr = localhost
103+ r.Host = host
101104 r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
102105 h.ServeHTTP(rw, r)
103106
main.go+12−1
@@ -23,6 +23,7 @@ func main() {
2323 whitelistDomains := StringArray{}
2424 upstreams := StringArray{}
2525 skipAuthRegex := StringArray{}
26+ jwtIssuers := StringArray{}
2627 googleGroups := StringArray{}
2728 redisSentinelConnectionURLs := StringArray{}
2829
@@ -48,6 +49,8 @@ func main() {
4849 flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
4950 flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS")
5051 flagSet.Duration("flush-interval", time.Duration(1)*time.Second, "period between response flushing when streaming responses")
52+ flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens (default false)")
53+ flagSet.Var(&jwtIssuers, "extra-jwt-issuers", "if skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)")
5154
5255 flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
5356 flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
@@ -63,6 +66,7 @@ func main() {
6366 flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption or \"htpasswd -B\" for bcrypt encryption")
6467 flagSet.Bool("display-htpasswd-form", true, "display username / password login form if an htpasswd file is provided")
6568 flagSet.String("custom-templates-dir", "", "path to custom html templates")
69+ flagSet.String("banner", "", "custom banner string. Use \"-\" to disable default banner.")
6670 flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.")
6771 flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)")
6872 flagSet.Bool("proxy-websockets", true, "enables WebSocket proxying")
@@ -100,6 +104,7 @@ func main() {
100104
101105 flagSet.String("provider", "google", "OAuth provider")
102106 flagSet.String("oidc-issuer-url", "", "OpenID Connect issuer URL (ie: https://accounts.google.com)")
107+ flagSet.Bool("insecure-oidc-allow-unverified-email", false, "Don't fail if an email address in an id_token is not verified")
103108 flagSet.Bool("skip-oidc-discovery", false, "Skip OIDC discovery and use manually supplied Endpoints")
104109 flagSet.String("oidc-jwks-url", "", "OpenID Connect JWKS URL (ie: https://www.googleapis.com/oauth2/v3/certs)")
105110 flagSet.String("login-url", "", "Authentication endpoint")
@@ -145,7 +150,13 @@ func main() {
145150 validator := NewValidator(opts.EmailDomains, opts.AuthenticatedEmailsFile)
146151 oauthproxy := NewOAuthProxy(opts, validator)
147152
148− if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
153+ if len(opts.Banner) >= 1 {
154+ if opts.Banner == "-" {
155+ oauthproxy.SignInMessage = ""
156+ } else {
157+ oauthproxy.SignInMessage = opts.Banner
158+ }
159+ } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
149160 if len(opts.EmailDomains) > 1 {
150161 oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
151162 } else if opts.EmailDomains[0] != "*" {
oauthproxy.go+227−75
@@ -1,6 +1,7 @@
11package main
22
33import (
4+ "context"
45 b64 "encoding/base64"
56 "errors"
67 "fmt"
@@ -13,6 +14,7 @@ import (
1314 "strings"
1415 "time"
1516
17+ "github.com/coreos/go-oidc"
1618 "github.com/mbland/hmacauth"
1719 sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
1820 "github.com/pusher/oauth2_proxy/pkg/encryption"
@@ -47,6 +49,11 @@ var SignatureHeaders = []string{
4749 "Gap-Auth",
4850}
4951
52+var (
53+ // ErrNeedsLogin means the user should be redirected to the login page
54+ ErrNeedsLogin = errors.New("redirect to login page")
55+)
56+
5057// OAuthProxy is the main authentication proxy
5158type OAuthProxy struct {
5259 CookieSeed string
@@ -87,8 +94,11 @@ type OAuthProxy struct {
8794 PassAuthorization bool
8895 skipAuthRegex []string
8996 skipAuthPreflight bool
97+ skipJwtBearerTokens bool
98+ jwtBearerVerifiers []*oidc.IDTokenVerifier
9099 compiledRegex []*regexp.Regexp
91100 templates *template.Template
101+ Banner string
92102 Footer string
93103}
94104
@@ -151,7 +161,7 @@ func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
151161}
152162
153163// NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url
154−func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) (restProxy http.Handler) {
164+func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) http.Handler {
155165 u.Path = ""
156166 proxy := NewReverseProxy(u, opts.FlushInterval)
157167 if !opts.PassHostHeader {
@@ -167,7 +177,12 @@ func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.Hma
167177 wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
168178 wsProxy = wsutil.NewSingleHostReverseProxy(wsURL)
169179 }
170− return &UpstreamProxy{u.Host, proxy, wsProxy, auth}
180+ return &UpstreamProxy{
181+ upstream: u.Host,
182+ handler: proxy,
183+ wsHandler: wsProxy,
184+ auth: auth,
185+ }
171186}
172187
173188// NewOAuthProxy creates a new instance of OOuthProxy from the options provided
@@ -192,7 +207,13 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
192207 }
193208 logger.Printf("mapping path %q => file system %q", path, u.Path)
194209 proxy := NewFileServer(path, u.Path)
195− serveMux.Handle(path, &UpstreamProxy{path, proxy, nil, nil})
210+ uProxy := UpstreamProxy{
211+ upstream: path,
212+ handler: proxy,
213+ wsHandler: nil,
214+ auth: nil,
215+ }
216+ serveMux.Handle(path, &uProxy)
196217 default:
197218 panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
198219 }
@@ -201,6 +222,12 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
201222 logger.Printf("compiled skip-auth-regex => %q", u)
202223 }
203224
225+ if opts.SkipJwtBearerTokens {
226+ logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
227+ for _, issuer := range opts.ExtraJwtIssuers {
228+ logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
229+ }
230+ }
204231 redirectURL := opts.redirectURL
205232 if redirectURL.Path == "" {
206233 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
@@ -234,25 +261,28 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
234261 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
235262 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
236263
237− ProxyPrefix: opts.ProxyPrefix,
238− provider: opts.provider,
239− sessionStore: opts.sessionStore,
240− serveMux: serveMux,
241− redirectURL: redirectURL,
242− whitelistDomains: opts.WhitelistDomains,
243− skipAuthRegex: opts.SkipAuthRegex,
244− skipAuthPreflight: opts.SkipAuthPreflight,
245− compiledRegex: opts.CompiledRegex,
246− SetXAuthRequest: opts.SetXAuthRequest,
247− PassBasicAuth: opts.PassBasicAuth,
248− PassUserHeaders: opts.PassUserHeaders,
249− BasicAuthPassword: opts.BasicAuthPassword,
250− PassAccessToken: opts.PassAccessToken,
251− SetAuthorization: opts.SetAuthorization,
252− PassAuthorization: opts.PassAuthorization,
253− SkipProviderButton: opts.SkipProviderButton,
254− templates: loadTemplates(opts.CustomTemplatesDir),
255− Footer: opts.Footer,
264+ ProxyPrefix: opts.ProxyPrefix,
265+ provider: opts.provider,
266+ sessionStore: opts.sessionStore,
267+ serveMux: serveMux,
268+ redirectURL: redirectURL,
269+ whitelistDomains: opts.WhitelistDomains,
270+ skipAuthRegex: opts.SkipAuthRegex,
271+ skipAuthPreflight: opts.SkipAuthPreflight,
272+ skipJwtBearerTokens: opts.SkipJwtBearerTokens,
273+ jwtBearerVerifiers: opts.jwtBearerVerifiers,
274+ compiledRegex: opts.CompiledRegex,
275+ SetXAuthRequest: opts.SetXAuthRequest,
276+ PassBasicAuth: opts.PassBasicAuth,
277+ PassUserHeaders: opts.PassUserHeaders,
278+ BasicAuthPassword: opts.BasicAuthPassword,
279+ PassAccessToken: opts.PassAccessToken,
280+ SetAuthorization: opts.SetAuthorization,
281+ PassAuthorization: opts.PassAuthorization,
282+ SkipProviderButton: opts.SkipProviderButton,
283+ templates: loadTemplates(opts.CustomTemplatesDir),
284+ Banner: opts.Banner,
285+ Footer: opts.Footer,
256286 }
257287}
258288
@@ -477,20 +507,19 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
477507}
478508
479509// IsWhitelistedRequest is used to check if auth should be skipped for this request
480−func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) (ok bool) {
510+func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
481511 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
482512 return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)
483513}
484514
485515// IsWhitelistedPath is used to check if the request path is allowed without auth
486−func (p *OAuthProxy) IsWhitelistedPath(path string) (ok bool) {
516+func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
487517 for _, u := range p.compiledRegex {
488− ok = u.MatchString(path)
489− if ok {
490− return
518+ if u.MatchString(path) {
519+ return true
491520 }
492521 }
493− return
522+ return false
494523}
495524
496525func getRemoteAddr(req *http.Request) (s string) {
@@ -634,63 +663,97 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
634663 }
635664 http.Redirect(rw, req, redirect, 302)
636665 } else {
637− logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Invalid authentication via OAuth2: unauthorized")
666+ logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
638667 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
639668 }
640669}
641670
642671// AuthenticateOnly checks whether the user is currently logged in
643672func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
644− status := p.Authenticate(rw, req)
645− if status == http.StatusAccepted {
646− rw.WriteHeader(http.StatusAccepted)
647− } else {
673+ session, err := p.getAuthenticatedSession(rw, req)
674+ if err != nil {
648675 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
676+ return
649677 }
678+
679+ // we are authenticated
680+ p.addHeadersForProxying(rw, req, session)
681+ rw.WriteHeader(http.StatusAccepted)
650682}
651683
652684// Proxy proxies the user request if the user is authenticated else it prompts
653685// them to authenticate
654686func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
655− status := p.Authenticate(rw, req)
656− if status == http.StatusInternalServerError {
657− p.ErrorPage(rw, http.StatusInternalServerError,
658− "Internal Error", "Internal Error")
659− } else if status == http.StatusForbidden {
687+ session, err := p.getAuthenticatedSession(rw, req)
688+ switch err {
689+ case nil:
690+ // we are authenticated
691+ p.addHeadersForProxying(rw, req, session)
692+ p.serveMux.ServeHTTP(rw, req)
693+
694+ case ErrNeedsLogin:
695+ // we need to send the user to a login screen
696+ if isAjax(req) {
697+ // no point redirecting an AJAX request
698+ p.ErrorJSON(rw, http.StatusUnauthorized)
699+ return
700+ }
701+
660702 if p.SkipProviderButton {
661703 p.OAuthStart(rw, req)
662704 } else {
663705 p.SignInPage(rw, req, http.StatusForbidden)
664706 }
665− } else if status == http.StatusUnauthorized {
666− p.ErrorJSON(rw, status)
667− } else {
668− p.serveMux.ServeHTTP(rw, req)
707+
708+ default:
709+ // unknown error
710+ logger.Printf("Unexpected internal error: %s", err)
711+ p.ErrorPage(rw, http.StatusInternalServerError,
712+ "Internal Error", "Internal Error")
669713 }
714+
670715}
671716
672−// Authenticate checks whether a user is authenticated
673−func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int {
717+// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
718+// Returns nil, ErrNeedsLogin if user needs to login.
719+// Set-Cookie headers may be set on the response as a side-effect of calling this method.
720+func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
721+ var session *sessionsapi.SessionState
722+ var err error
674723 var saveSession, clearSession, revalidated bool
675− remoteAddr := getRemoteAddr(req)
676724
677− session, err := p.LoadCookiedSession(req)
678− if err != nil {
679− logger.Printf("Error loading cookied session: %s", err)
680− }
681− if session != nil && session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
682− logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
683− saveSession = true
725+ if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
726+ session, err = p.GetJwtSession(req)
727+ if err != nil {
728+ logger.Printf("Error retrieving session from token in Authorization header: %s", err)
729+ }
730+ if session != nil {
731+ saveSession = false
732+ }
684733 }
685734
686− var ok bool
687− if ok, err = p.provider.RefreshSessionIfNeeded(session); err != nil {
688− logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
689− clearSession = true
690− session = nil
691− } else if ok {
692− saveSession = true
693− revalidated = true
735+ remoteAddr := getRemoteAddr(req)
736+ if session == nil {
737+ session, err = p.LoadCookiedSession(req)
738+ if err != nil {
739+ logger.Printf("Error loading cookied session: %s", err)
740+ }
741+
742+ if session != nil {
743+ if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
744+ logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
745+ saveSession = true
746+ }
747+
748+ if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {
749+ logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
750+ clearSession = true
751+ session = nil
752+ } else if ok {
753+ saveSession = true
754+ revalidated = true
755+ }
756+ }
694757 }
695758
696759 if session != nil && session.IsExpired() {
@@ -709,18 +772,20 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
709772 }
710773 }
711774
712− if session != nil && session.Email != "" && !p.Validator(session.Email) {
713− logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
714− session = nil
715− saveSession = false
716− clearSession = true
775+ if session != nil && session.Email != "" {
776+ if !p.Validator(session.Email) || !p.provider.ValidateGroup(session.Email) {
777+ logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
778+ session = nil
779+ saveSession = false
780+ clearSession = true
781+ }
717782 }
718783
719784 if saveSession && session != nil {
720785 err = p.SaveSession(rw, req, session)
721786 if err != nil {
722787 logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
723− return http.StatusInternalServerError
788+ return nil, err
724789 }
725790 }
726791
@@ -736,15 +801,14 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
736801 }
737802
738803 if session == nil {
739− // Check if is an ajax request and return unauthorized to avoid a redirect
740− // to the login page
741− if p.isAjax(req) {
742− return http.StatusUnauthorized
743− }
744− return http.StatusForbidden
804+ return nil, ErrNeedsLogin
745805 }
746806
747− // At this point, the user is authenticated. proxy normally
807+ return session, nil
808+}
809+
810+// addHeadersForProxying adds the appropriate headers the request / response for proxying
811+func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
748812 if p.PassBasicAuth {
749813 req.SetBasicAuth(session.User, p.BasicAuthPassword)
750814 req.Header["X-Forwarded-User"] = []string{session.User}
@@ -781,7 +845,6 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
781845 } else {
782846 rw.Header().Set("GAP-Auth", session.Email)
783847 }
784− return http.StatusAccepted
785848}
786849
787850// CheckBasicAuth checks the requests Authorization header for basic auth
@@ -815,7 +878,7 @@ func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionStat
815878}
816879
817880// isAjax checks if a request is an ajax request
818−func (p *OAuthProxy) isAjax(req *http.Request) bool {
881+func isAjax(req *http.Request) bool {
819882 acceptValues, ok := req.Header["accept"]
820883 if !ok {
821884 acceptValues = req.Header["Accept"]
@@ -834,3 +897,92 @@ func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
834897 rw.Header().Set("Content-Type", applicationJSON)
835898 rw.WriteHeader(code)
836899}
900+
901+// GetJwtSession loads a session based on a JWT token in the authorization header.
902+func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
903+ rawBearerToken, err := p.findBearerToken(req)
904+ if err != nil {
905+ return nil, err
906+ }
907+
908+ ctx := context.Background()
909+ var session *sessionsapi.SessionState
910+ for _, verifier := range p.jwtBearerVerifiers {
911+ bearerToken, err := verifier.Verify(ctx, rawBearerToken)
912+
913+ if err != nil {
914+ logger.Printf("failed to verify bearer token: %v", err)
915+ continue
916+ }
917+
918+ var claims struct {
919+ Subject string `json:"sub"`
920+ Email string `json:"email"`
921+ Verified *bool `json:"email_verified"`
922+ }
923+
924+ if err := bearerToken.Claims(&claims); err != nil {
925+ return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
926+ }
927+
928+ if claims.Email == "" {
929+ claims.Email = claims.Subject
930+ }
931+
932+ if claims.Verified != nil && !*claims.Verified {
933+ return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
934+ }
935+
936+ session = &sessionsapi.SessionState{
937+ AccessToken: rawBearerToken,
938+ IDToken: rawBearerToken,
939+ RefreshToken: "",
940+ ExpiresOn: bearerToken.Expiry,
941+ Email: claims.Email,
942+ User: claims.Email,
943+ }
944+ return session, nil
945+ }
946+ return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
947+}
948+
949+// findBearerToken finds a valid JWT token from the Authorization header of a given request.
950+func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
951+ auth := req.Header.Get("Authorization")
952+ s := strings.SplitN(auth, " ", 2)
953+ if len(s) != 2 {
954+ return "", fmt.Errorf("invalid authorization header %s", auth)
955+ }
956+ jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
957+ var rawBearerToken string
958+ if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
959+ rawBearerToken = s[1]
960+ } else if s[0] == "Basic" {
961+ // Check if we have a Bearer token masquerading in Basic
962+ b, err := b64.StdEncoding.DecodeString(s[1])
963+ if err != nil {
964+ return "", err
965+ }
966+ pair := strings.SplitN(string(b), ":", 2)
967+ if len(pair) != 2 {
968+ return "", fmt.Errorf("invalid format %s", b)
969+ }
970+ user, password := pair[0], pair[1]
971+
972+ // check user, user+password, or just password for a token
973+ if jwtRegex.MatchString(user) {
974+ // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
975+ if password == "" || password == "x-oauth-basic" {
976+ rawBearerToken = user
977+ }
978+ } else if jwtRegex.MatchString(password) {
979+ // support passwords and ignore user
980+ rawBearerToken = password
981+ }
982+ }
983+ if rawBearerToken == "" {
984+ return "", fmt.Errorf("no valid bearer token found in authorization header")
985+ }
986+
987+ return rawBearerToken, nil
988+}
oauthproxy_test.go+230−27
@@ -1,8 +1,10 @@
11package main
22
33import (
4+ "context"
45 "crypto"
56 "encoding/base64"
7+ "fmt"
68 "io"
79 "io/ioutil"
810 "net"
@@ -14,6 +16,7 @@ import (
1416 "testing"
1517 "time"
1618
19+ "github.com/coreos/go-oidc"
1720 "github.com/mbland/hmacauth"
1821 "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
1922 "github.com/pusher/oauth2_proxy/pkg/logger"
@@ -160,9 +163,9 @@ func TestEncodedSlashes(t *testing.T) {
160163
161164func TestRobotsTxt(t *testing.T) {
162165 opts := NewOptions()
163− opts.ClientID = "bazquux"
164− opts.ClientSecret = "foobar"
165− opts.CookieSecret = "xyzzyplugh"
166+ opts.ClientID = "asdlkjx"
167+ opts.ClientSecret = "alkgks"
168+ opts.CookieSecret = "asdkugkj"
166169 opts.Validate()
167170
168171 proxy := NewOAuthProxy(opts, func(string) bool { return true })
@@ -175,9 +178,9 @@ func TestRobotsTxt(t *testing.T) {
175178
176179func TestIsValidRedirect(t *testing.T) {
177180 opts := NewOptions()
178− opts.ClientID = "bazquux"
179− opts.ClientSecret = "foobar"
180− opts.CookieSecret = "xyzzyplugh"
181+ opts.ClientID = "skdlfj"
182+ opts.ClientSecret = "fgkdsgj"
183+ opts.CookieSecret = "ljgiogbj"
181184 // Should match domains that are exactly foo.bar and any subdomain of bar.foo
182185 opts.WhitelistDomains = []string{"foo.bar", ".bar.foo"}
183186 opts.Validate()
@@ -226,8 +229,9 @@ func TestIsValidRedirect(t *testing.T) {
226229
227230type TestProvider struct {
228231 *providers.ProviderData
229− EmailAddress string
230− ValidToken bool
232+ EmailAddress string
233+ ValidToken bool
234+ GroupValidator func(string) bool
231235}
232236
233237func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider {
@@ -252,6 +256,9 @@ func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider {
252256 Scope: "profile.email",
253257 },
254258 EmailAddress: emailAddress,
259+ GroupValidator: func(s string) bool {
260+ return true
261+ },
255262 }
256263}
257264
@@ -263,6 +270,13 @@ func (tp *TestProvider) ValidateSessionState(session *sessions.SessionState) boo
263270 return tp.ValidToken
264271}
265272
273+func (tp *TestProvider) ValidateGroup(email string) bool {
274+ if tp.GroupValidator != nil {
275+ return tp.GroupValidator(email)
276+ }
277+ return true
278+}
279+
266280func TestBasicAuthPassword(t *testing.T) {
267281 providerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
268282 logger.Printf("%#v", r)
@@ -284,8 +298,8 @@ func TestBasicAuthPassword(t *testing.T) {
284298 // The CookieSecret must be 32 bytes in order to create the AES
285299 // cipher.
286300 opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp"
287− opts.ClientID = "bazquux"
288− opts.ClientSecret = "foobar"
301+ opts.ClientID = "dlgkj"
302+ opts.ClientSecret = "alkgret"
289303 opts.CookieSecure = false
290304 opts.PassBasicAuth = true
291305 opts.PassUserHeaders = true
@@ -378,8 +392,8 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes
378392 // The CookieSecret must be 32 bytes in order to create the AES
379393 // cipher.
380394 t.opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp"
381− t.opts.ClientID = "bazquux"
382− t.opts.ClientSecret = "foobar"
395+ t.opts.ClientID = "slgkj"
396+ t.opts.ClientSecret = "gfjgojl"
383397 t.opts.CookieSecure = false
384398 t.opts.PassAccessToken = opts.PassAccessToken
385399 t.opts.Validate()
@@ -504,9 +518,9 @@ func NewSignInPageTest(skipProvider bool) *SignInPageTest {
504518 var sipTest SignInPageTest
505519
506520 sipTest.opts = NewOptions()
507− sipTest.opts.CookieSecret = "foobar"
508− sipTest.opts.ClientID = "bazquux"
509− sipTest.opts.ClientSecret = "xyzzyplugh"
521+ sipTest.opts.CookieSecret = "adklsj2"
522+ sipTest.opts.ClientID = "lkdgj"
523+ sipTest.opts.ClientSecret = "sgiufgoi"
510524 sipTest.opts.SkipProviderButton = skipProvider
511525 sipTest.opts.Validate()
512526
@@ -610,8 +624,8 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi
610624 for _, modifier := range modifiers {
611625 modifier(pcTest.opts)
612626 }
613− pcTest.opts.ClientID = "bazquux"
614− pcTest.opts.ClientSecret = "xyzzyplugh"
627+ pcTest.opts.ClientID = "asdfljk"
628+ pcTest.opts.ClientSecret = "lkjfdsig"
615629 pcTest.opts.CookieSecret = "0123456789abcdefabcd"
616630 // First, set the CookieRefresh option so proxy.AesCipher is created,
617631 // needed to encrypt the access_token.
@@ -788,6 +802,25 @@ func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
788802 assert.Equal(t, "unauthorized request\n", string(bodyBytes))
789803}
790804
805+func TestAuthOnlyEndpointUnauthorizedOnProviderGroupValidationFailure(t *testing.T) {
806+ test := NewAuthOnlyEndpointTest()
807+ startSession := &sessions.SessionState{
808+ Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: time.Now()}
809+ test.SaveSession(startSession)
810+ provider := &TestProvider{
811+ ValidToken: true,
812+ GroupValidator: func(s string) bool {
813+ return false
814+ },
815+ }
816+
817+ test.proxy.provider = provider
818+ test.proxy.ServeHTTP(test.rw, test.req)
819+ assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
820+ bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
821+ assert.Equal(t, "unauthorized request\n", string(bodyBytes))
822+}
823+
791824func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
792825 var pcTest ProcessCookieTest
793826
@@ -827,9 +860,9 @@ func TestAuthSkippedForPreflightRequests(t *testing.T) {
827860
828861 opts := NewOptions()
829862 opts.Upstreams = append(opts.Upstreams, upstream.URL)
830− opts.ClientID = "bazquux"
831− opts.ClientSecret = "foobar"
832− opts.CookieSecret = "xyzzyplugh"
863+ opts.ClientID = "aljsal"
864+ opts.ClientSecret = "jglkfsdgj"
865+ opts.CookieSecret = "dkfjgdls"
833866 opts.SkipAuthPreflight = true
834867 opts.Validate()
835868
@@ -966,8 +999,8 @@ func TestNoRequestSignature(t *testing.T) {
966999func TestRequestSignatureGetRequest(t *testing.T) {
9671000 st := NewSignatureTest()
9681001 defer st.Close()
969− st.opts.SignatureKey = "sha1:foobar"
970− st.MakeRequestWithExpectedKey("GET", "", "foobar")
1002+ st.opts.SignatureKey = "sha1:7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d"
1003+ st.MakeRequestWithExpectedKey("GET", "", "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d")
9711004 assert.Equal(t, 200, st.rw.Code)
9721005 assert.Equal(t, st.rw.Body.String(), "signatures match")
9731006}
@@ -975,9 +1008,9 @@ func TestRequestSignatureGetRequest(t *testing.T) {
9751008func TestRequestSignaturePostRequest(t *testing.T) {
9761009 st := NewSignatureTest()
9771010 defer st.Close()
978− st.opts.SignatureKey = "sha1:foobar"
1011+ st.opts.SignatureKey = "sha1:d90df39e2d19282840252612dd7c81421a372f61"
9791012 payload := `{ "hello": "world!" }`
980− st.MakeRequestWithExpectedKey("POST", payload, "foobar")
1013+ st.MakeRequestWithExpectedKey("POST", payload, "d90df39e2d19282840252612dd7c81421a372f61")
9811014 assert.Equal(t, 200, st.rw.Code)
9821015 assert.Equal(t, st.rw.Body.String(), "signatures match")
9831016}
@@ -1023,9 +1056,9 @@ type ajaxRequestTest struct {
10231056func newAjaxRequestTest() *ajaxRequestTest {
10241057 test := &ajaxRequestTest{}
10251058 test.opts = NewOptions()
1026− test.opts.CookieSecret = "foobar"
1027− test.opts.ClientID = "bazquux"
1028− test.opts.ClientSecret = "xyzzyplugh"
1059+ test.opts.CookieSecret = "sdflsw"
1060+ test.opts.ClientID = "gkljfdl"
1061+ test.opts.ClientSecret = "sdflkjs"
10291062 test.opts.Validate()
10301063 test.proxy = NewOAuthProxy(test.opts, func(email string) bool {
10311064 return true
@@ -1132,3 +1165,173 @@ func TestClearSingleCookie(t *testing.T) {
11321165
11331166 assert.Equal(t, 1, len(header["Set-Cookie"]), "should have 1 set-cookie header entries")
11341167}
1168+
1169+type NoOpKeySet struct {
1170+}
1171+
1172+func (NoOpKeySet) VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) {
1173+ splitStrings := strings.Split(jwt, ".")
1174+ payloadString := splitStrings[1]
1175+ jsonString, err := base64.RawURLEncoding.DecodeString(payloadString)
1176+ return []byte(jsonString), err
1177+}
1178+
1179+func TestGetJwtSession(t *testing.T) {
1180+ /* token payload:
1181+ {
1182+ "sub": "1234567890",
1183+ "aud": "https://test.myapp.com",
1184+ "name": "John Doe",
1185+ "email": "john@example.com",
1186+ "iss": "https://issuer.example.com",
1187+ "iat": 1553691215,
1188+ "exp": 1912151821
1189+ }
1190+ */
1191+ goodJwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." +
1192+ "eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly90ZXN0Lm15YXBwLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImVtY" +
1193+ "WlsIjoiam9obkBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNTUzNjkxMj" +
1194+ "E1LCJleHAiOjE5MTIxNTE4MjF9." +
1195+ "rLVyzOnEldUq_pNkfa-WiV8TVJYWyZCaM2Am_uo8FGg11zD7l-qmz3x1seTvqpH6Y0Ty00fmv6dJnGnC8WMnPXQiodRTfhBSe" +
1196+ "OKZMu0HkMD2sg52zlKkbfLTO6ic5VnbVgwjjrB8am_Ta6w7kyFUaB5C1BsIrrLMldkWEhynbb8"
1197+
1198+ keyset := NoOpKeySet{}
1199+ verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
1200+ &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
1201+
1202+ test := NewAuthOnlyEndpointTest(func(opts *Options) {
1203+ opts.PassAuthorization = true
1204+ opts.SetAuthorization = true
1205+ opts.SetXAuthRequest = true
1206+ opts.SkipJwtBearerTokens = true
1207+ opts.jwtBearerVerifiers = append(opts.jwtBearerVerifiers, verifier)
1208+ })
1209+ tp, _ := test.proxy.provider.(*TestProvider)
1210+ tp.GroupValidator = func(s string) bool {
1211+ return true
1212+ }
1213+
1214+ authHeader := fmt.Sprintf("Bearer %s", goodJwt)
1215+ test.req.Header = map[string][]string{
1216+ "Authorization": {authHeader},
1217+ }
1218+
1219+ // Bearer
1220+ session, _ := test.proxy.GetJwtSession(test.req)
1221+ assert.Equal(t, session.User, "john@example.com")
1222+ assert.Equal(t, session.Email, "john@example.com")
1223+ assert.Equal(t, session.ExpiresOn, time.Unix(1912151821, 0))
1224+ assert.Equal(t, session.IDToken, goodJwt)
1225+
1226+ test.proxy.ServeHTTP(test.rw, test.req)
1227+ if test.rw.Code >= 400 {
1228+ t.Fatalf("expected 3xx got %d", test.rw.Code)
1229+ }
1230+
1231+ // Check PassAuthorization, should overwrite Basic header
1232+ assert.Equal(t, test.req.Header.Get("Authorization"), authHeader)
1233+ assert.Equal(t, test.req.Header.Get("X-Forwarded-User"), "john@example.com")
1234+ assert.Equal(t, test.req.Header.Get("X-Forwarded-Email"), "john@example.com")
1235+
1236+ // SetAuthorization and SetXAuthRequest
1237+ assert.Equal(t, test.rw.Header().Get("Authorization"), authHeader)
1238+ assert.Equal(t, test.rw.Header().Get("X-Auth-Request-User"), "john@example.com")
1239+ assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com")
1240+}
1241+
1242+func TestJwtUnauthorizedOnGroupValidationFailure(t *testing.T) {
1243+ goodJwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." +
1244+ "eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly90ZXN0Lm15YXBwLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImVtY" +
1245+ "WlsIjoiam9obkBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNTUzNjkxMj" +
1246+ "E1LCJleHAiOjE5MTIxNTE4MjF9." +
1247+ "rLVyzOnEldUq_pNkfa-WiV8TVJYWyZCaM2Am_uo8FGg11zD7l-qmz3x1seTvqpH6Y0Ty00fmv6dJnGnC8WMnPXQiodRTfhBSe" +
1248+ "OKZMu0HkMD2sg52zlKkbfLTO6ic5VnbVgwjjrB8am_Ta6w7kyFUaB5C1BsIrrLMldkWEhynbb8"
1249+
1250+ keyset := NoOpKeySet{}
1251+ verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
1252+ &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
1253+
1254+ test := NewAuthOnlyEndpointTest(func(opts *Options) {
1255+ opts.PassAuthorization = true
1256+ opts.SetAuthorization = true
1257+ opts.SetXAuthRequest = true
1258+ opts.SkipJwtBearerTokens = true
1259+ opts.jwtBearerVerifiers = append(opts.jwtBearerVerifiers, verifier)
1260+ })
1261+ tp, _ := test.proxy.provider.(*TestProvider)
1262+ // Verify ValidateGroup fails JWT authorization
1263+ tp.GroupValidator = func(s string) bool {
1264+ return false
1265+ }
1266+
1267+ authHeader := fmt.Sprintf("Bearer %s", goodJwt)
1268+ test.req.Header = map[string][]string{
1269+ "Authorization": {authHeader},
1270+ }
1271+ test.proxy.ServeHTTP(test.rw, test.req)
1272+ if test.rw.Code != http.StatusUnauthorized {
1273+ t.Fatalf("expected 401 got %d", test.rw.Code)
1274+ }
1275+}
1276+
1277+func TestFindJwtBearerToken(t *testing.T) {
1278+ p := OAuthProxy{CookieName: "oauth2", CookieDomain: "abc"}
1279+ getReq := &http.Request{URL: &url.URL{Scheme: "http", Host: "example.com"}}
1280+
1281+ validToken := "eyJfoobar.eyJfoobar.12345asdf"
1282+ var token string
1283+
1284+ // Bearer
1285+ getReq.Header = map[string][]string{
1286+ "Authorization": {fmt.Sprintf("Bearer %s", validToken)},
1287+ }
1288+
1289+ token, _ = p.findBearerToken(getReq)
1290+ assert.Equal(t, validToken, token)
1291+
1292+ // Basic - no password
1293+ getReq.SetBasicAuth(token, "")
1294+ token, _ = p.findBearerToken(getReq)
1295+ assert.Equal(t, validToken, token)
1296+
1297+ // Basic - sentinel password
1298+ getReq.SetBasicAuth(token, "x-oauth-basic")
1299+ token, _ = p.findBearerToken(getReq)
1300+ assert.Equal(t, validToken, token)
1301+
1302+ // Basic - any username, password matching jwt pattern
1303+ getReq.SetBasicAuth("any-username-you-could-wish-for", token)
1304+ token, _ = p.findBearerToken(getReq)
1305+ assert.Equal(t, validToken, token)
1306+
1307+ failures := []string{
1308+ // Too many parts
1309+ "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1310+ // Not enough parts
1311+ "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA",
1312+ // Invalid encrypted key
1313+ "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.//////.dGVzdA.dGVzdA.dGVzdA",
1314+ // Invalid IV
1315+ "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.//////.dGVzdA.dGVzdA",
1316+ // Invalid ciphertext
1317+ "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.//////.dGVzdA",
1318+ // Invalid tag
1319+ "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.//////",
1320+ // Invalid header
1321+ "W10.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1322+ // Invalid header
1323+ "######.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1324+ // Missing alc/enc params
1325+ "e30.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
1326+ }
1327+
1328+ for _, failure := range failures {
1329+ getReq.Header = map[string][]string{
1330+ "Authorization": {fmt.Sprintf("Bearer %s", failure)},
1331+ }
1332+ _, err := p.findBearerToken(getReq)
1333+ assert.Error(t, err)
1334+ }
1335+
1336+ fmt.Printf("%s", token)
1337+}
options.go+112−41
@@ -51,6 +51,7 @@ type Options struct {
5151 HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file" env:"OAUTH2_PROXY_HTPASSWD_FILE"`
5252 DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form" env:"OAUTH2_PROXY_DISPLAY_HTPASSWD_FORM"`
5353 CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir" env:"OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR"`
54+ Banner string `flag:"banner" cfg:"banner" env:"OAUTH2_PROXY_BANNER"`
5455 Footer string `flag:"footer" cfg:"footer" env:"OAUTH2_PROXY_FOOTER"`
5556
5657 // Embed CookieOptions
@@ -61,6 +62,8 @@ type Options struct {
6162
6263 Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"`
6364 SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"`
65+ SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens" env:"OAUTH2_PROXY_SKIP_JWT_BEARER_TOKENS"`
66+ ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers" env:"OAUTH2_PROXY_EXTRA_JWT_ISSUERS"`
6467 PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"`
6568 BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"`
6669 PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"`
@@ -76,17 +79,18 @@ type Options struct {
7679
7780 // These options allow for other providers besides Google, with
7881 // potential overrides.
79− Provider string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"`
80− OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url" env:"OAUTH2_PROXY_OIDC_ISSUER_URL"`
81− SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_SKIP_OIDC_DISCOVERY"`
82− OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_OIDC_JWKS_URL"`
83− LoginURL string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"`
84− RedeemURL string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"`
85− ProfileURL string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"`
86− ProtectedResource string `flag:"resource" cfg:"resource" env:"OAUTH2_PROXY_RESOURCE"`
87− ValidateURL string `flag:"validate-url" cfg:"validate_url" env:"OAUTH2_PROXY_VALIDATE_URL"`
88− Scope string `flag:"scope" cfg:"scope" env:"OAUTH2_PROXY_SCOPE"`
89− ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"`
82+ Provider string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"`
83+ OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url" env:"OAUTH2_PROXY_OIDC_ISSUER_URL"`
84+ InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email" env:"OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL"`
85+ SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_SKIP_OIDC_DISCOVERY"`
86+ OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_OIDC_JWKS_URL"`
87+ LoginURL string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"`
88+ RedeemURL string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"`
89+ ProfileURL string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"`
90+ ProtectedResource string `flag:"resource" cfg:"resource" env:"OAUTH2_PROXY_RESOURCE"`
91+ ValidateURL string `flag:"validate-url" cfg:"validate_url" env:"OAUTH2_PROXY_VALIDATE_URL"`
92+ Scope string `flag:"scope" cfg:"scope" env:"OAUTH2_PROXY_SCOPE"`
93+ ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"`
9094
9195 // Configuration values for logging
9296 LoggingFilename string `flag:"logging-filename" cfg:"logging_filename" env:"OAUTH2_LOGGING_FILENAME"`
@@ -110,13 +114,14 @@ type Options struct {
110114 GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks" env:"OAUTH2_PROXY_GCP_HEALTHCHECKS"`
111115
112116 // internal values that are set after config validation
113− redirectURL *url.URL
114− proxyURLs []*url.URL
115− CompiledRegex []*regexp.Regexp
116− provider providers.Provider
117− sessionStore sessionsapi.SessionStore
118− signatureData *SignatureData
119− oidcVerifier *oidc.IDTokenVerifier
117+ redirectURL *url.URL
118+ proxyURLs []*url.URL
119+ CompiledRegex []*regexp.Regexp
120+ provider providers.Provider
121+ sessionStore sessionsapi.SessionStore
122+ signatureData *SignatureData
123+ oidcVerifier *oidc.IDTokenVerifier
124+ jwtBearerVerifiers []*oidc.IDTokenVerifier
120125}
121126
122127// SignatureData holds hmacauth signature hash and key
@@ -143,31 +148,38 @@ func NewOptions() *Options {
143148 SessionOptions: options.SessionOptions{
144149 Type: "cookie",
145150 },
146− SetXAuthRequest: false,
147− SkipAuthPreflight: false,
148− PassBasicAuth: true,
149− PassUserHeaders: true,
150− PassAccessToken: false,
151− PassHostHeader: true,
152− SetAuthorization: false,
153− PassAuthorization: false,
154− ApprovalPrompt: "force",
155− SkipOIDCDiscovery: false,
156− LoggingFilename: "",
157− LoggingMaxSize: 100,
158− LoggingMaxAge: 7,
159− LoggingMaxBackups: 0,
160− LoggingLocalTime: true,
161− LoggingCompress: false,
162− StandardLogging: true,
163− StandardLoggingFormat: logger.DefaultStandardLoggingFormat,
164− RequestLogging: true,
165− RequestLoggingFormat: logger.DefaultRequestLoggingFormat,
166− AuthLogging: true,
167− AuthLoggingFormat: logger.DefaultAuthLoggingFormat,
151+ SetXAuthRequest: false,
152+ SkipAuthPreflight: false,
153+ PassBasicAuth: true,
154+ PassUserHeaders: true,
155+ PassAccessToken: false,
156+ PassHostHeader: true,
157+ SetAuthorization: false,
158+ PassAuthorization: false,
159+ ApprovalPrompt: "force",
160+ InsecureOIDCAllowUnverifiedEmail: false,
161+ SkipOIDCDiscovery: false,
162+ LoggingFilename: "",
163+ LoggingMaxSize: 100,
164+ LoggingMaxAge: 7,
165+ LoggingMaxBackups: 0,
166+ LoggingLocalTime: true,
167+ LoggingCompress: false,
168+ StandardLogging: true,
169+ StandardLoggingFormat: logger.DefaultStandardLoggingFormat,
170+ RequestLogging: true,
171+ RequestLoggingFormat: logger.DefaultRequestLoggingFormat,
172+ AuthLogging: true,
173+ AuthLoggingFormat: logger.DefaultAuthLoggingFormat,
168174 }
169175}
170176
177+// jwtIssuer hold parsed JWT issuer info that's used to construct a verifier.
178+type jwtIssuer struct {
179+ issuerURI string
180+ audience string
181+}
182+
171183func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) {
172184 parsed, err := url.Parse(toParse)
173185 if err != nil {
@@ -244,6 +256,25 @@ func (o *Options) Validate() error {
244256 }
245257 }
246258
259+ if o.SkipJwtBearerTokens {
260+ // If we are using an oidc provider, go ahead and add that provider to the list
261+ if o.oidcVerifier != nil {
262+ o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, o.oidcVerifier)
263+ }
264+ // Configure extra issuers
265+ if len(o.ExtraJwtIssuers) > 0 {
266+ var jwtIssuers []jwtIssuer
267+ jwtIssuers, msgs = parseJwtIssuers(o.ExtraJwtIssuers, msgs)
268+ for _, jwtIssuer := range jwtIssuers {
269+ verifier, err := newVerifierFromJwtIssuer(jwtIssuer)
270+ if err != nil {
271+ msgs = append(msgs, fmt.Sprintf("error building verifiers: %s", err))
272+ }
273+ o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, verifier)
274+ }
275+ }
276+ }
277+
247278 o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
248279
249280 for _, u := range o.Upstreams {
@@ -368,6 +399,7 @@ func parseProviderInfo(o *Options, msgs []string) []string {
368399 }
369400 }
370401 case *providers.OIDCProvider:
402+ p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
371403 if o.oidcVerifier == nil {
372404 msgs = append(msgs, "oidc provider requires an oidc issuer URL")
373405 } else {
@@ -426,10 +458,49 @@ func parseSignatureKey(o *Options, msgs []string) []string {
426458 return append(msgs, "unsupported signature hash algorithm: "+
427459 o.SignatureKey)
428460 }
429− o.signatureData = &SignatureData{hash, secretKey}
461+ o.signatureData = &SignatureData{hash: hash, key: secretKey}
430462 return msgs
431463}
432464
465+// parseJwtIssuers takes in an array of strings in the form of issuer=audience
466+// and parses to an array of jwtIssuer structs.
467+func parseJwtIssuers(issuers []string, msgs []string) ([]jwtIssuer, []string) {
468+ var parsedIssuers []jwtIssuer
469+ for _, jwtVerifier := range issuers {
470+ components := strings.Split(jwtVerifier, "=")
471+ if len(components) < 2 {
472+ msgs = append(msgs, fmt.Sprintf("invalid jwt verifier uri=audience spec: %s", jwtVerifier))
473+ continue
474+ }
475+ uri, audience := components[0], strings.Join(components[1:], "=")
476+ parsedIssuers = append(parsedIssuers, jwtIssuer{issuerURI: uri, audience: audience})
477+ }
478+ return parsedIssuers, msgs
479+}
480+
481+// newVerifierFromJwtIssuer takes in issuer information in jwtIssuer info and returns
482+// a verifier for that issuer.
483+func newVerifierFromJwtIssuer(jwtIssuer jwtIssuer) (*oidc.IDTokenVerifier, error) {
484+ config := &oidc.Config{
485+ ClientID: jwtIssuer.audience,
486+ }
487+ // Try as an OpenID Connect Provider first
488+ var verifier *oidc.IDTokenVerifier
489+ provider, err := oidc.NewProvider(context.Background(), jwtIssuer.issuerURI)
490+ if err != nil {
491+ // Try as JWKS URI
492+ jwksURI := strings.TrimSuffix(jwtIssuer.issuerURI, "/") + "/.well-known/jwks.json"
493+ _, err := http.NewRequest("GET", jwksURI, nil)
494+ if err != nil {
495+ return nil, err
496+ }
497+ verifier = oidc.NewVerifier(jwtIssuer.issuerURI, oidc.NewRemoteKeySet(context.Background(), jwksURI), config)
498+ } else {
499+ verifier = provider.Verifier(config)
500+ }
501+ return verifier, nil
502+}
503+
433504func validateCookieName(o *Options, msgs []string) []string {
434505 cookie := &http.Cookie{Name: o.CookieName}
435506 if cookie.String() == "" {
providers/google.go+10−6
@@ -191,11 +191,9 @@ func getAdminService(adminEmail string, credentialsReader io.Reader) *admin.Serv
191191func userInGroup(service *admin.Service, groups []string, email string) bool {
192192 user, err := fetchUser(service, email)
193193 if err != nil {
194− logger.Printf("error fetching user: %v", err)
195− return false
194+ logger.Printf("Warning: unable to fetch user: %v", err)
195+ user = nil
196196 }
197− id := user.Id
198− custID := user.CustomerId
199197
200198 for _, group := range groups {
201199 members, err := fetchGroupMembers(service, group)
@@ -209,13 +207,19 @@ func userInGroup(service *admin.Service, groups []string, email string) bool {
209207 }
210208
211209 for _, member := range members {
210+ if member.Email == email {
211+ return true
212+ }
213+ if user == nil {
214+ continue
215+ }
212216 switch member.Type {
213217 case "CUSTOMER":
214− if member.Id == custID {
218+ if member.Id == user.CustomerId {
215219 return true
216220 }
217221 case "USER":
218− if member.Id == id {
222+ if member.Id == user.Id {
219223 return true
220224 }
221225 }
providers/google_test.go+37
@@ -3,12 +3,15 @@ package providers
33import (
44 "encoding/base64"
55 "encoding/json"
6+ "fmt"
67 "net/http"
78 "net/http/httptest"
89 "net/url"
910 "testing"
1011
1112 "github.com/stretchr/testify/assert"
13+
14+ admin "google.golang.org/api/admin/directory/v1"
1215)
1316
1417func newRedeemServer(body []byte) (*url.URL, *httptest.Server) {
@@ -179,3 +182,37 @@ func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
179182 }
180183
181184}
185+
186+func TestGoogleProviderUserInGroup(t *testing.T) {
187+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
188+ if r.URL.Path == "/users/member-by-email@example.com" {
189+ fmt.Fprintln(w, "{}")
190+ } else if r.URL.Path == "/users/non-member-by-email@example.com" {
191+ fmt.Fprintln(w, "{}")
192+ } else if r.URL.Path == "/users/member-by-id@example.com" {
193+ fmt.Fprintln(w, "{\"id\": \"member-id\"}")
194+ } else if r.URL.Path == "/users/non-member-by-id@example.com" {
195+ fmt.Fprintln(w, "{\"id\": \"non-member-id\"}")
196+ } else if r.URL.Path == "/groups/group@example.com/members" {
197+ fmt.Fprintln(w, "{\"members\": [{\"email\": \"member-by-email@example.com\"}, {\"id\": \"member-id\", \"type\": \"USER\"}]}")
198+ }
199+ }))
200+ defer ts.Close()
201+
202+ client := ts.Client()
203+ service, err := admin.New(client)
204+ service.BasePath = ts.URL
205+ assert.Equal(t, nil, err)
206+
207+ result := userInGroup(service, []string{"group@example.com"}, "member-by-email@example.com")
208+ assert.True(t, result)
209+
210+ result = userInGroup(service, []string{"group@example.com"}, "member-by-id@example.com")
211+ assert.True(t, result)
212+
213+ result = userInGroup(service, []string{"group@example.com"}, "non-member-by-id@example.com")
214+ assert.False(t, result)
215+
216+ result = userInGroup(service, []string{"group@example.com"}, "non-member-by-email@example.com")
217+ assert.False(t, result)
218+}
providers/internal_util.go+1−1
@@ -47,7 +47,7 @@ func stripParam(param, endpoint string) string {
4747
4848// validateToken returns true if token is valid
4949func validateToken(p Provider, accessToken string, header http.Header) bool {
50− if accessToken == "" || p.Data().ValidateURL == nil {
50+ if accessToken == "" || p.Data().ValidateURL == nil || p.Data().ValidateURL.String() == "" {
5151 return false
5252 }
5353 endpoint := p.Data().ValidateURL.String()
providers/oidc.go+4−3
@@ -14,7 +14,8 @@ import (
1414type OIDCProvider struct {
1515 *ProviderData
1616
17− Verifier *oidc.IDTokenVerifier
17+ Verifier *oidc.IDTokenVerifier
18+ AllowUnverifiedEmail bool
1819}
1920
2021// NewOIDCProvider initiates a new OIDCProvider
@@ -119,7 +120,7 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
119120 // TODO: Try getting email from /userinfo before falling back to Subject
120121 claims.Email = claims.Subject
121122 }
122− if claims.Verified != nil && !*claims.Verified {
123+ if !p.AllowUnverifiedEmail && claims.Verified != nil && !*claims.Verified {
123124 return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
124125 }
125126
@@ -128,7 +129,7 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
128129 IDToken: rawIDToken,
129130 RefreshToken: token.RefreshToken,
130131 CreatedAt: time.Now(),
131− ExpiresOn: token.Expiry,
132+ ExpiresOn: idToken.Expiry,
132133 Email: claims.Email,
133134 User: claims.Subject,
134135 }, nil