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