-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdatastore_postgresql.py
More file actions
209 lines (189 loc) · 8.22 KB
/
Copy pathdatastore_postgresql.py
File metadata and controls
209 lines (189 loc) · 8.22 KB
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
"""
Implementatio of datastore with PostgreSQL backend.
"""
import json
import time
from typing import Dict, List, Optional, Tuple
import psycopg2
from psycopg2.extras import RealDictCursor
from werkzeug.security import check_password_hash, generate_password_hash
from mcpnp.auth.datastore import OAuthDatastore
class PostgreSQLOAuthDatastore(OAuthDatastore):
"""PostgreSQL implementation of OAuth datastore."""
def __init__(self, connection_url: str):
self.connection_url = connection_url
self.init_database()
def init_database(self) -> None:
"""Initialize PostgreSQL database schema."""
with psycopg2.connect(self.connection_url) as conn:
with conn.cursor() as cursor:
# OAuth clients table
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS oauth_clients (
client_id TEXT PRIMARY KEY,
client_secret TEXT NOT NULL,
redirect_uris TEXT NOT NULL,
client_name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
)
# Users table
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
email TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
)
# OAuth tokens table
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS oauth_tokens (
token TEXT PRIMARY KEY,
token_type TEXT NOT NULL,
user_id TEXT NOT NULL,
client_id TEXT NOT NULL,
scopes TEXT NOT NULL,
expires_at BIGINT NOT NULL,
token_data TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.commit()
def register_client(
self,
client_id: str,
client_secret: str,
redirect_uris: List[str],
client_name: str,
) -> None:
"""Register a new OAuth client."""
redirect_uris_json = json.dumps(redirect_uris)
with psycopg2.connect(self.connection_url) as conn:
with conn.cursor() as cursor:
cursor.execute(
"""
INSERT INTO oauth_clients (client_id, client_secret, redirect_uris, client_name)
VALUES (%s, %s, %s, %s)
""",
(client_id, client_secret, redirect_uris_json, client_name),
)
conn.commit()
def validate_client(self, client_id: str, client_secret: str = None) -> bool:
"""Validate client credentials."""
with psycopg2.connect(self.connection_url) as conn:
with conn.cursor() as cursor:
if client_secret:
cursor.execute(
"""
SELECT client_id FROM oauth_clients
WHERE client_id = %s AND client_secret = %s
""",
(client_id, client_secret),
)
else:
cursor.execute(
"""
SELECT client_id FROM oauth_clients WHERE client_id = %s
""",
(client_id,),
)
return cursor.fetchone() is not None
def get_client_redirect_uris(self, client_id: str) -> List[str]:
"""Get redirect URIs for a client."""
with psycopg2.connect(self.connection_url) as conn:
with conn.cursor() as cursor:
cursor.execute(
"SELECT redirect_uris FROM oauth_clients WHERE client_id = %s",
(client_id,),
)
result = cursor.fetchone()
if result:
return json.loads(result[0])
return []
def create_user(self, username: str, password: str, email: str = None) -> str:
"""Create a new user account. Returns user ID."""
password_hash = generate_password_hash(password, method="scrypt")
with psycopg2.connect(self.connection_url) as conn:
with conn.cursor(cursor_factory=RealDictCursor) as cursor:
try:
cursor.execute(
"""
INSERT INTO users (username, password_hash, email)
VALUES (%s, %s, %s) RETURNING id
""",
(username, password_hash, email),
)
result = cursor.fetchone()
conn.commit()
return str(result["id"])
except psycopg2.IntegrityError as exc:
raise ValueError("Username already exists") from exc
def authenticate_user(self, username: str, password: str) -> Optional[str]:
"""Authenticate user credentials. Returns user ID if valid."""
with psycopg2.connect(self.connection_url) as conn:
with conn.cursor(cursor_factory=RealDictCursor) as cursor:
cursor.execute(
"SELECT id, username, password_hash FROM users WHERE username = %s",
(username,),
)
user = cursor.fetchone()
if not user:
return None
# Verify password using werkzeug's check_password_hash
if check_password_hash(user["password_hash"], password):
return str(user["id"])
return None
def save_token(self, token: str, token_type: str, token_data: Dict) -> None:
"""Save token to persistent storage."""
with psycopg2.connect(self.connection_url) as conn:
with conn.cursor() as cursor:
cursor.execute(
"""
INSERT INTO oauth_tokens
(token, token_type, user_id, client_id, scopes, expires_at, token_data)
VALUES (%s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (token) DO UPDATE SET
token_data = EXCLUDED.token_data, expires_at = EXCLUDED.expires_at
""",
(
token,
token_type,
token_data["user_id"],
token_data["client_id"],
token_data.get("scope", ""),
token_data.get("expires_at", 0),
json.dumps(token_data),
),
)
conn.commit()
def load_valid_tokens(self) -> Tuple[Dict[str, Dict], Dict[str, Dict]]:
"""Load all valid tokens from storage."""
access_tokens = {}
refresh_tokens = {}
with psycopg2.connect(self.connection_url) as conn:
with conn.cursor() as cursor:
cursor.execute(
"SELECT token, token_type, token_data FROM oauth_tokens WHERE expires_at > %s",
(int(time.time()),),
)
for token, token_type, token_data_json in cursor.fetchall():
token_data = json.loads(token_data_json)
if token_type == "access":
access_tokens[token] = token_data
elif token_type == "refresh":
refresh_tokens[token] = token_data
return access_tokens, refresh_tokens
def remove_token(self, token: str) -> None:
"""Remove token from storage."""
with psycopg2.connect(self.connection_url) as conn:
with conn.cursor() as cursor:
cursor.execute("DELETE FROM oauth_tokens WHERE token = %s", (token,))
conn.commit()