@oxyhq/core 3.4.1 → 3.4.3
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/AuthManager.js +91 -319
- package/dist/cjs/CrossDomainAuth.js +19 -106
- package/dist/cjs/HttpService.js +49 -73
- package/dist/cjs/OxyServices.base.js +2 -2
- package/dist/cjs/i18n/index.js +7 -1
- package/dist/cjs/i18n/locales/ar-SA.json +18 -2
- package/dist/cjs/i18n/locales/ca-ES.json +18 -2
- package/dist/cjs/i18n/locales/de-DE.json +18 -2
- package/dist/cjs/i18n/locales/en-US.json +16 -2
- package/dist/cjs/i18n/locales/es-ES.json +16 -2
- package/dist/cjs/i18n/locales/fr-FR.json +18 -2
- package/dist/cjs/i18n/locales/it-IT.json +18 -2
- package/dist/cjs/i18n/locales/ja-JP.json +18 -2
- package/dist/cjs/i18n/locales/ko-KR.json +18 -2
- package/dist/cjs/i18n/locales/locales/ar-SA.json +18 -2
- package/dist/cjs/i18n/locales/locales/ca-ES.json +18 -2
- package/dist/cjs/i18n/locales/locales/de-DE.json +18 -2
- package/dist/cjs/i18n/locales/locales/en-US.json +17 -3
- package/dist/cjs/i18n/locales/locales/es-ES.json +16 -2
- package/dist/cjs/i18n/locales/locales/fr-FR.json +18 -2
- package/dist/cjs/i18n/locales/locales/it-IT.json +18 -2
- package/dist/cjs/i18n/locales/locales/ja-JP.json +18 -2
- package/dist/cjs/i18n/locales/locales/ko-KR.json +18 -2
- package/dist/cjs/i18n/locales/locales/pt-PT.json +18 -2
- package/dist/cjs/i18n/locales/locales/zh-CN.json +18 -2
- package/dist/cjs/i18n/locales/pt-PT.json +18 -2
- package/dist/cjs/i18n/locales/zh-CN.json +18 -2
- package/dist/cjs/mixins/OxyServices.auth.js +20 -63
- package/dist/cjs/mixins/OxyServices.fedcm.js +10 -12
- package/dist/cjs/mixins/OxyServices.popup.js +50 -299
- package/dist/cjs/mixins/OxyServices.redirect.js +84 -348
- package/dist/cjs/mixins/OxyServices.silent.js +204 -0
- package/dist/cjs/mixins/OxyServices.sso.js +4 -5
- package/dist/cjs/mixins/OxyServices.utility.js +6 -15
- package/dist/cjs/mixins/index.js +5 -6
- package/dist/cjs/server/index.js +21 -0
- package/dist/cjs/server/rateLimit.js +77 -0
- package/dist/cjs/shared/utils/debugUtils.js +1 -1
- package/dist/cjs/utils/accountUtils.js +4 -4
- package/dist/cjs/utils/authHelpers.js +21 -15
- package/dist/cjs/utils/coldBoot.js +3 -3
- package/dist/cjs/utils/fapiAutoDetect.js +1 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/AuthManager.js +91 -319
- package/dist/esm/CrossDomainAuth.js +19 -106
- package/dist/esm/HttpService.js +49 -73
- package/dist/esm/OxyServices.base.js +2 -2
- package/dist/esm/i18n/index.js +7 -1
- package/dist/esm/i18n/locales/ar-SA.json +18 -2
- package/dist/esm/i18n/locales/ca-ES.json +18 -2
- package/dist/esm/i18n/locales/de-DE.json +18 -2
- package/dist/esm/i18n/locales/en-US.json +16 -2
- package/dist/esm/i18n/locales/es-ES.json +16 -2
- package/dist/esm/i18n/locales/fr-FR.json +18 -2
- package/dist/esm/i18n/locales/it-IT.json +18 -2
- package/dist/esm/i18n/locales/ja-JP.json +18 -2
- package/dist/esm/i18n/locales/ko-KR.json +18 -2
- package/dist/esm/i18n/locales/locales/ar-SA.json +18 -2
- package/dist/esm/i18n/locales/locales/ca-ES.json +18 -2
- package/dist/esm/i18n/locales/locales/de-DE.json +18 -2
- package/dist/esm/i18n/locales/locales/en-US.json +17 -3
- package/dist/esm/i18n/locales/locales/es-ES.json +16 -2
- package/dist/esm/i18n/locales/locales/fr-FR.json +18 -2
- package/dist/esm/i18n/locales/locales/it-IT.json +18 -2
- package/dist/esm/i18n/locales/locales/ja-JP.json +18 -2
- package/dist/esm/i18n/locales/locales/ko-KR.json +18 -2
- package/dist/esm/i18n/locales/locales/pt-PT.json +18 -2
- package/dist/esm/i18n/locales/locales/zh-CN.json +18 -2
- package/dist/esm/i18n/locales/pt-PT.json +18 -2
- package/dist/esm/i18n/locales/zh-CN.json +18 -2
- package/dist/esm/mixins/OxyServices.auth.js +20 -63
- package/dist/esm/mixins/OxyServices.fedcm.js +10 -12
- package/dist/esm/mixins/OxyServices.popup.js +52 -301
- package/dist/esm/mixins/OxyServices.redirect.js +84 -349
- package/dist/esm/mixins/OxyServices.silent.js +202 -0
- package/dist/esm/mixins/OxyServices.sso.js +4 -5
- package/dist/esm/mixins/OxyServices.utility.js +6 -15
- package/dist/esm/mixins/index.js +5 -6
- package/dist/esm/server/index.js +17 -0
- package/dist/esm/server/rateLimit.js +71 -0
- package/dist/esm/shared/utils/debugUtils.js +1 -1
- package/dist/esm/utils/accountUtils.js +4 -4
- package/dist/esm/utils/authHelpers.js +21 -15
- package/dist/esm/utils/coldBoot.js +3 -3
- package/dist/esm/utils/fapiAutoDetect.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/AuthManager.d.ts +26 -53
- package/dist/types/AuthManagerTypes.d.ts +5 -9
- package/dist/types/CrossDomainAuth.d.ts +13 -52
- package/dist/types/HttpService.d.ts +9 -8
- package/dist/types/OxyServices.base.d.ts +1 -1
- package/dist/types/OxyServices.d.ts +4 -10
- package/dist/types/index.d.ts +1 -1
- package/dist/types/mixins/OxyServices.analytics.d.ts +1 -1
- package/dist/types/mixins/OxyServices.appData.d.ts +1 -1
- package/dist/types/mixins/OxyServices.applications.d.ts +1 -1
- package/dist/types/mixins/OxyServices.assets.d.ts +1 -1
- package/dist/types/mixins/OxyServices.auth.d.ts +10 -31
- package/dist/types/mixins/OxyServices.contacts.d.ts +1 -1
- package/dist/types/mixins/OxyServices.devices.d.ts +1 -1
- package/dist/types/mixins/OxyServices.features.d.ts +1 -1
- package/dist/types/mixins/OxyServices.fedcm.d.ts +5 -5
- package/dist/types/mixins/OxyServices.language.d.ts +1 -1
- package/dist/types/mixins/OxyServices.location.d.ts +1 -1
- package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -1
- package/dist/types/mixins/OxyServices.payment.d.ts +1 -1
- package/dist/types/mixins/OxyServices.popup.d.ts +18 -120
- package/dist/types/mixins/OxyServices.privacy.d.ts +1 -1
- package/dist/types/mixins/OxyServices.redirect.d.ts +13 -174
- package/dist/types/mixins/OxyServices.reputation.d.ts +1 -1
- package/dist/types/mixins/OxyServices.security.d.ts +1 -1
- package/dist/types/mixins/OxyServices.silent.d.ts +131 -0
- package/dist/types/mixins/OxyServices.sso.d.ts +4 -5
- package/dist/types/mixins/OxyServices.topics.d.ts +1 -1
- package/dist/types/mixins/OxyServices.user.d.ts +1 -1
- package/dist/types/mixins/OxyServices.utility.d.ts +3 -8
- package/dist/types/mixins/OxyServices.workspaces.d.ts +1 -1
- package/dist/types/mixins/index.d.ts +3 -3
- package/dist/types/models/interfaces.d.ts +5 -16
- package/dist/types/models/session.d.ts +0 -2
- package/dist/types/server/index.d.ts +18 -0
- package/dist/types/server/rateLimit.d.ts +40 -0
- package/dist/types/shared/utils/debugUtils.d.ts +1 -1
- package/dist/types/utils/authHelpers.d.ts +4 -3
- package/dist/types/utils/coldBoot.d.ts +2 -2
- package/dist/types/utils/fapiAutoDetect.d.ts +1 -1
- package/package.json +24 -2
- package/src/AuthManager.ts +100 -370
- package/src/AuthManagerTypes.ts +5 -9
- package/src/CrossDomainAuth.ts +22 -129
- package/src/HttpService.ts +55 -73
- package/src/OxyServices.base.ts +2 -3
- package/src/OxyServices.ts +9 -11
- package/src/__tests__/authManager.cookiePath.test.ts +19 -17
- package/src/__tests__/authManager.security.test.ts +7 -3
- package/src/__tests__/crossDomainAuth.test.ts +26 -118
- package/src/i18n/index.ts +7 -1
- package/src/i18n/locales/ar-SA.json +18 -2
- package/src/i18n/locales/ca-ES.json +18 -2
- package/src/i18n/locales/de-DE.json +18 -2
- package/src/i18n/locales/en-US.json +17 -3
- package/src/i18n/locales/es-ES.json +16 -2
- package/src/i18n/locales/fr-FR.json +18 -2
- package/src/i18n/locales/it-IT.json +18 -2
- package/src/i18n/locales/ja-JP.json +18 -2
- package/src/i18n/locales/ko-KR.json +18 -2
- package/src/i18n/locales/pt-PT.json +18 -2
- package/src/i18n/locales/zh-CN.json +18 -2
- package/src/index.ts +1 -1
- package/src/mixins/OxyServices.auth.ts +23 -75
- package/src/mixins/OxyServices.fedcm.ts +10 -12
- package/src/mixins/OxyServices.redirect.ts +82 -371
- package/src/mixins/OxyServices.silent.ts +272 -0
- package/src/mixins/OxyServices.sso.ts +5 -6
- package/src/mixins/OxyServices.utility.ts +9 -22
- package/src/mixins/__tests__/appData.test.ts +1 -1
- package/src/mixins/__tests__/onTokensChanged.test.ts +1 -1
- package/src/mixins/__tests__/reputation.test.ts +1 -1
- package/src/mixins/__tests__/serviceAuth.test.ts +7 -5
- package/src/mixins/__tests__/silent.test.ts +102 -0
- package/src/mixins/__tests__/verifyChallenge.test.ts +9 -14
- package/src/mixins/index.ts +6 -8
- package/src/models/interfaces.ts +5 -16
- package/src/models/session.ts +1 -3
- package/src/server/index.ts +19 -0
- package/src/server/rateLimit.ts +170 -0
- package/src/shared/utils/debugUtils.ts +1 -1
- package/src/utils/accountUtils.ts +4 -4
- package/src/utils/authHelpers.ts +23 -15
- package/src/utils/coldBoot.ts +4 -4
- package/src/utils/fapiAutoDetect.ts +1 -1
- package/src/mixins/OxyServices.popup.ts +0 -631
- package/src/mixins/__tests__/popup.test.ts +0 -374
|
@@ -1,357 +1,92 @@
|
|
|
1
1
|
import { OxyAuthenticationError } from '../OxyServices.errors.js';
|
|
2
|
+
import { buildSsoBounceUrl, ssoAttemptedKey, ssoDestKey, ssoGuardKey, ssoStateKey, } from '../utils/ssoBounce.js';
|
|
2
3
|
/**
|
|
3
|
-
* Redirect-based
|
|
4
|
+
* Redirect-based authentication without bearer tokens in URLs.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 1. Save current URL
|
|
10
|
-
* 2. Redirect to auth.oxy.so/login
|
|
11
|
-
* 3. User signs in
|
|
12
|
-
* 4. Redirect back with token in URL
|
|
13
|
-
* 5. Extract token, restore session, clean URL
|
|
14
|
-
*
|
|
15
|
-
* Features:
|
|
16
|
-
* - Works on all browsers (including old mobile browsers)
|
|
17
|
-
* - Automatic URL cleanup after auth
|
|
18
|
-
* - State preservation option
|
|
19
|
-
* - CSRF protection via state parameter
|
|
20
|
-
*
|
|
21
|
-
* Trade-offs:
|
|
22
|
-
* - Loses JavaScript app state (full page navigation)
|
|
23
|
-
* - Visible redirect (user sees navigation)
|
|
24
|
-
* - Slower perceived performance
|
|
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.
|
|
25
10
|
*/
|
|
26
11
|
export function OxyServicesRedirectAuthMixin(Base) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// Store state for CSRF protection
|
|
68
|
-
this.storeAuthState(state, nonce);
|
|
69
|
-
// Save current URL to restore after auth (optional)
|
|
70
|
-
if (options.preserveUrl !== false) {
|
|
71
|
-
this.savePreAuthUrl(window.location.href);
|
|
72
|
-
}
|
|
73
|
-
const authUrl = this.buildAuthUrl({
|
|
74
|
-
mode,
|
|
75
|
-
redirectUri,
|
|
76
|
-
state,
|
|
77
|
-
nonce,
|
|
78
|
-
clientId: window.location.origin,
|
|
79
|
-
});
|
|
80
|
-
// Perform redirect
|
|
81
|
-
window.location.href = authUrl;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Sign up using full page redirect
|
|
85
|
-
*
|
|
86
|
-
* Same as signInWithRedirect but opens the signup page by default.
|
|
87
|
-
*/
|
|
88
|
-
signUpWithRedirect(options = {}) {
|
|
89
|
-
this.signInWithRedirect({ ...options, mode: 'signup' });
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Handle authentication callback
|
|
93
|
-
*
|
|
94
|
-
* Call this on app startup to check if the current page load is a
|
|
95
|
-
* redirect back from the authentication server. If it is, this method
|
|
96
|
-
* will extract the tokens, store them, and clean up the URL.
|
|
97
|
-
*
|
|
98
|
-
* @returns Session data if this is a callback, null otherwise
|
|
99
|
-
* @throws {OxyAuthenticationError} If state validation fails (CSRF attack)
|
|
100
|
-
*
|
|
101
|
-
* @example
|
|
102
|
-
* ```typescript
|
|
103
|
-
* // In your app's root component or startup logic
|
|
104
|
-
* useEffect(() => {
|
|
105
|
-
* try {
|
|
106
|
-
* const session = oxyServices.handleAuthCallback();
|
|
107
|
-
* if (session) {
|
|
108
|
-
* console.log('Logged in:', session.user);
|
|
109
|
-
* setUser(session.user);
|
|
110
|
-
* } else {
|
|
111
|
-
* // Not a callback, check for existing session
|
|
112
|
-
* const restored = oxyServices.restoreSession();
|
|
113
|
-
* if (!restored) {
|
|
114
|
-
* // No session, show login button
|
|
115
|
-
* }
|
|
116
|
-
* }
|
|
117
|
-
* } catch (error) {
|
|
118
|
-
* console.error('Auth callback failed:', error);
|
|
119
|
-
* }
|
|
120
|
-
* }, []);
|
|
121
|
-
* ```
|
|
122
|
-
*/
|
|
123
|
-
handleAuthCallback() {
|
|
124
|
-
if (typeof window === 'undefined') {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
const url = new URL(window.location.href);
|
|
128
|
-
const accessToken = url.searchParams.get('access_token');
|
|
129
|
-
const sessionId = url.searchParams.get('session_id');
|
|
130
|
-
const expiresAt = url.searchParams.get('expires_at');
|
|
131
|
-
const state = url.searchParams.get('state');
|
|
132
|
-
const error = url.searchParams.get('error');
|
|
133
|
-
const errorDescription = url.searchParams.get('error_description');
|
|
134
|
-
// Check if this is an error callback
|
|
135
|
-
if (error) {
|
|
136
|
-
this.clearAuthState();
|
|
137
|
-
throw new OxyAuthenticationError(errorDescription || error);
|
|
138
|
-
}
|
|
139
|
-
// Check if this is an auth callback
|
|
140
|
-
if (!accessToken || !sessionId) {
|
|
141
|
-
return null; // Not a callback
|
|
142
|
-
}
|
|
143
|
-
// Verify state to prevent CSRF attacks
|
|
144
|
-
const savedState = this.getStoredState();
|
|
145
|
-
if (!savedState || state !== savedState) {
|
|
146
|
-
this.clearAuthState();
|
|
147
|
-
throw new OxyAuthenticationError('Invalid state parameter. Possible CSRF attack.');
|
|
148
|
-
}
|
|
149
|
-
// Store tokens
|
|
150
|
-
this.storeTokens(accessToken, sessionId);
|
|
151
|
-
this.httpService.setTokens(accessToken);
|
|
152
|
-
// Build session response (minimal — full user data is fetched separately
|
|
153
|
-
// by the caller via getCurrentUser() once tokens are stored).
|
|
154
|
-
const session = {
|
|
155
|
-
sessionId,
|
|
156
|
-
deviceId: '', // Not available in redirect flow
|
|
157
|
-
expiresAt: expiresAt || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
158
|
-
// Placeholder user — caller MUST fetch real user data via getCurrentUser()
|
|
159
|
-
// before exposing this session to the application. The empty id signals
|
|
160
|
-
// that the user payload has not yet been populated.
|
|
161
|
-
user: { id: '', username: '' },
|
|
162
|
-
};
|
|
163
|
-
// Clean up URL (remove auth parameters)
|
|
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')) {
|
|
164
52
|
this.cleanAuthCallbackUrl(url);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Clear stored session
|
|
205
|
-
*
|
|
206
|
-
* Removes all authentication data from storage. Call this on logout.
|
|
207
|
-
*/
|
|
208
|
-
clearStoredSession() {
|
|
209
|
-
if (typeof window === 'undefined') {
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
localStorage.removeItem(this.constructor.TOKEN_STORAGE_KEY);
|
|
213
|
-
localStorage.removeItem(this.constructor.SESSION_STORAGE_KEY);
|
|
214
|
-
this.httpService.clearTokens();
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Get stored session ID
|
|
218
|
-
*/
|
|
219
|
-
getStoredSessionId() {
|
|
220
|
-
if (typeof window === 'undefined') {
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
return localStorage.getItem(this.constructor.SESSION_STORAGE_KEY);
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Build authentication URL with query parameters
|
|
227
|
-
*
|
|
228
|
-
* @private
|
|
229
|
-
*/
|
|
230
|
-
buildAuthUrl(params) {
|
|
231
|
-
const url = new URL(`${(this.config.authWebUrl || this.constructor.DEFAULT_AUTH_URL)}/${params.mode}`);
|
|
232
|
-
url.searchParams.set('redirect_uri', params.redirectUri);
|
|
233
|
-
url.searchParams.set('state', params.state);
|
|
234
|
-
url.searchParams.set('nonce', params.nonce);
|
|
235
|
-
url.searchParams.set('client_id', params.clientId);
|
|
236
|
-
url.searchParams.set('response_type', 'token');
|
|
237
|
-
return url.toString();
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Store tokens in localStorage
|
|
241
|
-
*
|
|
242
|
-
* @private
|
|
243
|
-
*/
|
|
244
|
-
storeTokens(accessToken, sessionId) {
|
|
245
|
-
if (typeof window === 'undefined') {
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
localStorage.setItem(this.constructor.TOKEN_STORAGE_KEY, accessToken);
|
|
249
|
-
localStorage.setItem(this.constructor.SESSION_STORAGE_KEY, sessionId);
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Generate cryptographically secure state for CSRF protection
|
|
253
|
-
*
|
|
254
|
-
* @private
|
|
255
|
-
*/
|
|
256
|
-
generateState() {
|
|
257
|
-
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
258
|
-
return crypto.randomUUID();
|
|
259
|
-
}
|
|
260
|
-
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
261
|
-
const bytes = new Uint8Array(16);
|
|
262
|
-
crypto.getRandomValues(bytes);
|
|
263
|
-
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
264
|
-
}
|
|
265
|
-
throw new Error('No secure random source available for state generation');
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Generate nonce for replay attack prevention
|
|
269
|
-
*
|
|
270
|
-
* @private
|
|
271
|
-
*/
|
|
272
|
-
generateNonce() {
|
|
273
|
-
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
274
|
-
return crypto.randomUUID();
|
|
275
|
-
}
|
|
276
|
-
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
277
|
-
const bytes = new Uint8Array(16);
|
|
278
|
-
crypto.getRandomValues(bytes);
|
|
279
|
-
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
280
|
-
}
|
|
281
|
-
throw new Error('No secure random source available for nonce generation');
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Store auth state in session storage
|
|
285
|
-
*
|
|
286
|
-
* @private
|
|
287
|
-
*/
|
|
288
|
-
storeAuthState(state, nonce) {
|
|
289
|
-
if (typeof window === 'undefined') {
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
sessionStorage.setItem(this.constructor.STATE_STORAGE_KEY, state);
|
|
293
|
-
sessionStorage.setItem(this.constructor.NONCE_STORAGE_KEY, nonce);
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Get stored state
|
|
297
|
-
*
|
|
298
|
-
* @private
|
|
299
|
-
*/
|
|
300
|
-
getStoredState() {
|
|
301
|
-
if (typeof window === 'undefined') {
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
|
-
return sessionStorage.getItem(this.constructor.STATE_STORAGE_KEY);
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Clear auth state from storage
|
|
308
|
-
*
|
|
309
|
-
* @private
|
|
310
|
-
*/
|
|
311
|
-
clearAuthState() {
|
|
312
|
-
if (typeof window === 'undefined') {
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
sessionStorage.removeItem(this.constructor.STATE_STORAGE_KEY);
|
|
316
|
-
sessionStorage.removeItem(this.constructor.NONCE_STORAGE_KEY);
|
|
317
|
-
sessionStorage.removeItem(this.constructor.PRE_AUTH_URL_KEY);
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Save pre-authentication URL to restore later
|
|
321
|
-
*
|
|
322
|
-
* @private
|
|
323
|
-
*/
|
|
324
|
-
savePreAuthUrl(url) {
|
|
325
|
-
if (typeof window === 'undefined') {
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
sessionStorage.setItem(this.constructor.PRE_AUTH_URL_KEY, url);
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Clean authentication parameters from URL
|
|
332
|
-
*
|
|
333
|
-
* @private
|
|
334
|
-
*/
|
|
335
|
-
cleanAuthCallbackUrl(url) {
|
|
336
|
-
// Remove auth parameters
|
|
337
|
-
url.searchParams.delete('access_token');
|
|
338
|
-
url.searchParams.delete('session_id');
|
|
339
|
-
url.searchParams.delete('expires_at');
|
|
340
|
-
url.searchParams.delete('state');
|
|
341
|
-
url.searchParams.delete('nonce');
|
|
342
|
-
url.searchParams.delete('error');
|
|
343
|
-
url.searchParams.delete('error_description');
|
|
344
|
-
// Update URL without reloading page
|
|
345
|
-
window.history.replaceState({}, '', url.toString());
|
|
346
|
-
}
|
|
347
|
-
},
|
|
348
|
-
_a.DEFAULT_AUTH_URL = 'https://auth.oxy.so',
|
|
349
|
-
_a.TOKEN_STORAGE_KEY = 'oxy_access_token',
|
|
350
|
-
_a.SESSION_STORAGE_KEY = 'oxy_session_id',
|
|
351
|
-
_a.STATE_STORAGE_KEY = 'oxy_auth_state',
|
|
352
|
-
_a.PRE_AUTH_URL_KEY = 'oxy_pre_auth_url',
|
|
353
|
-
_a.NONCE_STORAGE_KEY = 'oxy_auth_nonce',
|
|
354
|
-
_a;
|
|
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
|
+
};
|
|
355
91
|
}
|
|
356
|
-
// Export the mixin function as both named and default
|
|
357
92
|
export { OxyServicesRedirectAuthMixin as RedirectAuthMixin };
|
|
@@ -0,0 +1,202 @@
|
|
|
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 };
|