Retro Bulletin Board Systems on atproto. Web app and TUI. atbbs.xyz
python tui atproto bbs
at master 134 lines 4.2 kB view raw
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()