DEV Community

Ivan Rossouw
Ivan Rossouw

Posted on AI-assisted

Hand Off Intent, Not Authority

Moving a user from one application host to another often looks harmless. A button opens a browser, the user continues, and the original client waits for them to return.

Architecturally, that is not merely navigation. It is a trust-boundary decision.

This becomes especially important when one user experience runs across several .NET hosts: perhaps an ASP.NET Core site, a Blazor WebAssembly client, and a .NET MAUI hybrid app. The shared components may look identical, but the hosts do not own the same secrets, sessions, callbacks, or security guarantees.

The lesson is simple: when a workflow crosses hosts, hand off intent, not authority.

The tempting shortcut

Suppose a client has already loaded an order-like resource and can display the next sensitive action. It is tempting to let every host render the same provider form or carry the current access proof into the browser.

That preserves continuity, but it also widens the boundary. A client that only needed to display state may now receive signed fields, transaction details, callback assumptions, or a reusable proof. Putting any of that into a query string or fragment creates more problems: URLs are copied, logged, cached, placed in browser history, exposed to extensions, and sometimes sent as referrers.

Shared UI does not imply shared authority. Reuse the presentation and state model where it helps, but make each host's capabilities explicit.

A narrower handoff

A safer design gives the less-trusted client one small capability: open a fixed public entry point on the authoritative host.

The destination should be deliberately boring. It should not contain an order reference, email address, one-time code, access token, signed provider fields, or a claimed outcome. It should have no dynamic query string or fragment. It communicates only this intent: “the user wants to continue this workflow on the web.”

The authoritative host then starts again from its own security context. It asks for the identifiers appropriate to the workflow, performs anti-forgery checks, verifies ownership through its normal mechanism, and only then exposes the sensitive action.

That separation gives each statement a precise meaning:

  • Opening the browser is navigation.
  • Proving ownership grants access in the destination host.
  • Starting the action is a separate command.
  • A callback or authoritative read confirms the outcome.

None of those statements is allowed to stand in for another.

Treat public configuration as an API

Installable browser clients need runtime configuration, but a generated client configuration document is still a public API. Do not serialize a server configuration tree and hope that only safe values happen to be present.

Build a small allowlist instead. For this kind of handoff, the browser might receive an enablement flag and a reviewed public origin. That is enough. Return the document with a no-store policy so a stale capability does not linger after an operational change.

Validate the destination before the application starts. Require the exact shape you intend: HTTPS, a root origin, no embedded credentials, no path, no query, no fragment, and no unexpected local or alternate-port destination. Keep the feature disabled when configuration is absent. If someone explicitly enables it with an invalid value, fail startup with a value-free error rather than echoing the bad input into logs.

Failing closed here is not pessimism. It turns configuration from ambient text into an enforceable security boundary.

Make the destination defensive too

The source client should generate a clean URL, but the destination should not trust that promise.

If someone arrives with a query string, canonicalize to the fixed entry before rendering. Apply no-store and a restrictive referrer policy. Do not pre-fill sensitive identifiers from URL input. Start with the ordinary ownership flow and its existing anti-forgery protection.

This matters because links escape their intended callers. They are bookmarked, pasted into messages, opened by scanners, and reconstructed by users. A robust boundary remains safe even when the source is not your application.

Failure must remain boring

A browser launch can fail for mundane reasons: platform policy, missing handlers, lifecycle changes, or an exception from the native shell. None of those failures should mutate domain state.

The client should report only that the launch did not happen. It should not create the sensitive form, mark work as started, grant access, or imply success. Concurrent clicks can be collapsed while an open attempt is running, but a failed attempt should be retryable.

This is a useful design test: if navigation failure requires compensation, the handoff probably carried too much authority.

Test the negative space

Most happy-path tests prove that a button eventually opens the right page. Boundary tests need to prove what cannot happen.

Useful tests include:

  • the capability is off by default;
  • malformed enablement and invalid destinations fail safely;
  • the client opens only the fixed path;
  • the URL contains no proof or private state;
  • each host renders only its intended action;
  • public configuration contains only allowlisted fields;
  • the web entry removes unexpected query data before rendering;
  • caching and referrer protections are present;
  • ownership and anti-forgery checks happen in the destination;
  • launch failure changes no workflow state; and
  • repeated clicks do not create duplicate launches.

These are negative properties. They are easy to lose during refactoring unless tests name them directly.

The trade-off is honest friction

Fresh ownership proof adds a step. A dedicated entry route, host-specific capability registration, and configuration validation add code. The boundary also needs maintenance as new hosts appear.

That cost buys something valuable: the sensitive operation stays with the host designed to own it. The other clients remain simpler, public configuration stays small, and failures have a narrow blast radius.

When reviewing any cross-host workflow, write down four things: the exact destination, the data allowed to cross, the authority that must be re-established, and the state a failed launch may change. If those answers are vague, the implementation probably is too.

Intent can cross a boundary. Authority should have to earn its way back in.

Top comments (0)