Files
sm-user-plugin/models/device_auth_session.go
Jakub Zych 4e3e5b05b5 feat: device authorization by QR or short code
PHP's device flow on golem15_user_device_auth_sessions (PHP's columns,
the token stored as its SHA-256, plus jwt_jti): POST /devices/initiate
answers a 64-character token, an XXXX-XXXX short code, the configurable
auth_url (golem15.user.device_auth.auth_url) and the expiry; a signed-in
user authorizes it with POST /devices/authorize (short_code or token,
one guarded pending-to-confirmed UPDATE); the device's first poll of
GET /devices/status/{token} after that receives a session JWT through one
guarded confirmed-to-used UPDATE, so exactly one poll gets it. Used and
revoked sessions never turn into expired; each initiate deletes a bounded
batch of stale sessions.
2026-10-26 10:14:48 +01:00

67 lines
2.8 KiB
Go

package models
import "time"
// Device authorization session statuses, PHP's DeviceAuthSession constants.
// pending becomes confirmed when a signed-in user authorizes the device,
// confirmed becomes used on the device's first poll (which receives the
// session token), used becomes revoked when the user revokes the device,
// and pending or confirmed become expired once expires_at passes. used and
// revoked never become expired.
const (
DeviceStatusPending = "pending"
DeviceStatusConfirmed = "confirmed"
DeviceStatusUsed = "used"
DeviceStatusExpired = "expired"
DeviceStatusRevoked = "revoked"
)
// DeviceAuthSession is one QR or short-code device authorization
// (golem15_user_device_auth_sessions). TokenHash is the SHA-256 hex of the
// device token (column token, PHP's name); the token itself is never
// stored. JWTJTI is the jti of the session token the device received,
// followed through refresh.
type DeviceAuthSession struct {
ID uint `gorm:"column:id;primaryKey"`
TokenHash string `gorm:"column:token" json:"-"`
ShortCode *string `gorm:"column:short_code" json:"-"`
UserID *uint `gorm:"column:user_id"`
Status string `gorm:"column:status"`
ExpiresAt *time.Time `gorm:"column:expires_at"`
DeviceIP *string `gorm:"column:device_ip"`
DeviceUserAgent *string `gorm:"column:device_user_agent"`
DeviceName *string `gorm:"column:device_name"`
AuthIP *string `gorm:"column:auth_ip"`
ConfirmedAt *time.Time `gorm:"column:confirmed_at"`
UsedAt *time.Time `gorm:"column:used_at"`
SessionID *string `gorm:"column:session_id"`
LastActivityAt *time.Time `gorm:"column:last_activity_at"`
JWTJTI *string `gorm:"column:jwt_jti" json:"-"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
func (DeviceAuthSession) TableName() string { return "golem15_user_device_auth_sessions" }
func (DeviceAuthSession) Fillable() []string {
return []string{
"token", "short_code", "user_id", "status", "expires_at", "device_ip",
"device_user_agent", "device_name", "auth_ip", "confirmed_at", "used_at",
"session_id", "last_activity_at", "jwt_jti",
}
}
func (DeviceAuthSession) Hidden() []string { return []string{"token", "short_code", "jwt_jti"} }
// ExpiredAt reports whether a pending or confirmed session is past
// expires_at at now. A used, revoked or expired session, and one without
// expires_at, is never newly expired.
func (s DeviceAuthSession) ExpiredAt(now time.Time) bool {
if s.Status != DeviceStatusPending && s.Status != DeviceStatusConfirmed {
return false
}
return s.ExpiresAt != nil && !s.ExpiresAt.After(now)
}
func init() { Register(DeviceAuthSession{}) }