Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JWT Bearer Issuers #1

Merged
merged 6 commits into from
Jan 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
language: go
go:
- 1.9.x
- 1.10.x
- 1.11.x
install:
# Fetch dependencies
- wget -O dep https://github.com/golang/dep/releases/download/v0.3.2/dep-linux-amd64
- wget -O dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64
- chmod +x dep
- mv dep $GOPATH/bin/dep
script:
Expand Down
7 changes: 5 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
FROM golang:1.10 AS builder
FROM golang:1.11-stretch AS builder
WORKDIR /go/src/github.com/pusher/oauth2_proxy
COPY . .

# Fetch dependencies
RUN go get -u github.com/golang/dep/cmd/dep
RUN wget -O dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64
RUN chmod +x dep
RUN mv dep $GOPATH/bin/dep
RUN dep ensure --vendor-only

# Build image
RUN ./configure && make clean oauth2_proxy

# Copy binary to debian
FROM debian:stretch
RUN apt-get update && apt-get -y install ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy

ENTRYPOINT ["/bin/oauth2_proxy"]
4 changes: 2 additions & 2 deletions configure
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ check_for() {
check_go_version() {
echo -n "Checking go version... "
GO_VERSION=$(${tools[go]} version | ${tools[awk]} '{where = match($0, /[0-9]\.[0-9]+\.[0-9]*/); if (where != 0) print substr($0, RSTART, RLENGTH)}')
vercomp $GO_VERSION 1.9
vercomp $GO_VERSION 1.10
case $? in
0) ;&
1)
Expand All @@ -91,7 +91,7 @@ check_go_version() {
;;
2)
printf "${RED}"
echo "$GO_VERSION < 1.9"
echo "$GO_VERSION < 1.10"
exit 1
;;
esac
Expand Down
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func main() {
emailDomains := StringArray{}
upstreams := StringArray{}
skipAuthRegex := StringArray{}
jwtBearerIssuers := StringArray{}
googleGroups := StringArray{}

config := flagSet.String("config", "", "path to config file")
Expand All @@ -43,6 +44,8 @@ func main() {
flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start")
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.Bool("skip-jwt-bearer", false, "will skip requests that have verified JWT bearer tokens")
flagSet.Var(&jwtBearerIssuers, "jwt-bearer-issuers", "list of JWT bearer issuers URLs (with a .well-known/openid-configuration or a .well-known/jwks.json) and associated audience, in the form of (uri=audience)")

flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
Expand Down
174 changes: 139 additions & 35 deletions oauthproxy.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
b64 "encoding/base64"
"errors"
"fmt"
Expand All @@ -14,6 +15,7 @@ import (
"strings"
"time"

oidc "github.com/coreos/go-oidc"
"github.com/mbland/hmacauth"
"github.com/pusher/oauth2_proxy/cookie"
"github.com/pusher/oauth2_proxy/providers"
Expand Down Expand Up @@ -81,6 +83,8 @@ type OAuthProxy struct {
CookieCipher *cookie.Cipher
skipAuthRegex []string
skipAuthPreflight bool
skipJwtBearerTokens bool
jwtBearerVerifiers []*oidc.IDTokenVerifier
compiledRegex []*regexp.Regexp
templates *template.Template
Footer string
Expand Down Expand Up @@ -173,6 +177,11 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
log.Printf("compiled skip-auth-regex => %q", u)
}

if opts.SkipJwtBearerTokens {
for _, issuer := range opts.JwtBearerIssuers {
log.Printf("Skipping JWT tokens from verified issuer: %q", issuer)
}
}
redirectURL := opts.redirectURL
redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)

Expand Down Expand Up @@ -212,24 +221,26 @@ 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,
serveMux: serveMux,
redirectURL: redirectURL,
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,
CookieCipher: cipher,
templates: loadTemplates(opts.CustomTemplatesDir),
Footer: opts.Footer,
ProxyPrefix: opts.ProxyPrefix,
provider: opts.provider,
serveMux: serveMux,
redirectURL: redirectURL,
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,
CookieCipher: cipher,
templates: loadTemplates(opts.CustomTemplatesDir),
Footer: opts.Footer,
}
}

