mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 04:40:05 +01:00
feat: sdk migration in progress
This commit is contained in:
105
internal/auth/refresh.go
Normal file
105
internal/auth/refresh.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// random opaque token (returned to client once)
|
||||
func generateOpaqueToken(n int) (string, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
type RefreshPair struct {
|
||||
Plain string
|
||||
Record models.RefreshToken
|
||||
}
|
||||
|
||||
// Issue a new refresh token (new family if familyID == nil)
|
||||
func IssueRefreshToken(db *gorm.DB, userID uuid.UUID, ttl time.Duration, familyID *uuid.UUID) (RefreshPair, error) {
|
||||
plain, err := generateOpaqueToken(32)
|
||||
if err != nil {
|
||||
return RefreshPair{}, err
|
||||
}
|
||||
hash, err := HashSecretArgon2id(plain)
|
||||
if err != nil {
|
||||
return RefreshPair{}, err
|
||||
}
|
||||
|
||||
fid := uuid.New()
|
||||
if familyID != nil {
|
||||
fid = *familyID
|
||||
}
|
||||
|
||||
rec := models.RefreshToken{
|
||||
UserID: userID,
|
||||
FamilyID: fid,
|
||||
TokenHash: hash,
|
||||
ExpiresAt: time.Now().Add(ttl),
|
||||
}
|
||||
if err := db.Create(&rec).Error; err != nil {
|
||||
return RefreshPair{}, err
|
||||
}
|
||||
return RefreshPair{Plain: plain, Record: rec}, nil
|
||||
}
|
||||
|
||||
// ValidateRefreshToken refresh token; returns record if valid & not revoked/expired
|
||||
func ValidateRefreshToken(db *gorm.DB, plain string) (*models.RefreshToken, error) {
|
||||
if plain == "" {
|
||||
return nil, errors.New("empty")
|
||||
}
|
||||
// var rec models.RefreshToken
|
||||
// We can't query by hash w/ Argon; scan candidates by expiry window. Keep small TTL (e.g. 30d).
|
||||
if err := db.Where("expires_at > ? AND revoked_at IS NULL", time.Now()).
|
||||
Find(&[]models.RefreshToken{}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Better: add a prefix column to narrow scan; omitted for brevity.
|
||||
|
||||
// Pragmatic approach: single SELECT per token:
|
||||
// Add a TokenHashSHA256 column for deterministic lookup if you want O(1). (Optional)
|
||||
|
||||
// Minimal: iterate limited set; for simplicity we fetch by created window:
|
||||
var recs []models.RefreshToken
|
||||
if err := db.Where("expires_at > ? AND revoked_at IS NULL", time.Now()).
|
||||
Order("created_at desc").Limit(500).Find(&recs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range recs {
|
||||
ok, _ := VerifySecretArgon2id(r.TokenHash, plain)
|
||||
if ok {
|
||||
return &r, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("invalid")
|
||||
}
|
||||
|
||||
// RevokeFamily revokes all tokens in a family (logout everywhere)
|
||||
func RevokeFamily(db *gorm.DB, familyID uuid.UUID) error {
|
||||
now := time.Now()
|
||||
return db.Model(&models.RefreshToken{}).
|
||||
Where("family_id = ? AND revoked_at IS NULL", familyID).
|
||||
Update("revoked_at", &now).Error
|
||||
}
|
||||
|
||||
// RotateRefreshToken replaces one token with a fresh one within the same family
|
||||
func RotateRefreshToken(db *gorm.DB, used *models.RefreshToken, ttl time.Duration) (RefreshPair, error) {
|
||||
// revoke the used token (one-time use)
|
||||
now := time.Now()
|
||||
if err := db.Model(&models.RefreshToken{}).
|
||||
Where("id = ? AND revoked_at IS NULL", used.ID).
|
||||
Update("revoked_at", &now).Error; err != nil {
|
||||
return RefreshPair{}, err
|
||||
}
|
||||
return IssueRefreshToken(db, used.UserID, ttl, &used.FamilyID)
|
||||
}
|
||||
Reference in New Issue
Block a user