🔒️(frontend) restrict transit_code exchange to embedded context

Only run the transit_code exchange flow when the app is loaded in an
embedded context (i.e. inside an iframe).

Combined with the CSP rules that will restrict which origins are
allowed to embed the app, this gives us a client-side lever to
control which integrations can actually use this authentication
path.
This commit is contained in:
lebaudantoine
2026-08-18 11:56:59 +02:00
parent a07a456149
commit 98626c5215
3 changed files with 29 additions and 4 deletions
@@ -1,6 +1,9 @@
import { fetchApi } from '@/api/fetchApi'
import { setAccessToken } from '@/stores/accessToken'
import { consumeTransitCodeFromFragment } from '../utils/transitCode'
import {
consumeTransitCodeFromFragment,
isEmbedded,
} from '../utils/transitCode'
type ApiAccessToken = {
access_token: string
@@ -28,6 +31,11 @@ const runInitialization = async (): Promise<void> => {
return
}
if (!isEmbedded()) {
console.warn('Transit code ignored outside an embedded context')
return
}
try {
const { access_token } = await exchangeAccessToken(code)
setAccessToken(access_token)
@@ -23,9 +23,13 @@ export const TransitCodeGate = ({
}) => {
const hash = useHash()
// Latch the decision on the initial hash: the bootstrap scrubs the
// fragment as soon as it starts, and the gate must not flip back to the
// fast path while the exchange is still in flight.
// Note: the exchange only happens in an embedding context. This check lives
// in initializeAccessTokenFromFragment, the single funnel for all bootstrap paths.
// The gate still mounts top-level to scrub the fragment, but bootstrap then resolves
// immediately without exchanging.
//
// Latch the decision on the initial hash: bootstrap scrubs it immediately, and the
// gate must not switch back to the fast path while the exchange is in flight.
const [needsExchange] = useState(() => hasTransitCodeInFragment(hash))
if (!needsExchange) {
@@ -1,5 +1,18 @@
const TRANSIT_CODE_FRAGMENT_PARAM = 'transit_code'
/**
* Whether the app is rendered inside an embedding context (iframe).
*
* Comparing window references never throws, even when the parent is
* cross-origin. Defaults to false outside a browser environment.
*/
export const isEmbedded = (): boolean => {
if (typeof window === 'undefined') {
return false
}
return window.self !== window.top
}
/**
* Whether a URL fragment carries a transit code. Pure check, does not
* consume anything.