Skip to content

Commit

Permalink
Add tracking on CSRF error
Browse files Browse the repository at this point in the history
ref DEV-1337
  • Loading branch information
louischan-oursky committed Jun 20, 2024
2 parents ea1a860 + 36f2f94 commit bd935ac
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 1 deletion.
3 changes: 3 additions & 0 deletions pkg/auth/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ func NewRouter(p *deps.RootProvider, configSource *configsource.ConfigSource) *h
newWebappPageChain := func(idpSessionOnly bool) httproute.Middleware {
return httproute.Chain(
newWebappChain(idpSessionOnly),
p.Middleware(newCSRFDebugMiddleware),
p.Middleware(newCSRFMiddleware),
// Turbo no longer requires us to tell the redirected location.
// It can now determine redirection from the response.
Expand All @@ -178,6 +179,7 @@ func NewRouter(p *deps.RootProvider, configSource *configsource.ConfigSource) *h
webappPageChain := newWebappPageChain(false)
webappSIWEChain := httproute.Chain(
webappChain,
p.Middleware(newCSRFDebugMiddleware),
p.Middleware(newCSRFMiddleware),
p.Middleware(newUnsafeDynamicCSPMiddleware),
)
Expand Down Expand Up @@ -207,6 +209,7 @@ func NewRouter(p *deps.RootProvider, configSource *configsource.ConfigSource) *h
// consent page only accepts idp session
webappConsentPageChain := httproute.Chain(
newWebappChain(true),
p.Middleware(newCSRFDebugMiddleware),
p.Middleware(newCSRFMiddleware),
p.Middleware(newConsentPageDynamicCSPMiddleware),
)
Expand Down
49 changes: 48 additions & 1 deletion pkg/auth/webapp/csrf.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package webapp

import "github.com/authgear/authgear-server/pkg/lib/config"
import (
"net/http"

"github.com/authgear/authgear-server/pkg/lib/config"
"github.com/authgear/authgear-server/pkg/util/duration"
"github.com/authgear/authgear-server/pkg/util/httputil"
)

// CSRFFieldName is the same as the default, but public.
const CSRFFieldName = "gorilla.csrf.Token"
Expand All @@ -21,3 +27,44 @@ func NewCSRFCookieDef(cfg *config.HTTPConfig) CSRFCookieDef {

return def
}

type CSRFDebugMiddleware struct {
Cookies CookieManager
}

var CSRFDebugCookieMaxAge = int(duration.UserInteraction.Seconds())

// NOTE: SameSiteDefaultMode means do not emit attribute,
// ref: https://github.com/golang/go/blob/3e10c1ff8141fae6b4d35a42e2631e7830c79830/src/net/http/cookie.go#L279

var CSRFDebugCookieSameSiteOmitDef = &httputil.CookieDef{
NameSuffix: "debug_csrf_same_site_omit",
Path: "/",
AllowScriptAccess: false,
SameSite: http.SameSiteDefaultMode,
MaxAge: &CSRFDebugCookieMaxAge,
}

var CSRFDebugCookieSameSiteNoneDef = &httputil.CookieDef{
NameSuffix: "debug_csrf_same_site_none",
Path: "/",
AllowScriptAccess: false,
SameSite: http.SameSiteNoneMode,
MaxAge: &CSRFDebugCookieMaxAge,
}

var CSRFDebugCookieSameSiteLaxDef = &httputil.CookieDef{
NameSuffix: "debug_csrf_same_site_lax",
Path: "/",
AllowScriptAccess: false,
SameSite: http.SameSiteLaxMode,
MaxAge: &CSRFDebugCookieMaxAge,
}

var CSRFDebugCookieSameSiteStrictDef = &httputil.CookieDef{
NameSuffix: "debug_csrf_same_site_strict",
Path: "/",
AllowScriptAccess: false,
SameSite: http.SameSiteStrictMode,
MaxAge: &CSRFDebugCookieMaxAge,
}
21 changes: 21 additions & 0 deletions pkg/auth/webapp/csrf_debug_middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package webapp

import (
"net/http"

"github.com/authgear/authgear-server/pkg/util/httputil"
)

func (m *CSRFDebugMiddleware) Handle(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
omitCookie := m.Cookies.ValueCookie(CSRFDebugCookieSameSiteOmitDef, "exists")
httputil.UpdateCookie(w, omitCookie)
noneCookie := m.Cookies.ValueCookie(CSRFDebugCookieSameSiteNoneDef, "exists")
httputil.UpdateCookie(w, noneCookie)
laxCookie := m.Cookies.ValueCookie(CSRFDebugCookieSameSiteLaxDef, "exists")
httputil.UpdateCookie(w, laxCookie)
strictCookie := m.Cookies.ValueCookie(CSRFDebugCookieSameSiteStrictDef, "exists")
httputil.UpdateCookie(w, strictCookie)
next.ServeHTTP(w, r)
})
}
69 changes: 69 additions & 0 deletions pkg/auth/webapp/csrf_middleware.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
package webapp

import (
"encoding/base64"
"fmt"
"net/http"
"strings"

"github.com/gorilla/csrf"
"github.com/sirupsen/logrus"

"github.com/authgear/authgear-server/pkg/lib/config"
"github.com/authgear/authgear-server/pkg/util/duration"
"github.com/authgear/authgear-server/pkg/util/httputil"
"github.com/authgear/authgear-server/pkg/util/jwkutil"
"github.com/authgear/authgear-server/pkg/util/log"
)

