Files
nezha_domains/model/user.go
T
naibaandcloudcode 7b54a2d5ea 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>
2026-05-26 03:51:05 +00:00

67 lines
1.3 KiB
Go

package model
import (
"time"
"github.com/gorilla/websocket"
"github.com/nezhahq/nezha/pkg/utils"
"gorm.io/gorm"
)
type Role uint8
func (r Role) IsAdmin() bool {
return r == RoleAdmin
}
const (
RoleAdmin Role = iota
RoleMember
)
const DefaultAgentSecretLength = 32
type User struct {
Common
Username string `json:"username,omitempty" gorm:"uniqueIndex"`
Password string `json:"password,omitempty" gorm:"type:char(72)"`
Role Role `json:"role,omitempty"`
AgentSecret string `json:"agent_secret,omitempty" gorm:"type:char(32)"`
RejectPassword bool `json:"reject_password,omitempty"`
TokenVersion uint64 `json:"-" gorm:"not null;default:0"`
}
type UserInfo struct {
Role Role
Username string
AgentSecret string
}
func (u *User) BeforeSave(tx *gorm.DB) error {
if u.AgentSecret != "" {
return nil
}
key, err := utils.GenerateRandomString(DefaultAgentSecretLength)
if err != nil {
return err
}
u.AgentSecret = key
return nil
}
type Profile struct {
User
LoginIP string `json:"login_ip,omitempty"`
Oauth2Bind map[string]string `json:"oauth2_bind,omitempty"`
}
type OnlineUser struct {
UserID uint64 `json:"user_id,omitempty"`
ConnectedAt time.Time `json:"connected_at,omitempty"`
IP string `json:"ip,omitempty"`
Conn *websocket.Conn `json:"-"`
}