@@ -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+}