You've already forked oauth2-proxy
mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-06-15 00:15:00 +02:00
Support nonce checks in OIDC Provider (#967)
* Set and verify a nonce with OIDC * Create a CSRF object to manage nonces & cookies * Add missing generic cookie unit tests * Add config flag to control OIDC SkipNonce * Send hashed nonces in authentication requests * Encrypt the CSRF cookie * Add clarity to naming & add more helper methods * Make CSRF an interface and keep underlying nonces private * Add ReverseProxy scope to cookie tests * Align to new 1.16 SameSite cookie default * Perform SecretBytes conversion on CSRF cookie crypto * Make state encoding signatures consistent * Mock time in CSRF struct via Clock * Improve InsecureSkipNonce docstring
This commit is contained in:
141
oauthproxy.go
141
oauthproxy.go
@ -23,8 +23,8 @@ import (
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
|
||||
proxyhttp "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/http"
|
||||
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
|
||||
@ -60,14 +60,8 @@ type allowedRoute struct {
|
||||
|
||||
// OAuthProxy is the main authentication proxy
|
||||
type OAuthProxy struct {
|
||||
CSRFCookieName string
|
||||
CookieDomains []string
|
||||
CookiePath string
|
||||
CookieSecure bool
|
||||
CookieHTTPOnly bool
|
||||
CookieExpire time.Duration
|
||||
CookieSameSite string
|
||||
Validator func(string) bool
|
||||
CookieOptions *options.Cookie
|
||||
Validator func(string) bool
|
||||
|
||||
RobotsPath string
|
||||
SignInPath string
|
||||
@ -179,14 +173,8 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
|
||||
}
|
||||
|
||||
p := &OAuthProxy{
|
||||
CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
|
||||
CookieDomains: opts.Cookie.Domains,
|
||||
CookiePath: opts.Cookie.Path,
|
||||
CookieSecure: opts.Cookie.Secure,
|
||||
CookieHTTPOnly: opts.Cookie.HTTPOnly,
|
||||
CookieExpire: opts.Cookie.Expire,
|
||||
CookieSameSite: opts.Cookie.SameSite,
|
||||
Validator: validator,
|
||||
CookieOptions: &opts.Cookie,
|
||||
Validator: validator,
|
||||
|
||||
RobotsPath: "/robots.txt",
|
||||
SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
|
||||
@ -427,47 +415,6 @@ func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) {
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// MakeCSRFCookie creates a cookie for CSRF
|
||||
func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
|
||||
return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
|
||||
}
|
||||
|
||||
func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
|
||||
cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
|
||||
|
||||
if cookieDomain != "" {
|
||||
domain := requestutil.GetRequestHost(req)
|
||||
if h, _, err := net.SplitHostPort(domain); err == nil {
|
||||
domain = h
|
||||
}
|
||||
if !strings.HasSuffix(domain, cookieDomain) {
|
||||
logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
|
||||
}
|
||||
}
|
||||
|
||||
return &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
Path: p.CookiePath,
|
||||
Domain: cookieDomain,
|
||||
HttpOnly: p.CookieHTTPOnly,
|
||||
Secure: p.CookieSecure,
|
||||
Expires: now.Add(expiration),
|
||||
SameSite: cookies.ParseSameSite(p.CookieSameSite),
|
||||
}
|
||||
}
|
||||
|
||||
// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
|
||||
// session
|
||||
func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
|
||||
http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
|
||||
}
|
||||
|
||||
// SetCSRFCookie adds a CSRF cookie to the response
|
||||
func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
|
||||
http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
|
||||
}
|
||||
|
||||
// ClearSessionCookie creates a cookie to unset the user's authentication cookie
|
||||
// stored in the user's session
|
||||
func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
|
||||
@ -744,21 +691,35 @@ func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
|
||||
// OAuthStart starts the OAuth2 authentication flow
|
||||
func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
|
||||
prepareNoCache(rw)
|
||||
nonce, err := encryption.Nonce()
|
||||
|
||||
csrf, err := cookies.NewCSRF(p.CookieOptions)
|
||||
if err != nil {
|
||||
logger.Errorf("Error obtaining nonce: %v", err)
|
||||
logger.Errorf("Error creating CSRF nonce: %v", err)
|
||||
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
p.SetCSRFCookie(rw, req, nonce)
|
||||
redirect, err := p.getAppRedirect(req)
|
||||
|
||||
appRedirect, err := p.getAppRedirect(req)
|
||||
if err != nil {
|
||||
logger.Errorf("Error obtaining redirect: %v", err)
|
||||
logger.Errorf("Error obtaining application redirect: %v", err)
|
||||
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
redirectURI := p.getOAuthRedirectURI(req)
|
||||
http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
|
||||
|
||||
callbackRedirect := p.getOAuthRedirectURI(req)
|
||||
loginURL := p.provider.GetLoginURL(
|
||||
callbackRedirect,
|
||||
encodeState(csrf.HashOAuthState(), appRedirect),
|
||||
csrf.HashOIDCNonce(),
|
||||
)
|
||||
|
||||
if _, err := csrf.SetCookie(rw, req); err != nil {
|
||||
logger.Errorf("Error setting CSRF cookie: %v", err)
|
||||
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(rw, req, loginURL, http.StatusFound)
|
||||
}
|
||||
|
||||
// OAuthCallback is the OAuth2 authentication flow callback that finishes the
|
||||
@ -796,29 +757,33 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
state := strings.SplitN(req.Form.Get("state"), ":", 2)
|
||||
if len(state) != 2 {
|
||||
logger.Error("Error while parsing OAuth2 state: invalid length")
|
||||
p.ErrorPage(rw, req, http.StatusInternalServerError, "State paremeter did not have expected length", "Login Failed: Invalid State after login.")
|
||||
return
|
||||
}
|
||||
nonce := state[0]
|
||||
redirect := state[1]
|
||||
c, err := req.Cookie(p.CSRFCookieName)
|
||||
csrf, err := cookies.LoadCSRFCookie(req, p.CookieOptions)
|
||||
if err != nil {
|
||||
logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unable to obtain CSRF cookie")
|
||||
p.ErrorPage(rw, req, http.StatusForbidden, err.Error(), "Login Failed: Unable to find a valid CSRF token. Please try again.")
|
||||
return
|
||||
}
|
||||
p.ClearCSRFCookie(rw, req)
|
||||
if c.Value != nonce {
|
||||
|
||||
csrf.ClearCookie(rw, req)
|
||||
|
||||
nonce, appRedirect, err := decodeState(req)
|
||||
if err != nil {
|
||||
logger.Errorf("Error while parsing OAuth2 state: %v", err)
|
||||
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !csrf.CheckOAuthState(nonce) {
|
||||
logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
|
||||
p.ErrorPage(rw, req, http.StatusForbidden, "CSRF token mismatch, potential attack", "Login Failed: Unable to find a valid CSRF token. Please try again.")
|
||||
return
|
||||
}
|
||||
|
||||
if !p.IsValidRedirect(redirect) {
|
||||
redirect = "/"
|
||||
csrf.SetSessionNonce(session)
|
||||
p.provider.ValidateSession(req.Context(), session)
|
||||
|
||||
if !p.IsValidRedirect(appRedirect) {
|
||||
appRedirect = "/"
|
||||
}
|
||||
|
||||
// set cookie, or deny
|
||||
@ -834,7 +799,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
|
||||
p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
http.Redirect(rw, req, redirect, http.StatusFound)
|
||||
http.Redirect(rw, req, appRedirect, http.StatusFound)
|
||||
} else {
|
||||
logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
|
||||
p.ErrorPage(rw, req, http.StatusForbidden, "Invalid session: unauthorized")
|
||||
@ -966,7 +931,7 @@ func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string {
|
||||
|
||||
// If CookieSecure is true, return `https` no matter what
|
||||
// Not all reverse proxies set X-Forwarded-Proto
|
||||
if p.CookieSecure {
|
||||
if p.CookieOptions.Secure {
|
||||
rd.Scheme = schemeHTTPS
|
||||
}
|
||||
return rd.String()
|
||||
@ -1207,6 +1172,22 @@ func extractAllowedGroups(req *http.Request) map[string]struct{} {
|
||||
return groups
|
||||
}
|
||||
|
||||
// encodedState builds the OAuth state param out of our nonce and
|
||||
// original application redirect
|
||||
func encodeState(nonce string, redirect string) string {
|
||||
return fmt.Sprintf("%v:%v", nonce, redirect)
|
||||
}
|
||||
|
||||
// decodeState splits the reflected OAuth state response back into
|
||||
// the nonce and original application redirect
|
||||
func decodeState(req *http.Request) (string, string, error) {
|
||||
state := strings.SplitN(req.Form.Get("state"), ":", 2)
|
||||
if len(state) != 2 {
|
||||
return "", "", errors.New("invalid length")
|
||||
}
|
||||
return state[0], state[1], nil
|
||||
}
|
||||
|
||||
// addHeadersForProxying adds the appropriate headers the request / response for proxying
|
||||
func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) {
|
||||
if session.Email == "" {
|
||||
|
Reference in New Issue
Block a user