@spidy092/auth-client 2.0.3 → 2.0.4
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/core.js +155 -171
- package/package.json +1 -1
- package/react/AuthProvider.jsx +1 -1
- package/token.js +67 -47
package/core.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// auth-client/core.js
|
|
1
|
+
// auth-client/core.js - MINIMAL WORKING VERSION
|
|
2
|
+
|
|
2
3
|
import {
|
|
3
4
|
setToken,
|
|
4
5
|
clearToken,
|
|
@@ -9,10 +10,9 @@ import {
|
|
|
9
10
|
} from './token';
|
|
10
11
|
import { getConfig, isRouterMode } from './config';
|
|
11
12
|
|
|
12
|
-
// ✅ Track callback state with listeners
|
|
13
13
|
let callbackProcessed = false;
|
|
14
14
|
|
|
15
|
-
export
|
|
15
|
+
export function login(clientKeyArg, redirectUriArg, options = {}) {
|
|
16
16
|
// ✅ Reset callback state when starting new login
|
|
17
17
|
resetCallbackState();
|
|
18
18
|
|
|
@@ -25,129 +25,91 @@ export async function login(clientKeyArg, redirectUriArg) {
|
|
|
25
25
|
|
|
26
26
|
const clientKey = clientKeyArg || defaultClientKey;
|
|
27
27
|
const redirectUri = redirectUriArg || defaultRedirectUri;
|
|
28
|
+
const { codeChallenge, codeChallengeMethod, state } = options;
|
|
28
29
|
|
|
29
30
|
console.log('🔄 Smart Login initiated:', {
|
|
30
31
|
mode: isRouterMode() ? 'ROUTER' : 'CLIENT',
|
|
31
32
|
clientKey,
|
|
32
|
-
redirectUri
|
|
33
|
+
redirectUri,
|
|
34
|
+
hasPKCE: !!codeChallenge,
|
|
35
|
+
hasState: !!state
|
|
33
36
|
});
|
|
34
37
|
|
|
35
38
|
if (!clientKey || !redirectUri) {
|
|
36
39
|
throw new Error('Missing clientKey or redirectUri');
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
// Store app info
|
|
40
42
|
sessionStorage.setItem('originalApp', clientKey);
|
|
41
43
|
sessionStorage.setItem('returnUrl', redirectUri);
|
|
42
44
|
|
|
43
|
-
try {
|
|
44
|
-
const hasValidSession = await checkExistingTokens();
|
|
45
|
-
if (hasValidSession) {
|
|
46
|
-
console.log('✅ Valid session found, skipping login redirect');
|
|
47
|
-
return getToken();
|
|
48
|
-
}
|
|
49
|
-
} catch (err) {
|
|
50
|
-
console.log('⚠️ No valid session, proceeding with login flow');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ✅ Smart Router Logic
|
|
54
45
|
if (isRouterMode()) {
|
|
55
46
|
// Router mode: Direct backend authentication
|
|
56
|
-
return routerLogin(clientKey, redirectUri);
|
|
47
|
+
return routerLogin(clientKey, redirectUri, { codeChallenge, codeChallengeMethod, state });
|
|
57
48
|
} else {
|
|
58
49
|
// Client mode: Redirect to centralized login
|
|
59
|
-
return clientLogin(clientKey, redirectUri);
|
|
50
|
+
return clientLogin(clientKey, redirectUri, { codeChallenge, codeChallengeMethod, state });
|
|
60
51
|
}
|
|
61
52
|
}
|
|
62
53
|
|
|
63
54
|
// ✅ Router mode: Direct backend call
|
|
64
|
-
function routerLogin(clientKey, redirectUri) {
|
|
55
|
+
function routerLogin(clientKey, redirectUri, options = {}) {
|
|
65
56
|
const { authBaseUrl } = getConfig();
|
|
66
|
-
const
|
|
57
|
+
const { codeChallenge, codeChallengeMethod, state } = options;
|
|
58
|
+
|
|
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()}`;
|
|
67
74
|
|
|
68
75
|
console.log('🏭 Router Login: Direct backend authentication', {
|
|
69
76
|
clientKey,
|
|
70
77
|
redirectUri,
|
|
78
|
+
hasPKCE: !!codeChallenge,
|
|
79
|
+
hasState: !!state,
|
|
71
80
|
backendUrl: backendLoginUrl
|
|
72
81
|
});
|
|
73
82
|
|
|
74
83
|
window.location.href = backendLoginUrl;
|
|
75
84
|
}
|
|
76
85
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
const
|
|
86
|
+
// ✅ Client mode: Centralized login
|
|
87
|
+
function clientLogin(clientKey, redirectUri, options = {}) {
|
|
88
|
+
const { accountUiUrl } = getConfig();
|
|
89
|
+
const { codeChallenge, codeChallengeMethod, state } = options;
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
// Build URL with PKCE and state parameters
|
|
92
|
+
const params = new URLSearchParams({
|
|
93
|
+
client: clientKey,
|
|
94
|
+
redirect_uri: redirectUri
|
|
85
95
|
});
|
|
86
96
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return false;
|
|
97
|
+
if (codeChallenge) {
|
|
98
|
+
params.append('code_challenge', codeChallenge);
|
|
99
|
+
params.append('code_challenge_method', codeChallengeMethod || 'S256');
|
|
91
100
|
}
|
|
92
101
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
console.log('✅ Valid access token exists');
|
|
96
|
-
return true;
|
|
102
|
+
if (state) {
|
|
103
|
+
params.append('state', state);
|
|
97
104
|
}
|
|
98
105
|
|
|
99
|
-
|
|
100
|
-
if (refreshTokenValue) {
|
|
101
|
-
try {
|
|
102
|
-
console.log('🔄 Access token expired, attempting refresh...');
|
|
103
|
-
const newToken = await refreshToken();
|
|
104
|
-
console.log('✅ Token refreshed successfully');
|
|
105
|
-
return !!newToken;
|
|
106
|
-
} catch (err) {
|
|
107
|
-
console.warn('❌ Token refresh failed:', err);
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ✅ NEW HELPER: Check if token is expired
|
|
116
|
-
function isTokenExpiredLocal(token, bufferSeconds = 60) {
|
|
117
|
-
if (!token) return true;
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
const parts = token.split('.');
|
|
121
|
-
if (parts.length !== 3) return true;
|
|
122
|
-
|
|
123
|
-
const payload = JSON.parse(atob(parts[1]));
|
|
124
|
-
|
|
125
|
-
if (!payload.exp) return true;
|
|
126
|
-
|
|
127
|
-
const now = Date.now() / 1000;
|
|
128
|
-
const isExpired = payload.exp < (now + bufferSeconds);
|
|
129
|
-
|
|
130
|
-
console.log('🕐 Token expiry check:', {
|
|
131
|
-
expiresAt: new Date(payload.exp * 1000).toLocaleString(),
|
|
132
|
-
now: new Date(now * 1000).toLocaleString(),
|
|
133
|
-
isExpired
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
return isExpired;
|
|
137
|
-
} catch (err) {
|
|
138
|
-
console.error('❌ Failed to decode token:', err);
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ✅ Client mode: Centralized login
|
|
144
|
-
function clientLogin(clientKey, redirectUri) {
|
|
145
|
-
const { accountUiUrl } = getConfig();
|
|
146
|
-
const centralizedLoginUrl = `${accountUiUrl}/login?client=${clientKey}&redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
106
|
+
const centralizedLoginUrl = `${accountUiUrl}/login?${params.toString()}`;
|
|
147
107
|
|
|
148
108
|
console.log('🔄 Client Login: Redirecting to centralized login', {
|
|
149
109
|
clientKey,
|
|
150
110
|
redirectUri,
|
|
111
|
+
hasPKCE: !!codeChallenge,
|
|
112
|
+
hasState: !!state,
|
|
151
113
|
centralizedUrl: centralizedLoginUrl
|
|
152
114
|
});
|
|
153
115
|
|
|
@@ -155,19 +117,13 @@ function clientLogin(clientKey, redirectUri) {
|
|
|
155
117
|
}
|
|
156
118
|
|
|
157
119
|
export function logout() {
|
|
158
|
-
// ✅ Reset callback state on logout
|
|
159
120
|
resetCallbackState();
|
|
160
121
|
|
|
161
122
|
const { clientKey, authBaseUrl, accountUiUrl } = getConfig();
|
|
162
123
|
const token = getToken();
|
|
163
124
|
|
|
164
|
-
console.log('🚪 Smart Logout initiated
|
|
165
|
-
mode: isRouterMode() ? 'ROUTER' : 'CLIENT',
|
|
166
|
-
clientKey,
|
|
167
|
-
hasToken: !!token
|
|
168
|
-
});
|
|
125
|
+
console.log('🚪 Smart Logout initiated');
|
|
169
126
|
|
|
170
|
-
// Clear local storage immediately (this will trigger listeners)
|
|
171
127
|
clearToken();
|
|
172
128
|
clearRefreshToken();
|
|
173
129
|
sessionStorage.removeItem('originalApp');
|
|
@@ -180,35 +136,31 @@ export function logout() {
|
|
|
180
136
|
}
|
|
181
137
|
}
|
|
182
138
|
|
|
183
|
-
// ✅ Router logout
|
|
184
139
|
async function routerLogout(clientKey, authBaseUrl, accountUiUrl, token) {
|
|
185
|
-
console.log('🏭
|
|
140
|
+
console.log('🏭 Router Logout');
|
|
186
141
|
|
|
187
142
|
const refreshToken = getRefreshToken();
|
|
188
|
-
console.log('Refresh token available:', refreshToken ? 'FOUND' : 'MISSING');
|
|
189
143
|
|
|
190
144
|
try {
|
|
191
145
|
const response = await fetch(`${authBaseUrl}/logout/${clientKey}`, {
|
|
192
|
-
method: '
|
|
146
|
+
method: 'GET',
|
|
193
147
|
credentials: 'include',
|
|
194
148
|
headers: {
|
|
195
149
|
'Authorization': token ? `Bearer ${token}` : '',
|
|
196
150
|
'Content-Type': 'application/json'
|
|
197
151
|
},
|
|
198
152
|
body: JSON.stringify({
|
|
199
|
-
refreshToken: refreshToken
|
|
153
|
+
refreshToken: refreshToken
|
|
200
154
|
})
|
|
201
155
|
});
|
|
202
156
|
|
|
203
157
|
const data = await response.json();
|
|
204
158
|
console.log('✅ Logout response:', data);
|
|
205
159
|
|
|
206
|
-
// Clear stored tokens
|
|
207
160
|
clearRefreshToken();
|
|
208
161
|
clearToken();
|
|
209
162
|
|
|
210
|
-
|
|
211
|
-
await new Promise(resolve => setTimeout(resolve, 5000)); // ⏳ wait 5 sec
|
|
163
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
212
164
|
|
|
213
165
|
if (data.success && data.keycloakLogoutUrl) {
|
|
214
166
|
window.location.href = data.keycloakLogoutUrl;
|
|
@@ -221,16 +173,12 @@ async function routerLogout(clientKey, authBaseUrl, accountUiUrl, token) {
|
|
|
221
173
|
clearToken();
|
|
222
174
|
}
|
|
223
175
|
|
|
224
|
-
|
|
225
|
-
await new Promise(resolve => setTimeout(resolve, 5000)); // ⏳ wait 5 sec
|
|
176
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
226
177
|
window.location.href = '/login';
|
|
227
178
|
}
|
|
228
179
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// ✅ Client logout
|
|
232
180
|
function clientLogout(clientKey, accountUiUrl) {
|
|
233
|
-
console.log('🔄 Client Logout
|
|
181
|
+
console.log('🔄 Client Logout');
|
|
234
182
|
const logoutUrl = `${accountUiUrl}/login?client=${clientKey}&logout=true`;
|
|
235
183
|
window.location.href = logoutUrl;
|
|
236
184
|
}
|
|
@@ -238,115 +186,150 @@ function clientLogout(clientKey, accountUiUrl) {
|
|
|
238
186
|
export function handleCallback() {
|
|
239
187
|
const params = new URLSearchParams(window.location.search);
|
|
240
188
|
const accessToken = params.get('access_token');
|
|
241
|
-
const refreshToken = params.get('refresh_token'); // CAPTURE THIS
|
|
242
189
|
const error = params.get('error');
|
|
190
|
+
const state = params.get('state');
|
|
243
191
|
|
|
244
|
-
console.log('🔄
|
|
192
|
+
console.log('🔄 Callback handling:', {
|
|
245
193
|
hasAccessToken: !!accessToken,
|
|
246
|
-
|
|
247
|
-
|
|
194
|
+
error,
|
|
195
|
+
hasState: !!state
|
|
248
196
|
});
|
|
249
197
|
|
|
198
|
+
// ✅ Validate state parameter
|
|
199
|
+
if (state) {
|
|
200
|
+
const storedState = sessionStorage.getItem('oauth_state');
|
|
201
|
+
if (storedState && storedState !== state) {
|
|
202
|
+
console.error('❌ State mismatch - possible CSRF attack', {
|
|
203
|
+
received: state.substring(0, 10),
|
|
204
|
+
expected: storedState.substring(0, 10)
|
|
205
|
+
});
|
|
206
|
+
throw new Error('Invalid state parameter - authentication may have been compromised');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check state age (prevent replay attacks)
|
|
210
|
+
const stateTimestamp = parseInt(sessionStorage.getItem('pkce_timestamp') || '0', 10);
|
|
211
|
+
const stateAge = Date.now() - stateTimestamp;
|
|
212
|
+
const MAX_STATE_AGE = 10 * 60 * 1000; // 10 minutes
|
|
213
|
+
|
|
214
|
+
if (stateAge > MAX_STATE_AGE) {
|
|
215
|
+
console.error('❌ State expired', { stateAge });
|
|
216
|
+
throw new Error('Authentication state expired - please try again');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Clear state after validation
|
|
220
|
+
sessionStorage.removeItem('oauth_state');
|
|
221
|
+
sessionStorage.removeItem('pkce_timestamp');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ✅ Prevent duplicate callback processing
|
|
250
225
|
if (callbackProcessed) {
|
|
251
226
|
const existingToken = getToken();
|
|
252
|
-
if (existingToken)
|
|
227
|
+
if (existingToken) {
|
|
228
|
+
console.log('✅ Callback already processed, returning existing token');
|
|
229
|
+
return existingToken;
|
|
230
|
+
}
|
|
231
|
+
// Reset if no token found (might be a retry)
|
|
232
|
+
callbackProcessed = false;
|
|
253
233
|
}
|
|
254
234
|
|
|
255
235
|
callbackProcessed = true;
|
|
256
236
|
sessionStorage.removeItem('originalApp');
|
|
257
237
|
sessionStorage.removeItem('returnUrl');
|
|
238
|
+
sessionStorage.removeItem('pkce_code_verifier'); // Clear PKCE verifier after use
|
|
258
239
|
|
|
259
240
|
if (error) {
|
|
260
|
-
|
|
241
|
+
const errorDescription = params.get('error_description') || error;
|
|
242
|
+
throw new Error(`Authentication failed: ${errorDescription}`);
|
|
261
243
|
}
|
|
262
244
|
|
|
263
245
|
if (accessToken) {
|
|
264
246
|
setToken(accessToken);
|
|
265
|
-
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
247
|
+
|
|
248
|
+
// ✅ Refresh token should NOT be in URL - it's in httpOnly cookie
|
|
249
|
+
// If refresh token is in URL, log warning but don't store it client-side
|
|
250
|
+
const refreshTokenInUrl = params.get('refresh_token');
|
|
251
|
+
if (refreshTokenInUrl) {
|
|
252
|
+
console.warn('⚠️ SECURITY WARNING: Refresh token found in URL - this should not happen!');
|
|
253
|
+
// DO NOT store refresh token from URL - it should only be in httpOnly cookie
|
|
270
254
|
}
|
|
271
255
|
|
|
272
|
-
// Clean URL parameters
|
|
273
256
|
const url = new URL(window.location);
|
|
274
257
|
url.searchParams.delete('access_token');
|
|
275
|
-
url.searchParams.delete('refresh_token');
|
|
258
|
+
url.searchParams.delete('refresh_token');
|
|
259
|
+
url.searchParams.delete('refresh_token');
|
|
276
260
|
url.searchParams.delete('state');
|
|
261
|
+
url.searchParams.delete('error');
|
|
262
|
+
url.searchParams.delete('error_description');
|
|
277
263
|
window.history.replaceState({}, '', url);
|
|
278
264
|
|
|
265
|
+
console.log('✅ Callback processed successfully, token stored');
|
|
279
266
|
return accessToken;
|
|
280
267
|
}
|
|
281
268
|
|
|
282
269
|
throw new Error('No access token found in callback URL');
|
|
283
270
|
}
|
|
284
271
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// ✅ Reset callback state
|
|
288
272
|
export function resetCallbackState() {
|
|
289
273
|
callbackProcessed = false;
|
|
290
|
-
console.log('🔄 Callback state reset');
|
|
291
274
|
}
|
|
292
275
|
|
|
293
|
-
//
|
|
276
|
+
// ✅ Add refresh lock to prevent concurrent refresh calls
|
|
277
|
+
let refreshInProgress = false;
|
|
278
|
+
let refreshPromise = null;
|
|
279
|
+
|
|
294
280
|
export async function refreshToken() {
|
|
295
281
|
const { clientKey, authBaseUrl } = getConfig();
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
hasRefreshToken: !!refreshTokenValue
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
if (!refreshTokenValue) {
|
|
305
|
-
console.warn('⚠️ No refresh token available for refresh');
|
|
306
|
-
clearToken();
|
|
307
|
-
throw new Error('No refresh token available');
|
|
282
|
+
|
|
283
|
+
// ✅ Prevent concurrent refresh calls
|
|
284
|
+
if (refreshInProgress && refreshPromise) {
|
|
285
|
+
console.log('🔄 Token refresh already in progress, waiting...');
|
|
286
|
+
return refreshPromise;
|
|
308
287
|
}
|
|
288
|
+
|
|
289
|
+
refreshInProgress = true;
|
|
290
|
+
refreshPromise = (async () => {
|
|
291
|
+
try {
|
|
292
|
+
console.log('🔄 Refreshing token:', {
|
|
293
|
+
clientKey,
|
|
294
|
+
mode: isRouterMode() ? 'ROUTER' : 'CLIENT'
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
credentials: 'include', // ✅ Include httpOnly cookies
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
if (!response.ok) {
|
|
303
|
+
const errorText = await response.text();
|
|
304
|
+
console.error('❌ Token refresh failed:', response.status, errorText);
|
|
305
|
+
throw new Error(`Refresh failed: ${response.status}`);
|
|
306
|
+
}
|
|
309
307
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
// ✅ Update access token (triggers listeners)
|
|
330
|
-
setToken(access_token);
|
|
331
|
-
|
|
332
|
-
// ✅ Update refresh token in BOTH storages if backend returned new one
|
|
333
|
-
if (new_refresh_token) {
|
|
334
|
-
setRefreshToken(new_refresh_token);
|
|
308
|
+
const { access_token } = await response.json();
|
|
309
|
+
|
|
310
|
+
if (!access_token) {
|
|
311
|
+
throw new Error('No access token in refresh response');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ✅ This will trigger token listeners
|
|
315
|
+
setToken(access_token);
|
|
316
|
+
console.log('✅ Token refresh successful, listeners notified');
|
|
317
|
+
return access_token;
|
|
318
|
+
} catch (err) {
|
|
319
|
+
console.error('❌ Token refresh error:', err);
|
|
320
|
+
// ✅ This will trigger token listeners
|
|
321
|
+
clearToken();
|
|
322
|
+
clearRefreshToken();
|
|
323
|
+
throw err;
|
|
324
|
+
} finally {
|
|
325
|
+
refreshInProgress = false;
|
|
326
|
+
refreshPromise = null;
|
|
335
327
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
} catch (err) {
|
|
340
|
-
console.error('❌ Token refresh failed:', err);
|
|
341
|
-
// ✅ Clear everything on refresh failure
|
|
342
|
-
clearToken();
|
|
343
|
-
clearRefreshToken();
|
|
344
|
-
throw err;
|
|
345
|
-
}
|
|
328
|
+
})();
|
|
329
|
+
|
|
330
|
+
return refreshPromise;
|
|
346
331
|
}
|
|
347
332
|
|
|
348
|
-
|
|
349
|
-
|
|
350
333
|
export async function validateCurrentSession() {
|
|
351
334
|
try {
|
|
352
335
|
const { authBaseUrl } = getConfig();
|
|
@@ -386,6 +369,7 @@ export async function validateCurrentSession() {
|
|
|
386
369
|
|
|
387
370
|
|
|
388
371
|
|
|
372
|
+
|
|
389
373
|
// export async function refreshToken() {
|
|
390
374
|
// const { clientKey, authBaseUrl } = getConfig();
|
|
391
375
|
|
package/package.json
CHANGED
package/react/AuthProvider.jsx
CHANGED
package/token.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// auth-client/token.js -
|
|
1
|
+
// auth-client/token.js - MINIMAL WORKING VERSION
|
|
2
2
|
|
|
3
3
|
import { jwtDecode } from 'jwt-decode';
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ let accessToken = null;
|
|
|
6
6
|
const listeners = new Set();
|
|
7
7
|
|
|
8
8
|
const REFRESH_COOKIE = 'account_refresh_token';
|
|
9
|
-
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60;
|
|
9
|
+
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60;
|
|
10
10
|
|
|
11
11
|
function secureAttribute() {
|
|
12
12
|
try {
|
|
@@ -18,7 +18,7 @@ function secureAttribute() {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
// ========== ACCESS TOKEN
|
|
21
|
+
// ========== ACCESS TOKEN ==========
|
|
22
22
|
function writeAccessToken(token) {
|
|
23
23
|
if (!token) {
|
|
24
24
|
try {
|
|
@@ -45,8 +45,7 @@ function readAccessToken() {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// ========== REFRESH TOKEN (
|
|
49
|
-
|
|
48
|
+
// ========== REFRESH TOKEN (KEEP SIMPLE) ==========
|
|
50
49
|
export function setRefreshToken(token) {
|
|
51
50
|
if (!token) {
|
|
52
51
|
clearRefreshToken();
|
|
@@ -55,70 +54,38 @@ export function setRefreshToken(token) {
|
|
|
55
54
|
|
|
56
55
|
const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);
|
|
57
56
|
|
|
58
|
-
// ✅ REVERT: Use SameSite=Lax (NOT Strict) for SSO to work
|
|
59
57
|
try {
|
|
60
58
|
document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax${secureAttribute()}; Expires=${expires.toUTCString()}`;
|
|
61
|
-
console.log('✅ Refresh token cookie set (SameSite=Lax for SSO)');
|
|
62
59
|
} catch (err) {
|
|
63
|
-
console.warn('Could not
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ✅ REVERT: Keep sessionStorage (NOT localStorage) as fallback
|
|
67
|
-
try {
|
|
68
|
-
sessionStorage.setItem(REFRESH_COOKIE, token);
|
|
69
|
-
console.log('✅ Refresh token sessionStorage backup set');
|
|
70
|
-
} catch (err) {
|
|
71
|
-
console.warn('Could not persist refresh token to sessionStorage:', err);
|
|
60
|
+
console.warn('Could not set refresh token:', err);
|
|
72
61
|
}
|
|
73
62
|
}
|
|
74
63
|
|
|
75
64
|
export function getRefreshToken() {
|
|
76
|
-
// Prefer cookie to align with server expectations
|
|
77
|
-
let cookieMatch = null;
|
|
78
65
|
try {
|
|
79
|
-
|
|
66
|
+
const match = document.cookie
|
|
80
67
|
?.split('; ')
|
|
81
68
|
?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (cookieMatch) {
|
|
87
|
-
console.log('✅ Retrieved refresh token from cookie');
|
|
88
|
-
return decodeURIComponent(cookieMatch.split('=')[1]);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ✅ REVERT: Fallback to sessionStorage (NOT localStorage)
|
|
92
|
-
try {
|
|
93
|
-
const token = sessionStorage.getItem(REFRESH_COOKIE);
|
|
94
|
-
if (token) {
|
|
95
|
-
console.log('✅ Retrieved refresh token from sessionStorage (fallback)');
|
|
69
|
+
|
|
70
|
+
if (match) {
|
|
71
|
+
return decodeURIComponent(match.split('=')[1]);
|
|
96
72
|
}
|
|
97
|
-
return token;
|
|
98
73
|
} catch (err) {
|
|
99
|
-
console.warn('Could not read refresh token
|
|
100
|
-
return null;
|
|
74
|
+
console.warn('Could not read refresh token:', err);
|
|
101
75
|
}
|
|
76
|
+
|
|
77
|
+
return null;
|
|
102
78
|
}
|
|
103
79
|
|
|
104
80
|
export function clearRefreshToken() {
|
|
105
|
-
// ✅ REVERT: Clear with SameSite=Lax
|
|
106
81
|
try {
|
|
107
82
|
document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Lax${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
108
83
|
} catch (err) {
|
|
109
|
-
console.warn('Could not clear refresh token
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ✅ REVERT: Clear sessionStorage (NOT localStorage)
|
|
113
|
-
try {
|
|
114
|
-
sessionStorage.removeItem(REFRESH_COOKIE);
|
|
115
|
-
} catch (err) {
|
|
116
|
-
console.warn('Could not clear refresh token from sessionStorage:', err);
|
|
84
|
+
console.warn('Could not clear refresh token:', err);
|
|
117
85
|
}
|
|
118
86
|
}
|
|
119
87
|
|
|
120
|
-
// ========== ACCESS TOKEN FUNCTIONS
|
|
121
|
-
|
|
88
|
+
// ========== ACCESS TOKEN FUNCTIONS ==========
|
|
122
89
|
function decode(token) {
|
|
123
90
|
try {
|
|
124
91
|
return jwtDecode(token);
|
|
@@ -178,6 +145,58 @@ export function clearToken() {
|
|
|
178
145
|
});
|
|
179
146
|
}
|
|
180
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
|
+
|
|
181
200
|
export function addTokenListener(listener) {
|
|
182
201
|
if (typeof listener !== 'function') {
|
|
183
202
|
throw new Error('Token listener must be a function');
|
|
@@ -203,6 +222,7 @@ export function isAuthenticated() {
|
|
|
203
222
|
|
|
204
223
|
|
|
205
224
|
|
|
225
|
+
|
|
206
226
|
// // auth-client/token.js
|
|
207
227
|
// import { jwtDecode } from 'jwt-decode';
|
|
208
228
|
|