@@ -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 "github.com/pusher/oauth2_proxy/cookie"
1820 "github.com/pusher/oauth2_proxy/logger"
@@ -92,6 +94,8 @@ type OAuthProxy struct {
9294 PassAuthorization bool
9395 skipAuthRegex []string
9496 skipAuthPreflight bool
97+ skipJwtBearerTokens bool
98+ jwtBearerVerifiers []*oidc.IDTokenVerifier
9599 compiledRegex []*regexp.Regexp
96100 templates *template.Template
97101 Footer string
@@ -206,6 +210,12 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
206210 logger.Printf("compiled skip-auth-regex => %q", u)
207211 }
208212
213+ if opts.SkipJwtBearerTokens {
214+ logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
215+ for _, issuer := range opts.ExtraJwtIssuers {
216+ logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
217+ }
218+ }
209219 redirectURL := opts.redirectURL
210220 if redirectURL.Path == "" {
211221 redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
@@ -239,25 +249,27 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
239249 OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
240250 AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
241251
242− ProxyPrefix: opts.ProxyPrefix,
243− provider: opts.provider,
244− sessionStore: opts.sessionStore,
245− serveMux: serveMux,
246− redirectURL: redirectURL,
247− whitelistDomains: opts.WhitelistDomains,
248− skipAuthRegex: opts.SkipAuthRegex,
249− skipAuthPreflight: opts.SkipAuthPreflight,
250− compiledRegex: opts.CompiledRegex,
251− SetXAuthRequest: opts.SetXAuthRequest,
252− PassBasicAuth: opts.PassBasicAuth,
253− PassUserHeaders: opts.PassUserHeaders,
254− BasicAuthPassword: opts.BasicAuthPassword,
255− PassAccessToken: opts.PassAccessToken,
256− SetAuthorization: opts.SetAuthorization,
257− PassAuthorization: opts.PassAuthorization,
258− SkipProviderButton: opts.SkipProviderButton,
259− templates: loadTemplates(opts.CustomTemplatesDir),
260− Footer: opts.Footer,
252+ ProxyPrefix: opts.ProxyPrefix,
253+ provider: opts.provider,
254+ sessionStore: opts.sessionStore,
255+ serveMux: serveMux,
256+ redirectURL: redirectURL,
257+ whitelistDomains: opts.WhitelistDomains,
258+ skipAuthRegex: opts.SkipAuthRegex,
259+ skipAuthPreflight: opts.SkipAuthPreflight,
260+ skipJwtBearerTokens: opts.SkipJwtBearerTokens,
261+ jwtBearerVerifiers: opts.jwtBearerVerifiers,
262+ compiledRegex: opts.CompiledRegex,
263+ SetXAuthRequest: opts.SetXAuthRequest,
264+ PassBasicAuth: opts.PassBasicAuth,
265+ PassUserHeaders: opts.PassUserHeaders,
266+ BasicAuthPassword: opts.BasicAuthPassword,
267+ PassAccessToken: opts.PassAccessToken,
268+ SetAuthorization: opts.SetAuthorization,
269+ PassAuthorization: opts.PassAuthorization,
270+ SkipProviderButton: opts.SkipProviderButton,
271+ templates: loadTemplates(opts.CustomTemplatesDir),
272+ Footer: opts.Footer,
261273 }
262274}
263275
@@ -638,7 +650,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
638650 }
639651 http.Redirect(rw, req, redirect, 302)
640652 } else {
641− logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Invalid authentication via OAuth2: unauthorized")
653+ logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
642654 p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
643655 }
644656}
@@ -693,26 +705,42 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
693705// Returns nil, ErrNeedsLogin if user needs to login.
694706// Set-Cookie headers may be set on the response as a side-effect of calling this method.
695707func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
708+ var session *sessionsapi.SessionState
709+ var err error
696710 var saveSession, clearSession, revalidated bool
697− remoteAddr := getRemoteAddr(req)
698711
699− session, err := p.LoadCookiedSession(req)
700− if err != nil {
701− logger.Printf("Error loading cookied session: %s", err)
702− }
703− if session != nil && session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
704− logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
705− saveSession = true
712+ if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
713+ session, err = p.GetJwtSession(req)
714+ if err != nil {
715+ logger.Printf("Error retrieving session from token in Authorization header: %s", err)
716+ }
717+ if session != nil {
718+ saveSession = false
719+ }
706720 }
707721
708− var ok bool
709− if ok, err = p.provider.RefreshSessionIfNeeded(session); err != nil {
710− logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
711− clearSession = true
712− session = nil
713− } else if ok {
714− saveSession = true
715− revalidated = true
722+ remoteAddr := getRemoteAddr(req)
723+ if session == nil {
724+ session, err = p.LoadCookiedSession(req)
725+ if err != nil {
726+ logger.Printf("Error loading cookied session: %s", err)
727+ }
728+
729+ if session != nil {
730+ if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
731+ logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
732+ saveSession = true
733+ }
734+
735+ if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {
736+ logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
737+ clearSession = true
738+ session = nil
739+ } else if ok {
740+ saveSession = true
741+ revalidated = true
742+ }
743+ }
716744 }
717745
718746 if session != nil && session.IsExpired() {
@@ -731,11 +759,13 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
731759 }
732760 }
733761
734− if session != nil && session.Email != "" && !p.Validator(session.Email) {
735− logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
736− session = nil
737− saveSession = false
738− clearSession = true
762+ if session != nil && session.Email != "" {
763+ if !p.Validator(session.Email) || !p.provider.ValidateGroup(session.Email) {
764+ logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
765+ session = nil
766+ saveSession = false
767+ clearSession = true
768+ }
739769 }
740770
741771 if saveSession && session != nil {
@@ -854,3 +884,92 @@ func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
854884 rw.Header().Set("Content-Type", applicationJSON)
855885 rw.WriteHeader(code)
856886}
887+
888+// GetJwtSession loads a session based on a JWT token in the authorization header.
889+func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
890+ rawBearerToken, err := p.findBearerToken(req)
891+ if err != nil {
892+ return nil, err
893+ }
894+
895+ ctx := context.Background()
896+ var session *sessionsapi.SessionState
897+ for _, verifier := range p.jwtBearerVerifiers {
898+ bearerToken, err := verifier.Verify(ctx, rawBearerToken)
899+
900+ if err != nil {
901+ logger.Printf("failed to verify bearer token: %v", err)
902+ continue
903+ }
904+
905+ var claims struct {
906+ Subject string `json:"sub"`
907+ Email string `json:"email"`
908+ Verified *bool `json:"email_verified"`
909+ }
910+
911+ if err := bearerToken.Claims(&claims); err != nil {
912+ return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
913+ }
914+
915+ if claims.Email == "" {
916+ claims.Email = claims.Subject
917+ }
918+
919+ if claims.Verified != nil && !*claims.Verified {
920+ return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
921+ }
922+
923+ session = &sessionsapi.SessionState{
924+ AccessToken: rawBearerToken,
925+ IDToken: rawBearerToken,
926+ RefreshToken: "",
927+ ExpiresOn: bearerToken.Expiry,
928+ Email: claims.Email,
929+ User: claims.Email,
930+ }
931+ return session, nil
932+ }
933+ return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
934+}
935+
936+// findBearerToken finds a valid JWT token from the Authorization header of a given request.
937+func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
938+ auth := req.Header.Get("Authorization")
939+ s := strings.SplitN(auth, " ", 2)
940+ if len(s) != 2 {
941+ return "", fmt.Errorf("invalid authorization header %s", auth)
942+ }
943+ jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
944+ var rawBearerToken string
945+ if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
946+ rawBearerToken = s[1]
947+ } else if s[0] == "Basic" {
948+ // Check if we have a Bearer token masquerading in Basic
949+ b, err := b64.StdEncoding.DecodeString(s[1])
950+ if err != nil {
951+ return "", err
952+ }
953+ pair := strings.SplitN(string(b), ":", 2)
954+ if len(pair) != 2 {
955+ return "", fmt.Errorf("invalid format %s", b)
956+ }
957+ user, password := pair[0], pair[1]
958+
959+ // check user, user+password, or just password for a token
960+ if jwtRegex.MatchString(user) {
961+ // Support blank passwords or magic `x-oauth-basic` passwords - nothing else
962+ if password == "" || password == "x-oauth-basic" {
963+ rawBearerToken = user
964+ }
965+ } else if jwtRegex.MatchString(password) {
966+ // support passwords and ignore user
967+ rawBearerToken = password
968+ }
969+ }
970+ if rawBearerToken == "" {
971+ return "", fmt.Errorf("no valid bearer token found in authorization header")
972+ }
973+
974+ return rawBearerToken, nil
975+}