store.go
1package atproto
2
3import (
4 "context"
5 "database/sql"
6 "encoding/json"
7 "fmt"
8 "time"
9
10 "github.com/bluesky-social/indigo/atproto/auth/oauth"
11 "github.com/bluesky-social/indigo/atproto/syntax"
12)
13
14const (
15 SQLITE_TIMEFMT = "2006-01-02 15:04:05"
16)
17
18type Store struct {
19 db *sql.DB
20}
21
22func NewOauthStore(db *sql.DB) (*Store, error) {
23 if err := migrate(db); err != nil {
24 return nil, fmt.Errorf("failed to migrate oauth store: %w", err)
25 }
26 return &Store{db}, nil
27}
28
29// Migrate store database (Sqlite)
30func migrate(db *sql.DB) error {
31 s := `CREATE TABLE IF NOT EXISTS
32 oauth_session (
33 id INTEGER PRIMARY KEY AUTOINCREMENT,
34 did TEXT NOT NULL,
35 session_id TEXT NOT NULL,
36 client_session_data BLOB NOT NULL,
37 mtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP
38 );
39 CREATE TABLE IF NOT EXISTS
40 oauth_request (
41 id INTEGER PRIMARY KEY AUTOINCREMENT,
42 state TEXT NOT NULL,
43 auth_request_data BLOB NOT NULL,
44 mtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP
45 );
46 CREATE UNIQUE INDEX IF NOT EXISTS did_session_idx ON oauth_session(did, session_id);
47 CREATE UNIQUE INDEX IF NOT EXISTS state_idx ON oauth_request(state);
48 CREATE INDEX IF NOT EXISTS session_mtime_idx ON oauth_session(mtime);
49 CREATE INDEX IF NOT EXISTS request_mtime_idx ON oauth_request(mtime);
50 CREATE TRIGGER IF NOT EXISTS delete_expired_oauth_sessions AFTER INSERT ON oauth_session
51 BEGIN
52 DELETE FROM oauth_session WHERE mtime < datetime('now', '-6 months');
53 END;
54 CREATE TRIGGER IF NOT EXISTS delete_expired_oauth_requests AFTER INSERT ON oauth_request
55 BEGIN
56 DELETE FROM oauth_request WHERE mtime < datetime('now', '-6 months');
57 END;
58 `
59 if _, err := db.Exec(s); err != nil {
60 return fmt.Errorf("failed to migrate sessions database: %w", err)
61 }
62 return nil
63}
64
65func (s *Store) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) {
66 var blob []byte
67 if err := s.db.QueryRowContext(ctx, "SELECT client_session_data FROM oauth_session WHERE did = ? AND session_id = ?", did.String(), sessionID).Scan(&blob); err != nil {
68 return nil, fmt.Errorf("failed to get session: %w", err)
69 }
70 var sessionData oauth.ClientSessionData
71 if err := json.Unmarshal(blob, &sessionData); err != nil {
72 return nil, fmt.Errorf("failed to unmarshal session: %w", err)
73 }
74 return &sessionData, nil
75}
76
77func (s *Store) SaveSession(ctx context.Context, sess oauth.ClientSessionData) error {
78 blob, err := json.Marshal(sess)
79 if err != nil {
80 return fmt.Errorf("error marshaling client session data: %w", err)
81 }
82 if _, err := s.db.ExecContext(ctx, "INSERT INTO oauth_session (client_session_data, did, session_id, mtime) VALUES (?, ?, ?, ?) ON CONFLICT (did, session_id) DO UPDATE SET client_session_data=excluded.client_session_data, mtime=excluded.mtime", blob, sess.AccountDID.String(), sess.SessionID, time.Now().UTC().Format(SQLITE_TIMEFMT)); err != nil {
83 return fmt.Errorf("failed in save session: %w", err)
84 }
85 return nil
86}
87
88func (s *Store) DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error {
89 if _, err := s.db.ExecContext(ctx, "DELETE FROM oauth_session WHERE did=? AND session_id=?", did.String(), sessionID); err != nil {
90 return fmt.Errorf("failed to delete session: %w", err)
91 }
92 return nil
93}
94
95func (s *Store) GetAuthRequestInfo(ctx context.Context, state string) (*oauth.AuthRequestData, error) {
96 var blob []byte
97 if err := s.db.QueryRowContext(ctx, "SELECT auth_request_data FROM oauth_request WHERE state = ?", state).Scan(&blob); err != nil {
98 return nil, fmt.Errorf("failed to get auth request info: %w", err)
99 }
100 var requestData oauth.AuthRequestData
101 if err := json.Unmarshal(blob, &requestData); err != nil {
102 return nil, fmt.Errorf("failed to unmarshal auth request: %w", err)
103 }
104 return &requestData, nil
105}
106
107func (s *Store) SaveAuthRequestInfo(ctx context.Context, info oauth.AuthRequestData) error {
108 blob, err := json.Marshal(info)
109 if err != nil {
110 return fmt.Errorf("error marshaling oauth request data: %w", err)
111 }
112 if _, err := s.db.ExecContext(ctx, "INSERT INTO oauth_request (auth_request_data, state, mtime) VALUES (?, ?, ?)", blob, info.State, time.Now().UTC().Format(SQLITE_TIMEFMT)); err != nil {
113 return fmt.Errorf("failed to save oauth request: %w", err)
114 }
115 return nil
116}
117
118func (s *Store) DeleteAuthRequestInfo(ctx context.Context, state string) error {
119 if _, err := s.db.ExecContext(ctx, "DELETE FROM oauth_request WHERE state=?", state); err != nil {
120 return fmt.Errorf("failed to delete oauth request: %w", err)
121 }
122 return nil
123}
124
125// implements ClientAuthStore for ATProto oauth
126// See: https://github.com/bluesky-social/indigo/blob/main/atproto/auth/oauth/store.go
127var _ oauth.ClientAuthStore = &Store{}