@oxyhq/core 1.11.20 → 1.11.21
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/README.md +1 -1
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/mixins/OxyServices.auth.js +14 -1
- package/dist/cjs/mixins/OxyServices.fedcm.js +80 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/mixins/OxyServices.auth.js +14 -1
- package/dist/esm/mixins/OxyServices.fedcm.js +79 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/mixins/OxyServices.fedcm.d.ts +24 -0
- package/package.json +1 -1
- package/src/mixins/OxyServices.auth.ts +16 -1
- package/src/mixins/OxyServices.fedcm.ts +83 -0
- package/src/mixins/__tests__/fedcm.test.ts +182 -0
- package/src/mixins/__tests__/verifyChallenge.test.ts +135 -0
|
@@ -252,7 +252,7 @@ function OxyServicesAuthMixin(Base) {
|
|
|
252
252
|
*/
|
|
253
253
|
async verifyChallenge(publicKey, challenge, signature, timestamp, deviceName, deviceFingerprint) {
|
|
254
254
|
try {
|
|
255
|
-
|
|
255
|
+
const res = await this.makeRequest('POST', '/auth/verify', {
|
|
256
256
|
publicKey,
|
|
257
257
|
challenge,
|
|
258
258
|
signature,
|
|
@@ -260,6 +260,19 @@ function OxyServicesAuthMixin(Base) {
|
|
|
260
260
|
deviceName,
|
|
261
261
|
deviceFingerprint,
|
|
262
262
|
}, { cache: false });
|
|
263
|
+
// Plant the freshly-minted tokens, mirroring `claimSessionByToken`.
|
|
264
|
+
// `/auth/verify` returns the first access token (and refresh token) in
|
|
265
|
+
// its body, so installing it here means callers get an authenticated
|
|
266
|
+
// client without a second round-trip — and, critically, without
|
|
267
|
+
// falling back to the bearer-protected `GET /session/token/:sessionId`
|
|
268
|
+
// (C1 hardening), which 401s for a brand-new identity that has no
|
|
269
|
+
// bearer yet. `accessToken`/`refreshToken` are optional on
|
|
270
|
+
// SessionLoginResponse; only plant when an access token is present and
|
|
271
|
+
// default the refresh token to an empty string.
|
|
272
|
+
if (res?.accessToken) {
|
|
273
|
+
this.setTokens(res.accessToken, res.refreshToken ?? '');
|
|
274
|
+
}
|
|
275
|
+
return res;
|
|
263
276
|
}
|
|
264
277
|
catch (error) {
|
|
265
278
|
throw this.handleError(error);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.__resetSilentSSOMemoForTests = __resetSilentSSOMemoForTests;
|
|
3
4
|
exports.OxyServicesFedCMMixin = OxyServicesFedCMMixin;
|
|
4
5
|
exports.FedCMMixin = OxyServicesFedCMMixin;
|
|
5
6
|
const OxyServices_errors_1 = require("../OxyServices.errors");
|
|
@@ -43,6 +44,44 @@ const FEDCM_LOGIN_HINT_KEY = 'oxy_fedcm_login_hint';
|
|
|
43
44
|
let fedCMRequestInProgress = false;
|
|
44
45
|
let fedCMRequestPromise = null;
|
|
45
46
|
let currentMediationMode = null;
|
|
47
|
+
/**
|
|
48
|
+
* Page-load-persistent memo for SILENT FedCM sign-in.
|
|
49
|
+
*
|
|
50
|
+
* Silent SSO (`mediation: 'silent'`) is the one FedCM flow that runs WITHOUT a
|
|
51
|
+
* user gesture — on app startup / provider mount. Multiple consumers
|
|
52
|
+
* (`@oxyhq/auth`'s `WebOxyProvider` / `useWebSSO`, `@oxyhq/services`'
|
|
53
|
+
* `useWebSSO`) can each mount and trigger it, and a remount storm (route churn,
|
|
54
|
+
* React StrictMode double-invoke, error-boundary recovery) previously turned
|
|
55
|
+
* into a `navigator.credentials.get` storm. This memo collapses every silent
|
|
56
|
+
* attempt for a given `origin + baseURL` into AT MOST ONE browser credential
|
|
57
|
+
* request per page load:
|
|
58
|
+
*
|
|
59
|
+
* - the FIRST silent call runs the real flow and stores its in-flight promise;
|
|
60
|
+
* - concurrent silent calls share that same in-flight promise;
|
|
61
|
+
* - once it settles, the memo retains the resolved value (a session OR `null`)
|
|
62
|
+
* and every subsequent silent call returns it WITHOUT re-invoking the
|
|
63
|
+
* browser.
|
|
64
|
+
*
|
|
65
|
+
* Keyed on `origin + baseURL` (not the OxyServices instance) so it survives
|
|
66
|
+
* instance churn across remounts. Intentionally never cleared: only a fresh
|
|
67
|
+
* page load — which starts a fresh module scope — can change the IdP session
|
|
68
|
+
* state that silent mediation observes.
|
|
69
|
+
*
|
|
70
|
+
* This guard is SILENT-ONLY. Interactive flows (`signInWithFedCM`,
|
|
71
|
+
* `mediation: 'optional'|'required'`, `mode: 'active'|'passive'`) must always
|
|
72
|
+
* be able to re-prompt and are never memoized here.
|
|
73
|
+
*/
|
|
74
|
+
const silentSSOMemo = new Map();
|
|
75
|
+
/**
|
|
76
|
+
* Test-only reset of the page-load silent-SSO memo. The memo is module-scoped
|
|
77
|
+
* and never cleared at runtime (a fresh page load resets it naturally), but
|
|
78
|
+
* tests sharing one module instance need to start from a clean slate.
|
|
79
|
+
*
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
function __resetSilentSSOMemoForTests() {
|
|
83
|
+
silentSSOMemo.clear();
|
|
84
|
+
}
|
|
46
85
|
/**
|
|
47
86
|
* Federated Credential Management (FedCM) Authentication Mixin
|
|
48
87
|
*
|
|
@@ -214,6 +253,47 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
214
253
|
debug.log('Silent SSO: FedCM not supported in this browser');
|
|
215
254
|
return null;
|
|
216
255
|
}
|
|
256
|
+
// Page-load run-once guard. The first silent attempt for this
|
|
257
|
+
// origin + API runs; concurrent callers share the in-flight promise; once
|
|
258
|
+
// it settles, every later caller gets the memoized result (session OR
|
|
259
|
+
// null) WITHOUT re-invoking `navigator.credentials.get`. This is the single
|
|
260
|
+
// chokepoint for silent SSO across all consumers and remounts.
|
|
261
|
+
const memoKey = this.silentSSOMemoKey();
|
|
262
|
+
const existing = silentSSOMemo.get(memoKey);
|
|
263
|
+
if (existing) {
|
|
264
|
+
debug.log('Silent SSO: Returning memoized page-load result (no re-invocation)');
|
|
265
|
+
return existing;
|
|
266
|
+
}
|
|
267
|
+
const attempt = this._performSilentSignInWithFedCM();
|
|
268
|
+
silentSSOMemo.set(memoKey, attempt);
|
|
269
|
+
return attempt;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Build the page-load silent-SSO memo key from the current origin and the
|
|
273
|
+
* configured API base URL. Two providers pointed at the same API from the
|
|
274
|
+
* same origin share a single silent attempt per page load.
|
|
275
|
+
*
|
|
276
|
+
* @internal
|
|
277
|
+
*/
|
|
278
|
+
silentSSOMemoKey() {
|
|
279
|
+
const origin = typeof window !== 'undefined' ? window.location.origin : 'no-origin';
|
|
280
|
+
let baseURL = '';
|
|
281
|
+
try {
|
|
282
|
+
baseURL = this.getBaseURL();
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
baseURL = '';
|
|
286
|
+
}
|
|
287
|
+
return `${origin}|${baseURL}`;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Perform the actual silent FedCM sign-in. Always wrapped by
|
|
291
|
+
* {@link silentSignInWithFedCM}'s page-load memo — never call this directly
|
|
292
|
+
* (doing so bypasses the run-once guard).
|
|
293
|
+
*
|
|
294
|
+
* @internal
|
|
295
|
+
*/
|
|
296
|
+
async _performSilentSignInWithFedCM() {
|
|
217
297
|
const clientId = this.getClientId();
|
|
218
298
|
debug.log('Silent SSO: Starting for', clientId);
|
|
219
299
|
// Only try silent mediation (no UI) - works if user previously consented.
|