Merge branch 'master' into refactor
This commit is contained in:
commit
630db3769b
13
.golangci.yml
Normal file
13
.golangci.yml
Normal file
@ -0,0 +1,13 @@
|
||||
run:
|
||||
deadline: 120s
|
||||
linters:
|
||||
enable:
|
||||
- govet
|
||||
- golint
|
||||
- ineffassign
|
||||
- goconst
|
||||
- deadcode
|
||||
- gofmt
|
||||
- goimports
|
||||
enable-all: false
|
||||
disable-all: true
|
@ -6,8 +6,7 @@ install:
|
||||
- wget -O dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64
|
||||
- chmod +x dep
|
||||
- mv dep $GOPATH/bin/dep
|
||||
- go get github.com/alecthomas/gometalinter
|
||||
- gometalinter --install
|
||||
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $GOPATH/bin v1.17.1
|
||||
script:
|
||||
- ./configure && make test
|
||||
sudo: false
|
||||
|
12
CHANGELOG.md
12
CHANGELOG.md
@ -15,6 +15,11 @@
|
||||
## Changes since v3.2.0
|
||||
|
||||
- [#187](https://github.com/pusher/oauth2_proxy/pull/187) Move root packages to pkg folder (@JoelSpeed)
|
||||
- [#65](https://github.com/pusher/oauth2_proxy/pull/65) Improvements to authenticate requests with a JWT bearer token in the `Authorization` header via
|
||||
the `-skip-jwt-bearer-token` options.
|
||||
- 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
|
||||
(e.g. `https://example.com/.well-known/jwks.json`).
|
||||
- [#180](https://github.com/pusher/outh2_proxy/pull/180) Minor refactor of core proxying path (@aeijdenberg).
|
||||
- [#175](https://github.com/pusher/outh2_proxy/pull/175) Bump go-oidc to v2.0.0 (@aeijdenberg).
|
||||
- Includes fix for potential signature checking issue when OIDC discovery is skipped.
|
||||
- [#155](https://github.com/pusher/outh2_proxy/pull/155) Add RedisSessionStore implementation (@brianv0, @JoelSpeed)
|
||||
@ -55,6 +60,13 @@
|
||||
|
||||
- [#111](https://github.com/pusher/oauth2_proxy/pull/111) Add option for telling where to find a login.gov JWT key file (@timothy-spencer)
|
||||
- [#170](https://github.com/pusher/oauth2_proxy/pull/170) Restore binary tarball contents to be compatible with bitlys original tarballs (@zeha)
|
||||
- [#185](https://github.com/pusher/oauth2_proxy/pull/185) Fix an unsupported protocol scheme error during token validation when using the Azure provider (@jonas)
|
||||
- [#141](https://github.com/pusher/oauth2_proxy/pull/141) Check google group membership based on email address (@bchess)
|
||||
- Google Group membership is additionally checked via email address, allowing users outside a GSuite domain to be authorized.
|
||||
- [#195](https://github.com/pusher/outh2_proxy/pull/195) Add `-banner` flag for overriding the banner line that is displayed (@steakunderscore)
|
||||
- [#198](https://github.com/pusher/outh2_proxy/pull/198) Switch from gometalinter to golangci-lint (@steakunderscore)
|
||||
- [#159](https://github.com/pusher/oauth2_proxy/pull/159) Add option to skip the OIDC provider verified email check: `--insecure-oidc-allow-unverified-email`
|
||||
- [#210](https://github.com/pusher/oauth2_proxy/pull/210) Update base image from Alpine 3.9 to 3.10 (@steakunderscore)
|
||||
|
||||
# v3.2.0
|
||||
|
||||
|
@ -3,6 +3,7 @@ FROM golang:1.12-stretch AS builder
|
||||
# Download tools
|
||||
RUN wget -O $GOPATH/bin/dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64
|
||||
RUN chmod +x $GOPATH/bin/dep
|
||||
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
|
||||
|
||||
# Copy sources
|
||||
WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
|
||||
@ -20,7 +21,7 @@ RUN dep ensure --vendor-only
|
||||
RUN ./configure && make build && touch jwt_signing_key.pem
|
||||
|
||||
# Copy binary to alpine
|
||||
FROM alpine:3.9
|
||||
FROM alpine:3.10
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy
|
||||
COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem
|
||||
|
@ -3,6 +3,7 @@ FROM golang:1.12-stretch AS builder
|
||||
# Download tools
|
||||
RUN wget -O $GOPATH/bin/dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64
|
||||
RUN chmod +x $GOPATH/bin/dep
|
||||
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
|
||||
|
||||
# Copy sources
|
||||
WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
|
||||
@ -20,7 +21,7 @@ RUN dep ensure --vendor-only
|
||||
RUN ./configure && GOARCH=arm64 make build && touch jwt_signing_key.pem
|
||||
|
||||
# Copy binary to alpine
|
||||
FROM arm64v8/alpine:3.9
|
||||
FROM arm64v8/alpine:3.10
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy
|
||||
COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem
|
||||
|
@ -3,6 +3,7 @@ FROM golang:1.12-stretch AS builder
|
||||
# Download tools
|
||||
RUN wget -O $GOPATH/bin/dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64
|
||||
RUN chmod +x $GOPATH/bin/dep
|
||||
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
|
||||
|
||||
# Copy sources
|
||||
WORKDIR $GOPATH/src/github.com/pusher/oauth2_proxy
|
||||
@ -20,7 +21,7 @@ RUN dep ensure --vendor-only
|
||||
RUN ./configure && GOARCH=arm GOARM=6 make build && touch jwt_signing_key.pem
|
||||
|
||||
# Copy binary to alpine
|
||||
FROM arm32v6/alpine:3.9
|
||||
FROM arm32v6/alpine:3.10
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy
|
||||
COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem
|
||||
|
12
Makefile
12
Makefile
@ -17,17 +17,7 @@ distclean: clean
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
$(GOMETALINTER) --vendor --disable-all \
|
||||
--enable=vet \
|
||||
--enable=vetshadow \
|
||||
--enable=golint \
|
||||
--enable=ineffassign \
|
||||
--enable=goconst \
|
||||
--enable=deadcode \
|
||||
--enable=gofmt \
|
||||
--enable=goimports \
|
||||
--deadline=120s \
|
||||
--tests ./...
|
||||
$(GOLANGCILINT) run
|
||||
|
||||
.PHONY: dep
|
||||
dep:
|
||||
|
4
configure
vendored
4
configure
vendored
@ -126,7 +126,7 @@ check_for go
|
||||
check_go_version
|
||||
check_go_env
|
||||
check_for dep
|
||||
check_for gometalinter
|
||||
check_for golangci-lint
|
||||
|
||||
echo
|
||||
|
||||
@ -135,7 +135,7 @@ cat <<- EOF > .env
|
||||
GO := "${tools[go]}"
|
||||
GO_VERSION := ${tools[go_version]}
|
||||
DEP := "${tools[dep]}"
|
||||
GOMETALINTER := "${tools[gometalinter]}"
|
||||
GOLANGCILINT := "${tools[golangci-lint]}"
|
||||
EOF
|
||||
|
||||
echo "Environment configuration written to .env"
|
||||
|
@ -41,7 +41,9 @@ Usage of oauth2_proxy:
|
||||
-custom-templates-dir string: path to custom html templates
|
||||
-display-htpasswd-form: display username / password login form if an htpasswd file is provided (default true)
|
||||
-email-domain value: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email
|
||||
-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)
|
||||
-flush-interval: period between flushing response buffers when streaming responses (default "1s")
|
||||
-banner string: custom banner string. Use "-" to disable default banner.
|
||||
-footer string: custom footer string. Use "-" to disable default footer.
|
||||
-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)
|
||||
-github-org string: restrict logins to members of this organisation
|
||||
@ -61,6 +63,7 @@ Usage of oauth2_proxy:
|
||||
-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
|
||||
-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
|
||||
-login-url string: Authentication endpoint
|
||||
-insecure-oidc-allow-unverified-email: don't fail if an email address in an id_token is not verified
|
||||
-oidc-issuer-url: the OpenID Connect issuer URL. ie: "https://accounts.google.com"
|
||||
-oidc-jwks-url string: OIDC JWKS URI for token verification; required if OIDC discovery is disabled
|
||||
-pass-access-token: pass OAuth access_token to upstream via X-Forwarded-Access-Token header
|
||||
@ -89,6 +92,7 @@ Usage of oauth2_proxy:
|
||||
-signature-key string: GAP-Signature request signature key (algorithm:secretkey)
|
||||
-skip-auth-preflight: will skip authentication for OPTIONS requests
|
||||
-skip-auth-regex value: bypass authentication for requests path's that match (may be given multiple times)
|
||||
-skip-jwt-bearer-tokens: will skip requests that have verified JWT bearer tokens
|
||||
-skip-oidc-discovery: bypass OIDC endpoint discovery. login-url, redeem-url and oidc-jwks-url must be configured in this case
|
||||
-skip-provider-button: will skip sign-in-page to directly reach the next step: oauth/start
|
||||
-ssl-insecure-skip-verify: skip validation of certificates presented when using HTTPS
|
||||
@ -310,3 +314,5 @@ nginx.ingress.kubernetes.io/configuration-snippet: |
|
||||
end
|
||||
}
|
||||
```
|
||||
|
||||
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=".
|
||||
|
27
http_test.go
27
http_test.go
@ -8,6 +8,9 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const localhost = "127.0.0.1"
|
||||
const host = "test-server"
|
||||
|
||||
func TestGCPHealthcheckLiveness(t *testing.T) {
|
||||
handler := func(w http.ResponseWriter, req *http.Request) {
|
||||
w.Write([]byte("test"))
|
||||
@ -16,8 +19,8 @@ func TestGCPHealthcheckLiveness(t *testing.T) {
|
||||
h := gcpHealthcheck(http.HandlerFunc(handler))
|
||||
rw := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("GET", "/liveness_check", nil)
|
||||
r.RemoteAddr = "127.0.0.1"
|
||||
r.Host = "test-server"
|
||||
r.RemoteAddr = localhost
|
||||
r.Host = host
|
||||
h.ServeHTTP(rw, r)
|
||||
|
||||
assert.Equal(t, 200, rw.Code)
|
||||
@ -32,8 +35,8 @@ func TestGCPHealthcheckReadiness(t *testing.T) {
|
||||
h := gcpHealthcheck(http.HandlerFunc(handler))
|
||||
rw := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("GET", "/readiness_check", nil)
|
||||
r.RemoteAddr = "127.0.0.1"
|
||||
r.Host = "test-server"
|
||||
r.RemoteAddr = localhost
|
||||
r.Host = host
|
||||
h.ServeHTTP(rw, r)
|
||||
|
||||
assert.Equal(t, 200, rw.Code)
|
||||
@ -48,8 +51,8 @@ func TestGCPHealthcheckNotHealthcheck(t *testing.T) {
|
||||
h := gcpHealthcheck(http.HandlerFunc(handler))
|
||||
rw := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("GET", "/not_any_check", nil)
|
||||
r.RemoteAddr = "127.0.0.1"
|
||||
r.Host = "test-server"
|
||||
r.RemoteAddr = localhost
|
||||
r.Host = host
|
||||
h.ServeHTTP(rw, r)
|
||||
|
||||
assert.Equal(t, "test", rw.Body.String())
|
||||
@ -63,8 +66,8 @@ func TestGCPHealthcheckIngress(t *testing.T) {
|
||||
h := gcpHealthcheck(http.HandlerFunc(handler))
|
||||
rw := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("GET", "/", nil)
|
||||
r.RemoteAddr = "127.0.0.1"
|
||||
r.Host = "test-server"
|
||||
r.RemoteAddr = localhost
|
||||
r.Host = host
|
||||
r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
|
||||
h.ServeHTTP(rw, r)
|
||||
|
||||
@ -80,8 +83,8 @@ func TestGCPHealthcheckNotIngress(t *testing.T) {
|
||||
h := gcpHealthcheck(http.HandlerFunc(handler))
|
||||
rw := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("GET", "/foo", nil)
|
||||
r.RemoteAddr = "127.0.0.1"
|
||||
r.Host = "test-server"
|
||||
r.RemoteAddr = localhost
|
||||
r.Host = host
|
||||
r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
|
||||
h.ServeHTTP(rw, r)
|
||||
|
||||
@ -96,8 +99,8 @@ func TestGCPHealthcheckNotIngressPut(t *testing.T) {
|
||||
h := gcpHealthcheck(http.HandlerFunc(handler))
|
||||
rw := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("PUT", "/", nil)
|
||||
r.RemoteAddr = "127.0.0.1"
|
||||
r.Host = "test-server"
|
||||
r.RemoteAddr = localhost
|
||||
r.Host = host
|
||||
r.Header.Set(userAgentHeader, googleHealthCheckUserAgent)
|
||||
h.ServeHTTP(rw, r)
|
||||
|
||||
|
13
main.go
13
main.go
@ -23,6 +23,7 @@ func main() {
|
||||
whitelistDomains := StringArray{}
|
||||
upstreams := StringArray{}
|
||||
skipAuthRegex := StringArray{}
|
||||
jwtIssuers := StringArray{}
|
||||
googleGroups := StringArray{}
|
||||
redisSentinelConnectionURLs := StringArray{}
|
||||
|
||||
@ -48,6 +49,8 @@ func main() {
|
||||
flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests")
|
||||
flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS")
|
||||
flagSet.Duration("flush-interval", time.Duration(1)*time.Second, "period between response flushing when streaming responses")
|
||||
flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens (default false)")
|
||||
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)")
|
||||
|
||||
flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
|
||||
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() {
|
||||
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")
|
||||
flagSet.Bool("display-htpasswd-form", true, "display username / password login form if an htpasswd file is provided")
|
||||
flagSet.String("custom-templates-dir", "", "path to custom html templates")
|
||||
flagSet.String("banner", "", "custom banner string. Use \"-\" to disable default banner.")
|
||||
flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.")
|
||||
flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)")
|
||||
flagSet.Bool("proxy-websockets", true, "enables WebSocket proxying")
|
||||
@ -100,6 +104,7 @@ func main() {
|
||||
|
||||
flagSet.String("provider", "google", "OAuth provider")
|
||||
flagSet.String("oidc-issuer-url", "", "OpenID Connect issuer URL (ie: https://accounts.google.com)")
|
||||
flagSet.Bool("insecure-oidc-allow-unverified-email", false, "Don't fail if an email address in an id_token is not verified")
|
||||
flagSet.Bool("skip-oidc-discovery", false, "Skip OIDC discovery and use manually supplied Endpoints")
|
||||
flagSet.String("oidc-jwks-url", "", "OpenID Connect JWKS URL (ie: https://www.googleapis.com/oauth2/v3/certs)")
|
||||
flagSet.String("login-url", "", "Authentication endpoint")
|
||||
@ -145,7 +150,13 @@ func main() {
|
||||
validator := NewValidator(opts.EmailDomains, opts.AuthenticatedEmailsFile)
|
||||
oauthproxy := NewOAuthProxy(opts, validator)
|
||||
|
||||
if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
|
||||
if len(opts.Banner) >= 1 {
|
||||
if opts.Banner == "-" {
|
||||
oauthproxy.SignInMessage = ""
|
||||
} else {
|
||||
oauthproxy.SignInMessage = opts.Banner
|
||||
}
|
||||
} else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
|
||||
if len(opts.EmailDomains) > 1 {
|
||||
oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
|
||||
} else if opts.EmailDomains[0] != "*" {
|
||||
|
302
oauthproxy.go
302
oauthproxy.go
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
b64 "encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -13,6 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/mbland/hmacauth"
|
||||
sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
|
||||
"github.com/pusher/oauth2_proxy/pkg/encryption"
|
||||
@ -47,6 +49,11 @@ var SignatureHeaders = []string{
|
||||
"Gap-Auth",
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrNeedsLogin means the user should be redirected to the login page
|
||||
ErrNeedsLogin = errors.New("redirect to login page")
|
||||
)
|
||||
|
||||
// OAuthProxy is the main authentication proxy
|
||||
type OAuthProxy struct {
|
||||
CookieSeed string
|
||||
@ -87,8 +94,11 @@ type OAuthProxy struct {
|
||||
PassAuthorization bool
|
||||
skipAuthRegex []string
|
||||
skipAuthPreflight bool
|
||||
skipJwtBearerTokens bool
|
||||
jwtBearerVerifiers []*oidc.IDTokenVerifier
|
||||
compiledRegex []*regexp.Regexp
|
||||
templates *template.Template
|
||||
Banner string
|
||||
Footer string
|
||||
}
|
||||
|
||||
@ -151,7 +161,7 @@ func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
|
||||
}
|
||||
|
||||
// NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url
|
||||
func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) (restProxy http.Handler) {
|
||||
func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) http.Handler {
|
||||
u.Path = ""
|
||||
proxy := NewReverseProxy(u, opts.FlushInterval)
|
||||
if !opts.PassHostHeader {
|
||||
@ -167,7 +177,12 @@ func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.Hma
|
||||
wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
|
||||
wsProxy = wsutil.NewSingleHostReverseProxy(wsURL)
|
||||
}
|
||||
return &UpstreamProxy{u.Host, proxy, wsProxy, auth}
|
||||
return &UpstreamProxy{
|
||||
upstream: u.Host,
|
||||
handler: proxy,
|
||||
wsHandler: wsProxy,
|
||||
auth: auth,
|
||||
}
|
||||
}
|
||||
|
||||
// NewOAuthProxy creates a new instance of OOuthProxy from the options provided
|
||||
@ -192,7 +207,13 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
|
||||
}
|
||||
logger.Printf("mapping path %q => file system %q", path, u.Path)
|
||||
proxy := NewFileServer(path, u.Path)
|
||||
serveMux.Handle(path, &UpstreamProxy{path, proxy, nil, nil})
|
||||
uProxy := UpstreamProxy{
|
||||
upstream: path,
|
||||
handler: proxy,
|
||||
wsHandler: nil,
|
||||
auth: nil,
|
||||
}
|
||||
serveMux.Handle(path, &uProxy)
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
|
||||
}
|
||||
@ -201,6 +222,12 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
|
||||
logger.Printf("compiled skip-auth-regex => %q", u)
|
||||
}
|
||||
|
||||
if opts.SkipJwtBearerTokens {
|
||||
logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
|
||||
for _, issuer := range opts.ExtraJwtIssuers {
|
||||
logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
|
||||
}
|
||||
}
|
||||
redirectURL := opts.redirectURL
|
||||
if redirectURL.Path == "" {
|
||||
redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
|
||||
@ -234,25 +261,28 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
|
||||
OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
|
||||
AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
|
||||
|
||||
ProxyPrefix: opts.ProxyPrefix,
|
||||
provider: opts.provider,
|
||||
sessionStore: opts.sessionStore,
|
||||
serveMux: serveMux,
|
||||
redirectURL: redirectURL,
|
||||
whitelistDomains: opts.WhitelistDomains,
|
||||
skipAuthRegex: opts.SkipAuthRegex,
|
||||
skipAuthPreflight: opts.SkipAuthPreflight,
|
||||
compiledRegex: opts.CompiledRegex,
|
||||
SetXAuthRequest: opts.SetXAuthRequest,
|
||||
PassBasicAuth: opts.PassBasicAuth,
|
||||
PassUserHeaders: opts.PassUserHeaders,
|
||||
BasicAuthPassword: opts.BasicAuthPassword,
|
||||
PassAccessToken: opts.PassAccessToken,
|
||||
SetAuthorization: opts.SetAuthorization,
|
||||
PassAuthorization: opts.PassAuthorization,
|
||||
SkipProviderButton: opts.SkipProviderButton,
|
||||
templates: loadTemplates(opts.CustomTemplatesDir),
|
||||
Footer: opts.Footer,
|
||||
ProxyPrefix: opts.ProxyPrefix,
|
||||
provider: opts.provider,
|
||||
sessionStore: opts.sessionStore,
|
||||
serveMux: serveMux,
|
||||
redirectURL: redirectURL,
|
||||
whitelistDomains: opts.WhitelistDomains,
|
||||
skipAuthRegex: opts.SkipAuthRegex,
|
||||
skipAuthPreflight: opts.SkipAuthPreflight,
|
||||
skipJwtBearerTokens: opts.SkipJwtBearerTokens,
|
||||
jwtBearerVerifiers: opts.jwtBearerVerifiers,
|
||||
compiledRegex: opts.CompiledRegex,
|
||||
SetXAuthRequest: opts.SetXAuthRequest,
|
||||
PassBasicAuth: opts.PassBasicAuth,
|
||||
PassUserHeaders: opts.PassUserHeaders,
|
||||
BasicAuthPassword: opts.BasicAuthPassword,
|
||||
PassAccessToken: opts.PassAccessToken,
|
||||
SetAuthorization: opts.SetAuthorization,
|
||||
PassAuthorization: opts.PassAuthorization,
|
||||
SkipProviderButton: opts.SkipProviderButton,
|
||||
templates: loadTemplates(opts.CustomTemplatesDir),
|
||||
Banner: opts.Banner,
|
||||
Footer: opts.Footer,
|
||||
}
|
||||
}
|
||||
|
||||
@ -477,20 +507,19 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
|
||||
}
|
||||
|
||||
// IsWhitelistedRequest is used to check if auth should be skipped for this request
|
||||
func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) (ok bool) {
|
||||
func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
|
||||
isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
|
||||
return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)
|
||||
}
|
||||
|
||||
// IsWhitelistedPath is used to check if the request path is allowed without auth
|
||||
func (p *OAuthProxy) IsWhitelistedPath(path string) (ok bool) {
|
||||
func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
|
||||
for _, u := range p.compiledRegex {
|
||||
ok = u.MatchString(path)
|
||||
if ok {
|
||||
return
|
||||
if u.MatchString(path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
func getRemoteAddr(req *http.Request) (s string) {
|
||||
@ -634,63 +663,97 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
http.Redirect(rw, req, redirect, 302)
|
||||
} else {
|
||||
logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Invalid authentication via OAuth2: unauthorized")
|
||||
logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
|
||||
p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
|
||||
}
|
||||
}
|
||||
|
||||
// AuthenticateOnly checks whether the user is currently logged in
|
||||
func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
|
||||
status := p.Authenticate(rw, req)
|
||||
if status == http.StatusAccepted {
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
} else {
|
||||
session, err := p.getAuthenticatedSession(rw, req)
|
||||
if err != nil {
|
||||
http.Error(rw, "unauthorized request", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// we are authenticated
|
||||
p.addHeadersForProxying(rw, req, session)
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
// Proxy proxies the user request if the user is authenticated else it prompts
|
||||
// them to authenticate
|
||||
func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
|
||||
status := p.Authenticate(rw, req)
|
||||
if status == http.StatusInternalServerError {
|
||||
p.ErrorPage(rw, http.StatusInternalServerError,
|
||||
"Internal Error", "Internal Error")
|
||||
} else if status == http.StatusForbidden {
|
||||
session, err := p.getAuthenticatedSession(rw, req)
|
||||
switch err {
|
||||
case nil:
|
||||
// we are authenticated
|
||||
p.addHeadersForProxying(rw, req, session)
|
||||
p.serveMux.ServeHTTP(rw, req)
|
||||
|
||||
case ErrNeedsLogin:
|
||||
// we need to send the user to a login screen
|
||||
if isAjax(req) {
|
||||
// no point redirecting an AJAX request
|
||||
p.ErrorJSON(rw, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if p.SkipProviderButton {
|
||||
p.OAuthStart(rw, req)
|
||||
} else {
|
||||
p.SignInPage(rw, req, http.StatusForbidden)
|
||||
}
|
||||
} else if status == http.StatusUnauthorized {
|
||||
p.ErrorJSON(rw, status)
|
||||
} else {
|
||||
p.serveMux.ServeHTTP(rw, req)
|
||||
|
||||
default:
|
||||
// unknown error
|
||||
logger.Printf("Unexpected internal error: %s", err)
|
||||
p.ErrorPage(rw, http.StatusInternalServerError,
|
||||
"Internal Error", "Internal Error")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Authenticate checks whether a user is authenticated
|
||||
func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int {
|
||||
// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
|
||||
// Returns nil, ErrNeedsLogin if user needs to login.
|
||||
// Set-Cookie headers may be set on the response as a side-effect of calling this method.
|
||||
func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
|
||||
var session *sessionsapi.SessionState
|
||||
var err error
|
||||
var saveSession, clearSession, revalidated bool
|
||||
|
||||
if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
|
||||
session, err = p.GetJwtSession(req)
|
||||
if err != nil {
|
||||
logger.Printf("Error retrieving session from token in Authorization header: %s", err)
|
||||
}
|
||||
if session != nil {
|
||||
saveSession = false
|
||||
}
|
||||
}
|
||||
|
||||
remoteAddr := getRemoteAddr(req)
|
||||
if session == nil {
|
||||
session, err = p.LoadCookiedSession(req)
|
||||
if err != nil {
|
||||
logger.Printf("Error loading cookied session: %s", err)
|
||||
}
|
||||
|
||||
session, err := p.LoadCookiedSession(req)
|
||||
if err != nil {
|
||||
logger.Printf("Error loading cookied session: %s", err)
|
||||
}
|
||||
if session != nil && session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
|
||||
logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
|
||||
saveSession = true
|
||||
}
|
||||
if session != nil {
|
||||
if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
|
||||
logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
|
||||
saveSession = true
|
||||
}
|
||||
|
||||
var ok bool
|
||||
if ok, err = p.provider.RefreshSessionIfNeeded(session); err != nil {
|
||||
logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
|
||||
clearSession = true
|
||||
session = nil
|
||||
} else if ok {
|
||||
saveSession = true
|
||||
revalidated = true
|
||||
if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {
|
||||
logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
|
||||
clearSession = true
|
||||
session = nil
|
||||
} else if ok {
|
||||
saveSession = true
|
||||
revalidated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if session != nil && session.IsExpired() {
|
||||
@ -709,18 +772,20 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
|
||||
}
|
||||
}
|
||||
|
||||
if session != nil && session.Email != "" && !p.Validator(session.Email) {
|
||||
logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
|
||||
session = nil
|
||||
saveSession = false
|
||||
clearSession = true
|
||||
if session != nil && session.Email != "" {
|
||||
if !p.Validator(session.Email) || !p.provider.ValidateGroup(session.Email) {
|
||||
logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
|
||||
session = nil
|
||||
saveSession = false
|
||||
clearSession = true
|
||||
}
|
||||
}
|
||||
|
||||
if saveSession && session != nil {
|
||||
err = p.SaveSession(rw, req, session)
|
||||
if err != nil {
|
||||
logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
|
||||
return http.StatusInternalServerError
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -736,15 +801,14 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
|
||||
}
|
||||
|
||||
if session == nil {
|
||||
// Check if is an ajax request and return unauthorized to avoid a redirect
|
||||
// to the login page
|
||||
if p.isAjax(req) {
|
||||
return http.StatusUnauthorized
|
||||
}
|
||||
return http.StatusForbidden
|
||||
return nil, ErrNeedsLogin
|
||||
}
|
||||
|
||||
// At this point, the user is authenticated. proxy normally
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// addHeadersForProxying adds the appropriate headers the request / response for proxying
|
||||
func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
|
||||
if p.PassBasicAuth {
|
||||
req.SetBasicAuth(session.User, p.BasicAuthPassword)
|
||||
req.Header["X-Forwarded-User"] = []string{session.User}
|
||||
@ -781,7 +845,6 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
|
||||
} else {
|
||||
rw.Header().Set("GAP-Auth", session.Email)
|
||||
}
|
||||
return http.StatusAccepted
|
||||
}
|
||||
|
||||
// CheckBasicAuth checks the requests Authorization header for basic auth
|
||||
@ -815,7 +878,7 @@ func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionStat
|
||||
}
|
||||
|
||||
// isAjax checks if a request is an ajax request
|
||||
func (p *OAuthProxy) isAjax(req *http.Request) bool {
|
||||
func isAjax(req *http.Request) bool {
|
||||
acceptValues, ok := req.Header["accept"]
|
||||
if !ok {
|
||||
acceptValues = req.Header["Accept"]
|
||||
@ -834,3 +897,92 @@ func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
|
||||
rw.Header().Set("Content-Type", applicationJSON)
|
||||
rw.WriteHeader(code)
|
||||
}
|
||||
|
||||
// GetJwtSession loads a session based on a JWT token in the authorization header.
|
||||
func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
|
||||
rawBearerToken, err := p.findBearerToken(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var session *sessionsapi.SessionState
|
||||
for _, verifier := range p.jwtBearerVerifiers {
|
||||
bearerToken, err := verifier.Verify(ctx, rawBearerToken)
|
||||
|
||||
if err != nil {
|
||||
logger.Printf("failed to verify bearer token: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var claims struct {
|
||||
Subject string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
Verified *bool `json:"email_verified"`
|
||||
}
|
||||
|
||||
if err := bearerToken.Claims(&claims); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
|
||||
}
|
||||
|
||||
if claims.Email == "" {
|
||||
claims.Email = claims.Subject
|
||||
}
|
||||
|
||||
if claims.Verified != nil && !*claims.Verified {
|
||||
return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
|
||||
}
|
||||
|
||||
session = &sessionsapi.SessionState{
|
||||
AccessToken: rawBearerToken,
|
||||
IDToken: rawBearerToken,
|
||||
RefreshToken: "",
|
||||
ExpiresOn: bearerToken.Expiry,
|
||||
Email: claims.Email,
|
||||
User: claims.Email,
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
// findBearerToken finds a valid JWT token from the Authorization header of a given request.
|
||||
func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
|
||||
auth := req.Header.Get("Authorization")
|
||||
s := strings.SplitN(auth, " ", 2)
|
||||
if len(s) != 2 {
|
||||
return "", fmt.Errorf("invalid authorization header %s", auth)
|
||||
}
|
||||
jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
|
||||
var rawBearerToken string
|
||||
if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
|
||||
rawBearerToken = s[1]
|
||||
} else if s[0] == "Basic" {
|
||||
// Check if we have a Bearer token masquerading in Basic
|
||||
b, err := b64.StdEncoding.DecodeString(s[1])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pair := strings.SplitN(string(b), ":", 2)
|
||||
if len(pair) != 2 {
|
||||
return "", fmt.Errorf("invalid format %s", b)
|
||||
}
|
||||
user, password := pair[0], pair[1]
|
||||
|
||||
// check user, user+password, or just password for a token
|
||||
if jwtRegex.MatchString(user) {
|
||||
// Support blank passwords or magic `x-oauth-basic` passwords - nothing else
|
||||
if password == "" || password == "x-oauth-basic" {
|
||||
rawBearerToken = user
|
||||
}
|
||||
} else if jwtRegex.MatchString(password) {
|
||||
// support passwords and ignore user
|
||||
rawBearerToken = password
|
||||
}
|
||||
}
|
||||
if rawBearerToken == "" {
|
||||
return "", fmt.Errorf("no valid bearer token found in authorization header")
|
||||
}
|
||||
|
||||
return rawBearerToken, nil
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@ -14,6 +16,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/mbland/hmacauth"
|
||||
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
|
||||
"github.com/pusher/oauth2_proxy/pkg/logger"
|
||||
@ -160,9 +163,9 @@ func TestEncodedSlashes(t *testing.T) {
|
||||
|
||||
func TestRobotsTxt(t *testing.T) {
|
||||
opts := NewOptions()
|
||||
opts.ClientID = "bazquux"
|
||||
opts.ClientSecret = "foobar"
|
||||
opts.CookieSecret = "xyzzyplugh"
|
||||
opts.ClientID = "asdlkjx"
|
||||
opts.ClientSecret = "alkgks"
|
||||
opts.CookieSecret = "asdkugkj"
|
||||
opts.Validate()
|
||||
|
||||
proxy := NewOAuthProxy(opts, func(string) bool { return true })
|
||||
@ -175,9 +178,9 @@ func TestRobotsTxt(t *testing.T) {
|
||||
|
||||
func TestIsValidRedirect(t *testing.T) {
|
||||
opts := NewOptions()
|
||||
opts.ClientID = "bazquux"
|
||||
opts.ClientSecret = "foobar"
|
||||
opts.CookieSecret = "xyzzyplugh"
|
||||
opts.ClientID = "skdlfj"
|
||||
opts.ClientSecret = "fgkdsgj"
|
||||
opts.CookieSecret = "ljgiogbj"
|
||||
// Should match domains that are exactly foo.bar and any subdomain of bar.foo
|
||||
opts.WhitelistDomains = []string{"foo.bar", ".bar.foo"}
|
||||
opts.Validate()
|
||||
@ -226,8 +229,9 @@ func TestIsValidRedirect(t *testing.T) {
|
||||
|
||||
type TestProvider struct {
|
||||
*providers.ProviderData
|
||||
EmailAddress string
|
||||
ValidToken bool
|
||||
EmailAddress string
|
||||
ValidToken bool
|
||||
GroupValidator func(string) bool
|
||||
}
|
||||
|
||||
func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider {
|
||||
@ -252,6 +256,9 @@ func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider {
|
||||
Scope: "profile.email",
|
||||
},
|
||||
EmailAddress: emailAddress,
|
||||
GroupValidator: func(s string) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -263,6 +270,13 @@ func (tp *TestProvider) ValidateSessionState(session *sessions.SessionState) boo
|
||||
return tp.ValidToken
|
||||
}
|
||||
|
||||
func (tp *TestProvider) ValidateGroup(email string) bool {
|
||||
if tp.GroupValidator != nil {
|
||||
return tp.GroupValidator(email)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestBasicAuthPassword(t *testing.T) {
|
||||
providerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
logger.Printf("%#v", r)
|
||||
@ -284,8 +298,8 @@ func TestBasicAuthPassword(t *testing.T) {
|
||||
// The CookieSecret must be 32 bytes in order to create the AES
|
||||
// cipher.
|
||||
opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp"
|
||||
opts.ClientID = "bazquux"
|
||||
opts.ClientSecret = "foobar"
|
||||
opts.ClientID = "dlgkj"
|
||||
opts.ClientSecret = "alkgret"
|
||||
opts.CookieSecure = false
|
||||
opts.PassBasicAuth = true
|
||||
opts.PassUserHeaders = true
|
||||
@ -378,8 +392,8 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes
|
||||
// The CookieSecret must be 32 bytes in order to create the AES
|
||||
// cipher.
|
||||
t.opts.CookieSecret = "xyzzyplughxyzzyplughxyzzyplughxp"
|
||||
t.opts.ClientID = "bazquux"
|
||||
t.opts.ClientSecret = "foobar"
|
||||
t.opts.ClientID = "slgkj"
|
||||
t.opts.ClientSecret = "gfjgojl"
|
||||
t.opts.CookieSecure = false
|
||||
t.opts.PassAccessToken = opts.PassAccessToken
|
||||
t.opts.Validate()
|
||||
@ -504,9 +518,9 @@ func NewSignInPageTest(skipProvider bool) *SignInPageTest {
|
||||
var sipTest SignInPageTest
|
||||
|
||||
sipTest.opts = NewOptions()
|
||||
sipTest.opts.CookieSecret = "foobar"
|
||||
sipTest.opts.ClientID = "bazquux"
|
||||
sipTest.opts.ClientSecret = "xyzzyplugh"
|
||||
sipTest.opts.CookieSecret = "adklsj2"
|
||||
sipTest.opts.ClientID = "lkdgj"
|
||||
sipTest.opts.ClientSecret = "sgiufgoi"
|
||||
sipTest.opts.SkipProviderButton = skipProvider
|
||||
sipTest.opts.Validate()
|
||||
|
||||
@ -610,8 +624,8 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi
|
||||
for _, modifier := range modifiers {
|
||||
modifier(pcTest.opts)
|
||||
}
|
||||
pcTest.opts.ClientID = "bazquux"
|
||||
pcTest.opts.ClientSecret = "xyzzyplugh"
|
||||
pcTest.opts.ClientID = "asdfljk"
|
||||
pcTest.opts.ClientSecret = "lkjfdsig"
|
||||
pcTest.opts.CookieSecret = "0123456789abcdefabcd"
|
||||
// First, set the CookieRefresh option so proxy.AesCipher is created,
|
||||
// needed to encrypt the access_token.
|
||||
@ -788,6 +802,25 @@ func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
|
||||
assert.Equal(t, "unauthorized request\n", string(bodyBytes))
|
||||
}
|
||||
|
||||
func TestAuthOnlyEndpointUnauthorizedOnProviderGroupValidationFailure(t *testing.T) {
|
||||
test := NewAuthOnlyEndpointTest()
|
||||
startSession := &sessions.SessionState{
|
||||
Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: time.Now()}
|
||||
test.SaveSession(startSession)
|
||||
provider := &TestProvider{
|
||||
ValidToken: true,
|
||||
GroupValidator: func(s string) bool {
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
test.proxy.provider = provider
|
||||
test.proxy.ServeHTTP(test.rw, test.req)
|
||||
assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
|
||||
bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
|
||||
assert.Equal(t, "unauthorized request\n", string(bodyBytes))
|
||||
}
|
||||
|
||||
func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {
|
||||
var pcTest ProcessCookieTest
|
||||
|
||||
@ -827,9 +860,9 @@ func TestAuthSkippedForPreflightRequests(t *testing.T) {
|
||||
|
||||
opts := NewOptions()
|
||||
opts.Upstreams = append(opts.Upstreams, upstream.URL)
|
||||
opts.ClientID = "bazquux"
|
||||
opts.ClientSecret = "foobar"
|
||||
opts.CookieSecret = "xyzzyplugh"
|
||||
opts.ClientID = "aljsal"
|
||||
opts.ClientSecret = "jglkfsdgj"
|
||||
opts.CookieSecret = "dkfjgdls"
|
||||
opts.SkipAuthPreflight = true
|
||||
opts.Validate()
|
||||
|
||||
@ -966,8 +999,8 @@ func TestNoRequestSignature(t *testing.T) {
|
||||
func TestRequestSignatureGetRequest(t *testing.T) {
|
||||
st := NewSignatureTest()
|
||||
defer st.Close()
|
||||
st.opts.SignatureKey = "sha1:foobar"
|
||||
st.MakeRequestWithExpectedKey("GET", "", "foobar")
|
||||
st.opts.SignatureKey = "sha1:7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d"
|
||||
st.MakeRequestWithExpectedKey("GET", "", "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d")
|
||||
assert.Equal(t, 200, st.rw.Code)
|
||||
assert.Equal(t, st.rw.Body.String(), "signatures match")
|
||||
}
|
||||
@ -975,9 +1008,9 @@ func TestRequestSignatureGetRequest(t *testing.T) {
|
||||
func TestRequestSignaturePostRequest(t *testing.T) {
|
||||
st := NewSignatureTest()
|
||||
defer st.Close()
|
||||
st.opts.SignatureKey = "sha1:foobar"
|
||||
st.opts.SignatureKey = "sha1:d90df39e2d19282840252612dd7c81421a372f61"
|
||||
payload := `{ "hello": "world!" }`
|
||||
st.MakeRequestWithExpectedKey("POST", payload, "foobar")
|
||||
st.MakeRequestWithExpectedKey("POST", payload, "d90df39e2d19282840252612dd7c81421a372f61")
|
||||
assert.Equal(t, 200, st.rw.Code)
|
||||
assert.Equal(t, st.rw.Body.String(), "signatures match")
|
||||
}
|
||||
@ -1023,9 +1056,9 @@ type ajaxRequestTest struct {
|
||||
func newAjaxRequestTest() *ajaxRequestTest {
|
||||
test := &ajaxRequestTest{}
|
||||
test.opts = NewOptions()
|
||||
test.opts.CookieSecret = "foobar"
|
||||
test.opts.ClientID = "bazquux"
|
||||
test.opts.ClientSecret = "xyzzyplugh"
|
||||
test.opts.CookieSecret = "sdflsw"
|
||||
test.opts.ClientID = "gkljfdl"
|
||||
test.opts.ClientSecret = "sdflkjs"
|
||||
test.opts.Validate()
|
||||
test.proxy = NewOAuthProxy(test.opts, func(email string) bool {
|
||||
return true
|
||||
@ -1132,3 +1165,173 @@ func TestClearSingleCookie(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 1, len(header["Set-Cookie"]), "should have 1 set-cookie header entries")
|
||||
}
|
||||
|
||||
type NoOpKeySet struct {
|
||||
}
|
||||
|
||||
func (NoOpKeySet) VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) {
|
||||
splitStrings := strings.Split(jwt, ".")
|
||||
payloadString := splitStrings[1]
|
||||
jsonString, err := base64.RawURLEncoding.DecodeString(payloadString)
|
||||
return []byte(jsonString), err
|
||||
}
|
||||
|
||||
func TestGetJwtSession(t *testing.T) {
|
||||
/* token payload:
|
||||
{
|
||||
"sub": "1234567890",
|
||||
"aud": "https://test.myapp.com",
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"iss": "https://issuer.example.com",
|
||||
"iat": 1553691215,
|
||||
"exp": 1912151821
|
||||
}
|
||||
*/
|
||||
goodJwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." +
|
||||
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly90ZXN0Lm15YXBwLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImVtY" +
|
||||
"WlsIjoiam9obkBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNTUzNjkxMj" +
|
||||
"E1LCJleHAiOjE5MTIxNTE4MjF9." +
|
||||
"rLVyzOnEldUq_pNkfa-WiV8TVJYWyZCaM2Am_uo8FGg11zD7l-qmz3x1seTvqpH6Y0Ty00fmv6dJnGnC8WMnPXQiodRTfhBSe" +
|
||||
"OKZMu0HkMD2sg52zlKkbfLTO6ic5VnbVgwjjrB8am_Ta6w7kyFUaB5C1BsIrrLMldkWEhynbb8"
|
||||
|
||||
keyset := NoOpKeySet{}
|
||||
verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
|
||||
&oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
|
||||
|
||||
test := NewAuthOnlyEndpointTest(func(opts *Options) {
|
||||
opts.PassAuthorization = true
|
||||
opts.SetAuthorization = true
|
||||
opts.SetXAuthRequest = true
|
||||
opts.SkipJwtBearerTokens = true
|
||||
opts.jwtBearerVerifiers = append(opts.jwtBearerVerifiers, verifier)
|
||||
})
|
||||
tp, _ := test.proxy.provider.(*TestProvider)
|
||||
tp.GroupValidator = func(s string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
authHeader := fmt.Sprintf("Bearer %s", goodJwt)
|
||||
test.req.Header = map[string][]string{
|
||||
"Authorization": {authHeader},
|
||||
}
|
||||
|
||||
// Bearer
|
||||
session, _ := test.proxy.GetJwtSession(test.req)
|
||||
assert.Equal(t, session.User, "john@example.com")
|
||||
assert.Equal(t, session.Email, "john@example.com")
|
||||
assert.Equal(t, session.ExpiresOn, time.Unix(1912151821, 0))
|
||||
assert.Equal(t, session.IDToken, goodJwt)
|
||||
|
||||
test.proxy.ServeHTTP(test.rw, test.req)
|
||||
if test.rw.Code >= 400 {
|
||||
t.Fatalf("expected 3xx got %d", test.rw.Code)
|
||||
}
|
||||
|
||||
// Check PassAuthorization, should overwrite Basic header
|
||||
assert.Equal(t, test.req.Header.Get("Authorization"), authHeader)
|
||||
assert.Equal(t, test.req.Header.Get("X-Forwarded-User"), "john@example.com")
|
||||
assert.Equal(t, test.req.Header.Get("X-Forwarded-Email"), "john@example.com")
|
||||
|
||||
// SetAuthorization and SetXAuthRequest
|
||||
assert.Equal(t, test.rw.Header().Get("Authorization"), authHeader)
|
||||
assert.Equal(t, test.rw.Header().Get("X-Auth-Request-User"), "john@example.com")
|
||||
assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com")
|
||||
}
|
||||
|
||||
func TestJwtUnauthorizedOnGroupValidationFailure(t *testing.T) {
|
||||
goodJwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." +
|
||||
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly90ZXN0Lm15YXBwLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImVtY" +
|
||||
"WlsIjoiam9obkBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNTUzNjkxMj" +
|
||||
"E1LCJleHAiOjE5MTIxNTE4MjF9." +
|
||||
"rLVyzOnEldUq_pNkfa-WiV8TVJYWyZCaM2Am_uo8FGg11zD7l-qmz3x1seTvqpH6Y0Ty00fmv6dJnGnC8WMnPXQiodRTfhBSe" +
|
||||
"OKZMu0HkMD2sg52zlKkbfLTO6ic5VnbVgwjjrB8am_Ta6w7kyFUaB5C1BsIrrLMldkWEhynbb8"
|
||||
|
||||
keyset := NoOpKeySet{}
|
||||
verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
|
||||
&oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
|
||||
|
||||
test := NewAuthOnlyEndpointTest(func(opts *Options) {
|
||||
opts.PassAuthorization = true
|
||||
opts.SetAuthorization = true
|
||||
opts.SetXAuthRequest = true
|
||||
opts.SkipJwtBearerTokens = true
|
||||
opts.jwtBearerVerifiers = append(opts.jwtBearerVerifiers, verifier)
|
||||
})
|
||||
tp, _ := test.proxy.provider.(*TestProvider)
|
||||
// Verify ValidateGroup fails JWT authorization
|
||||
tp.GroupValidator = func(s string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
authHeader := fmt.Sprintf("Bearer %s", goodJwt)
|
||||
test.req.Header = map[string][]string{
|
||||
"Authorization": {authHeader},
|
||||
}
|
||||
test.proxy.ServeHTTP(test.rw, test.req)
|
||||
if test.rw.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401 got %d", test.rw.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindJwtBearerToken(t *testing.T) {
|
||||
p := OAuthProxy{CookieName: "oauth2", CookieDomain: "abc"}
|
||||
getReq := &http.Request{URL: &url.URL{Scheme: "http", Host: "example.com"}}
|
||||
|
||||
validToken := "eyJfoobar.eyJfoobar.12345asdf"
|
||||
var token string
|
||||
|
||||
// Bearer
|
||||
getReq.Header = map[string][]string{
|
||||
"Authorization": {fmt.Sprintf("Bearer %s", validToken)},
|
||||
}
|
||||
|
||||
token, _ = p.findBearerToken(getReq)
|
||||
assert.Equal(t, validToken, token)
|
||||
|
||||
// Basic - no password
|
||||
getReq.SetBasicAuth(token, "")
|
||||
token, _ = p.findBearerToken(getReq)
|
||||
assert.Equal(t, validToken, token)
|
||||
|
||||
// Basic - sentinel password
|
||||
getReq.SetBasicAuth(token, "x-oauth-basic")
|
||||
token, _ = p.findBearerToken(getReq)
|
||||
assert.Equal(t, validToken, token)
|
||||
|
||||
// Basic - any username, password matching jwt pattern
|
||||
getReq.SetBasicAuth("any-username-you-could-wish-for", token)
|
||||
token, _ = p.findBearerToken(getReq)
|
||||
assert.Equal(t, validToken, token)
|
||||
|
||||
failures := []string{
|
||||
// Too many parts
|
||||
"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
|
||||
// Not enough parts
|
||||
"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA",
|
||||
// Invalid encrypted key
|
||||
"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.//////.dGVzdA.dGVzdA.dGVzdA",
|
||||
// Invalid IV
|
||||
"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.//////.dGVzdA.dGVzdA",
|
||||
// Invalid ciphertext
|
||||
"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.//////.dGVzdA",
|
||||
// Invalid tag
|
||||
"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.//////",
|
||||
// Invalid header
|
||||
"W10.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
|
||||
// Invalid header
|
||||
"######.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
|
||||
// Missing alc/enc params
|
||||
"e30.dGVzdA.dGVzdA.dGVzdA.dGVzdA",
|
||||
}
|
||||
|
||||
for _, failure := range failures {
|
||||
getReq.Header = map[string][]string{
|
||||
"Authorization": {fmt.Sprintf("Bearer %s", failure)},
|
||||
}
|
||||
_, err := p.findBearerToken(getReq)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s", token)
|
||||
}
|
||||
|
153
options.go
153
options.go
@ -51,6 +51,7 @@ type Options struct {
|
||||
HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file" env:"OAUTH2_PROXY_HTPASSWD_FILE"`
|
||||
DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form" env:"OAUTH2_PROXY_DISPLAY_HTPASSWD_FORM"`
|
||||
CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir" env:"OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR"`
|
||||
Banner string `flag:"banner" cfg:"banner" env:"OAUTH2_PROXY_BANNER"`
|
||||
Footer string `flag:"footer" cfg:"footer" env:"OAUTH2_PROXY_FOOTER"`
|
||||
|
||||
// Embed CookieOptions
|
||||
@ -61,6 +62,8 @@ type Options struct {
|
||||
|
||||
Upstreams []string `flag:"upstream" cfg:"upstreams" env:"OAUTH2_PROXY_UPSTREAMS"`
|
||||
SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex" env:"OAUTH2_PROXY_SKIP_AUTH_REGEX"`
|
||||
SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens" env:"OAUTH2_PROXY_SKIP_JWT_BEARER_TOKENS"`
|
||||
ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers" env:"OAUTH2_PROXY_EXTRA_JWT_ISSUERS"`
|
||||
PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"`
|
||||
BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"`
|
||||
PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"`
|
||||
@ -76,17 +79,18 @@ type Options struct {
|
||||
|
||||
// These options allow for other providers besides Google, with
|
||||
// potential overrides.
|
||||
Provider string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"`
|
||||
OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url" env:"OAUTH2_PROXY_OIDC_ISSUER_URL"`
|
||||
SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_SKIP_OIDC_DISCOVERY"`
|
||||
OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_OIDC_JWKS_URL"`
|
||||
LoginURL string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"`
|
||||
RedeemURL string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"`
|
||||
ProfileURL string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"`
|
||||
ProtectedResource string `flag:"resource" cfg:"resource" env:"OAUTH2_PROXY_RESOURCE"`
|
||||
ValidateURL string `flag:"validate-url" cfg:"validate_url" env:"OAUTH2_PROXY_VALIDATE_URL"`
|
||||
Scope string `flag:"scope" cfg:"scope" env:"OAUTH2_PROXY_SCOPE"`
|
||||
ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"`
|
||||
Provider string `flag:"provider" cfg:"provider" env:"OAUTH2_PROXY_PROVIDER"`
|
||||
OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url" env:"OAUTH2_PROXY_OIDC_ISSUER_URL"`
|
||||
InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email" env:"OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL"`
|
||||
SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery" env:"OAUTH2_SKIP_OIDC_DISCOVERY"`
|
||||
OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url" env:"OAUTH2_OIDC_JWKS_URL"`
|
||||
LoginURL string `flag:"login-url" cfg:"login_url" env:"OAUTH2_PROXY_LOGIN_URL"`
|
||||
RedeemURL string `flag:"redeem-url" cfg:"redeem_url" env:"OAUTH2_PROXY_REDEEM_URL"`
|
||||
ProfileURL string `flag:"profile-url" cfg:"profile_url" env:"OAUTH2_PROXY_PROFILE_URL"`
|
||||
ProtectedResource string `flag:"resource" cfg:"resource" env:"OAUTH2_PROXY_RESOURCE"`
|
||||
ValidateURL string `flag:"validate-url" cfg:"validate_url" env:"OAUTH2_PROXY_VALIDATE_URL"`
|
||||
Scope string `flag:"scope" cfg:"scope" env:"OAUTH2_PROXY_SCOPE"`
|
||||
ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt" env:"OAUTH2_PROXY_APPROVAL_PROMPT"`
|
||||
|
||||
// Configuration values for logging
|
||||
LoggingFilename string `flag:"logging-filename" cfg:"logging_filename" env:"OAUTH2_LOGGING_FILENAME"`
|
||||
@ -110,13 +114,14 @@ type Options struct {
|
||||
GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks" env:"OAUTH2_PROXY_GCP_HEALTHCHECKS"`
|
||||
|
||||
// internal values that are set after config validation
|
||||
redirectURL *url.URL
|
||||
proxyURLs []*url.URL
|
||||
CompiledRegex []*regexp.Regexp
|
||||
provider providers.Provider
|
||||
sessionStore sessionsapi.SessionStore
|
||||
signatureData *SignatureData
|
||||
oidcVerifier *oidc.IDTokenVerifier
|
||||
redirectURL *url.URL
|
||||
proxyURLs []*url.URL
|
||||
CompiledRegex []*regexp.Regexp
|
||||
provider providers.Provider
|
||||
sessionStore sessionsapi.SessionStore
|
||||
signatureData *SignatureData
|
||||
oidcVerifier *oidc.IDTokenVerifier
|
||||
jwtBearerVerifiers []*oidc.IDTokenVerifier
|
||||
}
|
||||
|
||||
// SignatureData holds hmacauth signature hash and key
|
||||
@ -143,31 +148,38 @@ func NewOptions() *Options {
|
||||
SessionOptions: options.SessionOptions{
|
||||
Type: "cookie",
|
||||
},
|
||||
SetXAuthRequest: false,
|
||||
SkipAuthPreflight: false,
|
||||
PassBasicAuth: true,
|
||||
PassUserHeaders: true,
|
||||
PassAccessToken: false,
|
||||
PassHostHeader: true,
|
||||
SetAuthorization: false,
|
||||
PassAuthorization: false,
|
||||
ApprovalPrompt: "force",
|
||||
SkipOIDCDiscovery: false,
|
||||
LoggingFilename: "",
|
||||
LoggingMaxSize: 100,
|
||||
LoggingMaxAge: 7,
|
||||
LoggingMaxBackups: 0,
|
||||
LoggingLocalTime: true,
|
||||
LoggingCompress: false,
|
||||
StandardLogging: true,
|
||||
StandardLoggingFormat: logger.DefaultStandardLoggingFormat,
|
||||
RequestLogging: true,
|
||||
RequestLoggingFormat: logger.DefaultRequestLoggingFormat,
|
||||
AuthLogging: true,
|
||||
AuthLoggingFormat: logger.DefaultAuthLoggingFormat,
|
||||
SetXAuthRequest: false,
|
||||
SkipAuthPreflight: false,
|
||||
PassBasicAuth: true,
|
||||
PassUserHeaders: true,
|
||||
PassAccessToken: false,
|
||||
PassHostHeader: true,
|
||||
SetAuthorization: false,
|
||||
PassAuthorization: false,
|
||||
ApprovalPrompt: "force",
|
||||
InsecureOIDCAllowUnverifiedEmail: false,
|
||||
SkipOIDCDiscovery: false,
|
||||
LoggingFilename: "",
|
||||
LoggingMaxSize: 100,
|
||||
LoggingMaxAge: 7,
|
||||
LoggingMaxBackups: 0,
|
||||
LoggingLocalTime: true,
|
||||
LoggingCompress: false,
|
||||
StandardLogging: true,
|
||||
StandardLoggingFormat: logger.DefaultStandardLoggingFormat,
|
||||
RequestLogging: true,
|
||||
RequestLoggingFormat: logger.DefaultRequestLoggingFormat,
|
||||
AuthLogging: true,
|
||||
AuthLoggingFormat: logger.DefaultAuthLoggingFormat,
|
||||
}
|
||||
}
|
||||
|
||||
// jwtIssuer hold parsed JWT issuer info that's used to construct a verifier.
|
||||
type jwtIssuer struct {
|
||||
issuerURI string
|
||||
audience string
|
||||
}
|
||||
|
||||
func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) {
|
||||
parsed, err := url.Parse(toParse)
|
||||
if err != nil {
|
||||
@ -244,6 +256,25 @@ func (o *Options) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if o.SkipJwtBearerTokens {
|
||||
// If we are using an oidc provider, go ahead and add that provider to the list
|
||||
if o.oidcVerifier != nil {
|
||||
o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, o.oidcVerifier)
|
||||
}
|
||||
// Configure extra issuers
|
||||
if len(o.ExtraJwtIssuers) > 0 {
|
||||
var jwtIssuers []jwtIssuer
|
||||
jwtIssuers, msgs = parseJwtIssuers(o.ExtraJwtIssuers, msgs)
|
||||
for _, jwtIssuer := range jwtIssuers {
|
||||
verifier, err := newVerifierFromJwtIssuer(jwtIssuer)
|
||||
if err != nil {
|
||||
msgs = append(msgs, fmt.Sprintf("error building verifiers: %s", err))
|
||||
}
|
||||
o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, verifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
|
||||
|
||||
for _, u := range o.Upstreams {
|
||||
@ -368,6 +399,7 @@ func parseProviderInfo(o *Options, msgs []string) []string {
|
||||
}
|
||||
}
|
||||
case *providers.OIDCProvider:
|
||||
p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
|
||||
if o.oidcVerifier == nil {
|
||||
msgs = append(msgs, "oidc provider requires an oidc issuer URL")
|
||||
} else {
|
||||
@ -426,10 +458,49 @@ func parseSignatureKey(o *Options, msgs []string) []string {
|
||||
return append(msgs, "unsupported signature hash algorithm: "+
|
||||
o.SignatureKey)
|
||||
}
|
||||
o.signatureData = &SignatureData{hash, secretKey}
|
||||
o.signatureData = &SignatureData{hash: hash, key: secretKey}
|
||||
return msgs
|
||||
}
|
||||
|
||||
// parseJwtIssuers takes in an array of strings in the form of issuer=audience
|
||||
// and parses to an array of jwtIssuer structs.
|
||||
func parseJwtIssuers(issuers []string, msgs []string) ([]jwtIssuer, []string) {
|
||||
var parsedIssuers []jwtIssuer
|
||||
for _, jwtVerifier := range issuers {
|
||||
components := strings.Split(jwtVerifier, "=")
|
||||
if len(components) < 2 {
|
||||
msgs = append(msgs, fmt.Sprintf("invalid jwt verifier uri=audience spec: %s", jwtVerifier))
|
||||
continue
|
||||
}
|
||||
uri, audience := components[0], strings.Join(components[1:], "=")
|
||||
parsedIssuers = append(parsedIssuers, jwtIssuer{issuerURI: uri, audience: audience})
|
||||
}
|
||||
return parsedIssuers, msgs
|
||||
}
|
||||
|
||||
// newVerifierFromJwtIssuer takes in issuer information in jwtIssuer info and returns
|
||||
// a verifier for that issuer.
|
||||
func newVerifierFromJwtIssuer(jwtIssuer jwtIssuer) (*oidc.IDTokenVerifier, error) {
|
||||
config := &oidc.Config{
|
||||
ClientID: jwtIssuer.audience,
|
||||
}
|
||||
// Try as an OpenID Connect Provider first
|
||||
var verifier *oidc.IDTokenVerifier
|
||||
provider, err := oidc.NewProvider(context.Background(), jwtIssuer.issuerURI)
|
||||
if err != nil {
|
||||
// Try as JWKS URI
|
||||
jwksURI := strings.TrimSuffix(jwtIssuer.issuerURI, "/") + "/.well-known/jwks.json"
|
||||
_, err := http.NewRequest("GET", jwksURI, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verifier = oidc.NewVerifier(jwtIssuer.issuerURI, oidc.NewRemoteKeySet(context.Background(), jwksURI), config)
|
||||
} else {
|
||||
verifier = provider.Verifier(config)
|
||||
}
|
||||
return verifier, nil
|
||||
}
|
||||
|
||||
func validateCookieName(o *Options, msgs []string) []string {
|
||||
cookie := &http.Cookie{Name: o.CookieName}
|
||||
if cookie.String() == "" {
|
||||
|
@ -191,11 +191,9 @@ func getAdminService(adminEmail string, credentialsReader io.Reader) *admin.Serv
|
||||
func userInGroup(service *admin.Service, groups []string, email string) bool {
|
||||
user, err := fetchUser(service, email)
|
||||
if err != nil {
|
||||
logger.Printf("error fetching user: %v", err)
|
||||
return false
|
||||
logger.Printf("Warning: unable to fetch user: %v", err)
|
||||
user = nil
|
||||
}
|
||||
id := user.Id
|
||||
custID := user.CustomerId
|
||||
|
||||
for _, group := range groups {
|
||||
members, err := fetchGroupMembers(service, group)
|
||||
@ -209,13 +207,19 @@ func userInGroup(service *admin.Service, groups []string, email string) bool {
|
||||
}
|
||||
|
||||
for _, member := range members {
|
||||
if member.Email == email {
|
||||
return true
|
||||
}
|
||||
if user == nil {
|
||||
continue
|
||||
}
|
||||
switch member.Type {
|
||||
case "CUSTOMER":
|
||||
if member.Id == custID {
|
||||
if member.Id == user.CustomerId {
|
||||
return true
|
||||
}
|
||||
case "USER":
|
||||
if member.Id == id {
|
||||
if member.Id == user.Id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,15 @@ package providers
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
admin "google.golang.org/api/admin/directory/v1"
|
||||
)
|
||||
|
||||
func newRedeemServer(body []byte) (*url.URL, *httptest.Server) {
|
||||
@ -179,3 +182,37 @@ func TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGoogleProviderUserInGroup(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/users/member-by-email@example.com" {
|
||||
fmt.Fprintln(w, "{}")
|
||||
} else if r.URL.Path == "/users/non-member-by-email@example.com" {
|
||||
fmt.Fprintln(w, "{}")
|
||||
} else if r.URL.Path == "/users/member-by-id@example.com" {
|
||||
fmt.Fprintln(w, "{\"id\": \"member-id\"}")
|
||||
} else if r.URL.Path == "/users/non-member-by-id@example.com" {
|
||||
fmt.Fprintln(w, "{\"id\": \"non-member-id\"}")
|
||||
} else if r.URL.Path == "/groups/group@example.com/members" {
|
||||
fmt.Fprintln(w, "{\"members\": [{\"email\": \"member-by-email@example.com\"}, {\"id\": \"member-id\", \"type\": \"USER\"}]}")
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := ts.Client()
|
||||
service, err := admin.New(client)
|
||||
service.BasePath = ts.URL
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
result := userInGroup(service, []string{"group@example.com"}, "member-by-email@example.com")
|
||||
assert.True(t, result)
|
||||
|
||||
result = userInGroup(service, []string{"group@example.com"}, "member-by-id@example.com")
|
||||
assert.True(t, result)
|
||||
|
||||
result = userInGroup(service, []string{"group@example.com"}, "non-member-by-id@example.com")
|
||||
assert.False(t, result)
|
||||
|
||||
result = userInGroup(service, []string{"group@example.com"}, "non-member-by-email@example.com")
|
||||
assert.False(t, result)
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ func stripParam(param, endpoint string) string {
|
||||
|
||||
// validateToken returns true if token is valid
|
||||
func validateToken(p Provider, accessToken string, header http.Header) bool {
|
||||
if accessToken == "" || p.Data().ValidateURL == nil {
|
||||
if accessToken == "" || p.Data().ValidateURL == nil || p.Data().ValidateURL.String() == "" {
|
||||
return false
|
||||
}
|
||||
endpoint := p.Data().ValidateURL.String()
|
||||
|
@ -14,7 +14,8 @@ import (
|
||||
type OIDCProvider struct {
|
||||
*ProviderData
|
||||
|
||||
Verifier *oidc.IDTokenVerifier
|
||||
Verifier *oidc.IDTokenVerifier
|
||||
AllowUnverifiedEmail bool
|
||||
}
|
||||
|
||||
// NewOIDCProvider initiates a new OIDCProvider
|
||||
@ -119,7 +120,7 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
|
||||
// TODO: Try getting email from /userinfo before falling back to Subject
|
||||
claims.Email = claims.Subject
|
||||
}
|
||||
if claims.Verified != nil && !*claims.Verified {
|
||||
if !p.AllowUnverifiedEmail && claims.Verified != nil && !*claims.Verified {
|
||||
return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
|
||||
}
|
||||
|
||||
@ -128,7 +129,7 @@ func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Tok
|
||||
IDToken: rawIDToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: token.Expiry,
|
||||
ExpiresOn: idToken.Expiry,
|
||||
Email: claims.Email,
|
||||
User: claims.Subject,
|
||||
}, nil
|
||||
|
Loading…
Reference in New Issue
Block a user