@oxyhq/core 2.3.1 → 2.3.2
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/utils/ssoReturn.js +54 -24
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/utils/ssoReturn.js +54 -24
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/utils/ssoReturn.d.ts +18 -4
- package/package.json +1 -1
- package/src/utils/__tests__/consumeSsoReturn.test.ts +196 -3
- package/src/utils/ssoReturn.ts +67 -27
|
@@ -94,10 +94,16 @@ function parseSsoReturnFragment(hash) {
|
|
|
94
94
|
* outcome-independent attempted-flag (the load2 half of the loop proof).
|
|
95
95
|
* - A throwing exchange is caught, reported via `onExchangeError`, and
|
|
96
96
|
* treated exactly like "no session" (never loops, never rethrows).
|
|
97
|
-
* -
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* DEST key is
|
|
97
|
+
* - On EVERY consumed outcome (ok, none, error, state-mismatch, no-code,
|
|
98
|
+
* failed-exchange, no-sessionId) — not just ok — if the page landed on
|
|
99
|
+
* {@link SSO_CALLBACK_PATH}, the real pre-bounce destination is restored
|
|
100
|
+
* from the DEST key so the user is never stranded on the internal callback
|
|
101
|
+
* path. Same-origin only (an attacker-planted cross-origin or relative-evil
|
|
102
|
+
* dest is rejected). The DEST key is removed unconditionally.
|
|
103
|
+
* - After a same-origin dest restore (which uses `history.replaceState`, that
|
|
104
|
+
* does NOT itself emit `popstate`), a synthetic `popstate` is dispatched so
|
|
105
|
+
* URL-driven routers (Expo Router / React Navigation web) re-sync to the
|
|
106
|
+
* restored route. It is NOT dispatched when the dest is rejected/absent.
|
|
101
107
|
*
|
|
102
108
|
* Total: this function NEVER throws. Off-web it is a no-op returning `null`.
|
|
103
109
|
*
|
|
@@ -116,6 +122,21 @@ async function consumeSsoReturn(oxy, deps = {}) {
|
|
|
116
122
|
const location = deps.location ?? window.location;
|
|
117
123
|
const history = deps.history ?? window.history;
|
|
118
124
|
const onExchangeError = deps.onExchangeError;
|
|
125
|
+
// Default: emit a synthetic `popstate` so URL-driven routers re-sync after a
|
|
126
|
+
// `history.replaceState` (which does NOT emit `popstate` on its own). Feature-
|
|
127
|
+
// detected end to end so it never throws in any environment.
|
|
128
|
+
const dispatchPopState = deps.dispatchPopState ??
|
|
129
|
+
(() => {
|
|
130
|
+
if (typeof window === 'undefined' || typeof window.dispatchEvent !== 'function') {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (typeof PopStateEvent !== 'undefined') {
|
|
134
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
135
|
+
}
|
|
136
|
+
else if (typeof Event !== 'undefined') {
|
|
137
|
+
window.dispatchEvent(new Event('popstate'));
|
|
138
|
+
}
|
|
139
|
+
});
|
|
119
140
|
const ret = parseSsoReturnFragment(location.hash);
|
|
120
141
|
if (!ret) {
|
|
121
142
|
// Not an oxy_sso fragment — nothing to do (do NOT touch any flags).
|
|
@@ -138,16 +159,42 @@ async function consumeSsoReturn(oxy, deps = {}) {
|
|
|
138
159
|
// even if some consumer path skipped setting it pre-bounce.
|
|
139
160
|
storage.setItem((0, ssoBounce_1.ssoAttemptedKey)(origin), '1');
|
|
140
161
|
};
|
|
162
|
+
// Restore the user's real pre-bounce destination so they are never stranded
|
|
163
|
+
// on the internal callback path — invoked on EVERY consumed outcome, not just
|
|
164
|
+
// success. Same-origin only — never honour a cross-origin/protocol-relative
|
|
165
|
+
// dest that could have been planted to redirect the user. The DEST key is
|
|
166
|
+
// removed unconditionally. After a successful same-origin restore a synthetic
|
|
167
|
+
// `popstate` is dispatched so URL-driven routers re-sync.
|
|
168
|
+
const restoreDest = () => {
|
|
169
|
+
if (location.pathname === ssoBounce_1.SSO_CALLBACK_PATH) {
|
|
170
|
+
const dest = storage.getItem((0, ssoBounce_1.ssoDestKey)(origin));
|
|
171
|
+
if (dest) {
|
|
172
|
+
try {
|
|
173
|
+
const destUrl = new URL(dest, origin);
|
|
174
|
+
if (destUrl.origin === origin) {
|
|
175
|
+
history.replaceState(null, '', destUrl.pathname + destUrl.search + destUrl.hash);
|
|
176
|
+
dispatchPopState();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Malformed stored destination — leave the URL on the callback path.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
storage.removeItem((0, ssoBounce_1.ssoDestKey)(origin));
|
|
185
|
+
};
|
|
141
186
|
if (ret.kind === 'none' || ret.kind === 'error') {
|
|
142
187
|
// The central IdP had no session (or the bounce failed). Record it so we do
|
|
143
188
|
// not bounce again this tab — the definitive loop breaker.
|
|
144
189
|
markNoSession();
|
|
190
|
+
restoreDest();
|
|
145
191
|
return null;
|
|
146
192
|
}
|
|
147
193
|
if (!stateOk || !ret.code) {
|
|
148
194
|
// Forged / replayed / stale fragment, or a malformed ok with no code. Treat
|
|
149
195
|
// exactly like "no session": never exchange, never loop.
|
|
150
196
|
markNoSession();
|
|
197
|
+
restoreDest();
|
|
151
198
|
return null;
|
|
152
199
|
}
|
|
153
200
|
let session;
|
|
@@ -157,31 +204,14 @@ async function consumeSsoReturn(oxy, deps = {}) {
|
|
|
157
204
|
catch (error) {
|
|
158
205
|
onExchangeError?.(error);
|
|
159
206
|
markNoSession();
|
|
207
|
+
restoreDest();
|
|
160
208
|
return null;
|
|
161
209
|
}
|
|
162
210
|
if (!session?.sessionId) {
|
|
163
211
|
markNoSession();
|
|
212
|
+
restoreDest();
|
|
164
213
|
return null;
|
|
165
214
|
}
|
|
166
|
-
|
|
167
|
-
// destination (captured at bounce time). Same-origin only — never honour a
|
|
168
|
-
// cross-origin destination that could have been planted to redirect the
|
|
169
|
-
// freshly signed-in user. `new URL(dest, origin)` tolerates relative dests
|
|
170
|
-
// and is still re-checked against the page origin.
|
|
171
|
-
if (location.pathname === ssoBounce_1.SSO_CALLBACK_PATH) {
|
|
172
|
-
const dest = storage.getItem((0, ssoBounce_1.ssoDestKey)(origin));
|
|
173
|
-
if (dest) {
|
|
174
|
-
try {
|
|
175
|
-
const destUrl = new URL(dest, origin);
|
|
176
|
-
if (destUrl.origin === origin) {
|
|
177
|
-
history.replaceState(null, '', destUrl.pathname + destUrl.search + destUrl.hash);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
// Malformed stored destination — leave the URL on the callback path.
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
storage.removeItem((0, ssoBounce_1.ssoDestKey)(origin));
|
|
215
|
+
restoreDest();
|
|
186
216
|
return session;
|
|
187
217
|
}
|