Skip to content

Commit 9a2bb78

Browse files
split authz and authn into dedicated routes
1 parent 1bc99b8 commit 9a2bb78

File tree

2 files changed

+87
-13
lines changed

2 files changed

+87
-13
lines changed

src/millipds/auth_oauth.py

+69-12
Original file line numberDiff line numberDiff line change
@@ -125,22 +125,78 @@ async def oauth_authorization_server(request: web.Request):
125125
# this is where a client will redirect to during the auth flow.
126126
# they'll see a webpage asking them to login
127127
@routes.get("/oauth/authorize")
128-
async def oauth_authorize(request: web.Request):
129-
# TODO: extract request_uri
128+
async def oauth_authorize_get(request: web.Request):
129+
now = int(time.time())
130+
db = get_db(request)
131+
132+
session_token = request.cookies.get("millipds-oauth-session")
133+
row = db.con.execute(
134+
"""
135+
SELECT user_id FROM oauth_session_cookie
136+
WHERE token=? AND expires_at>?
137+
""",
138+
(session_token, now),
139+
).fetchone()
140+
if row is None:
141+
# no active oauth cookie session
142+
return web.HTTPTemporaryRedirect("/oauth/authenticate")
143+
144+
# if we reached here, either there was an existing session, or the user
145+
# just created a new one and got redirected back again
146+
147+
did, handle = db.con.execute(
148+
"SELECT did, handle FROM user WHERE id=?", row
149+
).fetchone()
150+
# TODO: check id hint in auth request matches!
151+
152+
# TODO: look at the requested scopes, see if the user already granted them already,
153+
# display UI as appropriate
154+
155+
return web.Response(
156+
text=html_templates.authz_page(handle=handle),
157+
content_type="text/html",
158+
headers=WEBUI_HEADERS,
159+
)
160+
161+
162+
@routes.post("/oauth/authorize")
163+
async def oauth_authorize_post(request: web.Request):
164+
now = int(time.time())
165+
db = get_db(request)
166+
167+
session_token = request.cookies.get("millipds-oauth-session")
168+
row = db.con.execute(
169+
"""
170+
SELECT user_id FROM oauth_session_cookie
171+
WHERE token=? AND expires_at>?
172+
""",
173+
(session_token, now),
174+
).fetchone()
175+
if row is None:
176+
# no active oauth cookie session
177+
return web.HTTPTemporaryRedirect("/oauth/authenticate")
178+
179+
# TODO: redirect back to app?
180+
return web.Response(
181+
text="TODO",
182+
content_type="text/html",
183+
headers=WEBUI_HEADERS,
184+
)
185+
186+
187+
@routes.get("/oauth/authenticate")
188+
async def oauth_authenticate_get(request: web.Request):
130189
return web.Response(
131190
text=html_templates.authn_page(
132191
identifier_hint="todo.invalid"
133-
), # this includes a login form that POSTs to /oauth/authorize (i.e. same endpoint)
192+
), # this includes a login form that POSTs to the same endpoint
134193
content_type="text/html",
135194
headers=WEBUI_HEADERS,
136195
)
137196

138197

139-
# after login, assuming the creds were good, the user will be prompted to
140-
# authorize the client application to access certain scopes
141-
@routes.post("/oauth/authorize")
142-
async def oauth_authorize_handle_login(request: web.Request):
143-
# TODO: CSRF tokens?
198+
@routes.post("/oauth/authenticate")
199+
async def oauth_authenticate_post(request: web.Request):
144200
form = await request.post()
145201
logging.info(form)
146202

@@ -170,22 +226,23 @@ async def oauth_authorize_handle_login(request: web.Request):
170226
now + static_config.OAUTH_COOKIE_EXP,
171227
),
172228
)
229+
# we can't use a 301/302 redirect because we need to produce a GET
173230
res = web.Response(
174-
text=html_templates.authz_page(handle=handle),
231+
text=html_templates.redirect("/oauth/authorize"),
175232
content_type="text/html",
176233
headers=WEBUI_HEADERS,
177234
)
178235
res.set_cookie(
179-
name="millipds-session",
236+
name="millipds-oauth-session",
180237
value=session_token,
181238
max_age=static_config.OAUTH_COOKIE_EXP,
239+
path="/oauth/authorize", # the only page that needs to see it
182240
secure=True, # prevents token from leaking over plaintext channels
183241
httponly=True, # prevents XSS from being able to steal tokens
184242
samesite="Strict", # mitigates CSRF
185243
)
186244
return res
187245
except:
188-
# TODO: error
189246
return web.Response(
190247
text=html_templates.authn_page(
191248
identifier_hint=form_identifier,
@@ -271,7 +328,7 @@ async def oauth_pushed_authorization_request(request: web.Request):
271328

272329
# TODO: request client metadata???
273330

274-
# TODO: we need to store the request somewhere, and associate it with the URI we return
331+
# TODO: we need to store the request somewhere, and associate it with the URI we return (and also the DPoP key)
275332

276333
return web.json_response(
277334
{

src/millipds/html_templates.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def authz_page(handle) -> str:
176176
<li>deprecate your dependencies</li>
177177
</ul>
178178
<p>this is just a UI test, it doesn't actually do anything yet.</p>
179-
<form action="/oauth/foobar" method="POST">
179+
<form action="" method="POST">
180180
<input type="submit" value="authorize">
181181
</form>"""
182182

@@ -195,6 +195,23 @@ def error_page(msg: str) -> str:
195195
return AUTH_PANEL_HEAD + error_body + AUTH_PANEL_TAIL
196196

197197

198+
def redirect(location: str) -> str:
199+
# TODO: consider CSS?
200+
redirect_html: html = f"""\
201+
<!DOCTYPE html>
202+
<html>
203+
<head>
204+
<meta charset="UTF-8">
205+
<meta http-equiv="refresh" content="0; url={escape(location)}" />
206+
</head>
207+
<body>
208+
<p>You are being redirected...</p>
209+
<p><a href="{escape(location)}">Click here</a> if it didn't work.</p>
210+
</body>
211+
</html>"""
212+
return redirect_html
213+
214+
198215
if __name__ == "__main__":
199216
with open("authn_test.html", "w") as authn:
200217
authn.write(authn_page())

0 commit comments

Comments
 (0)