Skip to content

Commit

Permalink
cookie request refactoring wip
Browse files Browse the repository at this point in the history
  • Loading branch information
byewokko committed Aug 16, 2024
1 parent c2aa03e commit 1876c77
Show file tree
Hide file tree
Showing 2 changed files with 304 additions and 192 deletions.
217 changes: 31 additions & 186 deletions seacatauth/cookie/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from .. import exceptions, AuditLogger
from .. import generic
from ..contextvars import AccessIps
from ..openidconnect.utils import TokenRequestErrorResponseCode

#
Expand Down Expand Up @@ -93,13 +94,13 @@ def __init__(self, app, cookie_svc, session_svc, credentials_svc):
web_app = app.WebContainer.WebApp
web_app.router.add_post("/nginx/introspect/cookie", self.nginx)
web_app.router.add_post("/nginx/introspect/cookie/anonymous", self.nginx_anonymous)
web_app.router.add_get("/cookie/entry", self.bouncer_get)
web_app.router.add_post("/cookie/entry", self.bouncer_post)
web_app.router.add_get("/cookie/entry", self.cookie_get)
web_app.router.add_post("/cookie/entry", self.cookie_post)

# Public endpoints
web_app_public = app.PublicWebContainer.WebApp
web_app_public.router.add_get("/cookie/entry", self.bouncer_get)
web_app_public.router.add_post("/cookie/entry", self.bouncer_post)
web_app_public.router.add_get("/cookie/entry", self.cookie_get)
web_app_public.router.add_post("/cookie/entry", self.cookie_post)

# TODO: Insecure, back-compat only - remove after 2024-03-31
if asab.Config.getboolean("seacatauth:introspection", "_enable_insecure_legacy_endpoints", fallback=False):
Expand Down Expand Up @@ -288,7 +289,7 @@ async def nginx_anonymous(self, request):
return response


async def bouncer_get(self, request):
async def cookie_get(self, request):
"""
Exchange authorization code for cookie and redirect to specified redirect URI.
Expand Down Expand Up @@ -320,10 +321,10 @@ async def bouncer_get(self, request):
type: string
"""
params = request.query
return await self._bouncer(request, params)
return await self._cookie(request, params)


async def bouncer_post(self, request):
async def cookie_post(self, request):
"""
Exchange authorization code for cookie and redirect to specified redirect URI.
Expand Down Expand Up @@ -355,132 +356,29 @@ async def bouncer_post(self, request):
- redirect_uri
"""
params = await request.post()
return await self._bouncer(request, params)
return await self._cookie(request, params)


async def _bouncer(self, request, parameters):
async def _cookie(self, request, parameters):
"""
Exchange authorization code for cookie and redirect to specified redirect URI.
"""
client_svc = self.App.get_service("seacatauth.ClientService")
from_ip = generic.get_request_access_ips(request)

client_id = parameters.get("client_id")
if client_id is None:
AuditLogger.log(
asab.LOG_NOTICE,
"Cookie request denied: No 'client_id' in request query",
struct_data={"from_ip": from_ip}
)
return asab.web.rest.json_response(
request, {"error": TokenRequestErrorResponseCode.InvalidRequest}, status=400)
try:
client = await client_svc.get(client_id)
except KeyError:
AuditLogger.log(
asab.LOG_NOTICE,
"Cookie request denied: Client not found",
struct_data={"from_ip": from_ip, "client_id": client_id}
)
return asab.web.rest.json_response(
request, {"error": TokenRequestErrorResponseCode.InvalidClient}, status=400)

grant_type = parameters.get("grant_type")
if grant_type != "authorization_code":
AuditLogger.log(
asab.LOG_NOTICE,
"Cookie request denied: Unsupported grant type",
struct_data={
"client_id": client_id,
"from_ip": from_ip,
"grant_type": grant_type,
}
)
return asab.web.rest.json_response(
request, {"error": TokenRequestErrorResponseCode.UnsupportedGrantType}, status=400)

# Use the code to get session ID
authorization_code = parameters.get("code")
if not authorization_code:
AuditLogger.log(
asab.LOG_NOTICE,
"Cookie request denied: No 'code' in request query",
struct_data={
"client_id": client_id,
"from_ip": from_ip,
}
)
return asab.web.rest.json_response(
request, {"error": TokenRequestErrorResponseCode.InvalidRequest}, status=400)
try:
session = await self.CookieService.get_session_by_authorization_code(authorization_code)
except KeyError:
AuditLogger.log(
asab.LOG_NOTICE,
"Cookie request denied: Invalid or expired authorization code",
struct_data={
"client_id": client_id,
"from_ip": from_ip,
}
)
return asab.web.rest.json_response(
request, {"error": TokenRequestErrorResponseCode.InvalidGrant}, status=400)

# Determine the destination URI
if "redirect_uri" in parameters:
# Use the redirect URI from request query
# TODO: Optionally validate the URI against client["redirect_uris"]
# and check if it is the same as in the authorization request
redirect_uri = parameters["redirect_uri"]
else:
# Fallback to client URI or Auth UI
redirect_uri = client.get("client_uri") or self.CookieService.AuthWebUiBaseUrl.rstrip("/")

