forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package oauth 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "time" 8 9 "github.com/bluesky-social/indigo/atproto/auth/oauth" 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 "github.com/redis/go-redis/v9" 12) 13 14// redis-backed implementation of ClientAuthStore. 15type RedisStore struct { 16 client *redis.Client 17 SessionTTL time.Duration 18 AuthRequestTTL time.Duration 19} 20 21var _ oauth.ClientAuthStore = &RedisStore{} 22 23func NewRedisStore(redisURL string) (*RedisStore, error) { 24 opts, err := redis.ParseURL(redisURL) 25 if err != nil { 26 return nil, fmt.Errorf("failed to parse redis URL: %w", err) 27 } 28 29 client := redis.NewClient(opts) 30 31 // test the connection 32 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 33 defer cancel() 34 35 if err := client.Ping(ctx).Err(); err != nil { 36 return nil, fmt.Errorf("failed to connect to redis: %w", err) 37 } 38 39 return &RedisStore{ 40 client: client, 41 SessionTTL: 30 * 24 * time.Hour, // 30 days 42 AuthRequestTTL: 10 * time.Minute, // 10 minutes 43 }, nil 44} 45 46func (r *RedisStore) Close() error { 47 return r.client.Close() 48} 49 50func sessionKey(did syntax.DID, sessionID string) string { 51 return fmt.Sprintf("oauth:session:%s:%s", did, sessionID) 52} 53 54func authRequestKey(state string) string { 55 return fmt.Sprintf("oauth:auth_request:%s", state) 56} 57 58func (r *RedisStore) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) { 59 key := sessionKey(did, sessionID) 60 data, err := r.client.Get(ctx, key).Bytes() 61 if err == redis.Nil { 62 return nil, fmt.Errorf("session not found: %s", did) 63 } 64 if err != nil { 65 return nil, fmt.Errorf("failed to get session: %w", err) 66 } 67 68 var sess oauth.ClientSessionData 69 if err := json.Unmarshal(data, &sess); err != nil { 70 return nil, fmt.Errorf("failed to unmarshal session: %w", err) 71 } 72 73 return &sess, nil 74} 75 76func (r *RedisStore) SaveSession(ctx context.Context, sess oauth.ClientSessionData) error { 77 key := sessionKey(sess.AccountDID, sess.SessionID) 78 79 data, err := json.Marshal(sess) 80 if err != nil { 81 return fmt.Errorf("failed to marshal session: %w", err) 82 } 83 84 if err := r.client.Set(ctx, key, data, r.SessionTTL).Err(); err != nil { 85 return fmt.Errorf("failed to save session: %w", err) 86 } 87 88 return nil 89} 90 91func (r *RedisStore) DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error { 92 key := sessionKey(did, sessionID) 93 if err := r.client.Del(ctx, key).Err(); err != nil { 94 return fmt.Errorf("failed to delete session: %w", err) 95 } 96 return nil 97} 98 99func (r *RedisStore) GetAuthRequestInfo(ctx context.Context, state string) (*oauth.AuthRequestData, error) { 100 key := authRequestKey(state) 101 data, err := r.client.Get(ctx, key).Bytes() 102 if err == redis.Nil { 103 return nil, fmt.Errorf("request info not found: %s", state) 104 } 105 if err != nil { 106 return nil, fmt.Errorf("failed to get auth request: %w", err) 107 } 108 109 var req oauth.AuthRequestData 110 if err := json.Unmarshal(data, &req); err != nil { 111 return nil, fmt.Errorf("failed to unmarshal auth request: %w", err) 112 } 113 114 return &req, nil 115} 116 117func (r *RedisStore) SaveAuthRequestInfo(ctx context.Context, info oauth.AuthRequestData) error { 118 key := authRequestKey(info.State) 119 120 // check if already exists (to match MemStore behavior) 121 exists, err := r.client.Exists(ctx, key).Result() 122 if err != nil { 123 return fmt.Errorf("failed to check auth request existence: %w", err) 124 } 125 if exists > 0 { 126 return fmt.Errorf("auth request already saved for state %s", info.State) 127 } 128 129 data, err := json.Marshal(info) 130 if err != nil { 131 return fmt.Errorf("failed to marshal auth request: %w", err) 132 } 133 134 if err := r.client.Set(ctx, key, data, r.AuthRequestTTL).Err(); err != nil { 135 return fmt.Errorf("failed to save auth request: %w", err) 136 } 137 138 return nil 139} 140 141func (r *RedisStore) DeleteAuthRequestInfo(ctx context.Context, state string) error { 142 key := authRequestKey(state) 143 if err := r.client.Del(ctx, key).Err(); err != nil { 144 return fmt.Errorf("failed to delete auth request: %w", err) 145 } 146 return nil 147}