@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 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 async function login(clientKeyArg, redirectUriArg) {
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 backendLoginUrl = `${authBaseUrl}/login/${clientKey}?redirect_uri=${encodeURIComponent(redirectUri)}`;
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
- async function checkExistingTokens() {
79
- const token = getToken();
80
- const refreshTokenValue = getRefreshToken();
86
+ // ✅ Client mode: Centralized login
87
+ function clientLogin(clientKey, redirectUri, options = {}) {
88
+ const { accountUiUrl } = getConfig();
89
+ const { codeChallenge, codeChallengeMethod, state } = options;
81
90
 
82
- console.log('🔍 Checking existing tokens:', {
83
- hasAccessToken: !!token,
84
- hasRefreshToken: !!refreshTokenValue
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
- // No tokens at all
88
- if (!token && !refreshTokenValue) {
89
- console.log(' No tokens found');
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
- // Have valid access token
94
- if (token && !isTokenExpiredLocal(token)) {
95
- console.log('✅ Valid access token exists');
96
- return true;
102
+ if (state) {
103
+ params.append('state', state);
97
104
  }
98
105
 
99
- // Have refresh token, try to get new access token
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('🏭 Enhanced Router Logout with sessionStorage');
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: 'POST',
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 // Send in body
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
- // Delay before redirect
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
- // Delay before fallback redirect
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: Redirecting to centralized login');
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('🔄 Enhanced callback handling:', {
192
+ console.log('🔄 Callback handling:', {
245
193
  hasAccessToken: !!accessToken,
246
- hasRefreshToken: !!refreshToken,
247
- error
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) return 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
- throw new Error(`Authentication failed: ${error}`);
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
- // Store refresh token for future refresh calls
267
- if (refreshToken) {
268
- setRefreshToken(refreshToken);
269
- console.log('✅ Refresh token persisted');
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'); // Remove this too
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
- // auth-client/core.js
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
- const refreshTokenValue = getRefreshToken(); // ✅ Now checks both cookie & localStorage
297
-
298
- console.log('🔄 Refreshing token:', {
299
- clientKey,
300
- mode: isRouterMode() ? 'ROUTER' : 'CLIENT',
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
- try {
311
- const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, {
312
- method: 'POST',
313
- credentials: 'include', // Sends cookie if available
314
- headers: {
315
- 'Content-Type': 'application/json',
316
- 'X-Refresh-Token': refreshTokenValue // ✅ Also send in header as fallback
317
- },
318
- body: JSON.stringify({
319
- refreshToken: refreshTokenValue // ✅ Also send in body
320
- })
321
- });
322
-
323
- if (!response.ok) {
324
- throw new Error('Refresh failed');
325
- }
326
-
327
- const { access_token, refresh_token: new_refresh_token } = await response.json();
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
- console.log('✅ Token refresh successful, listeners notified');
338
- return access_token;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spidy092/auth-client",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "Scalable frontend auth SDK for centralized login using Keycloak + Auth Service.",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -24,7 +24,7 @@ export function AuthProvider({ children }) {
24
24
  return;
25
25
  }
26
26
 
27
- fetch(`${authBaseUrl}/me`, {
27
+ fetch(`${authBaseUrl}/account/profile`, {
28
28
  headers: { Authorization: `Bearer ${token}` },
29
29
  credentials: 'include',
30
30
  })
package/token.js CHANGED
@@ -1,4 +1,4 @@
1
- // auth-client/token.js - CORRECTED VERSION
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; // 7 days in seconds
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 (localStorage - keeps working) ==========
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 (Cookie + sessionStorage - REVERTED) ==========
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 persist refresh token cookie:', err);
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
- cookieMatch = document.cookie
66
+ const match = document.cookie
80
67
  ?.split('; ')
81
68
  ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));
82
- } catch (err) {
83
- cookieMatch = null;
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 from sessionStorage:', err);
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 cookie:', err);
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 (unchanged) ==========
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