+10
-2
src/atproto2/__init__.py
+10
-2
src/atproto2/__init__.py
···
34
34
return (did, handle, doc)
35
35
36
36
if is_valid_did(query):
37
-
# TODO: resolve did identity
38
-
return None
37
+
did = query
38
+
doc = resolve_doc_from_did(did)
39
+
if not doc:
40
+
return None
41
+
handle = handle_from_doc(doc)
42
+
if not handle:
43
+
return None
44
+
if resolve_did_from_handle(handle) != did:
45
+
return None
46
+
return (did, handle, doc)
39
47
40
48
return None
41
49
+1
-2
src/atproto2/atproto_oauth.py
+1
-2
src/atproto2/atproto_oauth.py
···
120
120
def send_par_auth_request(
121
121
authserver_url: str,
122
122
authserver_meta: dict[str, str],
123
-
login_hint: str,
123
+
login_hint: str | None,
124
124
client_id: str,
125
125
redirect_uri: str,
126
126
scope: str,
···
159
159
}
160
160
if login_hint:
161
161
par_body["login_hint"] = login_hint
162
-
# print(par_body)
163
162
164
163
# IMPORTANT: Pushed Authorization Request URL is untrusted input, SSRF mitigations are needed
165
164
assert is_safe_url(par_url)
+3
-1
src/main.py
+3
-1
src/main.py
···
76
76
77
77
@app.post("/login")
78
78
def auth_login():
79
-
username = request.form.get("username")
79
+
username = request.form.get("username", "")
80
+
if username[0] == "@":
81
+
username = username[1:]
80
82
if not username:
81
83
return redirect(url_for("page_login"), 303)
82
84
return redirect(url_for("oauth.oauth_start", username=username), 303)
+51
-27
src/oauth.py
+51
-27
src/oauth.py
···
4
4
5
5
import json
6
6
7
+
from .atproto2.atproto_identity import is_valid_did, is_valid_handle
7
8
from .atproto2.atproto_oauth import initial_token_request, send_par_auth_request
8
9
from .atproto2.atproto_security import is_safe_url
9
10
from .atproto2 import (
···
26
27
username = request.args.get("username")
27
28
if not username:
28
29
return "missing ?username", 400
29
-
login_hint = username
30
-
identity = resolve_identity(username)
31
-
if identity is None:
32
-
return "couldnt resolve identity", 500
33
-
did, handle, doc = identity
34
-
pds_url = pds_endpoint_from_doc(doc)
35
-
if not pds_url:
36
-
return "pds not found", 404
37
-
current_app.logger.debug(f"account PDS: {pds_url}")
38
-
authserver_url = resolve_authserver_from_pds(pds_url)
39
-
if not authserver_url:
40
-
return "authserver not found", 404
30
+
31
+
if is_valid_handle(username) or is_valid_did(username):
32
+
login_hint = username
33
+
identity = resolve_identity(username)
34
+
if identity is None:
35
+
return "couldnt resolve identity", 500
36
+
did, handle, doc = identity
37
+
pds_url = pds_endpoint_from_doc(doc)
38
+
if not pds_url:
39
+
return "pds not found", 404
40
+
current_app.logger.debug(f"account PDS: {pds_url}")
41
+
authserver_url = resolve_authserver_from_pds(pds_url)
42
+
if not authserver_url:
43
+
return "authserver not found", 404
44
+
45
+
elif username.startswith("https://") and is_safe_url(username):
46
+
did, handle, pds_url = None, None, None
47
+
login_hint = None
48
+
authserver_url = resolve_authserver_from_pds(username) or username
49
+
50
+
else:
51
+
return "not a valid handle, did or auth server", 400
52
+
41
53
current_app.logger.debug(f"Authserver: {authserver_url}")
42
-
43
54
assert is_safe_url(authserver_url)
44
55
authserver_meta = resolve_authserver_meta(authserver_url)
45
56
if not authserver_meta:
···
49
60
dpop_private_jwk: Key = JsonWebKey.generate_key("EC", "P-256", is_private=True)
50
61
scope = "atproto transition:generic"
51
62
52
-
app_url = request.url_root.replace("http://", "https://")
53
-
redirect_uri = f"{app_url}oauth/callback"
54
-
client_id = f"{app_url}oauth/metadata"
63
+
host = request.host
64
+
metadata_endpoint = url_for("oauth.oauth_metadata")
65
+
client_id = f"https://{host}{metadata_endpoint}"
66
+
callback_endpoint = url_for("oauth.oauth_callback")
67
+
redirect_uri = f"https://{host}{callback_endpoint}"
68
+
69
+
current_app.logger.debug(client_id)
70
+
current_app.logger.debug(redirect_uri)
55
71
56
72
CLIENT_SECRET_JWK = JsonWebKey.import_key(current_app.config["CLIENT_SECRET_JWK"])
57
73
···
66
82
dpop_private_jwk,
67
83
)
68
84
if resp.status_code == 400:
69
-
current_app.logger.info(f"PAR HTTP 400: {resp.json()}")
85
+
current_app.logger.debug(f"PAR HTTP 400: {resp.json()}")
70
86
resp.raise_for_status()
71
87
72
-
par_request_uri = resp.json()["request_uri"]
88
+
par_request_uri: str = resp.json()["request_uri"]
73
89
current_app.logger.debug(f"saving oauth_auth_request to DB state={state}")
74
90
oauth_auth_requests[state] = {
75
91
"authserver_iss": authserver_meta["issuer"],
76
-
"did": did,
77
-
"handle": handle,
78
-
"pds_url": pds_url,
92
+
"did": did or "", # TODO: use actual typing
93
+
"handle": handle or "",
94
+
"pds_url": pds_url or "",
79
95
"pkce_verifier": pkce_verifier,
80
96
"scope": scope,
81
97
"dpop_authserver_nonce": dpop_authserver_nonce,
82
98
"dpop_private_jwk": dpop_private_jwk.as_json(is_private=True),
83
99
}
84
100
85
-
auth_url = authserver_meta["authorization_endpoint"]
86
-
assert is_safe_url(auth_url)
101
+
auth_endpoint = authserver_meta["authorization_endpoint"]
102
+
assert is_safe_url(auth_endpoint)
87
103
qparam = urlencode({"client_id": client_id, "request_uri": par_request_uri})
88
-
return redirect(f"{auth_url}?{qparam}")
104
+
return redirect(f"{auth_endpoint}?{qparam}")
89
105
90
106
91
107
@oauth.get("/callback")
···
121
137
did, handle, pds_url = row["did"], row["handle"], row["pds_url"]
122
138
assert tokens["sub"] == did
123
139
else:
124
-
# we started with auth server URL
125
-
raise Exception()
140
+
did = tokens["sub"]
141
+
assert is_valid_did(did)
142
+
identity = resolve_identity(did)
143
+
if not identity:
144
+
return "could not resolve identity", 500
145
+
did, handle, did_doc = identity
146
+
pds_url = pds_endpoint_from_doc(did_doc)
147
+
if not pds_url:
148
+
return "could not resolve pds", 500
149
+
authserver_url = resolve_authserver_from_pds(pds_url)
150
+
assert authserver_url == authserver_iss
126
151
127
152
assert row["scope"] == tokens["scope"]
128
153
···
161
186
return jsonify(
162
187
{
163
188
"client_id": f"https://{host}{metadata_endpoint}",
164
-
"application_type": "web",
165
189
"grant_types": ["authorization_code", "refresh_token"],
166
190
"scope": "atproto transition:generic",
167
191
"response_types": ["code"],