mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
fix(web): re-home hop peeks the post-login deep link instead of re-saving it
CodeQL flagged the re-save in AuthGate (js/clear-text-storage-of-sensitive-data, alert #82): the value flowed back into sessionStorage from the consumed return URL. The link only has to survive one more sign-in round trip, so the re-home path now reads it without clearing and never writes it again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import type { Route } from "next";
|
||||
import { trustedRoute } from "../../lib/routes";
|
||||
import { consumePostLoginReturnUrl, savePostLoginReturnUrl } from "../../lib/postLoginReturn";
|
||||
import { consumePostLoginReturnUrl, peekPostLoginReturnUrl } from "../../lib/postLoginReturn";
|
||||
import { hasOwnTenantOrPlatformRole, hasTenantReadScope } from "../../lib/tenantScope";
|
||||
import { portalHandoverUrl, shouldCrossOriginToPortal } from "../../lib/hostRouting";
|
||||
import { consumePendingRehome, getSelectedAuthApp, legacyConfigured, switchAuthApp, tryBeginRehome } from "../../lib/authApp";
|
||||
@@ -23,9 +23,19 @@ export function AuthGate() {
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && !isLoading && user) {
|
||||
// Always consume (read + clear) so a stale deep link can't misroute a later
|
||||
// sign-in; only honor it when the user's tenant is activated.
|
||||
const returnUrl = consumePostLoginReturnUrl();
|
||||
// Dual app-reg window: the sign-in that just completed ran on the other app than the
|
||||
// tenant is homed on (fresh browser, wrong default). Same-origin: re-home NOW — switch
|
||||
// the browser to the homed app and land on the target, where ProtectedRoute completes
|
||||
// the sign-in silently via the Entra session and returns here with the deep link intact.
|
||||
// One hop per tab; the pending request is consumed either way.
|
||||
const rehome = consumePendingRehome();
|
||||
const crossOrigin = shouldCrossOriginToPortal();
|
||||
const rehomeNow = rehome !== null && !crossOrigin && tryBeginRehome();
|
||||
// Normally consume (read + clear) so a stale deep link can't misroute a later
|
||||
// sign-in; only honor it when the user's tenant is activated. On the re-home hop only
|
||||
// PEEK: the link has to survive the extra sign-in round trip for the AuthGate pass that
|
||||
// follows, and leaving it in place beats re-saving it.
|
||||
const returnUrl = rehomeNow ? peekPostLoginReturnUrl() : consumePostLoginReturnUrl();
|
||||
let target: Route;
|
||||
if (isActivationPending) {
|
||||
target = "/activation";
|
||||
@@ -40,20 +50,13 @@ export function AuthGate() {
|
||||
} else {
|
||||
target = "/progress";
|
||||
}
|
||||
// Dual app-reg window: the sign-in that just completed ran on the other app than the
|
||||
// tenant is homed on (fresh browser, wrong default). Same-origin: re-home NOW — switch
|
||||
// the browser to the homed app and land on the target, where ProtectedRoute completes
|
||||
// the sign-in silently via the Entra session and returns here with the deep link intact.
|
||||
// One hop per tab; the pending request is consumed either way.
|
||||
const rehome = consumePendingRehome();
|
||||
// On the public host, hand over to the portal origin in ONE full-page
|
||||
// navigation instead of router.push + HostRoutingGuard bounce. Auth state
|
||||
// is per-origin — the portal side runs its own (silent) MSAL sign-in, on
|
||||
// the app this browser just learned (passed along as ?authapp=).
|
||||
if (shouldCrossOriginToPortal()) {
|
||||
if (crossOrigin) {
|
||||
window.location.href = portalHandoverUrl(target, legacyConfigured() ? getSelectedAuthApp() : null);
|
||||
} else if (rehome && tryBeginRehome()) {
|
||||
savePostLoginReturnUrl(target);
|
||||
} else if (rehomeNow) {
|
||||
trackEvent("auth_app_rehomed", { from: activeAuthApp, to: rehome });
|
||||
switchAuthApp(rehome, target);
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { savePostLoginReturnUrl, consumePostLoginReturnUrl } from "../postLoginReturn";
|
||||
import { savePostLoginReturnUrl, consumePostLoginReturnUrl, peekPostLoginReturnUrl } from "../postLoginReturn";
|
||||
|
||||
// Guards the deep-link-after-reauth contract: ProtectedRoute stashes the page the
|
||||
// user opened (e.g. a /sessions?id=… link in a new tab) before the MSAL login
|
||||
@@ -39,6 +39,14 @@ describe("postLoginReturn", () => {
|
||||
expect(consumePostLoginReturnUrl()).toBeNull();
|
||||
});
|
||||
|
||||
it("peek reads without clearing — the re-home hop leaves the deep link for the next pass", () => {
|
||||
savePostLoginReturnUrl("/sessions?id=abc-123");
|
||||
expect(peekPostLoginReturnUrl()).toBe("/sessions?id=abc-123");
|
||||
expect(peekPostLoginReturnUrl()).toBe("/sessions?id=abc-123");
|
||||
expect(consumePostLoginReturnUrl()).toBe("/sessions?id=abc-123");
|
||||
expect(peekPostLoginReturnUrl()).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores the bare root path", () => {
|
||||
savePostLoginReturnUrl("/");
|
||||
expect(consumePostLoginReturnUrl()).toBeNull();
|
||||
|
||||
@@ -36,6 +36,20 @@ export function savePostLoginReturnUrl(path: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the saved return URL WITHOUT clearing it. For the post-login re-home hop (AuthGate):
|
||||
* the deep link must survive one more sign-in round trip on the other app registration, so it
|
||||
* is left in place for the AuthGate pass that follows instead of being consumed and re-saved.
|
||||
*/
|
||||
export function peekPostLoginReturnUrl(): string | null {
|
||||
try {
|
||||
const value = window.sessionStorage.getItem(RETURN_URL_KEY);
|
||||
return isSafeReturnPath(value) ? value : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Read and clear the saved return URL. Returns null if absent or unsafe. */
|
||||
export function consumePostLoginReturnUrl(): string | null {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user