94 lines
2.1 KiB
Go
94 lines
2.1 KiB
Go
|
|
package auth
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/redis/go-redis/v9"
|
||
|
|
)
|
||
|
|
|
||
|
|
var (
|
||
|
|
ErrRefreshSessionNotFound = errors.New("refresh session not found")
|
||
|
|
ErrRefreshTokenMismatch = errors.New("refresh token mismatch")
|
||
|
|
)
|
||
|
|
|
||
|
|
var rotateRefreshScript = redis.NewScript(`
|
||
|
|
local current = redis.call("GET", KEYS[1])
|
||
|
|
if not current then
|
||
|
|
return 0
|
||
|
|
end
|
||
|
|
if current ~= ARGV[1] then
|
||
|
|
return -1
|
||
|
|
end
|
||
|
|
redis.call("DEL", KEYS[1])
|
||
|
|
redis.call("PSETEX", KEYS[2], ARGV[3], ARGV[2])
|
||
|
|
return 1
|
||
|
|
`)
|
||
|
|
|
||
|
|
type SessionStore struct {
|
||
|
|
rdb *redis.Client
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewSessionStore(rdb *redis.Client) *SessionStore {
|
||
|
|
return &SessionStore{rdb: rdb}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *SessionStore) SaveRefresh(ctx context.Context, jti string, tokenHash string, ttl time.Duration) error {
|
||
|
|
key := fmt.Sprintf("refresh:%s", jti)
|
||
|
|
return s.rdb.Set(ctx, key, tokenHash, ttl).Err()
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *SessionStore) GetRefresh(ctx context.Context, jti string) (string, error) {
|
||
|
|
key := fmt.Sprintf("refresh:%s", jti)
|
||
|
|
return s.rdb.Get(ctx, key).Result()
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *SessionStore) DeleteRefresh(ctx context.Context, jti string) error {
|
||
|
|
key := fmt.Sprintf("refresh:%s", jti)
|
||
|
|
return s.rdb.Del(ctx, key).Err()
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *SessionStore) RotateRefresh(ctx context.Context, oldJTI, oldHash, newJTI, newHash string, ttl time.Duration) error {
|
||
|
|
oldKey := fmt.Sprintf("refresh:%s", oldJTI)
|
||
|
|
newKey := fmt.Sprintf("refresh:%s", newJTI)
|
||
|
|
|
||
|
|
result, err := rotateRefreshScript.Run(
|
||
|
|
ctx,
|
||
|
|
s.rdb,
|
||
|
|
[]string{oldKey, newKey},
|
||
|
|
oldHash,
|
||
|
|
newHash,
|
||
|
|
ttl.Milliseconds(),
|
||
|
|
).Int()
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
switch result {
|
||
|
|
case 1:
|
||
|
|
return nil
|
||
|
|
case 0:
|
||
|
|
return ErrRefreshSessionNotFound
|
||
|
|
case -1:
|
||
|
|
return ErrRefreshTokenMismatch
|
||
|
|
default:
|
||
|
|
return fmt.Errorf("unexpected rotate refresh result: %d", result)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *SessionStore) Blacklist(ctx context.Context, accessJTI string, ttl time.Duration) error {
|
||
|
|
key := fmt.Sprintf("blacklist:%s", accessJTI)
|
||
|
|
return s.rdb.Set(ctx, key, "1", ttl).Err()
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *SessionStore) IsBlacklisted(ctx context.Context, accessJTI string) (bool, error) {
|
||
|
|
key := fmt.Sprintf("blacklist:%s", accessJTI)
|
||
|
|
n, err := s.rdb.Exists(ctx, key).Result()
|
||
|
|
if err != nil {
|
||
|
|
return false, err
|
||
|
|
}
|
||
|
|
return n > 0, nil
|
||
|
|
}
|