feat(auth): add device session tracking with configurable limits
Track authenticated sessions per device (parsing user-agent for desktop vs mobile via mileusna/useragent), enforce configurable concurrent device limits (Auth.DeviceLimits), and surface device status and "remove other devices" management in the account dialog. Adds device/session context to the auth module stores and exposes limits through the API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,67 +13,69 @@ import (
|
||||
)
|
||||
|
||||
type tokenPayload struct {
|
||||
Subject string `json:"sub"`
|
||||
Type string `json:"typ,omitempty"`
|
||||
OpenID string `json:"openid,omitempty"`
|
||||
UnionID string `json:"unionid,omitempty"`
|
||||
Issued int64 `json:"iat"`
|
||||
Expires int64 `json:"exp"`
|
||||
Subject string `json:"sub"`
|
||||
Type string `json:"typ,omitempty"`
|
||||
SessionID string `json:"sid,omitempty"`
|
||||
OpenID string `json:"openid,omitempty"`
|
||||
UnionID string `json:"unionid,omitempty"`
|
||||
Issued int64 `json:"iat"`
|
||||
Expires int64 `json:"exp"`
|
||||
}
|
||||
|
||||
func signToken(userID string, secret string, ttl time.Duration, now time.Time) (string, int64, error) {
|
||||
if ttl <= 0 {
|
||||
ttl = 7 * 24 * time.Hour
|
||||
}
|
||||
return signTypedToken(userID, "access", secret, ttl, now)
|
||||
return signTypedToken(userID, "access", "", secret, ttl, now)
|
||||
}
|
||||
|
||||
func signRefreshToken(userID string, secret string, ttl time.Duration, now time.Time) (string, int64, error) {
|
||||
if ttl <= 0 {
|
||||
ttl = 30 * 24 * time.Hour
|
||||
}
|
||||
return signTypedToken(userID, "refresh", secret, ttl, now)
|
||||
return signTypedToken(userID, "refresh", "", secret, ttl, now)
|
||||
}
|
||||
|
||||
func signTypedToken(userID string, tokenType string, secret string, ttl time.Duration, now time.Time) (string, int64, error) {
|
||||
func signSessionToken(userID string, sessionID string, secret string, ttl time.Duration, now time.Time) (string, int64, error) {
|
||||
if ttl <= 0 {
|
||||
ttl = 7 * 24 * time.Hour
|
||||
}
|
||||
return signTypedToken(userID, "access", sessionID, secret, ttl, now)
|
||||
}
|
||||
|
||||
func signSessionRefreshToken(userID string, sessionID string, secret string, ttl time.Duration, now time.Time) (string, int64, error) {
|
||||
if ttl <= 0 {
|
||||
ttl = 30 * 24 * time.Hour
|
||||
}
|
||||
return signTypedToken(userID, "refresh", sessionID, secret, ttl, now)
|
||||
}
|
||||
|
||||
func signTypedToken(userID string, tokenType string, sessionID string, secret string, ttl time.Duration, now time.Time) (string, int64, error) {
|
||||
payload := tokenPayload{
|
||||
Subject: userID,
|
||||
Type: tokenType,
|
||||
Issued: now.Unix(),
|
||||
Expires: now.Add(ttl).Unix(),
|
||||
Subject: userID,
|
||||
Type: tokenType,
|
||||
SessionID: sessionID,
|
||||
Issued: now.Unix(),
|
||||
Expires: now.Add(ttl).Unix(),
|
||||
}
|
||||
token, err := signPayload(payload, secret)
|
||||
return token, int64(ttl.Seconds()), err
|
||||
}
|
||||
|
||||
func parseToken(ctx context.Context, token string, secret string, now time.Time) (string, bool) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return "", false
|
||||
}
|
||||
parts := strings.Split(strings.TrimSpace(token), ".")
|
||||
if len(parts) != 2 {
|
||||
return "", false
|
||||
}
|
||||
expected := signBytes([]byte(parts[0]), secret)
|
||||
actual, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil || !hmac.Equal(expected, actual) {
|
||||
return "", false
|
||||
}
|
||||
data, err := base64.RawURLEncoding.DecodeString(parts[0])
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
var payload tokenPayload
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return "", false
|
||||
}
|
||||
if payload.Subject == "" || payload.Expires <= now.Unix() {
|
||||
return "", false
|
||||
payload, ok := parseAccessTokenPayload(ctx, token, secret, now)
|
||||
return payload.Subject, ok
|
||||
}
|
||||
|
||||
func parseAccessTokenPayload(ctx context.Context, token string, secret string, now time.Time) (tokenPayload, bool) {
|
||||
payload, ok := parsePayload(ctx, token, secret)
|
||||
if !ok || payload.Subject == "" || payload.Expires <= now.Unix() {
|
||||
return tokenPayload{}, false
|
||||
}
|
||||
if payload.Type != "" && payload.Type != "access" {
|
||||
return "", false
|
||||
return tokenPayload{}, false
|
||||
}
|
||||
return payload.Subject, true
|
||||
return payload, true
|
||||
}
|
||||
|
||||
func signWechatBindingToken(identity WechatIdentity, secret string, ttl time.Duration, now time.Time) (string, int64, error) {
|
||||
|
||||
Reference in New Issue
Block a user