src/atproto2/__init__.py
src/atproto/__init__.py
src/atproto2/__init__.py
src/atproto/__init__.py
src/atproto2/atproto_identity.py
src/atproto/atproto_identity.py
src/atproto2/atproto_identity.py
src/atproto/atproto_identity.py
+19
-19
src/atproto2/atproto_oauth.py
src/atproto/atproto_oauth.py
+19
-19
src/atproto2/atproto_oauth.py
src/atproto/atproto_oauth.py
···
9
9
from authlib.oauth2.rfc7636 import create_s256_code_challenge
10
10
from requests import Response
11
11
12
+
from ..types import OAuthAuthRequest, OAuthSession
13
+
12
14
from .atproto_security import is_safe_url, hardened_http
13
15
14
16
···
195
197
# Completes the auth flow by sending an initial auth token request.
196
198
# Returns token response (dict) and DPoP nonce (str)
197
199
def initial_token_request(
198
-
auth_request: dict[str, str],
200
+
auth_request: OAuthAuthRequest,
199
201
code: str,
200
202
app_url: str,
201
203
client_secret_jwk: Key,
202
204
) -> tuple[dict[str, str], str]:
203
-
authserver_url = auth_request["authserver_iss"]
205
+
authserver_url = auth_request.authserver_iss
204
206
205
207
# Re-fetch server metadata
206
208
authserver_meta = fetch_authserver_meta(authserver_url)
···
219
221
"redirect_uri": redirect_uri,
220
222
"grant_type": "authorization_code",
221
223
"code": code,
222
-
"code_verifier": auth_request["pkce_verifier"],
224
+
"code_verifier": auth_request.pkce_verifier,
223
225
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
224
226
"client_assertion": client_assertion,
225
227
}
226
228
227
229
# Create DPoP header JWT, using the existing DPoP signing key for this account/session
228
230
token_url = authserver_meta["token_endpoint"]
229
-
dpop_private_jwk = JsonWebKey.import_key(
230
-
json.loads(auth_request["dpop_private_jwk"])
231
-
)
232
-
dpop_authserver_nonce = auth_request["dpop_authserver_nonce"]
231
+
dpop_private_jwk = JsonWebKey.import_key(json.loads(auth_request.dpop_private_jwk))
232
+
dpop_authserver_nonce = auth_request.dpop_authserver_nonce
233
233
dpop_proof = authserver_dpop_jwt(
234
234
"POST", token_url, dpop_authserver_nonce, dpop_private_jwk
235
235
)
···
262
262
263
263
# Returns token response (dict) and DPoP nonce (str)
264
264
def refresh_token_request(
265
-
user: dict,
265
+
user: OAuthSession,
266
266
app_url: str,
267
267
client_secret_jwk: Key,
268
268
) -> tuple[dict[str, str], str]:
269
-
authserver_url = user["authserver_iss"]
269
+
authserver_url = user.authserver_iss
270
270
271
271
# Re-fetch server metadata
272
272
authserver_meta = fetch_authserver_meta(authserver_url)
···
282
282
params = {
283
283
"client_id": client_id,
284
284
"grant_type": "refresh_token",
285
-
"refresh_token": user["refresh_token"],
285
+
"refresh_token": user.refresh_token,
286
286
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
287
287
"client_assertion": client_assertion,
288
288
}
289
289
290
290
# Create DPoP header JWT, using the existing DPoP signing key for this account/session
291
291
token_url = authserver_meta["token_endpoint"]
292
-
dpop_private_jwk = JsonWebKey.import_key(json.loads(user["dpop_private_jwk"]))
293
-
dpop_authserver_nonce = user["dpop_authserver_nonce"]
292
+
dpop_private_jwk = JsonWebKey.import_key(json.loads(user.dpop_private_jwk))
293
+
dpop_authserver_nonce = user.dpop_authserver_nonce
294
294
dpop_proof = authserver_dpop_jwt(
295
295
"POST", token_url, dpop_authserver_nonce, dpop_private_jwk
296
296
)
···
323
323
def pds_dpop_jwt(
324
324
method: str,
325
325
url: str,
326
-
access_token: str,
327
-
nonce: str,
326
+
access_token: str | None,
327
+
nonce: str | None,
328
328
dpop_private_jwk: Key,
329
329
) -> str:
330
330
dpop_pub_jwk = json.loads(dpop_private_jwk.as_json(is_private=False))
···
352
352
def pds_authed_req(
353
353
method: str,
354
354
url: str,
355
-
user: dict[str, str],
355
+
user: OAuthSession,
356
356
db: sqlite3.Connection,
357
357
body: dict[str, Any] | None = None,
358
358
) -> Response | None:
359
-
dpop_private_jwk = JsonWebKey.import_key(json.loads(user["dpop_private_jwk"]))
360
-
dpop_pds_nonce = user["dpop_pds_nonce"]
361
-
access_token = user["access_token"]
359
+
dpop_private_jwk = JsonWebKey.import_key(json.loads(user.dpop_private_jwk))
360
+
dpop_pds_nonce = user.dpop_pds_nonce
361
+
access_token = user.access_token
362
362
363
363
response: Response | None = None
364
364
···
395
395
cur = db.cursor()
396
396
_ = cur.execute(
397
397
"UPDATE oauth_session SET dpop_pds_nonce = ? WHERE did = ?;",
398
-
[dpop_pds_nonce, user["did"]],
398
+
[dpop_pds_nonce, user.did],
399
399
)
400
400
db.commit()
401
401
cur.close()
src/atproto2/atproto_security.py
src/atproto/atproto_security.py
src/atproto2/atproto_security.py
src/atproto/atproto_security.py
+2
-2
src/db.py
+2
-2
src/db.py
+18
-16
src/main.py
+18
-16
src/main.py
···
2
2
from typing import Any
3
3
import json
4
4
5
-
from .atproto2 import PdsUrl, get_record, resolve_did_from_handle, resolve_pds_from_did
6
-
from .atproto2.atproto_oauth import pds_authed_req
5
+
from .atproto import PdsUrl, get_record, resolve_did_from_handle, resolve_pds_from_did
6
+
from .atproto.atproto_oauth import pds_authed_req
7
7
from .db import close_db_connection, get_db, init_db
8
8
from .oauth import oauth
9
+
from .types import OAuthSession
9
10
10
11
app = Flask(__name__)
11
12
_ = app.config.from_prefixed_env()
···
20
21
21
22
@app.before_request
22
23
def load_user_to_context():
24
+
user: OAuthSession | None = None
23
25
did: str | None = session.get("user_did")
24
-
if did is None:
25
-
g.user = None
26
-
else:
26
+
if did is not None:
27
27
db = get_db(app)
28
-
g.user = db.execute(
28
+
row = db.execute(
29
29
"select * from oauth_session where did = ?",
30
30
(did,),
31
31
).fetchone()
32
+
user = OAuthSession(**row)
33
+
g.user = user
32
34
33
35
34
-
def get_user() -> dict[str, str] | None:
36
+
def get_user() -> OAuthSession | None:
35
37
return g.user
36
38
37
39
···
90
92
if user is None:
91
93
return redirect("/login")
92
94
93
-
did: str = user["did"]
94
-
pds: str = user["pds_url"]
95
-
handle: str | None = user["handle"]
95
+
did: str = user.did
96
+
pds: str = user.pds_url
97
+
handle: str | None = user.handle
96
98
97
99
profile, from_bluesky = load_profile(pds, did, reload=True)
98
100
links = load_links(pds, did, reload=True) or [{"background": "#fa0"}]
···
119
121
120
122
put_record(
121
123
user=user,
122
-
pds=user["pds_url"],
123
-
repo=user["did"],
124
+
pds=user.pds_url,
125
+
repo=user.did,
124
126
collection=f"{SCHEMA}.actor.profile",
125
127
rkey="self",
126
128
record={
···
159
161
160
162
put_record(
161
163
user=user,
162
-
pds=user["pds_url"],
163
-
repo=user["did"],
164
+
pds=user.pds_url,
165
+
repo=user.did,
164
166
collection=f"{SCHEMA}.actor.links",
165
167
rkey="self",
166
168
record={
···
212
214
213
215
214
216
def put_record(
215
-
user: dict[str, str],
217
+
user: OAuthSession,
216
218
pds: PdsUrl,
217
219
repo: str,
218
220
collection: str,
···
246
248
if user is not None:
247
249
db = get_db(app)
248
250
cursor = db.cursor()
249
-
_ = cursor.execute("delete from oauth_session where did = ?", (user["did"],))
251
+
_ = cursor.execute("delete from oauth_session where did = ?", (user.did,))
250
252
db.commit()
251
253
cursor.close()
252
254
session.clear()
+43
-28
src/oauth.py
+43
-28
src/oauth.py
···
4
4
5
5
import json
6
6
7
-
from .atproto2.atproto_identity import is_valid_did, is_valid_handle
8
-
from .atproto2.atproto_oauth import initial_token_request, send_par_auth_request
9
-
from .atproto2.atproto_security import is_safe_url
10
-
from .atproto2 import (
7
+
from .atproto.atproto_identity import is_valid_did, is_valid_handle
8
+
from .atproto.atproto_oauth import initial_token_request, send_par_auth_request
9
+
from .atproto.atproto_security import is_safe_url
10
+
from .atproto import (
11
11
pds_endpoint_from_doc,
12
12
resolve_authserver_from_pds,
13
13
resolve_authserver_meta,
14
14
resolve_identity,
15
15
)
16
+
from .types import OAuthAuthRequest
16
17
from .db import get_db
17
18
18
19
oauth = Blueprint("oauth", __name__, url_prefix="/oauth")
19
-
20
-
21
-
oauth_auth_requests: dict[str, dict[str, str]] = {}
22
20
23
21
24
22
@oauth.get("/start")
···
87
85
88
86
par_request_uri: str = resp.json()["request_uri"]
89
87
current_app.logger.debug(f"saving oauth_auth_request to DB state={state}")
90
-
oauth_auth_requests[state] = {
91
-
"authserver_iss": authserver_meta["issuer"],
92
-
"did": did or "", # TODO: use actual typing
93
-
"handle": handle or "",
94
-
"pds_url": pds_url or "",
95
-
"pkce_verifier": pkce_verifier,
96
-
"scope": scope,
97
-
"dpop_authserver_nonce": dpop_authserver_nonce,
98
-
"dpop_private_jwk": dpop_private_jwk.as_json(is_private=True),
99
-
}
88
+
89
+
db = get_db(current_app)
90
+
cursor = db.cursor()
91
+
_ = cursor.execute(
92
+
"insert or replace into oauth_auth_requests values (?, ?, ?, ?, ?, ?, ?, ?, ?)",
93
+
(
94
+
state,
95
+
authserver_meta["issuer"],
96
+
did,
97
+
handle,
98
+
pds_url,
99
+
pkce_verifier,
100
+
scope,
101
+
dpop_authserver_nonce,
102
+
dpop_private_jwk.as_json(is_private=True),
103
+
),
104
+
)
105
+
db.commit()
106
+
cursor.close()
100
107
101
108
auth_endpoint = authserver_meta["authorization_endpoint"]
102
109
assert is_safe_url(auth_endpoint)
···
110
117
authserver_iss = request.args["iss"]
111
118
authorization_code = request.args["code"]
112
119
113
-
auth_request = oauth_auth_requests.get(state)
114
-
if auth_request is None:
120
+
db = get_db(current_app)
121
+
cursor = db.cursor()
122
+
123
+
row = cursor.execute(
124
+
"select * from oauth_auth_requests where state = ?", (state,)
125
+
).fetchone()
126
+
try:
127
+
auth_request = OAuthAuthRequest(**row)
128
+
except TypeError:
115
129
return redirect(url_for("page_login"), 303)
116
130
117
131
current_app.logger.debug(f"Deleting auth request for state={state}")
118
-
_ = oauth_auth_requests.pop(state)
132
+
_ = cursor.execute("delete from oauth_auth_requests where state = ?", (state,))
133
+
db.commit()
119
134
120
-
assert auth_request["authserver_iss"] == authserver_iss
121
-
# assert state ????
135
+
assert auth_request.authserver_iss == authserver_iss
136
+
assert auth_request.state == state
122
137
123
138
app_url = request.url_root.replace("http://", "https://")
124
139
CLIENT_SECRET_JWK = JsonWebKey.import_key(current_app.config["CLIENT_SECRET_JWK"])
···
131
146
132
147
row = auth_request
133
148
134
-
did = auth_request["did"]
135
-
if row["did"]:
149
+
did = auth_request.did
150
+
if row.did:
136
151
# If we started with an account identifier, this is simple
137
-
did, handle, pds_url = row["did"], row["handle"], row["pds_url"]
152
+
did, handle, pds_url = row.did, row.handle, row.pds_url
138
153
assert tokens["sub"] == did
139
154
else:
140
155
did = tokens["sub"]
···
149
164
authserver_url = resolve_authserver_from_pds(pds_url)
150
165
assert authserver_url == authserver_iss
151
166
152
-
assert row["scope"] == tokens["scope"]
167
+
assert row.scope == tokens["scope"]
153
168
154
169
current_app.logger.debug("storing user did and handle")
155
170
db = get_db(current_app)
···
165
180
tokens["refresh_token"],
166
181
dpop_authserver_nonce,
167
182
None,
168
-
auth_request["dpop_private_jwk"],
183
+
auth_request.dpop_private_jwk,
169
184
),
170
185
)
171
186
db.commit()
172
187
cursor.close()
173
188
174
189
session["user_did"] = did
175
-
session["user_handle"] = auth_request["handle"]
190
+
session["user_handle"] = auth_request.handle
176
191
177
192
return redirect(url_for("page_login"))
178
193
+13
-1
src/schema.sql
+13
-1
src/schema.sql
···
1
-
create table if not exists oauth_session (
1
+
create table if not exists oauth_auth_requests (
2
+
state text not null primary key,
3
+
authserver_iss text not null,
4
+
did text,
5
+
handle text,
6
+
pds_url text,
7
+
pkce_verifier text not null,
8
+
scope text not null,
9
+
dpop_authserver_nonce text not null,
10
+
dpop_private_jwk text not null
11
+
) strict, without rowid;
12
+
13
+
create table if not exists oauth_sessions (
2
14
did text not null primary key,
3
15
handle text,
4
16
pds_url text not null,
+25
src/types.py
+25
src/types.py
···
1
+
from typing import NamedTuple
2
+
3
+
4
+
class OAuthAuthRequest(NamedTuple):
5
+
state: str
6
+
authserver_iss: str
7
+
did: str | None
8
+
handle: str | None
9
+
pds_url: str | None
10
+
pkce_verifier: str
11
+
scope: str
12
+
dpop_authserver_nonce: str
13
+
dpop_private_jwk: str
14
+
15
+
16
+
class OAuthSession(NamedTuple):
17
+
did: str
18
+
handle: str | None
19
+
pds_url: str
20
+
authserver_iss: str
21
+
access_token: str | None
22
+
refresh_token: str | None
23
+
dpop_authserver_nonce: str
24
+
dpop_pds_nonce: str | None
25
+
dpop_private_jwk: str