@oxyhq/core 5.5.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +7 -4
- package/dist/cjs/OxyServices.base.js +7 -102
- package/dist/cjs/boot/coldBootV2.js +350 -0
- package/dist/cjs/boot/deviceBootReturn.js +152 -0
- package/dist/cjs/crypto/keyManager.js +95 -0
- package/dist/cjs/i18n/locales/en-US.json +13 -1
- package/dist/cjs/i18n/locales/es-ES.json +13 -1
- package/dist/cjs/i18n/locales/locales/en-US.json +13 -1
- package/dist/cjs/i18n/locales/locales/es-ES.json +13 -1
- package/dist/cjs/index.js +54 -45
- package/dist/cjs/mixins/OxyServices.accounts.js +13 -26
- package/dist/cjs/mixins/OxyServices.auth.js +66 -201
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +119 -0
- package/dist/cjs/mixins/index.js +8 -17
- package/dist/cjs/server/index.js +2 -2
- package/dist/cjs/session/SessionClient.js +40 -1
- package/dist/cjs/session/authStateStore.js +284 -0
- package/dist/cjs/session/refresh.js +264 -0
- package/dist/cjs/shared/utils/debugUtils.js +3 -3
- package/dist/cjs/utils/accountUtils.js +1 -55
- package/dist/cjs/utils/authWebUrl.js +10 -39
- package/dist/cjs/utils/coldBoot.js +10 -8
- package/dist/cjs/utils/platform.js +19 -0
- package/dist/cjs/utils/registrableApex.js +49 -0
- package/dist/cjs/utils/ssoBounce.js +15 -362
- package/dist/cjs/utils/validationUtils.js +57 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +7 -4
- package/dist/esm/OxyServices.base.js +7 -102
- package/dist/esm/boot/coldBootV2.js +344 -0
- package/dist/esm/boot/deviceBootReturn.js +146 -0
- package/dist/esm/crypto/keyManager.js +95 -0
- package/dist/esm/i18n/locales/en-US.json +13 -1
- package/dist/esm/i18n/locales/es-ES.json +13 -1
- package/dist/esm/i18n/locales/locales/en-US.json +13 -1
- package/dist/esm/i18n/locales/locales/es-ES.json +13 -1
- package/dist/esm/index.js +34 -17
- package/dist/esm/mixins/OxyServices.accounts.js +13 -26
- package/dist/esm/mixins/OxyServices.auth.js +66 -201
- package/dist/esm/mixins/OxyServices.deviceBoot.js +116 -0
- package/dist/esm/mixins/index.js +8 -17
- package/dist/esm/server/index.js +1 -1
- package/dist/esm/session/SessionClient.js +40 -1
- package/dist/esm/session/authStateStore.js +278 -0
- package/dist/esm/session/refresh.js +257 -0
- package/dist/esm/shared/utils/debugUtils.js +3 -3
- package/dist/esm/utils/accountUtils.js +0 -53
- package/dist/esm/utils/authWebUrl.js +9 -37
- package/dist/esm/utils/coldBoot.js +10 -8
- package/dist/esm/utils/platform.js +18 -0
- package/dist/esm/utils/registrableApex.js +46 -0
- package/dist/esm/utils/ssoBounce.js +14 -345
- package/dist/esm/utils/validationUtils.js +56 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +14 -1
- package/dist/types/OxyServices.base.d.ts +0 -52
- package/dist/types/OxyServices.d.ts +0 -25
- package/dist/types/boot/coldBootV2.d.ts +76 -0
- package/dist/types/boot/deviceBootReturn.d.ts +83 -0
- package/dist/types/crypto/keyManager.d.ts +21 -0
- package/dist/types/index.d.ts +14 -21
- package/dist/types/mixins/OxyServices.accounts.d.ts +24 -29
- package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
- package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
- package/dist/types/mixins/OxyServices.assets.d.ts +0 -2
- package/dist/types/mixins/OxyServices.auth.d.ts +35 -77
- package/dist/types/mixins/OxyServices.civic.d.ts +0 -2
- package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
- package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +110 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
- package/dist/types/mixins/OxyServices.features.d.ts +0 -2
- package/dist/types/mixins/OxyServices.identity.d.ts +0 -2
- package/dist/types/mixins/OxyServices.language.d.ts +0 -2
- package/dist/types/mixins/OxyServices.links.d.ts +0 -2
- package/dist/types/mixins/OxyServices.location.d.ts +0 -2
- package/dist/types/mixins/OxyServices.nodes.d.ts +0 -2
- package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
- package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
- package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
- package/dist/types/mixins/OxyServices.security.d.ts +0 -2
- package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
- package/dist/types/mixins/OxyServices.user.d.ts +0 -2
- package/dist/types/mixins/OxyServices.utility.d.ts +0 -2
- package/dist/types/mixins/index.d.ts +5 -9
- package/dist/types/models/interfaces.d.ts +0 -67
- package/dist/types/models/session.d.ts +4 -5
- package/dist/types/server/index.d.ts +1 -1
- package/dist/types/session/SessionClient.d.ts +26 -1
- package/dist/types/session/authStateStore.d.ts +119 -0
- package/dist/types/session/refresh.d.ts +93 -0
- package/dist/types/shared/utils/debugUtils.d.ts +3 -3
- package/dist/types/utils/accountUtils.d.ts +2 -17
- package/dist/types/utils/authWebUrl.d.ts +9 -35
- package/dist/types/utils/coldBoot.d.ts +17 -14
- package/dist/types/utils/platform.d.ts +14 -0
- package/dist/types/utils/registrableApex.d.ts +31 -0
- package/dist/types/utils/ssoBounce.d.ts +14 -280
- package/dist/types/utils/validationUtils.d.ts +15 -0
- package/package.json +2 -2
- package/src/HttpService.ts +20 -4
- package/src/OxyServices.base.ts +7 -112
- package/src/OxyServices.ts +0 -38
- package/src/boot/__tests__/coldBootV2.test.ts +317 -0
- package/src/boot/__tests__/deviceBootReturn.test.ts +158 -0
- package/src/boot/coldBootV2.ts +426 -0
- package/src/boot/deviceBootReturn.ts +195 -0
- package/src/crypto/__tests__/sharedDeviceToken.test.ts +24 -0
- package/src/crypto/keyManager.ts +101 -0
- package/src/i18n/locales/en-US.json +13 -1
- package/src/i18n/locales/es-ES.json +13 -1
- package/src/index.ts +75 -65
- package/src/mixins/OxyServices.accounts.ts +27 -39
- package/src/mixins/OxyServices.auth.ts +78 -253
- package/src/mixins/OxyServices.deviceBoot.ts +146 -0
- package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +107 -0
- package/src/mixins/__tests__/accounts.test.ts +17 -44
- package/src/mixins/__tests__/passwordSignIn.test.ts +91 -0
- package/src/mixins/index.ts +10 -23
- package/src/models/interfaces.ts +0 -79
- package/src/models/session.ts +4 -5
- package/src/server/index.ts +1 -1
- package/src/session/SessionClient.ts +53 -2
- package/src/session/__tests__/SessionClient.additive.test.ts +92 -0
- package/src/session/__tests__/SessionClient.rest.test.ts +25 -0
- package/src/session/__tests__/SessionClient.state.test.ts +18 -5
- package/src/session/__tests__/authStateStore.test.ts +209 -0
- package/src/session/__tests__/refresh.test.ts +256 -0
- package/src/session/authStateStore.ts +335 -0
- package/src/session/refresh.ts +334 -0
- package/src/shared/utils/debugUtils.ts +3 -3
- package/src/utils/__tests__/authWebUrl.test.ts +5 -40
- package/src/utils/__tests__/registrableApex.test.ts +62 -0
- package/src/utils/__tests__/validationUtils.test.ts +30 -0
- package/src/utils/accountUtils.ts +2 -65
- package/src/utils/authWebUrl.ts +9 -39
- package/src/utils/coldBoot.ts +17 -14
- package/src/utils/platform.ts +21 -0
- package/src/utils/registrableApex.ts +45 -0
- package/src/utils/ssoBounce.ts +14 -393
- package/src/utils/validationUtils.ts +62 -0
- package/dist/cjs/AuthManager.js +0 -1110
- package/dist/cjs/AuthManagerTypes.js +0 -13
- package/dist/cjs/CrossDomainAuth.js +0 -206
- package/dist/cjs/mixins/OxyServices.fedcm.js +0 -823
- package/dist/cjs/mixins/OxyServices.redirect.js +0 -95
- package/dist/cjs/mixins/OxyServices.silent.js +0 -204
- package/dist/cjs/mixins/OxyServices.sso.js +0 -208
- package/dist/cjs/utils/fapiAutoDetect.js +0 -99
- package/dist/cjs/utils/ssoEstablish.js +0 -110
- package/dist/cjs/utils/ssoReturn.js +0 -275
- package/dist/esm/AuthManager.js +0 -1105
- package/dist/esm/AuthManagerTypes.js +0 -12
- package/dist/esm/CrossDomainAuth.js +0 -201
- package/dist/esm/mixins/OxyServices.fedcm.js +0 -821
- package/dist/esm/mixins/OxyServices.redirect.js +0 -92
- package/dist/esm/mixins/OxyServices.silent.js +0 -202
- package/dist/esm/mixins/OxyServices.sso.js +0 -204
- package/dist/esm/utils/fapiAutoDetect.js +0 -95
- package/dist/esm/utils/ssoEstablish.js +0 -107
- package/dist/esm/utils/ssoReturn.js +0 -271
- package/dist/types/AuthManager.d.ts +0 -380
- package/dist/types/AuthManagerTypes.d.ts +0 -81
- package/dist/types/CrossDomainAuth.d.ts +0 -164
- package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -331
- package/dist/types/mixins/OxyServices.redirect.d.ts +0 -92
- package/dist/types/mixins/OxyServices.silent.d.ts +0 -132
- package/dist/types/mixins/OxyServices.sso.d.ts +0 -138
- package/dist/types/utils/fapiAutoDetect.d.ts +0 -56
- package/dist/types/utils/ssoEstablish.d.ts +0 -85
- package/dist/types/utils/ssoReturn.d.ts +0 -156
- package/src/AuthManager.ts +0 -1269
- package/src/AuthManagerTypes.ts +0 -86
- package/src/CrossDomainAuth.ts +0 -243
- package/src/__tests__/authManager.cookiePath.test.ts +0 -390
- package/src/__tests__/authManager.security.test.ts +0 -377
- package/src/__tests__/crossDomainAuth.test.ts +0 -116
- package/src/__tests__/establishDeviceRefreshSlot.test.ts +0 -221
- package/src/mixins/OxyServices.fedcm.ts +0 -1026
- package/src/mixins/OxyServices.redirect.ts +0 -122
- package/src/mixins/OxyServices.silent.ts +0 -272
- package/src/mixins/OxyServices.sso.ts +0 -261
- package/src/mixins/__tests__/constructorAuthWebUrl.test.ts +0 -85
- package/src/mixins/__tests__/fedcm.test.ts +0 -667
- package/src/mixins/__tests__/sessionBaseUrl.test.ts +0 -61
- package/src/mixins/__tests__/silent.test.ts +0 -102
- package/src/mixins/__tests__/sso.test.ts +0 -228
- package/src/utils/__tests__/consumeSsoReturn.test.ts +0 -816
- package/src/utils/__tests__/fapiAutoDetect.test.ts +0 -183
- package/src/utils/__tests__/ssoBounce.test.ts +0 -219
- package/src/utils/__tests__/ssoEstablish.test.ts +0 -204
- package/src/utils/__tests__/ssoReturn.test.ts +0 -276
- package/src/utils/fapiAutoDetect.ts +0 -91
- package/src/utils/ssoEstablish.ts +0 -174
- package/src/utils/ssoReturn.ts +0 -389
package/src/utils/ssoReturn.ts
DELETED
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parse the SSO return fragment delivered by the central IdP.
|
|
3
|
-
*
|
|
4
|
-
* After a top-level redirect bounce to `auth.oxy.so/sso` (prompt=none), the
|
|
5
|
-
* central IdP returns the Relying Party to its `redirect_uri` with the result
|
|
6
|
-
* encoded in the URL fragment (the `#…` part). The fragment is used — not a
|
|
7
|
-
* query string — so the opaque single-use code never reaches a server access
|
|
8
|
-
* log, a `Referer` header, or browser history in a recoverable form.
|
|
9
|
-
*
|
|
10
|
-
* Three outcomes are possible:
|
|
11
|
-
* - `#oxy_sso=ok&code=<opaque>&state=<state>` — the IdP had a session; the RP
|
|
12
|
-
* exchanges `code` (via `oxy.exchangeSsoCode`) for the real session. NO
|
|
13
|
-
* token/JWT ever appears in the URL — only the opaque code.
|
|
14
|
-
* - `#oxy_sso=none&state=<state>` — the IdP had no session (prompt=none, user
|
|
15
|
-
* not signed in centrally). The RP shows its own signed-out UI.
|
|
16
|
-
* - `#oxy_sso=error&state=<state>` — the bounce failed. The RP recovers.
|
|
17
|
-
*
|
|
18
|
-
* This parser is pure and defensive: it never throws, and `kind` is strictly
|
|
19
|
-
* one of `'ok' | 'none' | 'error'`. It returns `null` when the fragment is not
|
|
20
|
-
* an oxy_sso fragment at all (i.e. `oxy_sso` is absent or an unrecognised
|
|
21
|
-
* value), so the caller can ignore unrelated fragments without special-casing.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import type { SessionLoginResponse } from '../models/session';
|
|
25
|
-
import {
|
|
26
|
-
SSO_CALLBACK_PATH,
|
|
27
|
-
ssoStateKey,
|
|
28
|
-
ssoGuardKey,
|
|
29
|
-
ssoDestKey,
|
|
30
|
-
ssoNoSessionKey,
|
|
31
|
-
ssoAttemptedKey,
|
|
32
|
-
ssoCallbackBootstrapKey,
|
|
33
|
-
} from './ssoBounce';
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The recognised outcomes of an SSO bounce.
|
|
37
|
-
*/
|
|
38
|
-
export type SsoReturnKind = 'ok' | 'none' | 'error';
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* The parsed result of an SSO return fragment.
|
|
42
|
-
*
|
|
43
|
-
* `code` is present only for `kind: 'ok'`. `state` echoes the CSRF state the RP
|
|
44
|
-
* generated for the bounce (when the IdP round-tripped it). `reason` is present
|
|
45
|
-
* only for a NON-`ok` outcome when the IdP supplied one.
|
|
46
|
-
*/
|
|
47
|
-
export interface SsoReturnResult {
|
|
48
|
-
kind: SsoReturnKind;
|
|
49
|
-
code?: string;
|
|
50
|
-
state?: string;
|
|
51
|
-
/**
|
|
52
|
-
* Machine-readable reason accompanying a NON-`ok` outcome, when the central
|
|
53
|
-
* IdP supplies one (e.g. `no_cookie` | `stale_session` | `no_grant` |
|
|
54
|
-
* `no_grant_establish` on a `none` bounce). Purely informational: it lets a
|
|
55
|
-
* Relying Party surface WHY a silent probe returned no session (e.g. to brand
|
|
56
|
-
* a "sign in" screen) without re-deriving it. Absent on `ok`, and whenever the
|
|
57
|
-
* IdP did not include a `reason` param.
|
|
58
|
-
*/
|
|
59
|
-
reason?: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const VALID_KINDS: ReadonlySet<string> = new Set<SsoReturnKind>(['ok', 'none', 'error']);
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Parse an SSO return fragment.
|
|
66
|
-
*
|
|
67
|
-
* @param hash - The URL fragment, with or without the leading `#`
|
|
68
|
-
* (e.g. `location.hash`). May be `undefined`/empty.
|
|
69
|
-
* @returns The parsed result when `hash` is a recognised oxy_sso fragment,
|
|
70
|
-
* otherwise `null`. Never throws.
|
|
71
|
-
*/
|
|
72
|
-
export function parseSsoReturnFragment(hash: string | undefined | null): SsoReturnResult | null {
|
|
73
|
-
if (typeof hash !== 'string' || hash.length === 0) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Strip a single leading '#'. A bare '#' (empty fragment) yields no params.
|
|
78
|
-
const raw = hash.startsWith('#') ? hash.slice(1) : hash;
|
|
79
|
-
if (raw.length === 0) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
let params: URLSearchParams;
|
|
84
|
-
try {
|
|
85
|
-
params = new URLSearchParams(raw);
|
|
86
|
-
} catch {
|
|
87
|
-
// URLSearchParams does not throw for malformed input in practice, but guard
|
|
88
|
-
// against any environment/polyfill that might so this stays total.
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const kind = params.get('oxy_sso');
|
|
93
|
-
if (kind === null || !VALID_KINDS.has(kind)) {
|
|
94
|
-
// Not an oxy_sso fragment (absent or unrecognised value) — ignore it.
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const result: SsoReturnResult = { kind: kind as SsoReturnKind };
|
|
99
|
-
|
|
100
|
-
const state = params.get('state');
|
|
101
|
-
if (state !== null && state.length > 0) {
|
|
102
|
-
result.state = state;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// The opaque code is only meaningful on success; ignore any stray `code` on
|
|
106
|
-
// none/error so callers never attempt an exchange for a non-ok outcome.
|
|
107
|
-
if (result.kind === 'ok') {
|
|
108
|
-
const code = params.get('code');
|
|
109
|
-
if (code !== null && code.length > 0) {
|
|
110
|
-
result.code = code;
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
// A machine-readable reason accompanies a NON-`ok` outcome when the IdP
|
|
114
|
-
// supplies one. Success carries no reason, so it is only read here.
|
|
115
|
-
const reason = params.get('reason');
|
|
116
|
-
if (reason !== null && reason.length > 0) {
|
|
117
|
-
result.reason = reason;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return result;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Injectable dependencies for {@link consumeSsoReturn}.
|
|
126
|
-
*
|
|
127
|
-
* Every web seam (storage, location, history, web-detection) is injectable so
|
|
128
|
-
* the function is fully unit-testable with fakes and so SSR / native callers
|
|
129
|
-
* can supply their own (or rely on the defaults, which resolve to `window.*`
|
|
130
|
-
* only when a browser is present). Defaults are evaluated lazily inside
|
|
131
|
-
* `consumeSsoReturn` so importing this module never touches `window`.
|
|
132
|
-
*/
|
|
133
|
-
export interface ConsumeSsoReturnDeps {
|
|
134
|
-
/** Per-tab SSO state store. Default: `window.sessionStorage`. */
|
|
135
|
-
storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
|
|
136
|
-
/** The current location. Default: `window.location`. */
|
|
137
|
-
location?: Pick<Location, 'hash' | 'origin' | 'pathname' | 'search'>;
|
|
138
|
-
/** History API for fragment stripping / dest restore. Default: `window.history`. */
|
|
139
|
-
history?: Pick<History, 'replaceState'>;
|
|
140
|
-
/**
|
|
141
|
-
* Whether the current environment is a web browser with usable
|
|
142
|
-
* `sessionStorage`. Default: `typeof window !== 'undefined' && typeof
|
|
143
|
-
* window.sessionStorage !== 'undefined'`.
|
|
144
|
-
*/
|
|
145
|
-
isWeb?: () => boolean;
|
|
146
|
-
/**
|
|
147
|
-
* Optional debug hook invoked with the thrown error when the code exchange
|
|
148
|
-
* fails. NEVER rethrown — `consumeSsoReturn` is total. Default: no-op.
|
|
149
|
-
*/
|
|
150
|
-
onExchangeError?: (error: unknown) => void;
|
|
151
|
-
/**
|
|
152
|
-
* Notify URL-driven routers (Expo Router / React Navigation web) that the
|
|
153
|
-
* location changed via `history.replaceState`, which does NOT itself emit
|
|
154
|
-
* `popstate`. Default: dispatch a real `PopStateEvent` on `window` when
|
|
155
|
-
* present; no-op off-web. Called ONLY after a successful same-origin
|
|
156
|
-
* dest restore on the `ok` path (never when the dest is rejected/absent).
|
|
157
|
-
* NEVER throws.
|
|
158
|
-
*/
|
|
159
|
-
dispatchPopState?: () => void;
|
|
160
|
-
/**
|
|
161
|
-
* Hard, full-document navigation used to leave the internal callback path on
|
|
162
|
-
* every NON-`ok` outcome (`none`/`error`, state-mismatch, missing code,
|
|
163
|
-
* failed exchange, missing sessionId). A SOFT `history.replaceState` +
|
|
164
|
-
* synthetic `popstate` does NOT reliably make Expo Router / TanStack Router
|
|
165
|
-
* re-resolve away from the 404 they have already rendered for the
|
|
166
|
-
* unregistered callback route — so for these outcomes (where there is no
|
|
167
|
-
* in-memory session to preserve) a full navigation is both safe and
|
|
168
|
-
* guaranteed to clear the 404. Default: `window.location.replace(url)` when
|
|
169
|
-
* present; feature-detected end to end so it never throws off-web.
|
|
170
|
-
*/
|
|
171
|
-
hardRedirect?: (url: string) => void;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Consume an SSO return: the commit-free, security-critical kernel of the
|
|
176
|
-
* cross-domain SSO `sso-return` cold-boot step.
|
|
177
|
-
*
|
|
178
|
-
* This performs the CSRF/fragment/exchange/dest-restore/loop-breaker sequence
|
|
179
|
-
* and RETURNS the exchanged session (or `null`). It deliberately does NOT
|
|
180
|
-
* commit any UI/auth state — each provider commits its own way AROUND this
|
|
181
|
-
* (e.g. `@oxyhq/services` `OxyContext` calls its `handleWebSSOSession`,
|
|
182
|
-
* `@oxyhq/auth` `WebOxyProvider` updates its React state). Hoisting the kernel
|
|
183
|
-
* here keeps the two providers byte-for-byte identical on the parts that matter
|
|
184
|
-
* for security (state validation, fragment stripping order, loop prevention).
|
|
185
|
-
*
|
|
186
|
-
* Security/loop invariants (preserved exactly from both former copies):
|
|
187
|
-
* - The fragment is stripped via `history.replaceState` FIRST — before the
|
|
188
|
-
* exchange — so the opaque code never lingers in the URL, browser history,
|
|
189
|
-
* or a `Referer` header even if a later step throws.
|
|
190
|
-
* - `state` must match (CSRF). A mismatch or a missing code sets the
|
|
191
|
-
* NO_SESSION flag so `sso-bounce` is disabled (no rebounce loop).
|
|
192
|
-
* - `none`/`error` outcomes set BOTH the NO_SESSION flag and the
|
|
193
|
-
* outcome-independent attempted-flag (the load2 half of the loop proof).
|
|
194
|
-
* - A throwing exchange is caught, reported via `onExchangeError`, and
|
|
195
|
-
* treated exactly like "no session" (never loops, never rethrows).
|
|
196
|
-
* - On EVERY consumed outcome (ok, none, error, state-mismatch, no-code,
|
|
197
|
-
* failed-exchange, no-sessionId) — not just ok — if the page landed on
|
|
198
|
-
* {@link SSO_CALLBACK_PATH}, the user is taken to a same-origin TARGET so
|
|
199
|
-
* they are never stranded on the internal callback path (which is an
|
|
200
|
-
* unregistered route in every consumer router → a hard 404). The target is
|
|
201
|
-
* the stored DEST when it parses as same-origin (an attacker-planted
|
|
202
|
-
* cross-origin / protocol-relative dest is rejected), ELSE the app root
|
|
203
|
-
* (`origin + '/'`). The DEST key is removed unconditionally.
|
|
204
|
-
* - For the `ok` outcome the target is applied via a SOFT
|
|
205
|
-
* `history.replaceState` + synthetic `popstate` so the freshly exchanged
|
|
206
|
-
* in-memory session the provider is about to commit is preserved (no
|
|
207
|
-
* reload). `popstate` is dispatched only on the `ok` same-origin restore.
|
|
208
|
-
* - For every NON-`ok` outcome there is no in-memory session to preserve, and
|
|
209
|
-
* the consumer router has ALREADY synchronously rendered its 404 for the
|
|
210
|
-
* unregistered callback route — a soft replaceState+popstate does not
|
|
211
|
-
* reliably make it re-resolve. So these outcomes perform a HARD
|
|
212
|
-
* full-document navigation to the target (`hardRedirect`), which is both
|
|
213
|
-
* safe (nothing to lose) and guaranteed to clear the 404 in every router.
|
|
214
|
-
*
|
|
215
|
-
* Total: this function NEVER throws. Off-web it is a no-op returning `null`.
|
|
216
|
-
*
|
|
217
|
-
* @param oxy - The exchange surface (`oxyServices.exchangeSsoCode`).
|
|
218
|
-
* @param deps - Injectable web seams; see {@link ConsumeSsoReturnDeps}.
|
|
219
|
-
* @returns The exchanged session on success, otherwise `null`.
|
|
220
|
-
*/
|
|
221
|
-
export async function consumeSsoReturn(
|
|
222
|
-
oxy: { exchangeSsoCode: (code: string, state?: string) => Promise<SessionLoginResponse> },
|
|
223
|
-
deps: ConsumeSsoReturnDeps = {},
|
|
224
|
-
): Promise<SessionLoginResponse | null> {
|
|
225
|
-
const isWeb =
|
|
226
|
-
deps.isWeb ??
|
|
227
|
-
(() =>
|
|
228
|
-
typeof window !== 'undefined' &&
|
|
229
|
-
typeof window.sessionStorage !== 'undefined');
|
|
230
|
-
|
|
231
|
-
if (!isWeb()) {
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const storage = deps.storage ?? window.sessionStorage;
|
|
236
|
-
const location = deps.location ?? window.location;
|
|
237
|
-
const history = deps.history ?? window.history;
|
|
238
|
-
const onExchangeError = deps.onExchangeError;
|
|
239
|
-
|
|
240
|
-
// Default: emit a synthetic `popstate` so URL-driven routers re-sync after a
|
|
241
|
-
// `history.replaceState` (which does NOT emit `popstate` on its own). Feature-
|
|
242
|
-
// detected end to end so it never throws in any environment.
|
|
243
|
-
const dispatchPopState =
|
|
244
|
-
deps.dispatchPopState ??
|
|
245
|
-
(() => {
|
|
246
|
-
if (typeof window === 'undefined' || typeof window.dispatchEvent !== 'function') {
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
if (typeof PopStateEvent !== 'undefined') {
|
|
250
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
251
|
-
} else if (typeof Event !== 'undefined') {
|
|
252
|
-
window.dispatchEvent(new Event('popstate'));
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// Default: a hard, full-document navigation used to leave the callback path
|
|
257
|
-
// on non-`ok` outcomes. Feature-detected end to end so it never throws in any
|
|
258
|
-
// environment (SSR / native / a stubbed location without `replace`).
|
|
259
|
-
const hardRedirect =
|
|
260
|
-
deps.hardRedirect ??
|
|
261
|
-
((url: string) => {
|
|
262
|
-
if (
|
|
263
|
-
typeof window !== 'undefined' &&
|
|
264
|
-
window.location &&
|
|
265
|
-
typeof window.location.replace === 'function'
|
|
266
|
-
) {
|
|
267
|
-
window.location.replace(url);
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
const ret = parseSsoReturnFragment(location.hash);
|
|
272
|
-
if (!ret) {
|
|
273
|
-
// Not an oxy_sso fragment — nothing to do (do NOT touch any flags).
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const origin = location.origin;
|
|
278
|
-
const callbackBootstrapKey = ssoCallbackBootstrapKey(origin);
|
|
279
|
-
const wasCallbackBootstrapped = storage.getItem(callbackBootstrapKey) === '1';
|
|
280
|
-
const expectedState = storage.getItem(ssoStateKey(origin));
|
|
281
|
-
const stateOk = !!ret.state && !!expectedState && ret.state === expectedState;
|
|
282
|
-
|
|
283
|
-
// Strip the fragment FIRST so the opaque code never lingers in the address
|
|
284
|
-
// bar, history, or a `Referer` — even if a later step throws.
|
|
285
|
-
history.replaceState(null, '', location.pathname + location.search);
|
|
286
|
-
storage.removeItem(ssoStateKey(origin));
|
|
287
|
-
|
|
288
|
-
// The in-flight bounce is now resolved — drop its guard so a later cold boot
|
|
289
|
-
// (e.g. after sign-out) can bounce again.
|
|
290
|
-
storage.removeItem(ssoGuardKey(origin));
|
|
291
|
-
|
|
292
|
-
const markNoSession = () => {
|
|
293
|
-
storage.setItem(ssoNoSessionKey(origin), '1');
|
|
294
|
-
// A return was consumed, so the probe definitively happened. Set the
|
|
295
|
-
// outcome-independent attempted-flag too so the bounce can never re-fire
|
|
296
|
-
// even if some consumer path skipped setting it pre-bounce.
|
|
297
|
-
storage.setItem(ssoAttemptedKey(origin), '1');
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// Compute the same-origin TARGET to leave the callback path for. Returns the
|
|
301
|
-
// stored DEST when present AND it parses as same-origin (never honour a
|
|
302
|
-
// cross-origin / protocol-relative dest that could have been planted to
|
|
303
|
-
// redirect the user), ELSE the app root (`origin + '/'`) so the user is never
|
|
304
|
-
// stranded on the internal callback path even when no dest was stored. The
|
|
305
|
-
// DEST key is removed unconditionally. Returns the relative path+search+hash
|
|
306
|
-
// (so it can be fed to either `history.replaceState` or a `hardRedirect`),
|
|
307
|
-
// or `null` when the page is not on the callback path (nothing to leave).
|
|
308
|
-
const consumeCallbackTarget = (): string | null => {
|
|
309
|
-
storage.removeItem(callbackBootstrapKey);
|
|
310
|
-
if (location.pathname !== SSO_CALLBACK_PATH && !wasCallbackBootstrapped) {
|
|
311
|
-
// Not on the callback path — still drop the dest key (consumed) but there
|
|
312
|
-
// is nothing to navigate away from.
|
|
313
|
-
storage.removeItem(ssoDestKey(origin));
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
316
|
-
const dest = storage.getItem(ssoDestKey(origin));
|
|
317
|
-
storage.removeItem(ssoDestKey(origin));
|
|
318
|
-
if (dest) {
|
|
319
|
-
try {
|
|
320
|
-
const destUrl = new URL(dest, origin);
|
|
321
|
-
if (destUrl.origin === origin) {
|
|
322
|
-
return destUrl.pathname + destUrl.search + destUrl.hash;
|
|
323
|
-
}
|
|
324
|
-
} catch {
|
|
325
|
-
// Malformed stored destination — fall through to the app-root fallback.
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
// No dest, a cross-origin/protocol-relative dest, or an unparseable dest:
|
|
329
|
-
// fall back to the app root so the router always leaves the 404.
|
|
330
|
-
return '/';
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
// Non-`ok` outcomes: there is no in-memory session to preserve, and the
|
|
334
|
-
// consumer router has already rendered its 404 for the unregistered callback
|
|
335
|
-
// route — a soft replaceState+popstate does not reliably make it re-resolve.
|
|
336
|
-
// Perform a HARD full-document navigation to the target (safe: nothing to
|
|
337
|
-
// lose; guaranteed: every router leaves the 404). Off the callback path this
|
|
338
|
-
// is a no-op (target is null).
|
|
339
|
-
const leaveCallbackHard = (): void => {
|
|
340
|
-
const target = consumeCallbackTarget();
|
|
341
|
-
if (target !== null) {
|
|
342
|
-
hardRedirect(origin + target);
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
if (ret.kind === 'none' || ret.kind === 'error') {
|
|
347
|
-
// The central IdP had no session (or the bounce failed). Record it so we do
|
|
348
|
-
// not bounce again this tab — the definitive loop breaker.
|
|
349
|
-
markNoSession();
|
|
350
|
-
leaveCallbackHard();
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (!stateOk || !ret.code) {
|
|
355
|
-
// Forged / replayed / stale fragment, or a malformed ok with no code. Treat
|
|
356
|
-
// exactly like "no session": never exchange, never loop.
|
|
357
|
-
markNoSession();
|
|
358
|
-
leaveCallbackHard();
|
|
359
|
-
return null;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
let session: SessionLoginResponse | undefined;
|
|
363
|
-
try {
|
|
364
|
-
session = await oxy.exchangeSsoCode(ret.code, ret.state);
|
|
365
|
-
} catch (error) {
|
|
366
|
-
onExchangeError?.(error);
|
|
367
|
-
markNoSession();
|
|
368
|
-
leaveCallbackHard();
|
|
369
|
-
return null;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (!session?.sessionId) {
|
|
373
|
-
markNoSession();
|
|
374
|
-
leaveCallbackHard();
|
|
375
|
-
return null;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// `ok`: the provider is about to commit the freshly exchanged in-memory
|
|
379
|
-
// session — do NOT hard-redirect (a full navigation would discard it). Use a
|
|
380
|
-
// SOFT `history.replaceState` to the target + a synthetic `popstate` so
|
|
381
|
-
// URL-driven routers re-sync to the restored route without a reload.
|
|
382
|
-
const target = consumeCallbackTarget();
|
|
383
|
-
if (target !== null) {
|
|
384
|
-
history.replaceState(null, '', target);
|
|
385
|
-
dispatchPopState();
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return session;
|
|
389
|
-
}
|