feat: opt-in golem15.user.jwt.token_cookie sets the HttpOnly session cookie on /login, /register and /activate-by-code instead of returning the token
Default off, so body-token clients are unchanged. When on, the cookie carries the auth.cookie attributes via bouncer.LoadCookieConfig and the body keeps user (and message); SameSite stays Lax, with no CSRF token; personal API tokens are untouched.
This commit is contained in:
@@ -56,13 +56,21 @@ const invalidLoginBody = "Invalid email or password"
|
||||
// stays deactivated and gets no mail. With the key off such an account signs
|
||||
// in as before. Activation gates login, deactivation does not. The throttle
|
||||
// counts failed attempts per user and client address, which is
|
||||
// surf.ClientIP with http.trusted_proxies, as for /register.
|
||||
// surf.ClientIP with http.trusted_proxies, as for /register. With
|
||||
// golem15.user.jwt.token_cookie on, the token goes into the auth.cookie
|
||||
// session cookie instead of the body.
|
||||
func Login(app *backpack.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
gdb, secret, ok := sessionDeps(w, app)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cc, err := sessionCookie(app)
|
||||
if err != nil {
|
||||
pluginLog.Error("golem15.user: token cookie config", "err", err)
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
fields, err := readFields(r)
|
||||
if err != nil {
|
||||
writeOpaque500(w)
|
||||
@@ -136,7 +144,7 @@ func Login(app *backpack.App) http.HandlerFunc {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"token": token, "user": payload})
|
||||
writeSession(w, app, cc, token, map[string]any{"user": payload})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,12 +497,20 @@ func Activate(app *backpack.App) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// ActivateByCode activates from a public "{id}!{code}" value and mints a token.
|
||||
// With golem15.user.jwt.token_cookie on, the token goes into the auth.cookie
|
||||
// session cookie instead of the body.
|
||||
func ActivateByCode(app *backpack.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
gdb, secret, ok := sessionDeps(w, app)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cc, err := sessionCookie(app)
|
||||
if err != nil {
|
||||
pluginLog.Error("golem15.user: token cookie config", "err", err)
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
fields, err := readFields(r)
|
||||
if err != nil {
|
||||
writeOpaque500(w)
|
||||
@@ -540,11 +556,7 @@ func ActivateByCode(app *backpack.App) http.HandlerFunc {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"message": "Account activated",
|
||||
"token": token,
|
||||
"user": payload,
|
||||
})
|
||||
writeSession(w, app, cc, token, map[string]any{"message": "Account activated", "user": payload})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -760,12 +772,20 @@ func MarketingConsent(app *backpack.App) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// Register creates an account and follows activate_mode: auto, user, or admin.
|
||||
// With golem15.user.jwt.token_cookie on, the token of an account it signs in
|
||||
// goes into the auth.cookie session cookie instead of the body.
|
||||
func Register(app *backpack.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
gdb, _, ok := sessionDeps(w, app)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cc, err := sessionCookie(app)
|
||||
if err != nil {
|
||||
pluginLog.Error("golem15.user: token cookie config", "err", err)
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
allow, throttle, mode, require := registrationSettings(app)
|
||||
ip := surf.ClientIP(r, surf.TrustedProxies(appConfig(app)))
|
||||
if !allow {
|
||||
@@ -829,7 +849,7 @@ func Register(app *backpack.App) http.HandlerFunc {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"token": token, "user": payload})
|
||||
writeSession(w, app, cc, token, map[string]any{"user": payload})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{})
|
||||
@@ -1212,6 +1232,22 @@ func tokenCookieConfig(app *backpack.App) (bouncer.CookieConfig, error) {
|
||||
return bouncer.LoadCookieConfig(app.Config)
|
||||
}
|
||||
|
||||
// sessionCookie is the golem15.sociallogin token_cookie twin: with
|
||||
// golem15.user.jwt.token_cookie on it returns the auth.cookie config the
|
||||
// session routes write their token cookie with, or the error that block
|
||||
// gives. A nil config means the token goes into the body; auth.cookie is
|
||||
// then not read.
|
||||
func sessionCookie(app *backpack.App) (*bouncer.CookieConfig, error) {
|
||||
if app == nil || app.Config == nil || !app.Config.Bool("golem15.user.jwt.token_cookie") {
|
||||
return nil, nil
|
||||
}
|
||||
cc, err := tokenCookieConfig(app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cc, nil
|
||||
}
|
||||
|
||||
// writeSession answers 200 with a session token, the user plugin's twin of
|
||||
// golem15.sociallogin's writeSession. With cc nil the token goes into
|
||||
// body["token"], the key set the session routes have always answered. With
|
||||
|
||||
Reference in New Issue
Block a user