Skip to content

Commit d13f998

Browse files
committed
Verify authorization_endpoint at end of flow, refs #22
1 parent 02f27ce commit d13f998

File tree

2 files changed

+89
-19
lines changed

2 files changed

+89
-19
lines changed

datasette_indieauth/__init__.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,24 @@ async def indieauth_done(request, datasette):
141141
)
142142
me = info["me"]
143143

144-
# Returned me must be on the same domain as original_me
144+
# Verify returned me - must be same domain and link to same authorization_endpoint
145+
me_error = None
145146
if not verify_same_domain(me, original_me):
147+
me_error = '"me" value returned by authorization server had a domain that did not match the initial URL'
148+
149+
canonical_me, me_authorization_endpoint, _ = await utils.discover_endpoints(me)
150+
if me_authorization_endpoint != authorization_endpoint:
151+
me_error = '"me" value resolves to a different authorization_endpoint'
152+
153+
if me_error:
146154
return await indieauth_page(
147155
request,
148156
datasette,
149-
error='"me" value returned by authorization server had a domain that did not match the initial URL',
157+
error=me_error,
150158
)
151159

160+
me = canonical_me
161+
152162
actor = {
153163
"me": me,
154164
"display": display_url(me),

tests/test_indieauth.py

+77-17
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,23 @@ async def test_indieauth_flow(
153153
data=auth_response_body.encode("utf-8"),
154154
status_code=auth_response_status,
155155
)
156+
if not expected_error:
157+
httpx_mock.add_response(
158+
url="https://indieauth.simonwillison.net/index.php/author/simonw/",
159+
method="GET",
160+
data=b'<link rel="authorization_endpoint" href="https://indieauth.simonwillison.net/auth">',
161+
)
162+
if "indieauth.simonwillison.com" in auth_response_body:
163+
httpx_mock.add_response(
164+
url="https://indieauth.simonwillison.com",
165+
method="GET",
166+
data=b'<link rel="authorization_endpoint" href="https://indieauth.simonwillison.net/auth">',
167+
)
156168
datasette = Datasette([], memory=True)
157169
app = datasette.app()
158170
async with httpx.AsyncClient(app=app) as client:
159171
# Get CSRF token
160-
csrftoken = (
161-
await client.get(
162-
"http://localhost/-/indieauth",
163-
)
164-
).cookies["ds_csrftoken"]
172+
csrftoken = await _get_csrftoken(client)
165173
# Submit the form
166174
post_response = await client.post(
167175
"http://localhost/-/indieauth",
@@ -196,8 +204,12 @@ async def test_indieauth_flow(
196204
allow_redirects=False,
197205
)
198206
# This should have made a POST to https://indieauth.simonwillison.net/auth
199-
last_request = httpx_mock.get_requests()[-1]
200-
post_bits = dict(urllib.parse.parse_qsl(last_request.read().decode("utf-8")))
207+
last_post_request = [
208+
r for r in httpx_mock.get_requests() if r.method == "POST"
209+
][-1]
210+
post_bits = dict(
211+
urllib.parse.parse_qsl(last_post_request.read().decode("utf-8"))
212+
)
201213
assert post_bits == {
202214
"grant_type": "authorization_code",
203215
"code": "123",
@@ -262,11 +274,7 @@ async def test_indieauth_errors(httpx_mock, me, bodies, expected_error):
262274
datasette = Datasette([], memory=True)
263275
app = datasette.app()
264276
async with httpx.AsyncClient(app=app) as client:
265-
csrftoken = (
266-
await client.get(
267-
"http://localhost/-/indieauth",
268-
)
269-
).cookies["ds_csrftoken"]
277+
csrftoken = await _get_csrftoken(client)
270278
# Submit the form
271279
post_response = await client.post(
272280
"http://localhost/-/indieauth",
@@ -314,11 +322,7 @@ def raise_timeout(request, ext):
314322
datasette = Datasette([], memory=True)
315323
app = datasette.app()
316324
async with httpx.AsyncClient(app=app) as client:
317-
csrftoken = (
318-
await client.get(
319-
"http://localhost/-/indieauth",
320-
)
321-
).cookies["ds_csrftoken"]
325+
csrftoken = await _get_csrftoken(client)
322326
# Submit the form
323327
post_response = await client.post(
324328
"http://localhost/-/indieauth",
@@ -327,3 +331,59 @@ def raise_timeout(request, ext):
327331
allow_redirects=False,
328332
)
329333
assert "Invalid IndieAuth identifier: HTTP error occurred" in post_response.text
334+
335+
336+
@pytest.mark.asyncio
337+
async def test_non_matching_authorization_endpoint(httpx_mock):
338+
# See https://github.com/simonw/datasette-indieauth/issues/22
339+
httpx_mock.add_response(
340+
url="https://simonwillison.net",
341+
data=b'<link rel="authorization_endpoint" href="https://indieauth.simonwillison.net/auth">',
342+
)
343+
httpx_mock.add_response(
344+
url="https://indieauth.simonwillison.net/auth",
345+
method="POST",
346+
data="me=https%3A%2F%2Fsimonwillison.net%2Fme".encode("utf-8"),
347+
)
348+
httpx_mock.add_response(
349+
url="https://simonwillison.net/me",
350+
data=b'<link rel="authorization_endpoint" href="https://example.com">',
351+
)
352+
datasette = Datasette([], memory=True)
353+
app = datasette.app()
354+
async with httpx.AsyncClient(app=app) as client:
355+
csrftoken = await _get_csrftoken(client)
356+
# Submit the form
357+
post_response = await client.post(
358+
"http://localhost/-/indieauth",
359+
data={"csrftoken": csrftoken, "me": "https://simonwillison.net/"},
360+
cookies={"ds_csrftoken": csrftoken},
361+
allow_redirects=False,
362+
)
363+
ds_indieauth = post_response.cookies["ds_indieauth"]
364+
state = dict(
365+
urllib.parse.parse_qsl(post_response.headers["location"].split("?", 1)[1])
366+
)["state"]
367+
# ... after redirecting back again
368+
response = await client.get(
369+
"http://localhost/-/indieauth/done",
370+
params={
371+
"state": state,
372+
"code": "123",
373+
},
374+
cookies={"ds_indieauth": ds_indieauth},
375+
allow_redirects=False,
376+
)
377+
# This should be an error because the authorization_endpoint did not match
378+
assert (
379+
"&#34;me&#34; value resolves to a different authorization_endpoint"
380+
in response.text
381+
)
382+
383+
384+
async def _get_csrftoken(client):
385+
return (
386+
await client.get(
387+
"http://localhost/-/indieauth",
388+
)
389+
).cookies["ds_csrftoken"]

0 commit comments

Comments
 (0)