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{}