Add KeyCape PKCE browser sessions
This commit is contained in:
parent
654880ec91
commit
67ad2af640
6 changed files with 224 additions and 4 deletions
|
|
@ -19,6 +19,7 @@ from urllib.parse import parse_qs
|
|||
|
||||
from user_engine.domain import AccountStatus
|
||||
from user_engine.errors import AuthorizationDenied, ConflictError, NotFoundError, ValidationError
|
||||
from user_engine.oidc import OIDCClient, cookie_value
|
||||
from user_engine.service import UserEngineService
|
||||
|
||||
StartResponse = Callable[[str, list[tuple[str, str]]], Any]
|
||||
|
|
@ -48,6 +49,7 @@ class PortalApplication:
|
|||
trusted_proxy_secret: str,
|
||||
login_url: str,
|
||||
public_registration: bool = True,
|
||||
oidc_client: OIDCClient | None = None,
|
||||
) -> None:
|
||||
if len(trusted_proxy_secret) < 24:
|
||||
raise ValueError("trusted proxy secret must contain at least 24 characters")
|
||||
|
|
@ -55,12 +57,13 @@ class PortalApplication:
|
|||
self.trusted_proxy_secret = trusted_proxy_secret
|
||||
self.login_url = login_url
|
||||
self.public_registration = public_registration
|
||||
self.oidc_client = oidc_client
|
||||
|
||||
def __call__(self, environ: Mapping[str, Any], start_response: StartResponse) -> Iterable[bytes]:
|
||||
correlation_id = environ.get("HTTP_X_REQUEST_ID") or f"corr_{secrets.token_hex(12)}"
|
||||
try:
|
||||
return self._dispatch(environ, start_response, str(correlation_id))
|
||||
except (ValidationError, ConflictError) as exc:
|
||||
except (ValidationError, ConflictError, ValueError) as exc:
|
||||
return self._error(start_response, "400 Bad Request", "invalid_request", str(exc), correlation_id)
|
||||
except AuthorizationDenied:
|
||||
return self._error(start_response, "403 Forbidden", "access_denied", "Access denied.", correlation_id)
|
||||
|
|
@ -77,8 +80,35 @@ class PortalApplication:
|
|||
if path == "/readyz":
|
||||
report = self.service.readiness()
|
||||
return self._json(start_response, "200 OK" if report.ready else "503 Service Unavailable", _jsonable(report), correlation_id)
|
||||
if path == "/login":
|
||||
start_response("303 See Other", [("Location", self.login_url), *self._security_headers(correlation_id)])
|
||||
if path in {"/login", "/oidc/start"}:
|
||||
location = self.oidc_client.begin() if self.oidc_client else self.login_url
|
||||
start_response("303 See Other", [("Location", location), *self._security_headers(correlation_id)])
|
||||
return [b""]
|
||||
if path == "/oidc/callback":
|
||||
if self.oidc_client is None:
|
||||
raise NotFoundError("OIDC login is not configured")
|
||||
query = parse_qs(str(environ.get("QUERY_STRING", "")))
|
||||
if query.get("error"):
|
||||
raise AuthorizationDenied("OIDC login failed")
|
||||
session_id = self.oidc_client.complete(
|
||||
code=query.get("code", [""])[0],
|
||||
state=query.get("state", [""])[0],
|
||||
)
|
||||
headers = [
|
||||
("Location", "/"),
|
||||
("Set-Cookie", f"ue_session={session_id}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600"),
|
||||
*self._security_headers(correlation_id),
|
||||
]
|
||||
start_response("303 See Other", headers)
|
||||
return [b""]
|
||||
if path == "/logout" and method == "POST":
|
||||
session_id = cookie_value(str(environ.get("HTTP_COOKIE", "")), "ue_session")
|
||||
if session_id and self.oidc_client:
|
||||
self.oidc_client.logout(session_id)
|
||||
start_response(
|
||||
"303 See Other",
|
||||
[("Location", "/"), ("Set-Cookie", "ue_session=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0"), *self._security_headers(correlation_id)],
|
||||
)
|
||||
return [b""]
|
||||
if path == "/" and method == "GET":
|
||||
actor = self._optional_actor(environ)
|
||||
|
|
@ -133,6 +163,12 @@ class PortalApplication:
|
|||
return self._error(start_response, "404 Not Found", "not_found", "Resource not found.", correlation_id)
|
||||
|
||||
def _claims(self, environ: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
if self.oidc_client is not None:
|
||||
session_id = cookie_value(str(environ.get("HTTP_COOKIE", "")), "ue_session")
|
||||
if session_id:
|
||||
claims = self.oidc_client.claims(session_id)
|
||||
if claims is not None:
|
||||
return claims
|
||||
marker = str(environ.get("HTTP_X_USER_ENGINE_PROXY_SECRET", ""))
|
||||
if not secrets.compare_digest(marker, self.trusted_proxy_secret):
|
||||
raise AuthorizationDenied("untrusted identity source")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue