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.
This commit is contained in:
66
models/device_auth_session.go
Normal file
66
models/device_auth_session.go
Normal file
@@ -0,0 +1,66 @@
|
||||
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{}) }
|
||||
Reference in New Issue
Block a user