Connect P04 audited recovery to fresh-MFA platform browser flow
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
parent
b2cce8dd7c
commit
58e07dd4df
7 changed files with 201 additions and 7 deletions
|
|
@ -78,6 +78,7 @@ class PortalApplication:
|
|||
public_registration: bool = True,
|
||||
mfa_management_url: str = "",
|
||||
oidc_client: OIDCClient | None = None,
|
||||
factor_recovery: Any = None,
|
||||
provisioning: IdentityProvisioningPort | None = None,
|
||||
tenant_management: TenantManagementPort | None = None,
|
||||
outbox_delivery: Callable[[Any], None] | None = None,
|
||||
|
|
@ -104,6 +105,7 @@ class PortalApplication:
|
|||
raise ValueError("MFA management URL must be a fixed HTTPS destination without credentials or query")
|
||||
self.mfa_management_url = mfa_management_url
|
||||
self.oidc_client = oidc_client
|
||||
self.factor_recovery = factor_recovery
|
||||
self.provisioning = provisioning
|
||||
self.tenant_management = tenant_management
|
||||
self.outbox_delivery = outbox_delivery
|
||||
|
|
@ -187,7 +189,7 @@ class PortalApplication:
|
|||
if tenant_hint is not None and not str(tenant_hint).startswith("tenant:"):
|
||||
raise ValidationError("tenant_hint must be a tenant identifier")
|
||||
location = (
|
||||
self.oidc_client.begin(tenant_hint=str(tenant_hint) if tenant_hint else None)
|
||||
self.oidc_client.begin(tenant_hint=str(tenant_hint) if tenant_hint else None, **({"recovery": True} if query.get("recovery") == ["1"] else {}))
|
||||
if self.oidc_client else self.login_url
|
||||
)
|
||||
start_response("303 See Other", [("Location", location), *self._security_headers(correlation_id)])
|
||||
|
|
@ -207,7 +209,7 @@ class PortalApplication:
|
|||
except (ValueError, URLError, OSError):
|
||||
return self._redirect(start_response, "/access-recovery", correlation_id)
|
||||
headers = [
|
||||
("Location", "/"),
|
||||
("Location", "/platform/factor-recovery" if self.oidc_client.sessions[session_id].recovery else "/"),
|
||||
("Set-Cookie", f"ue_session={session_id}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600"),
|
||||
*self._security_headers(correlation_id),
|
||||
]
|
||||
|
|
@ -753,6 +755,34 @@ class PortalApplication:
|
|||
"status": "removed", "tenant_account": _jsonable(account),
|
||||
"provider_identity_removed": False,
|
||||
}, correlation_id)
|
||||
if path == "/platform/factor-recovery" and method in {"GET", "POST"}:
|
||||
from user_engine.factor_recovery import fresh, page
|
||||
actor = self._actor(environ)
|
||||
if "platform-operator" not in actor.roles:
|
||||
raise AuthorizationDenied("platform operator required")
|
||||
csrf = self._csrf_token(environ)
|
||||
submitted = self._form_body(environ) if method == "POST" else {}
|
||||
if method == "POST":
|
||||
self._require_csrf(environ, submitted.get("csrf_token", ""))
|
||||
session_id = cookie_value(str(environ.get("HTTP_COOKIE", "")), "ue_session")
|
||||
session = self.oidc_client.sessions.get(session_id or "")
|
||||
result = None
|
||||
if not session or not session.id_token or not fresh(session.claims):
|
||||
result = {"failure": "fresh_platform_mfa_required"}
|
||||
elif self.factor_recovery is None:
|
||||
result = {"failure": "recovery_unavailable"}
|
||||
elif method == "POST":
|
||||
try:
|
||||
result = self.factor_recovery.call(session.id_token, {
|
||||
"action": submitted.get("action"), "user": submitted.get("user", ""),
|
||||
"reference": submitted.get("reference", ""),
|
||||
"confirmation": submitted.get("confirmation", ""),
|
||||
"identity_verified": submitted.get("identity_verified") == "yes",
|
||||
})
|
||||
except Exception:
|
||||
result = {"failure": "recovery_unavailable"}
|
||||
return self._html(start_response, self._page_html("Authenticator recovery",
|
||||
page(csrf, result, submitted, self.mfa_management_url)), correlation_id)
|
||||
if path == "/platform/activity":
|
||||
self.service.resolve_tenant_context(actor, PLATFORM_TENANT)
|
||||
if method != "GET":
|
||||
|
|
@ -1232,7 +1262,7 @@ class PortalApplication:
|
|||
rows = "".join(f'<tr><td>{escape(name)}</td><td>{"Configured; live health unverified" if configured else "Unavailable in this portal"}</td><td>{escape(help_text)}</td></tr>'
|
||||
for name, configured, help_text in capabilities)
|
||||
return ('<section><h2>Service capabilities</h2><table><thead><tr><th>Service</th><th>Known state</th><th>Recovery step</th></tr></thead><tbody>'
|
||||
+ rows + '</tbody></table><p>Authenticator recovery and authentication policy changes are unavailable in this portal. The sign-in service owner must verify factor lookup, recovery and policy enforcement. A configured adapter is not a health check.</p></section>')
|
||||
+ rows + '</tbody></table>' + ('<p><a href="/platform/factor-recovery">Recover a lost authenticator</a></p>' if self.factor_recovery else '<p>Authenticator recovery is unavailable in this portal.</p>') + '<p>Other authentication policy changes are unavailable in this portal. A configured adapter is not a health check.</p></section>')
|
||||
|
||||
def _require_setup_access(self, tenant: str, user_id: str) -> None:
|
||||
account = self.service.store.tenant_account(tenant, user_id)
|
||||
|
|
@ -2173,7 +2203,7 @@ Use the login name they provide; it may differ from your display name.</p></sect
|
|||
return
|
||||
links = '<a href="/">Home</a><a href="/onboarding">My account</a><a href="/security">Sign-in security</a>'
|
||||
if "platform-operator" in actor.roles:
|
||||
links += '<a href="/platform">Platform administration</a><a href="/platform/operations">Service recovery</a><a href="/platform/activity">Platform activity</a>'
|
||||
links += '<a href="/platform/factor-recovery">Authenticator recovery</a><a href="/platform">Platform administration</a><a href="/platform/operations">Service recovery</a><a href="/platform/activity">Platform activity</a>'
|
||||
elif "tenant-admin" in actor.roles:
|
||||
links += f'<a href="/admin/{escape(quote(actor.tenant, safe=""))}">Manage users</a>'
|
||||
session_id = cookie_value(str(environ.get("HTTP_COOKIE", "")), "ue_session")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue