fix(auth): address PR #758 round-3 review
- Flow 2 (oauth_authorize_nextcloud) now generates a nonce, stores it on the oauth_session row, forwards it to the IdP, and verifies it via expected_nonce in oauth_callback_nextcloud — closes the last replay- protection gap (round-3 finding 1). - _origin_matches_self fails closed when mcp_server_url is missing instead of allowing the logout, and the diagnostic log is promoted from warning to error so the misconfiguration is monitorable (round-3 finding 2). New regression test pins the new behaviour. - The five user_id-accepting helpers in oauth_tools.py (get_provisioning_status, provision_nextcloud_access, revoke_nextcloud_access, check_provisioning_status, check_logged_in) are renamed with leading underscores to make the trust boundary structural rather than documentary (round-3 finding 3). - create_browser_session and delete_browser_session now emit audit_log rows so session establishment / teardown match the pattern used by the rest of the security-relevant storage operations (round-3 nit 5). delete_browser_session selects user_id before delete so the audit row is attributable. - oauth_login_callback no longer reflects raw IdP-error text or exception strings into the HTML failure page; users see a generic "internal error occurred" message + a correlation ID, with the detail logged server-side keyed by the same ID (round-3 nit 6). The XSS regression test is updated to pin the stricter contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
c33d52ea91
commit
9d0e7dcebe
@@ -1178,6 +1178,16 @@ class RefreshTokenStorage:
|
||||
ttl_seconds,
|
||||
)
|
||||
|
||||
# Audit log to match the pattern used by the other security-relevant
|
||||
# storage operations (PR #758 round-3 nit 5). Browser session
|
||||
# establishment is a security-relevant event.
|
||||
await self._audit_log(
|
||||
event="create_browser_session",
|
||||
user_id=user_id,
|
||||
resource_type="browser_session",
|
||||
resource_id=session_id[:8],
|
||||
)
|
||||
|
||||
async def get_browser_session_user(self, session_id: str) -> str | None:
|
||||
"""Look up the user_id bound to a browser session_id, or None.
|
||||
|
||||
@@ -1210,7 +1220,16 @@ class RefreshTokenStorage:
|
||||
if not self._initialized:
|
||||
await self.initialize()
|
||||
|
||||
# SELECT the row before DELETE so we can attribute the audit log
|
||||
# entry to the right user (PR #758 round-3 nit 5).
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
async with db.execute(
|
||||
"SELECT user_id FROM browser_sessions WHERE session_id = ?",
|
||||
(session_id,),
|
||||
) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
user_id = row[0] if row else None
|
||||
|
||||
cursor = await db.execute(
|
||||
"DELETE FROM browser_sessions WHERE session_id = ?", (session_id,)
|
||||
)
|
||||
@@ -1219,6 +1238,13 @@ class RefreshTokenStorage:
|
||||
|
||||
if deleted:
|
||||
logger.debug("Deleted browser session %s", session_id[:8])
|
||||
if user_id:
|
||||
await self._audit_log(
|
||||
event="delete_browser_session",
|
||||
user_id=user_id,
|
||||
resource_type="browser_session",
|
||||
resource_id=session_id[:8],
|
||||
)
|
||||
return deleted
|
||||
|
||||
async def cleanup_expired_browser_sessions(self) -> int:
|
||||
|
||||
Reference in New Issue
Block a user