mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
feat(jwt): server-side session table with keyId + obfuscated uid claims
Replace the {user_id, ip} claim pair with {keyId, uid}:
- keyId is a 32-byte random id that points to a row in the new
jwt_sessions table holding the real user id, bound IP, UA hash,
TokenVersion and expiry.
- uid is the user id encoded through pkg/idcodec; mismatch between
claim uid and session.UserID trips WAF block on the caller IP.
- identityHandler now rejects unknown/revoked/expired sessions, IP
drift and stale TokenVersion. Refresh updates session.ExpiresAt.
User.TokenVersion bumps on password change and revokes outstanding
sessions, so a leaked JWT secret alone is no longer enough to forge
a token. JWTSession rows are GC'd every 10 minutes (expired + grace
or revoked >24h). OAuth2 callback shares the same issue path.
Includes regression tests for happy path, mismatched claim uid,
revoked session, TokenVersion bump, IP drift and unknown keyId.
Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
package singleton
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
)
|
||||
|
||||
const (
|
||||
JWTSessionGCSchedule = "@every 10m"
|
||||
JWTSessionRevokedRetention = 24 * time.Hour
|
||||
JWTSessionExpiredGrace = 1 * time.Hour
|
||||
)
|
||||
|
||||
func StartJWTSessionGC() error {
|
||||
if _, err := CronShared.AddFunc(JWTSessionGCSchedule, RunJWTSessionGC); err != nil {
|
||||
return err
|
||||
}
|
||||
RunJWTSessionGC()
|
||||
return nil
|
||||
}
|
||||
|
||||
func RunJWTSessionGC() {
|
||||
if DB == nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
|
||||
if err := DB.
|
||||
Where("expires_at < ?", now.Add(-JWTSessionExpiredGrace)).
|
||||
Delete(&model.JWTSession{}).Error; err != nil {
|
||||
log.Printf("NEZHA>> JWTSession GC delete expired failed: %v", err)
|
||||
}
|
||||
|
||||
if err := DB.
|
||||
Where("revoked_at IS NOT NULL AND revoked_at < ?", now.Add(-JWTSessionRevokedRetention)).
|
||||
Delete(&model.JWTSession{}).Error; err != nil {
|
||||
log.Printf("NEZHA>> JWTSession GC delete revoked failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func RevokeJWTSession(keyID string) error {
|
||||
now := time.Now()
|
||||
return DB.Model(&model.JWTSession{}).
|
||||
Where("key_id = ? AND revoked_at IS NULL", keyID).
|
||||
Update("revoked_at", &now).Error
|
||||
}
|
||||
|
||||
func RevokeJWTSessionsByUser(userID uint64) error {
|
||||
now := time.Now()
|
||||
return DB.Model(&model.JWTSession{}).
|
||||
Where("user_id = ? AND revoked_at IS NULL", userID).
|
||||
Update("revoked_at", &now).Error
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func InitDBFromPath(path string) error {
|
||||
model.Notification{}, model.AlertRule{}, model.Service{}, model.NotificationGroupNotification{},
|
||||
model.Cron{}, model.Transfer{}, model.ServerGroupServer{},
|
||||
model.NAT{}, model.DDNSProfile{}, model.NotificationGroupNotification{},
|
||||
model.WAF{}, model.Oauth2Bind{}, model.ServerTransfer{})
|
||||
model.WAF{}, model.Oauth2Bind{}, model.ServerTransfer{}, model.JWTSession{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user