@@ -23,8 +23,8 @@ import (
2323 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
2424 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
2525 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
26− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
2726 proxyhttp "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/http"
27+
2828 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
2929 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
3030 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
@@ -60,14 +60,8 @@ type allowedRoute struct {
6060
6161// OAuthProxy is the main authentication proxy
6262type OAuthProxy struct {
63− CSRFCookieName string
64− CookieDomains []string
65− CookiePath string
66− CookieSecure bool
67− CookieHTTPOnly bool
68− CookieExpire time.Duration
69− CookieSameSite string
70− Validator func(string) bool
63+ CookieOptions *options.Cookie
64+ Validator func(string) bool
7165
7266 RobotsPath string
7367 SignInPath string
@@ -179,14 +173,8 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
179173 }
180174
181175 p := &OAuthProxy{
182− CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
183− CookieDomains: opts.Cookie.Domains,
184− CookiePath: opts.Cookie.Path,
185− CookieSecure: opts.Cookie.Secure,
186− CookieHTTPOnly: opts.Cookie.HTTPOnly,
187− CookieExpire: opts.Cookie.Expire,
188− CookieSameSite: opts.Cookie.SameSite,
189− Validator: validator,
176+ CookieOptions: &opts.Cookie,
177+ Validator: validator,
190178
191179 RobotsPath: "/robots.txt",
192180 SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
@@ -427,47 +415,6 @@ func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
427415 return routes, nil
428416}
429417
430−// MakeCSRFCookie creates a cookie for CSRF
431−func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
432− return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
433−}
434−
435−func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
436− cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
437−
438− if cookieDomain != "" {
439− domain := requestutil.GetRequestHost(req)
440− if h, _, err := net.SplitHostPort(domain); err == nil {
441− domain = h
442− }
443− if !strings.HasSuffix(domain, cookieDomain) {
444− logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
445− }
446− }
447−
448− return &http.Cookie{
449− Name: name,
450− Value: value,
451− Path: p.CookiePath,
452− Domain: cookieDomain,
453− HttpOnly: p.CookieHTTPOnly,
454− Secure: p.CookieSecure,
455− Expires: now.Add(expiration),
456− SameSite: cookies.ParseSameSite(p.CookieSameSite),
457− }
458−}
459−
460−// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
461−// session
462−func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
463− http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
464−}
465−
466−// SetCSRFCookie adds a CSRF cookie to the response
467−func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
468− http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
469−}
470−
471418// ClearSessionCookie creates a cookie to unset the user's authentication cookie
472419// stored in the user's session
473420func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
@@ -744,21 +691,35 @@ func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
744691// OAuthStart starts the OAuth2 authentication flow
745692func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
746693 prepareNoCache(rw)
747− nonce, err := encryption.Nonce()
694+
695+ csrf, err := cookies.NewCSRF(p.CookieOptions)
748696 if err != nil {
749− logger.Errorf("Error obtaining nonce: %v", err)
697+ logger.Errorf("Error creating CSRF nonce: %v", err)
750698 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
751699 return
752700 }
753− p.SetCSRFCookie(rw, req, nonce)
754− redirect, err := p.getAppRedirect(req)
701+
702+ appRedirect, err := p.getAppRedirect(req)
755703 if err != nil {
756− logger.Errorf("Error obtaining redirect: %v", err)
704+ logger.Errorf("Error obtaining application redirect: %v", err)
757705 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
758706 return
759707 }
760− redirectURI := p.getOAuthRedirectURI(req)
761− http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
708+
709+ callbackRedirect := p.getOAuthRedirectURI(req)
710+ loginURL := p.provider.GetLoginURL(
711+ callbackRedirect,
712+ encodeState(csrf.HashOAuthState(), appRedirect),
713+ csrf.HashOIDCNonce(),
714+ )
715+
716+ if _, err := csrf.SetCookie(rw, req); err != nil {
717+ logger.Errorf("Error setting CSRF cookie: %v", err)
718+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
719+ return
720+ }
721+
722+ http.Redirect(rw, req, loginURL, http.StatusFound)
762723}
763724
764725// OAuthCallback is the OAuth2 authentication flow callback that finishes the
@@ -796,29 +757,33 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
796757 return
797758 }
798759
799− state := strings.SplitN(req.Form.Get("state"), ":", 2)
800− if len(state) != 2 {
801− logger.Error("Error while parsing OAuth2 state: invalid length")
802− p.ErrorPage(rw, req, http.StatusInternalServerError, "State paremeter did not have expected length", "Login Failed: Invalid State after login.")
803− return
804− }
805− nonce := state[0]
806− redirect := state[1]
807− c, err := req.Cookie(p.CSRFCookieName)
760+ csrf, err := cookies.LoadCSRFCookie(req, p.CookieOptions)
808761 if err != nil {
809762 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
810763 p.ErrorPage(rw, req, http.StatusForbidden, err.Error(), "Login Failed: Unable to find a valid CSRF token. Please try again.")
811764 return
812765 }
813− p.ClearCSRFCookie(rw, req)
814− if c.Value != nonce {
766+
767+ csrf.ClearCookie(rw, req)
768+
769+ nonce, appRedirect, err := decodeState(req)
770+ if err != nil {
771+ logger.Errorf("Error while parsing OAuth2 state: %v", err)
772+ p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
773+ return
774+ }
775+
776+ if !csrf.CheckOAuthState(nonce) {
815777 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
816778 p.ErrorPage(rw, req, http.StatusForbidden, "CSRF token mismatch, potential attack", "Login Failed: Unable to find a valid CSRF token. Please try again.")
817779 return
818780 }
819781
820− if !p.IsValidRedirect(redirect) {
821− redirect = "/"
782+ csrf.SetSessionNonce(session)
783+ p.provider.ValidateSession(req.Context(), session)
784+
785+ if !p.IsValidRedirect(appRedirect) {
786+ appRedirect = "/"
822787 }
823788
824789 // set cookie, or deny
@@ -834,7 +799,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
834799 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
835800 return
836801 }
837− http.Redirect(rw, req, redirect, http.StatusFound)
802+ http.Redirect(rw, req, appRedirect, http.StatusFound)
838803 } else {
839804 logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
840805 p.ErrorPage(rw, req, http.StatusForbidden, "Invalid session: unauthorized")
@@ -966,7 +931,7 @@ func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string {
966931
967932 // If CookieSecure is true, return `https` no matter what
968933 // Not all reverse proxies set X-Forwarded-Proto
969− if p.CookieSecure {
934+ if p.CookieOptions.Secure {
970935 rd.Scheme = schemeHTTPS
971936 }
972937 return rd.String()
@@ -1207,6 +1172,22 @@ func extractAllowedGroups(req *http.Request) map[string]struct{} {
12071172 return groups
12081173}
12091174
1175+// encodedState builds the OAuth state param out of our nonce and
1176+// original application redirect
1177+func encodeState(nonce string, redirect string) string {
1178+ return fmt.Sprintf("%v:%v", nonce, redirect)
1179+}
1180+
1181+// decodeState splits the reflected OAuth state response back into
1182+// the nonce and original application redirect
1183+func decodeState(req *http.Request) (string, string, error) {
1184+ state := strings.SplitN(req.Form.Get("state"), ":", 2)
1185+ if len(state) != 2 {
1186+ return "", "", errors.New("invalid length")
1187+ }
1188+ return state[0], state[1], nil
1189+}
1190+
12101191// addHeadersForProxying adds the appropriate headers the request / response for proxying
12111192func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
12121193 if session.Email == "" {