chore(security): pin JWT algorithm + SameSite and harden OAuth2 state cookie

- Set GinJWTMiddleware.SigningAlgorithm to "HS256" explicitly so a future
  library default change (or an alg:none confusion attempt) cannot weaken
  token validation. This matches the current gin-jwt default, so behaviour
  is unchanged.
- Set CookieSameSite to Lax: same as the modern-browser default, but
  pinned so server-side intent is clear and CSRF on cross-site POST is
  blocked while top-level GET (OAuth callback) still works.
- Move the nz-o2s OAuth2 state cookie into writeOauth2StateCookie and
  set HttpOnly=true. The frontend does not read this cookie, so HttpOnly
  is strictly an XSS-hardening win with no behaviour change.

JWT Cookie HttpOnly/Secure are intentionally left default for now: the
frontend reads \`!!document.cookie\` to display login state and many
deployments terminate TLS at an upstream proxy — flipping those would
require a coordinated frontend change.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
naiba
2026-05-18 15:17:59 +00:00
co-authored by naiba/CloudCode
parent 7c493e8f0b
commit fb120074ac
3 changed files with 76 additions and 9 deletions
+19 -7
View File
@@ -22,18 +22,30 @@ func initParams() *jwt.GinJWTMiddleware {
Key: []byte(singleton.Conf.JWTSecretKey),
CookieName: "nz-jwt",
SendCookie: true,
Timeout: time.Hour * time.Duration(singleton.Conf.JWTTimeout),
MaxRefresh: time.Hour * time.Duration(singleton.Conf.JWTTimeout),
IdentityKey: model.CtxKeyAuthorizedUser,
PayloadFunc: payloadFunc(),
// Pin the signing algorithm so a future library default change (or an
// `alg: none` confusion attempt) cannot weaken token validation.
SigningAlgorithm: "HS256",
// Lax keeps OAuth callback redirects (top-level GET navigations from
// the provider domain) working while blocking cross-site POST CSRF.
// HttpOnly/Secure are intentionally left default: the frontend reads
// `!!document.cookie` for login-state display and many deployments
// terminate TLS at a proxy upstream — both warrant a separate change.
CookieSameSite: http.SameSiteLaxMode,
Timeout: time.Hour * time.Duration(singleton.Conf.JWTTimeout),
MaxRefresh: time.Hour * time.Duration(singleton.Conf.JWTTimeout),
IdentityKey: model.CtxKeyAuthorizedUser,
PayloadFunc: payloadFunc(),
IdentityHandler: identityHandler(),
Authenticator: authenticator(),
Authorizator: authorizator(),
Unauthorized: unauthorized(),
TokenLookup: "header: Authorization, query: token, cookie: nz-jwt",
TokenHeadName: "Bearer",
TimeFunc: time.Now,
// query: token still accepted because the WebSocket browser API
// cannot set Authorization headers; removing it would break the
// /ws/* routes until the frontend migrates to cookie auth.
TokenLookup: "header: Authorization, query: token, cookie: nz-jwt",
TokenHeadName: "Bearer",
TimeFunc: time.Now,
LoginResponse: func(c *gin.Context, code int, token string, expire time.Time) {
c.JSON(http.StatusOK, model.CommonResponse[model.LoginResponse]{