Retro Bulletin Board Systems on atproto. Web app and TUI.
atbbs.xyz
python
tui
atproto
bbs
1"""OAuth session storage using SQLite. Framework-agnostic."""
2
3import sqlite3
4
5SCHEMA = """
6CREATE TABLE IF NOT EXISTS oauth_auth_request (
7 state TEXT NOT NULL PRIMARY KEY,
8 authserver_iss TEXT NOT NULL,
9 did TEXT,
10 handle TEXT,
11 pds_url TEXT,
12 pkce_verifier TEXT NOT NULL,
13 scope TEXT NOT NULL,
14 dpop_authserver_nonce TEXT NOT NULL,
15 dpop_private_jwk TEXT NOT NULL
16);
17
18CREATE TABLE IF NOT EXISTS oauth_session (
19 did TEXT NOT NULL PRIMARY KEY,
20 handle TEXT,
21 pds_url TEXT NOT NULL,
22 authserver_iss TEXT NOT NULL,
23 access_token TEXT,
24 refresh_token TEXT,
25 dpop_authserver_nonce TEXT NOT NULL,
26 dpop_pds_nonce TEXT,
27 dpop_private_jwk TEXT NOT NULL,
28 client_id TEXT
29);
30"""
31
32
33class SessionStore:
34 """SQLite-backed OAuth session store."""
35
36 def __init__(self, db_path: str = "atbbs.db"):
37 self.db_path = db_path
38 self._init_db()
39
40 def _init_db(self):
41 con = sqlite3.connect(self.db_path)
42 con.executescript(SCHEMA)
43 con.close()
44
45 def _connect(self) -> sqlite3.Connection:
46 con = sqlite3.connect(self.db_path)
47 con.row_factory = sqlite3.Row
48 return con
49
50 # --- Auth requests (temporary, during login flow) ---
51
52 def save_auth_request(self, **kwargs):
53 con = self._connect()
54 con.execute(
55 """INSERT OR REPLACE INTO oauth_auth_request
56 (state, authserver_iss, did, handle, pds_url, pkce_verifier,
57 scope, dpop_authserver_nonce, dpop_private_jwk)
58 VALUES (:state, :authserver_iss, :did, :handle, :pds_url,
59 :pkce_verifier, :scope, :dpop_authserver_nonce, :dpop_private_jwk)""",
60 kwargs,
61 )
62 con.commit()
63 con.close()
64
65 def get_auth_request(self, state: str) -> dict | None:
66 con = self._connect()
67 row = con.execute(
68 "SELECT * FROM oauth_auth_request WHERE state = ?", [state]
69 ).fetchone()
70 con.close()
71 return dict(row) if row else None
72
73 def delete_auth_request(self, state: str):
74 con = self._connect()
75 con.execute("DELETE FROM oauth_auth_request WHERE state = ?", [state])
76 con.commit()
77 con.close()
78
79 # --- Sessions (persistent, per logged-in user) ---
80
81 def save_session(self, **kwargs):
82 con = self._connect()
83 con.execute(
84 """INSERT OR REPLACE INTO oauth_session
85 (did, handle, pds_url, authserver_iss, access_token, refresh_token,
86 dpop_authserver_nonce, dpop_pds_nonce, dpop_private_jwk, client_id)
87 VALUES (:did, :handle, :pds_url, :authserver_iss, :access_token,
88 :refresh_token, :dpop_authserver_nonce, :dpop_pds_nonce,
89 :dpop_private_jwk, :client_id)""",
90 kwargs,
91 )
92 con.commit()
93 con.close()
94
95 def get_session(self, did: str) -> dict | None:
96 con = self._connect()
97 row = con.execute("SELECT * FROM oauth_session WHERE did = ?", [did]).fetchone()
98 con.close()
99 return dict(row) if row else None
100
101 ALLOWED_FIELDS = {
102 "dpop_pds_nonce",
103 "dpop_authserver_nonce",
104 "access_token",
105 "refresh_token",
106 "client_id",
107 }
108
109 def update_session_field(self, did: str, field: str, value: str):
110 if field not in self.ALLOWED_FIELDS:
111 raise ValueError(f"Invalid field: {field}")
112 con = self._connect()
113 con.execute(f"UPDATE oauth_session SET {field} = ? WHERE did = ?", [value, did])
114 con.commit()
115 con.close()
116
117 def update_session_tokens(
118 self, did: str, access_token: str, refresh_token: str, dpop_nonce: str
119 ):
120 con = self._connect()
121 con.execute(
122 """UPDATE oauth_session
123 SET access_token = ?, refresh_token = ?, dpop_authserver_nonce = ?
124 WHERE did = ?""",
125 [access_token, refresh_token, dpop_nonce, did],
126 )
127 con.commit()
128 con.close()
129
130 def delete_session(self, did: str):
131 con = self._connect()
132 con.execute("DELETE FROM oauth_session WHERE did = ?", [did])
133 con.commit()
134 con.close()