1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
package pds
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/oauth"
)
const schemaOAuthSessions = `CREATE TABLE IF NOT EXISTS pds_oauth_sessions (
id TEXT PRIMARY KEY,
did TEXT NOT NULL,
handle TEXT NOT NULL,
client_id TEXT NOT NULL,
client_auth_method TEXT NOT NULL,
client_key_jkt TEXT NOT NULL,
scope TEXT NOT NULL,
jkt TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
revoked_at INTEGER
)`
const schemaOAuthSessionsDID = `CREATE INDEX IF NOT EXISTS pds_oauth_sessions_did ON pds_oauth_sessions (did)`
const schemaOAuthCodes = `CREATE TABLE IF NOT EXISTS pds_oauth_codes (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
client_id TEXT NOT NULL,
client_auth_method TEXT NOT NULL,
client_key_jkt TEXT NOT NULL,
did TEXT NOT NULL,
handle TEXT NOT NULL,
scope TEXT NOT NULL,
redirect_uri TEXT NOT NULL,
code_challenge TEXT NOT NULL,
code_challenge_method TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
used_at INTEGER
)`
const schemaOAuthPARs = `CREATE TABLE IF NOT EXISTS pds_oauth_pars (
request_uri TEXT PRIMARY KEY,
client_id TEXT NOT NULL,
client_auth_method TEXT NOT NULL,
client_key_jkt TEXT NOT NULL,
redirect_uri TEXT NOT NULL,
scope TEXT NOT NULL,
state TEXT NOT NULL,
code_challenge TEXT NOT NULL,
code_challenge_method TEXT NOT NULL,
response_type TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL
)`
const schemaOAuthTokens = `CREATE TABLE IF NOT EXISTS pds_oauth_tokens (
jti TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
kind TEXT NOT NULL,
issued_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
revoked_at INTEGER
)`
const schemaOAuthTokensSession = `CREATE INDEX IF NOT EXISTS pds_oauth_tokens_session ON pds_oauth_tokens (session_id)`
var (
_ oauth.AccountStore = (*Store)(nil)
_ oauth.SessionStore = (*Store)(nil)
_ oauth.AuthorizationCodeStore = (*Store)(nil)
_ oauth.PARStore = (*Store)(nil)
_ oauth.TokenStore = (*Store)(nil)
)
func (s *Store) CreateSession(ctx context.Context, sess oauth.Session) error {
_, err := s.db.ExecContext(ctx,
`INSERT INTO pds_oauth_sessions
(id, did, handle, client_id, client_auth_method, client_key_jkt, scope, jkt, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
sess.ID, sess.Account.DID.String(), sess.Account.Handle,
sess.ClientID, sess.ClientAuthMethod, sess.ClientKeyJKT, sess.Scope, sess.JKT,
unixMicro(sess.CreatedAt), unixMicro(sess.ExpiresAt))
if err != nil {
return fmt.Errorf("pds: creating oauth session: %w", err)
}
return nil
}
func (s *Store) GetSession(ctx context.Context, id string) (*oauth.Session, error) {
var didRaw, handle, clientID, authMethod, clientJKT, scope, jkt string
var created, expires int64
var revoked sql.NullInt64
err := s.db.QueryRowContext(ctx,
`SELECT did, handle, client_id, client_auth_method, client_key_jkt, scope, jkt, created_at, expires_at, revoked_at
FROM pds_oauth_sessions WHERE id = ?`, id).
Scan(&didRaw, &handle, &clientID, &authMethod, &clientJKT, &scope, &jkt, &created, &expires, &revoked)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("pds: loading oauth session: %w", err)
}
did, err := syntax.ParseDID(didRaw)
if err != nil {
return nil, fmt.Errorf("pds: stored oauth session did: %w", err)
}
return &oauth.Session{
ID: id,
Account: oauth.Account{DID: did, Handle: handle},
ClientID: clientID,
ClientAuthMethod: authMethod,
ClientKeyJKT: clientJKT,
Scope: scope,
JKT: jkt,
CreatedAt: fromUnixMicro(created),
ExpiresAt: fromUnixMicro(expires),
RevokedAt: scanTimePtr(revoked),
}, nil
}
func (s *Store) ExtendSession(ctx context.Context, id string, expiresAt time.Time) error {
res, err := s.db.ExecContext(ctx,
`UPDATE pds_oauth_sessions SET expires_at = ? WHERE id = ? AND revoked_at IS NULL`,
unixMicro(expiresAt), id)
if err != nil {
return fmt.Errorf("pds: extending oauth session: %w", err)
}
n, err := res.RowsAffected()
if err != nil {
return err
}
if n == 0 {
return oauth.ErrSessionNotFound
}
return nil
}
func (s *Store) RevokeSession(ctx context.Context, id string, at time.Time) error {
_, err := s.db.ExecContext(ctx,
`UPDATE pds_oauth_sessions SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL`,
unixMicro(at), id)
if err != nil {
return fmt.Errorf("pds: revoking oauth session: %w", err)
}
return nil
}
func (s *Store) CreateCode(ctx context.Context, code oauth.AuthorizationCode) error {
_, err := s.db.ExecContext(ctx,
`INSERT INTO pds_oauth_codes
(id, session_id, client_id, client_auth_method, client_key_jkt, did, handle, scope, redirect_uri,
code_challenge, code_challenge_method, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
code.ID, code.SessionID, code.ClientID, code.ClientAuthMethod, code.ClientKeyJKT,
code.Account.DID.String(), code.Account.Handle, code.Scope, code.RedirectURI,
code.CodeChallenge, code.CodeChallengeMethod, unixMicro(code.CreatedAt), unixMicro(code.ExpiresAt))
if err != nil {
return fmt.Errorf("pds: creating oauth code: %w", err)
}
return nil
}
func (s *Store) ConsumeCode(ctx context.Context, id string, at time.Time) (*oauth.AuthorizationCode, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("pds: consuming oauth code: %w", err)
}
defer tx.Rollback()
var code oauth.AuthorizationCode
var didRaw string
var created, expires int64
var used sql.NullInt64
err = tx.QueryRowContext(ctx,
`SELECT session_id, client_id, client_auth_method, client_key_jkt, did, handle, scope, redirect_uri,
code_challenge, code_challenge_method, created_at, expires_at, used_at
FROM pds_oauth_codes WHERE id = ?`, id).
Scan(&code.SessionID, &code.ClientID, &code.ClientAuthMethod, &code.ClientKeyJKT, &didRaw, &code.Account.Handle,
&code.Scope, &code.RedirectURI, &code.CodeChallenge, &code.CodeChallengeMethod, &created, &expires, &used)
if errors.Is(err, sql.ErrNoRows) {
return nil, oauth.ErrCodeNotFound
}
if err != nil {
return nil, fmt.Errorf("pds: loading oauth code: %w", err)
}
code.ID = id
did, err := syntax.ParseDID(didRaw)
if err != nil {
return nil, fmt.Errorf("pds: stored oauth code did: %w", err)
}
code.Account.DID = did
code.CreatedAt = fromUnixMicro(created)
code.ExpiresAt = fromUnixMicro(expires)
code.UsedAt = scanTimePtr(used)
if code.UsedAt != nil {
return &code, oauth.ErrCodeUsed
}
if !code.ExpiresAt.After(at) {
return &code, oauth.ErrCodeExpired
}
if _, err := tx.ExecContext(ctx, `UPDATE pds_oauth_codes SET used_at = ? WHERE id = ?`, unixMicro(at), id); err != nil {
return nil, fmt.Errorf("pds: consuming oauth code: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("pds: consuming oauth code: %w", err)
}
usedAt := at.UTC()
code.UsedAt = &usedAt
return &code, nil
}
func (s *Store) StorePAR(ctx context.Context, req oauth.PARRequest) error {
_, err := s.db.ExecContext(ctx,
`INSERT OR REPLACE INTO pds_oauth_pars
(request_uri, client_id, client_auth_method, client_key_jkt, redirect_uri, scope, state,
code_challenge, code_challenge_method, response_type, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
req.RequestURI, req.ClientID, req.ClientAuthMethod, req.ClientKeyJKT, req.RedirectURI,
req.Scope, req.State, req.CodeChallenge, req.CodeChallengeMethod, req.ResponseType,
unixMicro(req.CreatedAt), unixMicro(req.ExpiresAt))
if err != nil {
return fmt.Errorf("pds: storing par: %w", err)
}
return nil
}
func (s *Store) ConsumePAR(ctx context.Context, requestURI string, at time.Time) (*oauth.PARRequest, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("pds: consuming par: %w", err)
}
defer tx.Rollback()
var req oauth.PARRequest
var created, expires int64
err = tx.QueryRowContext(ctx,
`SELECT client_id, client_auth_method, client_key_jkt, redirect_uri, scope, state,
code_challenge, code_challenge_method, response_type, created_at, expires_at
FROM pds_oauth_pars WHERE request_uri = ?`, requestURI).
Scan(&req.ClientID, &req.ClientAuthMethod, &req.ClientKeyJKT, &req.RedirectURI, &req.Scope, &req.State,
&req.CodeChallenge, &req.CodeChallengeMethod, &req.ResponseType, &created, &expires)
if errors.Is(err, sql.ErrNoRows) {
return nil, oauth.ErrPARNotFound
}
if err != nil {
return nil, fmt.Errorf("pds: loading par: %w", err)
}
req.RequestURI = requestURI
req.CreatedAt = fromUnixMicro(created)
req.ExpiresAt = fromUnixMicro(expires)
if _, err := tx.ExecContext(ctx, `DELETE FROM pds_oauth_pars WHERE request_uri = ?`, requestURI); err != nil {
return nil, fmt.Errorf("pds: consuming par: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("pds: consuming par: %w", err)
}
if !req.ExpiresAt.After(at) {
return nil, oauth.ErrPARExpired
}
return &req, nil
}
func (s *Store) CreateToken(ctx context.Context, token oauth.TokenRecord) error {
_, err := s.db.ExecContext(ctx,
`INSERT INTO pds_oauth_tokens (jti, session_id, kind, issued_at, expires_at)
VALUES (?, ?, ?, ?, ?)`,
token.JTI, token.SessionID, string(token.Kind), unixMicro(token.IssuedAt), unixMicro(token.ExpiresAt))
if err != nil {
return fmt.Errorf("pds: creating token: %w", err)
}
return nil
}
func (s *Store) GetToken(ctx context.Context, jti string) (*oauth.TokenRecord, error) {
var sessionID, kind string
var issued, expires int64
var revoked sql.NullInt64
err := s.db.QueryRowContext(ctx,
`SELECT session_id, kind, issued_at, expires_at, revoked_at FROM pds_oauth_tokens WHERE jti = ?`, jti).
Scan(&sessionID, &kind, &issued, &expires, &revoked)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("pds: loading token: %w", err)
}
return &oauth.TokenRecord{
JTI: jti,
SessionID: sessionID,
Kind: oauth.TokenKind(kind),
IssuedAt: fromUnixMicro(issued),
ExpiresAt: fromUnixMicro(expires),
RevokedAt: scanTimePtr(revoked),
}, nil
}
func (s *Store) RevokeToken(ctx context.Context, jti string, at time.Time) error {
_, err := s.db.ExecContext(ctx,
`UPDATE pds_oauth_tokens SET revoked_at = ? WHERE jti = ? AND revoked_at IS NULL`,
unixMicro(at), jti)
if err != nil {
return fmt.Errorf("pds: revoking token: %w", err)
}
return nil
}
func (s *Store) RevokeSessionTokens(ctx context.Context, sessionID string, at time.Time) error {
_, err := s.db.ExecContext(ctx,
`UPDATE pds_oauth_tokens SET revoked_at = ? WHERE session_id = ? AND revoked_at IS NULL`,
unixMicro(at), sessionID)
if err != nil {
return fmt.Errorf("pds: revoking session tokens: %w", err)
}
return nil
}
func (s *Store) RotateRefreshToken(ctx context.Context, oldJTI string, now time.Time, newTokens []oauth.TokenRecord) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("pds: rotating refresh token: %w", err)
}
defer tx.Rollback()
var kind string
var revoked sql.NullInt64
err = tx.QueryRowContext(ctx,
`SELECT kind, revoked_at FROM pds_oauth_tokens WHERE jti = ?`, oldJTI).Scan(&kind, &revoked)
if errors.Is(err, sql.ErrNoRows) {
return oauth.ErrTokenNotFound
}
if err != nil {
return fmt.Errorf("pds: rotating refresh token: %w", err)
}
if oauth.TokenKind(kind) != oauth.TokenRefresh || revoked.Valid {
return oauth.ErrTokenReused
}
if _, err := tx.ExecContext(ctx,
`UPDATE pds_oauth_tokens SET revoked_at = ? WHERE jti = ? AND revoked_at IS NULL`,
unixMicro(now), oldJTI); err != nil {
return fmt.Errorf("pds: rotating refresh token: %w", err)
}
for _, token := range newTokens {
if _, err := tx.ExecContext(ctx,
`INSERT INTO pds_oauth_tokens (jti, session_id, kind, issued_at, expires_at)
VALUES (?, ?, ?, ?, ?)`,
token.JTI, token.SessionID, string(token.Kind), unixMicro(token.IssuedAt), unixMicro(token.ExpiresAt)); err != nil {
return fmt.Errorf("pds: rotating refresh token: %w", err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("pds: rotating refresh token: %w", err)
}
return nil
}