Expand Down Expand Up @@ -745,26 +756,45 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {

// Authenticate checks whether a user is authenticated
func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int {
var session *providers.SessionState
var err error
var saveSession, clearSession, revalidated bool
remoteAddr := getRemoteAddr(req)
refreshableSession := true

session, sessionAge, err := p.LoadCookiedSession(req)
if err != nil {
log.Printf("%s %s", remoteAddr, err)
if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
session, err = p.GetJwtSession(req)
if err != nil {
log.Printf("Error validating JWT token: %s", err)
}
if session != nil {
saveSession = false
refreshableSession = false
}
}
if session != nil && sessionAge > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
log.Printf("%s refreshing %s old session cookie for %s (refresh after %s)", remoteAddr, sessionAge, session, p.CookieRefresh)
saveSession = true

remoteAddr := getRemoteAddr(req)
if session == nil {
var sessionAge time.Duration
session, sessionAge, err = p.LoadCookiedSession(req)
if err != nil {
log.Printf("%s %s", remoteAddr, err)
}
if session != nil && sessionAge > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
log.Printf("%s refreshing %s old session cookie for %s (refresh after %s)", remoteAddr, sessionAge, session, p.CookieRefresh)
saveSession = true
}
}

var ok bool
if ok, err = p.provider.RefreshSessionIfNeeded(session); err != nil {
log.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 refreshableSession {
if ok, err = p.provider.RefreshSessionIfNeeded(session); err != nil {
log.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
session = nil
clearSession = true
} else if ok {
saveSession = true
revalidated = true
}
}

if session != nil && session.IsExpired() {
Expand All @@ -774,11 +804,11 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
clearSession = true
}

if saveSession && !revalidated && session != nil && session.AccessToken != "" {
if session != nil && saveSession && !revalidated && session.AccessToken != "" {
if !p.provider.ValidateSessionState(session) {
log.Printf("%s removing session. error validating %s", remoteAddr, session)
saveSession = false
session = nil
saveSession = false
clearSession = true
}
}
Expand All @@ -790,7 +820,7 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
clearSession = true
}

if saveSession && session != nil {
if session != nil && saveSession {
err = p.SaveSession(rw, req, session)
if err != nil {
log.Printf("%s %s", remoteAddr, err)
Expand Down Expand Up @@ -878,3 +908,77 @@ func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*providers.SessionState,
}
return nil, fmt.Errorf("%s not in HtpasswdFile", pair[0])
}

// GetJwtSession checks the Authorization header for bearer tokens, both by looking
// for explicit tokens after the `Bearer` keyword, or also tokens hiding out after the
// `Basic` keyword with a username/password of `x-oauth-basic`.
func (p *OAuthProxy) GetJwtSession(req *http.Request) (*providers.SessionState, error) {
auth := req.Header.Get("Authorization")
ctx := context.Background()
var session *providers.SessionState

s := strings.SplitN(auth, " ", 2)
if len(s) != 2 {
return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
}

var rawBearerToken string
// Check if we have a Bearer token Masquerading as Basic
if s[0] == "Basic" {
b, err := b64.StdEncoding.DecodeString(s[1])
if err != nil {
return nil, err
}
pair := strings.SplitN(string(b), ":", 2)
if len(pair) != 2 {
return nil, fmt.Errorf("invalid format %s", b)
}
user, password := pair[0], pair[1]
if password == "x-oauth-basic" || password == "" {
rawBearerToken = user
} else if user == "x-oauth-basic" {
rawBearerToken = password
}
} else if s[0] == "Bearer" {
rawBearerToken = s[1]
} else {
return nil, fmt.Errorf("invalid Authorization header %s", req.Header.Get("Authorization"))
}

for _, verifier := range p.jwtBearerVerifiers {
bearerToken, err := verifier.Verify(ctx, rawBearerToken)

if err != nil {
log.Printf("failed to verify bearer token: %v", err)
continue
}

var claims struct {
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 == "" {
return nil, fmt.Errorf("id_token did not contain an email")
}

if claims.Verified != nil && !*claims.Verified {
return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
}
user := strings.Split(claims.Email, "@")[0]

session = &providers.SessionState{
AccessToken: rawBearerToken,
IDToken: rawBearerToken,
RefreshToken: "",
ExpiresOn: bearerToken.Expiry,
Email: claims.Email,
User: user,
}
}
return session, nil
}
57 changes: 51 additions & 6 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ type Options struct {

Upstreams []string `flag:"upstream" cfg:"upstreams"`
SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
SkipJwtBearerTokens bool `flag:"skip-jwt-bearer" cfg:"skip_jwt_bearer"`
JwtBearerIssuers []string `flag:"jwt-bearer-issuers" cfg:"jwt_bearer_issuers"`
PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password"`
PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token"`
Expand Down Expand Up @@ -83,12 +85,14 @@ type Options struct {
SignatureKey string `flag:"signature-key" cfg:"signature_key" env:"OAUTH2_PROXY_SIGNATURE_KEY"`

// internal values that are set after config validation
redirectURL *url.URL
proxyURLs []*url.URL
CompiledRegex []*regexp.Regexp
provider providers.Provider
signatureData *SignatureData
oidcVerifier *oidc.IDTokenVerifier
redirectURL *url.URL
proxyURLs []*url.URL
CompiledRegex []*regexp.Regexp
provider providers.Provider
signatureData *SignatureData
oidcVerifier *oidc.IDTokenVerifier
jwtBearerIssuerPairs [][]string
jwtBearerVerifiers []*oidc.IDTokenVerifier
}

// SignatureData holds hmacauth signature hash and key
Expand Down Expand Up @@ -174,6 +178,34 @@ func (o *Options) Validate() error {
}
}

if o.SkipJwtBearerTokens {
if len(o.JwtBearerIssuers) < 1 {
msgs = append(msgs, "missing setting: issuers for jwt_bearer_issuers")
}
msgs = parseJwtIssuers(o, msgs)
for _, pair := range o.jwtBearerIssuerPairs {
issuer, audience := pair[0], pair[1]
config := &oidc.Config{
ClientID: audience,
}
// Try as an OpenID Connect Provider first
var verifier *oidc.IDTokenVerifier
provider, err := oidc.NewProvider(context.Background(), issuer)
if err != nil {
// Try as JWKS URI
jwksURI := strings.TrimSuffix(issuer, "/") + "/.well-known/jwks.json"
_, err := http.NewRequest("GET", jwksURI, nil)
if err != nil {
return err
}
verifier = oidc.NewVerifier(issuer, oidc.NewRemoteKeySet(context.Background(), jwksURI), config)
} else {
verifier = provider.Verifier(config)
}
o.jwtBearerVerifiers = append(o.jwtBearerVerifiers, verifier)
}
}

o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)

for _, u := range o.Upstreams {
Expand Down Expand Up @@ -313,6 +345,19 @@ func parseSignatureKey(o *Options, msgs []string) []string {
return msgs
}

func parseJwtIssuers(o *Options, msgs []string) []string {
for _, jwtVerifier := range o.JwtBearerIssuers {
components := strings.Split(jwtVerifier, "=")
if len(components) < 2 {
return append(msgs, "invalid jwt verifier uri=audience spec: "+
jwtVerifier)
}
uri, audience := components[0], strings.Join(components[1:], "=")
o.jwtBearerIssuerPairs = append(o.jwtBearerIssuerPairs, []string{uri, audience})
}
return msgs
}

func validateCookieName(o *Options, msgs []string) []string {
cookie := &http.Cookie{Name: o.CookieName}
if cookie.String() == "" {
Expand Down