@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
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { OxyAuthenticationError } from '../OxyServices.errors.js';
|
|
2
|
-
import { buildSsoBounceUrl, ssoAttemptedKey, ssoDestKey, ssoGuardKey, ssoStateKey, } from '../utils/ssoBounce.js';
|
|
3
|
-
/**
|
|
4
|
-
* Redirect-based authentication without bearer tokens in URLs.
|
|
5
|
-
*
|
|
6
|
-
* The redirect fallback now uses the same central SSO code-return contract as
|
|
7
|
-
* cold boot: the RP stores CSRF/destination state in sessionStorage, navigates
|
|
8
|
-
* to the central IdP, receives only an opaque one-time code in the URL fragment,
|
|
9
|
-
* and the provider's `sso-return` step exchanges that code for the real session.
|
|
10
|
-
*/
|
|
11
|
-
export function OxyServicesRedirectAuthMixin(Base) {
|
|
12
|
-
return class extends Base {
|
|
13
|
-
constructor(...args) {
|
|
14
|
-
super(...args);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Start a full-page redirect through the central SSO flow.
|
|
18
|
-
*
|
|
19
|
-
* No access token, refresh token, or session id is ever put in the URL or
|
|
20
|
-
* localStorage. The caller's provider must run `consumeSsoReturn` on startup
|
|
21
|
-
* to complete the code exchange and commit the session.
|
|
22
|
-
*/
|
|
23
|
-
signInWithRedirect(options = {}) {
|
|
24
|
-
if (typeof window === 'undefined' || typeof window.sessionStorage === 'undefined') {
|
|
25
|
-
throw new OxyAuthenticationError('Redirect authentication requires browser sessionStorage');
|
|
26
|
-
}
|
|
27
|
-
const origin = window.location.origin;
|
|
28
|
-
const state = this.generateState();
|
|
29
|
-
const destination = options.preserveUrl === false
|
|
30
|
-
? (options.redirectUri || origin)
|
|
31
|
-
: (options.redirectUri || window.location.href);
|
|
32
|
-
window.sessionStorage.setItem(ssoStateKey(origin), state);
|
|
33
|
-
window.sessionStorage.setItem(ssoGuardKey(origin), String(Date.now()));
|
|
34
|
-
window.sessionStorage.setItem(ssoDestKey(origin), destination);
|
|
35
|
-
window.sessionStorage.setItem(ssoAttemptedKey(origin), '1');
|
|
36
|
-
window.location.assign(buildSsoBounceUrl(origin, state, this.config.authWebUrl));
|
|
37
|
-
}
|
|
38
|
-
signUpWithRedirect(options = {}) {
|
|
39
|
-
this.signInWithRedirect({ ...options, mode: 'signup' });
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Legacy token-query callbacks are intentionally rejected. Modern providers
|
|
43
|
-
* complete redirect auth through `consumeSsoReturn`, which accepts only
|
|
44
|
-
* `#oxy_sso=ok&code=...`.
|
|
45
|
-
*/
|
|
46
|
-
handleAuthCallback() {
|
|
47
|
-
if (typeof window === 'undefined') {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
const url = new URL(window.location.href);
|
|
51
|
-
if (url.searchParams.has('access_token') || url.searchParams.has('session_id')) {
|
|
52
|
-
this.cleanAuthCallbackUrl(url);
|
|
53
|
-
throw new OxyAuthenticationError('Legacy access-token redirect callbacks are no longer accepted.');
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
restoreSession() {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
clearStoredSession() {
|
|
61
|
-
this.httpService.clearTokens();
|
|
62
|
-
}
|
|
63
|
-
getStoredSessionId() {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
generateState() {
|
|
67
|
-
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
68
|
-
return crypto.randomUUID();
|
|
69
|
-
}
|
|
70
|
-
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
71
|
-
const bytes = new Uint8Array(16);
|
|
72
|
-
crypto.getRandomValues(bytes);
|
|
73
|
-
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
74
|
-
}
|
|
75
|
-
throw new Error('No secure random source available for state generation');
|
|
76
|
-
}
|
|
77
|
-
generateNonce() {
|
|
78
|
-
return this.generateState();
|
|
79
|
-
}
|
|
80
|
-
cleanAuthCallbackUrl(url) {
|
|
81
|
-
url.searchParams.delete('access_token');
|
|
82
|
-
url.searchParams.delete('session_id');
|
|
83
|
-
url.searchParams.delete('expires_at');
|
|
84
|
-
url.searchParams.delete('state');
|
|
85
|
-
url.searchParams.delete('nonce');
|
|
86
|
-
url.searchParams.delete('error');
|
|
87
|
-
url.searchParams.delete('error_description');
|
|
88
|
-
window.history.replaceState({}, '', url.toString());
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
export { OxyServicesRedirectAuthMixin as RedirectAuthMixin };
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import { createDebugLogger } from "../shared/utils/debugUtils.js";
|
|
2
|
-
const debug = createDebugLogger("SilentAuth");
|
|
3
|
-
/**
|
|
4
|
-
* Cross-domain silent browser auth helpers.
|
|
5
|
-
*
|
|
6
|
-
* The clean session model supports FedCM, tokenless redirect SSO, and silent
|
|
7
|
-
* iframe SSO. Bearer-token callback URLs are not part of this surface.
|
|
8
|
-
*/
|
|
9
|
-
export function OxyServicesSilentAuthMixin(Base) {
|
|
10
|
-
var _a;
|
|
11
|
-
return _a = class extends Base {
|
|
12
|
-
constructor(...args) {
|
|
13
|
-
super(...args);
|
|
14
|
-
}
|
|
15
|
-
/** Resolve auth URL from config or static default (method, not getter — getters break in TS mixins) */
|
|
16
|
-
resolveAuthUrl() {
|
|
17
|
-
return (this.config.authWebUrl || this.constructor.DEFAULT_AUTH_URL);
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Silent sign-in using hidden iframe
|
|
21
|
-
*
|
|
22
|
-
* Attempts to automatically re-authenticate the user without any UI.
|
|
23
|
-
* This is what enables seamless SSO across all Oxy domains.
|
|
24
|
-
*
|
|
25
|
-
* How it works:
|
|
26
|
-
* 1. Creates hidden iframe pointing to auth.oxy.so/silent-auth
|
|
27
|
-
* 2. If user has valid session at auth.oxy.so, it exchanges an opaque SSO code
|
|
28
|
-
* 3. If not, iframe responds with null (no error thrown)
|
|
29
|
-
*
|
|
30
|
-
* This should be called on app startup to check for existing sessions.
|
|
31
|
-
*
|
|
32
|
-
* @param options - Silent auth options
|
|
33
|
-
* @returns Session if user is signed in, null otherwise
|
|
34
|
-
*
|
|
35
|
-
* @example
|
|
36
|
-
* ```typescript
|
|
37
|
-
* useEffect(() => {
|
|
38
|
-
* const checkAuth = async () => {
|
|
39
|
-
* const session = await oxyServices.silentSignIn();
|
|
40
|
-
* if (session) {
|
|
41
|
-
* setUser(session.user);
|
|
42
|
-
* }
|
|
43
|
-
* };
|
|
44
|
-
* checkAuth();
|
|
45
|
-
* }, []);
|
|
46
|
-
* ```
|
|
47
|
-
*/
|
|
48
|
-
async silentSignIn(options = {}) {
|
|
49
|
-
if (typeof window === "undefined") {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
const timeout = options.timeout || this.constructor.SILENT_TIMEOUT;
|
|
53
|
-
const nonce = this.generateNonce();
|
|
54
|
-
const clientId = window.location.origin;
|
|
55
|
-
// Resolve the IdP origin for the iframe. An explicit per-apex override (the
|
|
56
|
-
// durable cross-domain reload path — see `SilentAuthOptions.authWebUrlOverride`)
|
|
57
|
-
// wins over the instance's configured central auth URL. The SAME origin is
|
|
58
|
-
// handed to `waitForIframeAuth` so the postMessage origin check matches the
|
|
59
|
-
// exact host the iframe was loaded from.
|
|
60
|
-
const authOrigin = options.authWebUrlOverride && options.authWebUrlOverride.length > 0
|
|
61
|
-
? options.authWebUrlOverride
|
|
62
|
-
: this.resolveAuthUrl();
|
|
63
|
-
const iframe = document.createElement("iframe");
|
|
64
|
-
iframe.style.display = "none";
|
|
65
|
-
iframe.style.position = "absolute";
|
|
66
|
-
iframe.style.width = "0";
|
|
67
|
-
iframe.style.height = "0";
|
|
68
|
-
iframe.style.border = "none";
|
|
69
|
-
const silentUrl = `${authOrigin}/auth/silent?` +
|
|
70
|
-
`client_id=${encodeURIComponent(clientId)}&` +
|
|
71
|
-
`nonce=${nonce}`;
|
|
72
|
-
iframe.src = silentUrl;
|
|
73
|
-
document.body.appendChild(iframe);
|
|
74
|
-
try {
|
|
75
|
-
const session = await this.waitForIframeAuth(iframe, timeout, authOrigin);
|
|
76
|
-
// Bail early on incomplete responses. The iframe contract requires
|
|
77
|
-
// both an access token and a session id; anything less is unusable.
|
|
78
|
-
// Returning `null` here (without installing the token) prevents a
|
|
79
|
-
// stale credential from being committed to HttpService when the
|
|
80
|
-
// user is actually signed out — that pattern caused a `getCurrentUser`
|
|
81
|
-
// -> 401 -> token-clear loop in consumer apps because callers gated
|
|
82
|
-
// on `session?.user` and never installed the user via
|
|
83
|
-
// `handleAuthSuccess`, while HttpService quietly held the token.
|
|
84
|
-
const accessToken = session
|
|
85
|
-
? session.accessToken
|
|
86
|
-
: undefined;
|
|
87
|
-
if (!session || !accessToken || !session.sessionId) {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
// Snapshot the previous token so we can roll back if the user
|
|
91
|
-
// lookup below fails — this avoids leaving a half-committed session
|
|
92
|
-
// (token installed, user missing) which would let the next
|
|
93
|
-
// authenticated request 401 with no way to recover.
|
|
94
|
-
const previousAccessToken = this.httpService.getAccessToken();
|
|
95
|
-
this.httpService.setTokens(accessToken);
|
|
96
|
-
// The iframe typically returns `{ sessionId, accessToken }` without
|
|
97
|
-
// user data. Fetch the user explicitly so callers receive a
|
|
98
|
-
// fully-formed session and never need a second `/users/me` round
|
|
99
|
-
// trip. If this fails the session is unusable — revert the token
|
|
100
|
-
// and return null so the caller treats this exactly like a
|
|
101
|
-
// missing-session response.
|
|
102
|
-
if (!session.user) {
|
|
103
|
-
try {
|
|
104
|
-
const userData = await this.makeRequest("GET", `/session/user/${session.sessionId}`, undefined, { cache: false, retry: false });
|
|
105
|
-
if (!userData) {
|
|
106
|
-
throw new Error("Empty user response");
|
|
107
|
-
}
|
|
108
|
-
session.user = userData;
|
|
109
|
-
}
|
|
110
|
-
catch (userError) {
|
|
111
|
-
debug.warn("silentSignIn: failed to fetch user data, rolling back token", userError);
|
|
112
|
-
if (previousAccessToken) {
|
|
113
|
-
this.httpService.setTokens(previousAccessToken);
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
this.httpService.clearTokens();
|
|
117
|
-
}
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return session;
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
finally {
|
|
127
|
-
document.body.removeChild(iframe);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Wait for authentication response from iframe
|
|
132
|
-
*
|
|
133
|
-
* @private
|
|
134
|
-
*/
|
|
135
|
-
async waitForIframeAuth(iframe, timeout, expectedOrigin) {
|
|
136
|
-
return new Promise((resolve) => {
|
|
137
|
-
const timeoutId = setTimeout(() => {
|
|
138
|
-
cleanup();
|
|
139
|
-
resolve(null); // Silent failure - don't throw
|
|
140
|
-
}, timeout);
|
|
141
|
-
const messageHandler = (event) => {
|
|
142
|
-
// Verify origin against the EXACT host the iframe was loaded from
|
|
143
|
-
// (`expectedOrigin`). For the per-apex durable-restore path this is
|
|
144
|
-
// `auth.<rp-apex>`, not the instance's central `resolveAuthUrl()` — so
|
|
145
|
-
// we must honour the caller-supplied origin, never re-derive it here.
|
|
146
|
-
if (event.origin !== expectedOrigin) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
const { type, session } = event.data;
|
|
150
|
-
if (type !== "oxy_silent_auth") {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
cleanup();
|
|
154
|
-
resolve(session || null);
|
|
155
|
-
};
|
|
156
|
-
// Fail-fast on a load failure. When the per-apex `/auth/silent` host is
|
|
157
|
-
// unreachable, blocked by CSP `frame-ancestors`/`X-Frame-Options`, or the
|
|
158
|
-
// network drops, the iframe never posts a message — without this handler
|
|
159
|
-
// the silent restore would block for the FULL `timeout` (dead latency in
|
|
160
|
-
// the cold-boot critical path). `onerror`/`onabort` fire on a failed load,
|
|
161
|
-
// so resolve `null` immediately and let the next cold-boot step run. The
|
|
162
|
-
// success path posts a message and is handled above; these only catch the
|
|
163
|
-
// no-message failure modes.
|
|
164
|
-
const failFast = () => {
|
|
165
|
-
cleanup();
|
|
166
|
-
resolve(null);
|
|
167
|
-
};
|
|
168
|
-
iframe.onerror = failFast;
|
|
169
|
-
iframe.onabort = failFast;
|
|
170
|
-
const cleanup = () => {
|
|
171
|
-
clearTimeout(timeoutId);
|
|
172
|
-
iframe.onerror = null;
|
|
173
|
-
iframe.onabort = null;
|
|
174
|
-
window.removeEventListener("message", messageHandler);
|
|
175
|
-
};
|
|
176
|
-
window.addEventListener("message", messageHandler);
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Generate nonce for replay attack prevention
|
|
181
|
-
*
|
|
182
|
-
* @private
|
|
183
|
-
*/
|
|
184
|
-
generateNonce() {
|
|
185
|
-
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
186
|
-
return crypto.randomUUID();
|
|
187
|
-
}
|
|
188
|
-
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
189
|
-
const bytes = new Uint8Array(16);
|
|
190
|
-
crypto.getRandomValues(bytes);
|
|
191
|
-
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
192
|
-
}
|
|
193
|
-
throw new Error("No secure random source available for nonce generation");
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
_a.DEFAULT_AUTH_URL = "https://auth.oxy.so",
|
|
197
|
-
_a.SILENT_TIMEOUT = 5000 // 5 seconds
|
|
198
|
-
,
|
|
199
|
-
_a;
|
|
200
|
-
}
|
|
201
|
-
// Export the mixin function as both named and default
|
|
202
|
-
export { OxyServicesSilentAuthMixin as SilentAuthMixin };
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Central Cross-Domain SSO (opaque-code) Mixin
|
|
3
|
-
*
|
|
4
|
-
* Implements the Relying-Party half of TRUE central cross-domain SSO
|
|
5
|
-
* (Google/Meta/Clerk style). The central IdP at `auth.oxy.so` owns the session;
|
|
6
|
-
* an RP bounces a top-level redirect (prompt=none) to `auth.oxy.so/sso`, which
|
|
7
|
-
* returns an OPAQUE single-use code in the redirect fragment. The RP then
|
|
8
|
-
* exchanges that code here for the real session.
|
|
9
|
-
*
|
|
10
|
-
* Security properties:
|
|
11
|
-
* - NO token/JWT ever travels in a URL — only the opaque code does. The real
|
|
12
|
-
* `accessToken` is delivered exclusively in this exchange response body.
|
|
13
|
-
* - The exchange is a CORS POST with NO credentials/cookies — the opaque code
|
|
14
|
-
* is the only bearer of authority, and the central store burns it atomically
|
|
15
|
-
* (single-use). Sending no cookies keeps the request a clean, ambient-
|
|
16
|
-
* authority-free bearer exchange that the central `POST /sso/exchange`
|
|
17
|
-
* endpoint validates by `Origin` against the code's bound `clientOrigin`.
|
|
18
|
-
* - The code is minted server-side bound to the RP origin and expires in
|
|
19
|
-
* seconds, so a leaked code is useless cross-origin and short-lived.
|
|
20
|
-
*
|
|
21
|
-
* On success the mixin plants the returned access token via
|
|
22
|
-
* `httpService.setTokens(...)` — mirroring `exchangeIdTokenForSession` /
|
|
23
|
-
* `verifyChallenge` — so callers do NOT need to plant tokens manually.
|
|
24
|
-
*/
|
|
25
|
-
import { createDebugLogger } from '../shared/utils/debugUtils.js';
|
|
26
|
-
import { ssoStateKey } from '../utils/ssoBounce.js';
|
|
27
|
-
const debug = createDebugLogger('SSO');
|
|
28
|
-
/**
|
|
29
|
-
* Generate a cryptographically secure state value for the SSO bounce.
|
|
30
|
-
*
|
|
31
|
-
* Exposed as a module-level helper (in addition to the instance method below)
|
|
32
|
-
* so consumers that do not yet hold an `OxyServices` instance can still mint a
|
|
33
|
-
* bounce state. Uses `crypto.randomUUID` with a `getRandomValues` fallback.
|
|
34
|
-
*/
|
|
35
|
-
export function generateSsoState() {
|
|
36
|
-
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
37
|
-
return crypto.randomUUID();
|
|
38
|
-
}
|
|
39
|
-
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
40
|
-
const bytes = new Uint8Array(16);
|
|
41
|
-
crypto.getRandomValues(bytes);
|
|
42
|
-
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
43
|
-
}
|
|
44
|
-
throw new Error('No secure random source available for SSO state generation');
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Read the SSO bounce state stored for the current browser origin, if any.
|
|
48
|
-
*
|
|
49
|
-
* Returns `null` outside a browser (no `window`/`sessionStorage`) or when no
|
|
50
|
-
* state is stored — in which case the caller cannot (and must not) enforce a
|
|
51
|
-
* state match, e.g. native flows or pre-hydration callbacks that already
|
|
52
|
-
* validated the state before this exchange.
|
|
53
|
-
*/
|
|
54
|
-
function getStoredSsoStateForCurrentOrigin() {
|
|
55
|
-
if (typeof window === 'undefined' || !window.location || !window.sessionStorage) {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
try {
|
|
59
|
-
return window.sessionStorage.getItem(ssoStateKey(window.location.origin));
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
export function OxyServicesSsoMixin(Base) {
|
|
66
|
-
return class extends Base {
|
|
67
|
-
constructor(...args) {
|
|
68
|
-
super(...args);
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Generate cryptographically secure state for the SSO bounce (CSRF
|
|
72
|
-
* protection). Delegates to the module-level {@link generateSsoState}
|
|
73
|
-
* helper, which uses `crypto.randomUUID` when available and falls back to
|
|
74
|
-
* `crypto.getRandomValues`.
|
|
75
|
-
*/
|
|
76
|
-
generateSsoState() {
|
|
77
|
-
return generateSsoState();
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Exchange an opaque single-use SSO code for the real Oxy session.
|
|
81
|
-
*
|
|
82
|
-
* POSTs `{ code }` to `${getSessionBaseUrl()}/sso/exchange` as a CORS
|
|
83
|
-
* request with NO credentials/cookies. On success the returned access token
|
|
84
|
-
* is planted via `httpService.setTokens(...)` (matching
|
|
85
|
-
* `exchangeIdTokenForSession` / `verifyChallenge`), so callers do not need
|
|
86
|
-
* to plant tokens manually.
|
|
87
|
-
*
|
|
88
|
-
* @param code - The opaque single-use code delivered in the SSO return
|
|
89
|
-
* fragment (see {@link parseSsoReturnFragment}). The central store burns
|
|
90
|
-
* it atomically on exchange.
|
|
91
|
-
* @param state - The state value returned alongside the code. In browsers,
|
|
92
|
-
* when an SSO bounce state is still stored for the current origin, this
|
|
93
|
-
* must match before any token-committing exchange is attempted.
|
|
94
|
-
* @returns The resolved {@link SessionLoginResponse}.
|
|
95
|
-
*/
|
|
96
|
-
async exchangeSsoCode(code, state) {
|
|
97
|
-
if (typeof code !== 'string' || code.length === 0) {
|
|
98
|
-
throw this.handleError(new Error('exchangeSsoCode requires a non-empty code'));
|
|
99
|
-
}
|
|
100
|
-
const expectedState = getStoredSsoStateForCurrentOrigin();
|
|
101
|
-
if (expectedState !== null && (typeof state !== 'string' || state.length === 0 || state !== expectedState)) {
|
|
102
|
-
throw this.handleError(new Error('SSO exchange state mismatch'));
|
|
103
|
-
}
|
|
104
|
-
const url = `${this.getSessionBaseUrl().replace(/\/$/, '')}/sso/exchange`;
|
|
105
|
-
debug.log('Exchanging SSO code for session...');
|
|
106
|
-
let response;
|
|
107
|
-
try {
|
|
108
|
-
response = await fetch(url, {
|
|
109
|
-
method: 'POST',
|
|
110
|
-
// No cookies: the opaque code is the sole bearer of authority and the
|
|
111
|
-
// server validates by Origin against the code's bound clientOrigin.
|
|
112
|
-
credentials: 'omit',
|
|
113
|
-
headers: {
|
|
114
|
-
'Content-Type': 'application/json',
|
|
115
|
-
Accept: 'application/json',
|
|
116
|
-
},
|
|
117
|
-
body: JSON.stringify({ code }),
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
debug.error('SSO exchange request failed:', error instanceof Error ? error.message : String(error));
|
|
122
|
-
throw this.handleError(error);
|
|
123
|
-
}
|
|
124
|
-
if (!response.ok) {
|
|
125
|
-
throw this.handleError(new Error(`SSO exchange failed with HTTP ${response.status}`));
|
|
126
|
-
}
|
|
127
|
-
let payload;
|
|
128
|
-
try {
|
|
129
|
-
payload = (await response.json());
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
throw this.handleError(error);
|
|
133
|
-
}
|
|
134
|
-
if (!payload || typeof payload.accessToken !== 'string' || payload.accessToken.length === 0) {
|
|
135
|
-
throw this.handleError(new Error('SSO exchange returned no access token'));
|
|
136
|
-
}
|
|
137
|
-
if (typeof payload.sessionId !== 'string' || payload.sessionId.length === 0) {
|
|
138
|
-
throw this.handleError(new Error('SSO exchange returned no sessionId'));
|
|
139
|
-
}
|
|
140
|
-
const userId = payload.user?.id ?? payload.user?._id;
|
|
141
|
-
if (!userId || typeof payload.user?.username !== 'string') {
|
|
142
|
-
throw this.handleError(new Error('SSO exchange returned an invalid user'));
|
|
143
|
-
}
|
|
144
|
-
const user = {
|
|
145
|
-
id: userId,
|
|
146
|
-
username: payload.user.username,
|
|
147
|
-
// `name.displayName` is optional on the contract. A no-name user is
|
|
148
|
-
// valid; carry whatever structured name the server sent (possibly an
|
|
149
|
-
// empty object) and let consumers fall back to a handle.
|
|
150
|
-
name: payload.user.name ?? {},
|
|
151
|
-
avatar: payload.user.avatar,
|
|
152
|
-
};
|
|
153
|
-
// Plant the access token exactly like exchangeIdTokenForSession does.
|
|
154
|
-
// The SSO exchange does not return a refresh token (the central store
|
|
155
|
-
// holds the refresh credential), so default it to an empty string.
|
|
156
|
-
this.httpService.setTokens(payload.accessToken);
|
|
157
|
-
debug.log('SSO exchange complete:', { hasSession: !!payload.sessionId });
|
|
158
|
-
const session = {
|
|
159
|
-
sessionId: payload.sessionId,
|
|
160
|
-
deviceId: '',
|
|
161
|
-
expiresAt: payload.expiresAt ?? '',
|
|
162
|
-
user,
|
|
163
|
-
accessToken: payload.accessToken,
|
|
164
|
-
};
|
|
165
|
-
return session;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Mint a server-formed `/sso/establish` URL for the caller's OWN session,
|
|
169
|
-
* bound to an approved RP `origin`.
|
|
170
|
-
*
|
|
171
|
-
* Bearer-authenticated (the session id is taken from the caller's own
|
|
172
|
-
* bearer, server-side — never from any argument). The server validates that
|
|
173
|
-
* `origin` is an approved client origin (and matches the request `Origin`),
|
|
174
|
-
* derives the per-apex IdP host (`auth.<apex>`), mints a short-lived HS256
|
|
175
|
-
* establish-token, and returns a fully-formed
|
|
176
|
-
* `https://<auth-host>/sso/establish?et=…&return_to=<origin>/__oxy/sso-callback&state=<state>`.
|
|
177
|
-
*
|
|
178
|
-
* Used AFTER a web device-flow claim to plant the durable first-party
|
|
179
|
-
* `fedcm_session` cookie so a reload can re-mint a token (see
|
|
180
|
-
* {@link establishIdpSessionAfterClaim}). Cache-free (a POST is never
|
|
181
|
-
* cached, but `cache: false` is explicit).
|
|
182
|
-
*
|
|
183
|
-
* @param origin - The RP origin (`window.location.origin`) to establish for.
|
|
184
|
-
* @param state - The CSRF state echoed back in the callback fragment; the
|
|
185
|
-
* caller persists the SAME value under `ssoStateKey(origin)` so the
|
|
186
|
-
* post-bounce `sso-return` step validates it.
|
|
187
|
-
*/
|
|
188
|
-
async requestSsoEstablishUrl(origin, state) {
|
|
189
|
-
if (typeof origin !== 'string' || origin.length === 0) {
|
|
190
|
-
throw this.handleError(new Error('requestSsoEstablishUrl requires a non-empty origin'));
|
|
191
|
-
}
|
|
192
|
-
if (typeof state !== 'string' || state.length === 0) {
|
|
193
|
-
throw this.handleError(new Error('requestSsoEstablishUrl requires a non-empty state'));
|
|
194
|
-
}
|
|
195
|
-
const response = await this.makeRequest('POST', '/sso/establish-token', { origin, state }, { cache: false });
|
|
196
|
-
if (!response ||
|
|
197
|
-
typeof response.establishUrl !== 'string' ||
|
|
198
|
-
response.establishUrl.length === 0) {
|
|
199
|
-
throw this.handleError(new Error('SSO establish-token returned no establishUrl'));
|
|
200
|
-
}
|
|
201
|
-
return { establishUrl: response.establishUrl };
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auto-detect the FAPI (IdP) URL from the current browser hostname.
|
|
3
|
-
*
|
|
4
|
-
* This is the canonical cross-domain IdP-resolution primitive for the Oxy
|
|
5
|
-
* ecosystem. Both candidate cross-domain SSO designs derive `auth.<rp-apex>`
|
|
6
|
-
* through this helper; do not fork it.
|
|
7
|
-
*
|
|
8
|
-
* Clerk-style multi-domain SSO depends on the IdP being reachable on a
|
|
9
|
-
* subdomain of the RP's own apex (e.g. `auth.mention.earth` CNAMEd to the
|
|
10
|
-
* central Oxy IdP). That way every FedCM endpoint, the session cookie,
|
|
11
|
-
* and any redirect target are same-site with the RP — the only way
|
|
12
|
-
* to get first-party cookies in Safari ITP and Firefox Total Cookie
|
|
13
|
-
* Protection.
|
|
14
|
-
*
|
|
15
|
-
* This helper computes `https://auth.<rp-apex>` from
|
|
16
|
-
* `window.location.hostname` so a consuming app doesn't have to pass
|
|
17
|
-
* `authWebUrl` explicitly. Returns `undefined` for environments where
|
|
18
|
-
* auto-detection would be wrong:
|
|
19
|
-
*
|
|
20
|
-
* - SSR / non-browser (no `window`).
|
|
21
|
-
* - `localhost`, `127.0.0.1`, IPv4/IPv6 literals.
|
|
22
|
-
* - Hostnames with fewer than two labels.
|
|
23
|
-
* - Hostnames where a registrable domain cannot be determined from the
|
|
24
|
-
* Public Suffix List, including private hosted suffixes such as
|
|
25
|
-
* `github.io`, `pages.dev`, and `netlify.app`.
|
|
26
|
-
*
|
|
27
|
-
* When the page is already loaded ON the IdP itself (`auth.<anything>`),
|
|
28
|
-
* the helper returns the current origin so the SDK keeps everything
|
|
29
|
-
* same-origin instead of hopping to a different IdP host.
|
|
30
|
-
*
|
|
31
|
-
* The IdP backend independently derives `iss`, `provider_urls`, and the
|
|
32
|
-
* `fedcm.json` icon URLs from the request host
|
|
33
|
-
* (`packages/auth/server/index.ts`), so an honest CNAME pair is all that
|
|
34
|
-
* is required for end-to-end FedCM correctness — no per-RP config.
|
|
35
|
-
*/
|
|
36
|
-
import { getDomain } from 'tldts';
|
|
37
|
-
/**
|
|
38
|
-
* Compute the bare registrable apex (eTLD+1) of a hostname using the Public
|
|
39
|
-
* Suffix List, including private hosted suffixes.
|
|
40
|
-
*
|
|
41
|
-
* This is the pure host-handling kernel shared by {@link autoDetectAuthWebUrl}
|
|
42
|
-
* and the IdP worker — it performs NO protocol handling, NO `auth.` prefixing,
|
|
43
|
-
* and builds NO URL. It only answers "what is the registrable domain of this
|
|
44
|
-
* host, or is that undefinable?".
|
|
45
|
-
*
|
|
46
|
-
* Returns `null` (apex undefinable) for:
|
|
47
|
-
* - empty input;
|
|
48
|
-
* - IPv4 literals (`192.168.1.10`);
|
|
49
|
-
* - IPv6 literals or any host carrying a port (`[::1]`, anything with `:`);
|
|
50
|
-
* - single-label hosts (`intranet`, `localhost`);
|
|
51
|
-
* - public suffixes without a registrable label (e.g. `co.uk`, `github.io`).
|
|
52
|
-
*
|
|
53
|
-
* @param hostname - A bare hostname (no scheme), e.g. `www.mention.earth`.
|
|
54
|
-
* @returns The eTLD+1 (`mention.earth`), or `null` when undefinable.
|
|
55
|
-
*/
|
|
56
|
-
export function registrableApex(hostname) {
|
|
57
|
-
if (!hostname)
|
|
58
|
-
return null;
|
|
59
|
-
const host = hostname.toLowerCase();
|
|
60
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(host))
|
|
61
|
-
return null;
|
|
62
|
-
// IPv6 literals are bracketed; any remaining ':' implies a port — neither
|
|
63
|
-
// yields a registrable apex.
|
|
64
|
-
if (host.startsWith('[') || host.includes(':'))
|
|
65
|
-
return null;
|
|
66
|
-
// The Public Suffix List (private suffixes included) is the source of truth
|
|
67
|
-
// for what an attacker can register. A multi-part public suffix like `co.uk`
|
|
68
|
-
// or a hosted suffix like `github.io` returns no registrable domain, so
|
|
69
|
-
// deriving `auth.<apex>` against it is impossible — `getDomain` returns null.
|
|
70
|
-
return getDomain(host, { allowPrivateDomains: true });
|
|
71
|
-
}
|
|
72
|
-
export function autoDetectAuthWebUrl(location = typeof window !== 'undefined' ? window.location : undefined) {
|
|
73
|
-
if (!location)
|
|
74
|
-
return undefined;
|
|
75
|
-
const { hostname, protocol } = location;
|
|
76
|
-
if (!hostname)
|
|
77
|
-
return undefined;
|
|
78
|
-
if (protocol !== 'https:' && protocol !== 'http:')
|
|
79
|
-
return undefined;
|
|
80
|
-
if (hostname === 'localhost' || hostname === '127.0.0.1')
|
|
81
|
-
return undefined;
|
|
82
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname))
|
|
83
|
-
return undefined;
|
|
84
|
-
if (hostname.startsWith('['))
|
|
85
|
-
return undefined;
|
|
86
|
-
// Already ON the IdP — keep everything same-origin instead of hopping to a
|
|
87
|
-
// sibling host.
|
|
88
|
-
if (hostname.startsWith('auth.')) {
|
|
89
|
-
return `${protocol}//${hostname}`;
|
|
90
|
-
}
|
|
91
|
-
const apex = registrableApex(hostname);
|
|
92
|
-
if (apex === null)
|
|
93
|
-
return undefined;
|
|
94
|
-
return `${protocol}//auth.${apex}`;
|
|
95
|
-
}
|