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}