type CSRFMiddlewareLogger struct{ *log.Logger }

func NewCSRFMiddlewareLogger(lf *log.Factory) CSRFMiddlewareLogger {
return CSRFMiddlewareLogger{lf.New("webapp-csrf-middleware")}
}

type CSRFMiddleware struct {
Secret *config.CSRFKeyMaterials
CookieDef CSRFCookieDef
TrustProxy config.TrustProxy
Cookies CookieManager
Logger CSRFMiddlewareLogger
}

func (m *CSRFMiddleware) Handle(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
secure := httputil.GetProto(r, bool(m.TrustProxy)) == "https"
options := []csrf.Option{
csrf.MaxAge(int(duration.UserInteraction.Seconds())),
csrf.FieldName(CSRFFieldName),
csrf.CookieName(m.CookieDef.Name),
csrf.Path("/"),
Expand All @@ -43,6 +58,8 @@ func (m *CSRFMiddleware) Handle(next http.Handler) http.Handler {
options = append(options, csrf.SameSite(0))
}

options = append(options, csrf.ErrorHandler(http.HandlerFunc(m.unauthorizedHandler)))

key, err := jwkutil.ExtractOctetKey(m.Secret.Set, "")
if err != nil {
panic("webapp: CSRF key not found")
Expand All @@ -52,3 +69,55 @@ func (m *CSRFMiddleware) Handle(next http.Handler) http.Handler {
h.ServeHTTP(w, r)
})
}

func (m *CSRFMiddleware) unauthorizedHandler(w http.ResponseWriter, r *http.Request) {
// Check debug cookies and inject info for reporting
omitCookie, err := m.Cookies.GetCookie(r, CSRFDebugCookieSameSiteOmitDef)
hasOmitCookie := (err == nil && omitCookie.Value == "exists")

noneCookie, err := m.Cookies.GetCookie(r, CSRFDebugCookieSameSiteNoneDef)
hasNoneCookie := (err == nil && noneCookie.Value == "exists")

laxCookie, err := m.Cookies.GetCookie(r, CSRFDebugCookieSameSiteLaxDef)
hasLaxCookie := (err == nil && laxCookie.Value == "exists")

strictCookie, err := m.Cookies.GetCookie(r, CSRFDebugCookieSameSiteStrictDef)
hasStrictCookie := (err == nil && strictCookie.Value == "exists")

csrfCookie, _ := r.Cookie(m.CookieDef.Name)
csrfCookieSizeInBytes := 0
maskedCsrfCookieContent := ""
if csrfCookie != nil {
// do not return value but length only for debug.
csrfCookieSizeInBytes = len([]byte(csrfCookie.Value))
if data, err := base64.StdEncoding.DecodeString(csrfCookie.Value); err != nil {
csrfToken := string(data)
maskedTokenParts := make([]string, 0, 4)
for i, part := range strings.Split(csrfToken, "|") {
// token format is date|value|mac
// ref: https://github.com/gorilla/securecookie/blob/eae3c1840ec4adda88a4af683ad0f60bb690e7c2/securecookie.go#L320C30-L320C44
// we will mask value and sig
if i == 0 {
maskedTokenParts = append(maskedTokenParts, part)
continue
}
maskedTokenParts = append(maskedTokenParts, strings.Repeat("*", len(part)))
}
maskedCsrfCookieContent = strings.Join(maskedTokenParts, "|")
}
}

m.Logger.WithFields(logrus.Fields{
"hasOmitCookie": hasOmitCookie,
"hasNoneCookie": hasNoneCookie,
"hasLaxCookie": hasLaxCookie,
"hasStrictCookie": hasStrictCookie,
"csrfCookieSizeInBytes": csrfCookieSizeInBytes,
"maskedCsrfCookieContent": maskedCsrfCookieContent,
}).Errorf("CSRF Forbidden: %s", csrf.FailureReason(r))

// TODO: beautify error page ui
http.Error(w, fmt.Sprintf("%s - %s",
http.StatusText(http.StatusForbidden), csrf.FailureReason(r)),
http.StatusForbidden)
}
2 changes: 2 additions & 0 deletions pkg/auth/webapp/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ var DependencySet = wire.NewSet(
NewSignedUpCookieDef,
wire.Struct(new(ErrorCookie), "*"),

NewCSRFMiddlewareLogger,
wire.Struct(new(CSRFMiddleware), "*"),
wire.Struct(new(CSRFDebugMiddleware), "*"),
wire.Struct(new(SessionMiddleware), "*"),
wire.Bind(new(SessionMiddlewareStore), new(*SessionStoreRedis)),
wire.Bind(new(SessionMiddlewareSessionService), new(*Service2)),
Expand Down
23 changes: 23 additions & 0 deletions pkg/auth/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions pkg/auth/wire_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ func newCSRFMiddleware(p *deps.RequestProvider) httproute.Middleware {
))
}

func newCSRFDebugMiddleware(p *deps.RequestProvider) httproute.Middleware {
panic(wire.Build(
DependencySet,
wire.Bind(new(httproute.Middleware), new(*webapp.CSRFDebugMiddleware)),
))
}

func newAuthEntryPointMiddleware(p *deps.RequestProvider) httproute.Middleware {
panic(wire.Build(
DependencySet,
Expand Down
1 change: 1 addition & 0 deletions pkg/util/sentry/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
)

var HeaderWhiteList = []string{
"Origin",
"Referer",
"User-Agent",
"X-Original-For",
Expand Down

0 comments on commit bd935ac

Please sign in to comment.