# Set track ID if not set yet
if session.TrackId is None:
session = await self.SessionService.inherit_track_id_from_root(session)
if session.TrackId is None:
# Obtain the old session by request cookie or access token
try:
old_session = await self.CookieService.get_session_by_request_cookie(
request, session.OAuth2.ClientId)
except exceptions.SessionNotFoundError:
old_session = None
except exceptions.NoCookieError:
old_session = None

token_value = generic.get_bearer_token_value(request)
if old_session is None and token_value is not None:
try:
old_session = await self.CookieService.OpenIdConnectService.get_session_by_access_token(token_value)
except exceptions.SessionNotFoundError:
# Invalid access token should result in error
AuditLogger.log(
asab.LOG_NOTICE,
"Cookie request denied: Track ID transfer failed because of invalid Authorization header",
struct_data={
"cid": session.Credentials.Id,
"sid": session.Id,
"client_id": session.OAuth2.ClientId,
"from_ip": from_ip,
"redirect_uri": redirect_uri
}
)
return aiohttp.web.HTTPBadRequest()
try:
session = await self.SessionService.inherit_or_generate_new_track_id(session, old_session)
except ValueError as e:
# Return 400 to prevent disclosure while keeping the stacktrace
L.error("Failed to produce session track ID")
raise aiohttp.web.HTTPBadRequest() from e

session = await self.CookieService.extend_session_expiration(session, client)
for param in {"client_id", "grant_type", "code"}:
if param not in parameters:
AuditLogger.log(
asab.LOG_NOTICE,
"Cookie request denied: No '{}' in request query".format(param),
struct_data={"access_ips": AccessIps.get()}
)
return asab.web.rest.json_response(
request, {"error": TokenRequestErrorResponseCode.InvalidRequest}, status=400)

# Construct the response
if client.get("cookie_domain") not in (None, ""):
cookie_domain = client["cookie_domain"]
else:
cookie_domain = self.CookieService.RootCookieDomain
cookie, redirect_uri, client_headers = await self.CookieService.process_cookie_request(
request,
client_id=parameters["client_id"],
grant_type=parameters["grant_type"],
code=parameters["code"],
)

response = aiohttp.web.HTTPFound(
redirect_uri,
Expand All @@ -492,39 +390,17 @@ async def _bouncer(self, request, parameters):
text="<!doctype html>\n<html lang=\"en\">\n<head></head><body>...</body>\n</html>\n"
)

# TODO: Verify that the request came from the correct domain
# Add headers from webhook
response.headers.update(client_headers)

# Add Seacat Auth cookie
self.CookieService.set_session_cookie(
response=response,
cookie_value=session.Cookie.Id,
client_id=client_id,
cookie_domain=cookie_domain
client_id=parameters["client_id"],
cookie_value=cookie["value"],
cookie_domain=cookie.get("domain"),
)

# Trigger webhook and set custom client response headers
try:
data = await self._fetch_webhook_data(client, session)
if data is not None:
response.headers.update(data.get("response_headers", {}))
except exceptions.ClientResponseError as e:
L.log(asab.LOG_NOTICE, "Webhook responded with error", struct_data={
"status": e.Status, "text": e.Data})
AuditLogger.log(asab.LOG_NOTICE, "Cookie request denied: Webhook error", struct_data={
"cid": session.Credentials.Id,
"sid": session.Id,
"client_id": session.OAuth2.ClientId,
"from_ip": from_ip,
"redirect_uri": redirect_uri})
return asab.web.rest.json_response(
request, {"error": TokenRequestErrorResponseCode.InvalidRequest}, status=400)

AuditLogger.log(asab.LOG_NOTICE, "Cookie request granted", struct_data={
"cid": session.Credentials.Id,
"sid": session.Id,
"client_id": session.OAuth2.ClientId,
"from_ip": from_ip,
"redirect_uri": redirect_uri})

return response


Expand All @@ -542,34 +418,3 @@ async def _authenticate_request(self, request, client_id=None):
return None

return session


async def _fetch_webhook_data(self, client, session):
"""
Make a webhook request and return the response body.
The response should match the following schema:
```json
{
"type": "object",
"properties": {
"response_headers": {
"type": "object",
"description": "HTTP headers and their values that will be added to the response."
}
}
}
```
"""
cookie_webhook_uri = client.get("cookie_webhook_uri")
if cookie_webhook_uri is None:
return None
async with aiohttp.ClientSession() as http_session:
# TODO: Better serialization
userinfo = await self.CookieService.OpenIdConnectService.build_userinfo(session)
data = asab.web.rest.json.JSONDumper(pretty=False)(userinfo)
async with http_session.put(cookie_webhook_uri, data=data, headers={
"Content-Type": "application/json"}) as resp:
if resp.status != 200:
text = await resp.text()
raise exceptions.ClientResponseError(resp.status, text)
return await resp.json()
Loading

0 comments on commit 1876c77

Please sign in to comment.