@spidy092/auth-client 2.0.5 → 2.0.7
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/api.js +2 -1
- package/core.js +149 -60
- package/package.json +1 -1
- package/react/AuthProvider.jsx +1 -1
- package/token.js +84 -32
package/api.js
CHANGED
|
@@ -12,7 +12,8 @@ api.interceptors.request.use((config) => {
|
|
|
12
12
|
const runtimeConfig = getConfig();
|
|
13
13
|
|
|
14
14
|
if (!config.baseURL) {
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
config.baseURL = runtimeConfig?.authBaseUrl || 'http://auth.local.test:4000/auth';
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
if (!config.headers) {
|
package/core.js
CHANGED
|
@@ -12,7 +12,8 @@ import { getConfig, isRouterMode } from './config';
|
|
|
12
12
|
|
|
13
13
|
let callbackProcessed = false;
|
|
14
14
|
|
|
15
|
-
export function login(clientKeyArg, redirectUriArg) {
|
|
15
|
+
export function login(clientKeyArg, redirectUriArg, options = {}) {
|
|
16
|
+
// ✅ Reset callback state when starting new login
|
|
16
17
|
resetCallbackState();
|
|
17
18
|
|
|
18
19
|
const {
|
|
@@ -24,11 +25,14 @@ export function login(clientKeyArg, redirectUriArg) {
|
|
|
24
25
|
|
|
25
26
|
const clientKey = clientKeyArg || defaultClientKey;
|
|
26
27
|
const redirectUri = redirectUriArg || defaultRedirectUri;
|
|
28
|
+
const { codeChallenge, codeChallengeMethod, state } = options;
|
|
27
29
|
|
|
28
30
|
console.log('🔄 Smart Login initiated:', {
|
|
29
31
|
mode: isRouterMode() ? 'ROUTER' : 'CLIENT',
|
|
30
32
|
clientKey,
|
|
31
|
-
redirectUri
|
|
33
|
+
redirectUri,
|
|
34
|
+
hasPKCE: !!codeChallenge,
|
|
35
|
+
hasState: !!state
|
|
32
36
|
});
|
|
33
37
|
|
|
34
38
|
if (!clientKey || !redirectUri) {
|
|
@@ -39,25 +43,76 @@ export function login(clientKeyArg, redirectUriArg) {
|
|
|
39
43
|
sessionStorage.setItem('returnUrl', redirectUri);
|
|
40
44
|
|
|
41
45
|
if (isRouterMode()) {
|
|
42
|
-
|
|
46
|
+
// Router mode: Direct backend authentication
|
|
47
|
+
return routerLogin(clientKey, redirectUri, { codeChallenge, codeChallengeMethod, state });
|
|
43
48
|
} else {
|
|
44
|
-
|
|
49
|
+
// Client mode: Redirect to centralized login
|
|
50
|
+
return clientLogin(clientKey, redirectUri, { codeChallenge, codeChallengeMethod, state });
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
// ✅ Router mode: Direct backend call
|
|
55
|
+
function routerLogin(clientKey, redirectUri, options = {}) {
|
|
49
56
|
const { authBaseUrl } = getConfig();
|
|
50
|
-
const
|
|
57
|
+
const { codeChallenge, codeChallengeMethod, state } = options;
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
// Build URL with PKCE and state parameters
|
|
60
|
+
const params = new URLSearchParams({
|
|
61
|
+
redirect_uri: redirectUri
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (codeChallenge) {
|
|
65
|
+
params.append('code_challenge', codeChallenge);
|
|
66
|
+
params.append('code_challenge_method', codeChallengeMethod || 'S256');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (state) {
|
|
70
|
+
params.append('state', state);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const backendLoginUrl = `${authBaseUrl}/login/${clientKey}?${params.toString()}`;
|
|
74
|
+
|
|
75
|
+
console.log('🏭 Router Login: Direct backend authentication', {
|
|
76
|
+
clientKey,
|
|
77
|
+
redirectUri,
|
|
78
|
+
hasPKCE: !!codeChallenge,
|
|
79
|
+
hasState: !!state,
|
|
80
|
+
backendUrl: backendLoginUrl
|
|
81
|
+
});
|
|
82
|
+
|
|
53
83
|
window.location.href = backendLoginUrl;
|
|
54
84
|
}
|
|
55
85
|
|
|
56
|
-
|
|
86
|
+
// ✅ Client mode: Centralized login
|
|
87
|
+
function clientLogin(clientKey, redirectUri, options = {}) {
|
|
57
88
|
const { accountUiUrl } = getConfig();
|
|
58
|
-
const
|
|
89
|
+
const { codeChallenge, codeChallengeMethod, state } = options;
|
|
90
|
+
|
|
91
|
+
// Build URL with PKCE and state parameters
|
|
92
|
+
const params = new URLSearchParams({
|
|
93
|
+
client: clientKey,
|
|
94
|
+
redirect_uri: redirectUri
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (codeChallenge) {
|
|
98
|
+
params.append('code_challenge', codeChallenge);
|
|
99
|
+
params.append('code_challenge_method', codeChallengeMethod || 'S256');
|
|
100
|
+
}
|
|
59
101
|
|
|
60
|
-
|
|
102
|
+
if (state) {
|
|
103
|
+
params.append('state', state);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const centralizedLoginUrl = `${accountUiUrl}/login?${params.toString()}`;
|
|
107
|
+
|
|
108
|
+
console.log('🔄 Client Login: Redirecting to centralized login', {
|
|
109
|
+
clientKey,
|
|
110
|
+
redirectUri,
|
|
111
|
+
hasPKCE: !!codeChallenge,
|
|
112
|
+
hasState: !!state,
|
|
113
|
+
centralizedUrl: centralizedLoginUrl
|
|
114
|
+
});
|
|
115
|
+
|
|
61
116
|
window.location.href = centralizedLoginUrl;
|
|
62
117
|
}
|
|
63
118
|
|
|
@@ -88,7 +143,7 @@ async function routerLogout(clientKey, authBaseUrl, accountUiUrl, token) {
|
|
|
88
143
|
|
|
89
144
|
try {
|
|
90
145
|
const response = await fetch(`${authBaseUrl}/logout/${clientKey}`, {
|
|
91
|
-
method: '
|
|
146
|
+
method: 'GET',
|
|
92
147
|
credentials: 'include',
|
|
93
148
|
headers: {
|
|
94
149
|
'Authorization': token ? `Bearer ${token}` : '',
|
|
@@ -131,42 +186,66 @@ function clientLogout(clientKey, accountUiUrl) {
|
|
|
131
186
|
export function handleCallback() {
|
|
132
187
|
const params = new URLSearchParams(window.location.search);
|
|
133
188
|
const accessToken = params.get('access_token');
|
|
134
|
-
const refreshToken = params.get('refresh_token');
|
|
135
189
|
const error = params.get('error');
|
|
190
|
+
const state = params.get('state');
|
|
136
191
|
|
|
137
192
|
console.log('🔄 Callback handling:', {
|
|
138
193
|
hasAccessToken: !!accessToken,
|
|
139
|
-
|
|
140
|
-
|
|
194
|
+
error,
|
|
195
|
+
hasState: !!state
|
|
141
196
|
});
|
|
142
197
|
|
|
198
|
+
// ✅ Validate state parameter
|
|
199
|
+
if (state) {
|
|
200
|
+
console.warn("⚠️ State returned but validation disabled (demo mode)");
|
|
201
|
+
|
|
202
|
+
// Clean up any existing stored state
|
|
203
|
+
sessionStorage.removeItem('oauth_state');
|
|
204
|
+
sessionStorage.removeItem('pkce_timestamp');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ✅ Prevent duplicate callback processing
|
|
143
208
|
if (callbackProcessed) {
|
|
144
209
|
const existingToken = getToken();
|
|
145
|
-
if (existingToken)
|
|
210
|
+
if (existingToken) {
|
|
211
|
+
console.log('✅ Callback already processed, returning existing token');
|
|
212
|
+
return existingToken;
|
|
213
|
+
}
|
|
214
|
+
// Reset if no token found (might be a retry)
|
|
215
|
+
callbackProcessed = false;
|
|
146
216
|
}
|
|
147
217
|
|
|
148
218
|
callbackProcessed = true;
|
|
149
219
|
sessionStorage.removeItem('originalApp');
|
|
150
220
|
sessionStorage.removeItem('returnUrl');
|
|
221
|
+
sessionStorage.removeItem('pkce_code_verifier'); // Clear PKCE verifier after use
|
|
151
222
|
|
|
152
223
|
if (error) {
|
|
153
|
-
|
|
224
|
+
const errorDescription = params.get('error_description') || error;
|
|
225
|
+
throw new Error(`Authentication failed: ${errorDescription}`);
|
|
154
226
|
}
|
|
155
227
|
|
|
156
228
|
if (accessToken) {
|
|
157
229
|
setToken(accessToken);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
230
|
+
|
|
231
|
+
// ✅ Refresh token should NOT be in URL - it's in httpOnly cookie
|
|
232
|
+
// If refresh token is in URL, log warning but don't store it client-side
|
|
233
|
+
const refreshTokenInUrl = params.get('refresh_token');
|
|
234
|
+
if (refreshTokenInUrl) {
|
|
235
|
+
console.warn('⚠️ SECURITY WARNING: Refresh token found in URL - this should not happen!');
|
|
236
|
+
// DO NOT store refresh token from URL - it should only be in httpOnly cookie
|
|
162
237
|
}
|
|
163
238
|
|
|
164
239
|
const url = new URL(window.location);
|
|
165
240
|
url.searchParams.delete('access_token');
|
|
166
241
|
url.searchParams.delete('refresh_token');
|
|
242
|
+
url.searchParams.delete('refresh_token');
|
|
167
243
|
url.searchParams.delete('state');
|
|
244
|
+
url.searchParams.delete('error');
|
|
245
|
+
url.searchParams.delete('error_description');
|
|
168
246
|
window.history.replaceState({}, '', url);
|
|
169
247
|
|
|
248
|
+
console.log('✅ Callback processed successfully, token stored');
|
|
170
249
|
return accessToken;
|
|
171
250
|
}
|
|
172
251
|
|
|
@@ -177,51 +256,61 @@ export function resetCallbackState() {
|
|
|
177
256
|
callbackProcessed = false;
|
|
178
257
|
}
|
|
179
258
|
|
|
259
|
+
// ✅ Add refresh lock to prevent concurrent refresh calls
|
|
260
|
+
let refreshInProgress = false;
|
|
261
|
+
let refreshPromise = null;
|
|
262
|
+
|
|
180
263
|
export async function refreshToken() {
|
|
181
264
|
const { clientKey, authBaseUrl } = getConfig();
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
console.warn('⚠️ No refresh token available');
|
|
188
|
-
clearToken();
|
|
189
|
-
throw new Error('No refresh token available');
|
|
265
|
+
|
|
266
|
+
// ✅ Prevent concurrent refresh calls
|
|
267
|
+
if (refreshInProgress && refreshPromise) {
|
|
268
|
+
console.log('🔄 Token refresh already in progress, waiting...');
|
|
269
|
+
return refreshPromise;
|
|
190
270
|
}
|
|
271
|
+
|
|
272
|
+
refreshInProgress = true;
|
|
273
|
+
refreshPromise = (async () => {
|
|
274
|
+
try {
|
|
275
|
+
console.log('🔄 Refreshing token:', {
|
|
276
|
+
clientKey,
|
|
277
|
+
mode: isRouterMode() ? 'ROUTER' : 'CLIENT'
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
credentials: 'include', // ✅ Include httpOnly cookies
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (!response.ok) {
|
|
286
|
+
const errorText = await response.text();
|
|
287
|
+
console.error('❌ Token refresh failed:', response.status, errorText);
|
|
288
|
+
throw new Error(`Refresh failed: ${response.status}`);
|
|
289
|
+
}
|
|
191
290
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
setToken(access_token);
|
|
212
|
-
|
|
213
|
-
if (new_refresh_token) {
|
|
214
|
-
setRefreshToken(new_refresh_token);
|
|
291
|
+
const { access_token } = await response.json();
|
|
292
|
+
|
|
293
|
+
if (!access_token) {
|
|
294
|
+
throw new Error('No access token in refresh response');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ✅ This will trigger token listeners
|
|
298
|
+
setToken(access_token);
|
|
299
|
+
console.log('✅ Token refresh successful, listeners notified');
|
|
300
|
+
return access_token;
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.error('❌ Token refresh error:', err);
|
|
303
|
+
// ✅ This will trigger token listeners
|
|
304
|
+
clearToken();
|
|
305
|
+
clearRefreshToken();
|
|
306
|
+
throw err;
|
|
307
|
+
} finally {
|
|
308
|
+
refreshInProgress = false;
|
|
309
|
+
refreshPromise = null;
|
|
215
310
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
} catch (err) {
|
|
220
|
-
console.error('❌ Token refresh failed:', err);
|
|
221
|
-
clearToken();
|
|
222
|
-
clearRefreshToken();
|
|
223
|
-
throw err;
|
|
224
|
-
}
|
|
311
|
+
})();
|
|
312
|
+
|
|
313
|
+
return refreshPromise;
|
|
225
314
|
}
|
|
226
315
|
|
|
227
316
|
export async function validateCurrentSession() {
|
package/package.json
CHANGED
package/react/AuthProvider.jsx
CHANGED
package/token.js
CHANGED
|
@@ -46,44 +46,44 @@ function readAccessToken() {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// ========== REFRESH TOKEN (KEEP SIMPLE) ==========
|
|
49
|
-
export function setRefreshToken(token) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
// export function setRefreshToken(token) {
|
|
50
|
+
// if (!token) {
|
|
51
|
+
// clearRefreshToken();
|
|
52
|
+
// return;
|
|
53
|
+
// }
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
// const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
57
|
+
// try {
|
|
58
|
+
// document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax${secureAttribute()}; Expires=${expires.toUTCString()}`;
|
|
59
|
+
// } catch (err) {
|
|
60
|
+
// console.warn('Could not set refresh token:', err);
|
|
61
|
+
// }
|
|
62
|
+
// }
|
|
63
63
|
|
|
64
|
-
export function getRefreshToken() {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
// export function getRefreshToken() {
|
|
65
|
+
// try {
|
|
66
|
+
// const match = document.cookie
|
|
67
|
+
// ?.split('; ')
|
|
68
|
+
// ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
// if (match) {
|
|
71
|
+
// return decodeURIComponent(match.split('=')[1]);
|
|
72
|
+
// }
|
|
73
|
+
// } catch (err) {
|
|
74
|
+
// console.warn('Could not read refresh token:', err);
|
|
75
|
+
// }
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
}
|
|
77
|
+
// return null;
|
|
78
|
+
// }
|
|
79
79
|
|
|
80
|
-
export function clearRefreshToken() {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
80
|
+
// export function clearRefreshToken() {
|
|
81
|
+
// try {
|
|
82
|
+
// document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Lax${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
83
|
+
// } catch (err) {
|
|
84
|
+
// console.warn('Could not clear refresh token:', err);
|
|
85
|
+
// }
|
|
86
|
+
// }
|
|
87
87
|
|
|
88
88
|
// ========== ACCESS TOKEN FUNCTIONS ==========
|
|
89
89
|
function decode(token) {
|
|
@@ -145,6 +145,58 @@ export function clearToken() {
|
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
export function setRefreshToken(token) {
|
|
149
|
+
// ✅ SECURITY: Refresh tokens should ONLY be in httpOnly cookies set by server
|
|
150
|
+
// This function should NOT be used - refresh tokens must come from server cookies
|
|
151
|
+
// Keeping for backwards compatibility but logging warning
|
|
152
|
+
|
|
153
|
+
if (!token) {
|
|
154
|
+
clearRefreshToken();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.warn('⚠️ SECURITY WARNING: setRefreshToken() called - refresh tokens should only be in httpOnly cookies!');
|
|
159
|
+
console.warn('⚠️ Refresh tokens set client-side are insecure and should be removed');
|
|
160
|
+
|
|
161
|
+
// ❌ DO NOT store refresh token in client-side storage
|
|
162
|
+
// The server sets it in httpOnly cookie, which is the only secure way
|
|
163
|
+
|
|
164
|
+
// Only clear any existing client-side storage
|
|
165
|
+
try {
|
|
166
|
+
sessionStorage.removeItem(REFRESH_COOKIE);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
// Ignore
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function getRefreshToken() {
|
|
173
|
+
// ✅ Refresh tokens are stored in httpOnly cookies by the server
|
|
174
|
+
// We cannot read httpOnly cookies from JavaScript - they're only sent with requests
|
|
175
|
+
// This function is kept for backwards compatibility but returns null
|
|
176
|
+
// The refresh endpoint will automatically use the httpOnly cookie via credentials: 'include'
|
|
177
|
+
|
|
178
|
+
// ❌ DO NOT try to read refresh token from client-side storage
|
|
179
|
+
// httpOnly cookies are not accessible via document.cookie
|
|
180
|
+
|
|
181
|
+
console.warn('⚠️ getRefreshToken() called - refresh tokens are in httpOnly cookies and cannot be read from JavaScript');
|
|
182
|
+
console.warn('⚠️ The refresh endpoint will automatically use the httpOnly cookie via credentials: "include"');
|
|
183
|
+
|
|
184
|
+
return null; // Refresh token is in httpOnly cookie, not accessible to JavaScript
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function clearRefreshToken() {
|
|
188
|
+
try {
|
|
189
|
+
document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Strict${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.warn('Could not clear refresh token cookie:', err);
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
sessionStorage.removeItem(REFRESH_COOKIE);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.warn('Could not clear refresh token from sessionStorage:', err);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
148
200
|
export function addTokenListener(listener) {
|
|
149
201
|
if (typeof listener !== 'function') {
|
|
150
202
|
throw new Error('Token listener must be a function');
|