AID-I-008.005: Magic-Link & Authentication-Bearing Navigation Isolation
Pillars: app, data
Lifecycle: building, operation
Scope boundary
Owns brokered isolation of magic links, OAuth redirects and callbacks, password resets, invitations, email verification, and other authentication-bearing navigation in a dedicated context with issuer, audience, redirect, transaction, expiry, state, nonce, PKCE, replay, origin, and destruction checks. Raw links, codes, and tokens must not enter model context.
AID-I-008.003: AID-I-008.005 brokers authentication-bearing navigation and returns only a scoped session reference; AID-I-008.003 quarantines browser downloads and clipboard transfers. Treating a magic link as an ordinary download or clipboard item does not protect transaction binding, one-time replay state, redirect trust, or token secrecy.
AID-H-019: AID-I-008.005 owns the dedicated authentication-navigation state machine; AID-H-019 owns general URL admission and safe fetch outside authentication transitions. A URL that passes generic admission still requires brokered authentication checks when it carries or creates authority.
Implementation guidance
- AID-I-008.005-G001: Treat magic-link and auth-bearing link flows as approval-required authentication transitions in a dedicated browser context.
AID-I-008.005-G001
Authoritative transaction model
Do not decide that a link is safe by searching its path or query for words such as token or callback; encodings and redirects make that heuristic incomplete. Pattern matching may nominate a link for quarantine, but a root-owned authentication broker is authoritative. The broker resolves an opaque link handle, validates the expected issuer and one-time transaction, holds OAuth PKCE verifier or magic-link verification keys, and returns signed receipts. Raw token-bearing URLs never enter model context, prompts, ordinary logs, or evidence.
Dedicated-context authentication controller
The browser dependency below is a privileged lifecycle adapter that returns the real platform context handle plus a creation receipt and later enumerates and destroys the complete page/popup population. A caller-supplied label is never accepted as context identity.
// File: browser/auth_transition_controller.js
import { createHash } from 'node:crypto';
function digest(text) {
return 'sha256:' + createHash('sha256').update(text).digest('hex');
}
function canonicalOrigin(raw) {
const url = new URL(raw);
if (url.protocol !== 'https:' || url.username || url.password) {
throw new Error('authentication navigation must use credential-free HTTPS');
}
return url.origin;
}
export class AuthTransitionController {
constructor({browser, taskId, signedPolicy, broker, evidence}) {
if (
!taskId
|| signedPolicy.schema_version !== 'aidefend.auth-transition-policy.v1'
|| !signedPolicy.policy_sha256
) throw new Error('verified auth-transition policy required');
Object.assign(this, {
browser, taskId, policy: signedPolicy, broker, evidence
});
}
async complete({opaqueLinkHandle, expectedTransactionId}) {
// The injected browser object is a privileged lifecycle adapter, not a raw
// Playwright Browser. It allocates the context and attests its platform ID.
const allocation = await this.browser.allocateIsolatedContextAndVerify({
task_id: this.taskId,
transaction_id: expectedTransactionId,
options: {
storageState: undefined,
serviceWorkers: 'block',
permissions: [],
acceptDownloads: false
}
});
if (
!allocation
|| allocation.schema_version !== 'aidefend.browser-context-allocation.v1'
|| !allocation.context
|| !allocation.context_id
|| !/^sha256:[0-9a-f]{64}$/.test(
allocation.context_creation_receipt_sha256
)
) throw new Error('browser control plane did not attest the actual context');
const context = allocation.context;
const contextId = allocation.context_id;
let prepared = null;
let completion = null;
let destruction = null;
let brokerClosure = null;
let navigationUrl = '';
try {
prepared = await this.broker.prepareAndVerify({
task_id: this.taskId,
opaque_link_handle: opaqueLinkHandle,
expected_transaction_id: expectedTransactionId,
dedicated_context_id: contextId,
context_creation_receipt_sha256:
allocation.context_creation_receipt_sha256,
policy_sha256: this.policy.policy_sha256
});
if (
prepared.decision !== 'ALLOW'
|| prepared.transaction_id !== expectedTransactionId
|| prepared.dedicated_context_id !== contextId
|| prepared.context_creation_receipt_sha256
!== allocation.context_creation_receipt_sha256
|| !['oauth_authorization_code_pkce', 'signed_magic_link'].includes(
prepared.flow_type
)
) throw new Error('authentication broker denied or misbound transition');
navigationUrl = prepared.navigation_url;
const navigationSha256 = digest(navigationUrl);
if (navigationSha256 !== prepared.navigation_url_sha256) {
throw new Error('broker navigation URL digest mismatch');
}
const allowedOrigins = new Set(
prepared.allowed_navigation_origins.map(canonicalOrigin)
);
if (!allowedOrigins.has(canonicalOrigin(navigationUrl))) {
throw new Error('initial issuer origin not in signed transition');
}
await context.route('**/*', async (route) => {
const request = route.request();
// Enforce the signed origin boundary on navigation and every subresource.
// A trusted top-level page may still attempt script, image, iframe, or fetch egress.
let targetOrigin;
try {
targetOrigin = canonicalOrigin(request.url());
} catch {
await route.abort('blockedbyclient');
return;
}
if (!allowedOrigins.has(targetOrigin)) {
await route.abort('blockedbyclient');
return;
}
await route.fallback();
});
const page = await context.newPage();
await page.goto(navigationUrl, {waitUntil: 'domcontentloaded'});
navigationUrl = '';
await page.waitForURL(
(url) => digest(url.origin + url.pathname)
=== prepared.redirect_endpoint_sha256,
{timeout: this.policy.transition_timeout_ms}
);
let callbackUrl = page.url();
completion = await this.broker.completeAndVerify({
task_id: this.taskId,
transaction_id: expectedTransactionId,
prepare_receipt_jws: prepared.receipt_jws,
callback_url: callbackUrl,
callback_url_sha256: digest(callbackUrl),
dedicated_context_id: contextId,
policy_sha256: this.policy.policy_sha256
});
callbackUrl = '';
if (
completion.decision !== 'COMPLETED'
|| completion.transaction_id !== expectedTransactionId
|| completion.prepare_receipt_sha256 !== prepared.receipt_sha256
|| !completion.one_time_consumed
) throw new Error('authentication completion receipt invalid');
} finally {
// The browser adapter owns a durable, idempotent terminal record. It
// enumerates every page/popup in this context and confirms destruction.
destruction = await allocation.closeAndVerify({
reason: 'authentication transition completed',
completion_receipt_sha256: completion
? completion.receipt_sha256
: null
});
if (
destruction.state !== 'DESTROYED'
|| destruction.dedicated_context_id !== contextId
|| destruction.context_creation_receipt_sha256
!== allocation.context_creation_receipt_sha256
|| destruction.page_population_complete !== true
|| !/^sha256:[0-9a-f]{64}$/.test(
destruction.page_population_sha256
)
|| !Number.isSafeInteger(destruction.page_population_count)
|| destruction.page_population_count < 1
) throw new Error('actual browser context destruction is unresolved');
brokerClosure =
await this.broker.confirmContextDestroyedAndVerify({
task_id: this.taskId,
transaction_id: expectedTransactionId,
dedicated_context_id: contextId,
context_creation_receipt_sha256:
allocation.context_creation_receipt_sha256,
browser_destruction_receipt_sha256:
destruction.receipt_sha256,
completion_receipt_sha256: completion
? completion.receipt_sha256
: null
});
if (
brokerClosure.state !== 'DESTROYED'
|| brokerClosure.dedicated_context_id !== contextId
|| brokerClosure.browser_destruction_receipt_sha256
!== destruction.receipt_sha256
) throw new Error('broker and browser terminal receipts differ');
}
const event = await this.evidence.appendAndVerify({
schema_version: 'aidefend.auth-transition-event.v1',
control: 'AID-I-008.005',
task_id: this.taskId,
transaction_id: expectedTransactionId,
flow_type: prepared.flow_type,
issuer_origin: prepared.issuer_origin,
navigation_url_sha256: prepared.navigation_url_sha256,
redirect_endpoint_sha256: prepared.redirect_endpoint_sha256,
prepare_receipt_sha256: prepared.receipt_sha256,
completion_receipt_sha256: completion.receipt_sha256,
resulting_session_reference: completion.session_reference,
dedicated_context_id: contextId,
context_creation_receipt_sha256:
allocation.context_creation_receipt_sha256,
browser_destruction_receipt_sha256: destruction.receipt_sha256,
broker_destruction_receipt_sha256:
brokerClosure.receipt_sha256,
page_population_sha256: destruction.page_population_sha256,
page_population_count: destruction.page_population_count,
policy_sha256: this.policy.policy_sha256,
outcome: 'PASS'
});
return {
session_reference: completion.session_reference,
evidence_receipt_sha256: event.receipt_sha256
};
}
}Broker contract: for OAuth, the broker verifies the registered issuer, authorization endpoint, client and audience, exact redirect URI, state, nonce where used, PKCE S256 challenge, transaction expiry, and one-time state before returning the URL; it exchanges the authorization code server-side with the stored verifier. For a signed magic link, it verifies issuer key, audience, subject binding, expiry, intended action, redirect allowlist, and one-time token ID. The prepare, complete, browser-lifecycle, and broker-destruction receipts bind the task, transaction, link digest, flow, issuer, policy, actual platform context, complete page/popup population, and one-time ledger. Terminal state is durably retrievable by transaction ID after restart or evidence-delivery failure. The controller receives only a scoped session reference, never access or refresh tokens.
Verify safely
replay broker signatures and transaction state and test lookalike domain, redirect, encoding, token leakage, state or PKCE mismatch, replay, cancellation, and reuse.
Open-source tools
- Playwright (dedicated browser-context isolation and all-request origin interception; authentication verification remains broker-owned)
- oauth4webapi
- Authlib (OAuth 2.0 and OpenID Connect broker validation)
Commercial / hosted tools
- Cloudflare Browser Isolation (remote browser containment; authentication-transaction verification still requires a trusted broker)
Threat mappings
MITRE ATLAS
- AML.T0011.003 User Execution: Malicious Link
- AML.T0100 AI Agent Clickbait
- AML.T0113 Steal Web Session Cookie (dedicated authentication context keeps session material out of model context and shared browser state)
- AML.T0091.001 Use Alternate Authentication Material: Web Session Cookie (one-time transaction binding and context teardown prevent replay of authenticated browser state)
MAESTRO
- Integration Risks (L7)
OWASP LLM Top 10 2026
- LLM02:2026 Sensitive Information Disclosure (raw links, authorization codes, and tokens stay outside model context and logs)
- LLM03:2026 Excessive Agency (a trusted broker rather than the model controls authentication-bearing navigation)
- LLM10:2026 Improper Output Handling (authentication-bearing links are verified and brokered before navigation)
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- ASI02:2026 Tool Misuse and Exploitation
- ASI03:2026 Identity and Privilege Abuse
NIST Adversarial Machine Learning 2025
- NISTAML.039 Compromising connected resources
Cisco Integrated AI Security and Safety Framework
- AISubtech-12.1.3 Unsafe System / Browser / File Execution (authentication-bearing browser actions are isolated and broker-authorized)
- AISubtech-14.1.1 Credential Theft
- AISubtech-14.1.2 Insufficient Access Controls
- AISubtech-2.1.1 Execution Approval Bypass (current broker approval is required for the exact authentication transition)
- AISubtech-4.3.4 Replay Exploitation
- AISubtech-4.3.6 Cross-Origin Exploitation (exact broker-approved origins constrain authentication navigation and subresource transfers)
- AISubtech-8.2.3 Data Exfiltration via Agent Tooling (brokered authentication keeps link and token secrets outside model-visible browser data)
- AITech-12.1 Tool Exploitation (authentication-bearing browser actions are isolated and broker-authorized)
- AITech-2.1 Excessive Agency (current broker approval is required for the exact authentication transition)
- AITech-8.2 Data Exfiltration / Exposure (brokered authentication keeps link and token secrets outside model-visible browser data)
- AITech-8.3 Information Disclosure (brokered authentication keeps link and token secrets outside model-visible browser data)
Google Secure AI Framework 2.0 - Risks
- RA: Rogue Actions
Databricks AI Security Framework 3.0
- Agents - Core 13.3: Privilege Compromise
- Agents - Tools MCP Client 13.28: UI/UX Deception
- Agents - Tools MCP Client 13.34: Session and State Management Failures