@spidy092/auth-client 2.0.5 → 2.0.6

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 CHANGED
@@ -12,7 +12,8 @@ api.interceptors.request.use((config) => {
12
12
  const runtimeConfig = getConfig();
13
13
 
14
14
  if (!config.baseURL) {
15
- config.baseURL = runtimeConfig?.authBaseUrl || 'http://localhost:4000/auth';
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
- return routerLogin(clientKey, redirectUri);
46
+ // Router mode: Direct backend authentication
47
+ return routerLogin(clientKey, redirectUri, { codeChallenge, codeChallengeMethod, state });
43
48
  } else {
44
- return clientLogin(clientKey, redirectUri);
49
+ // Client mode: Redirect to centralized login
50
+ return clientLogin(clientKey, redirectUri, { codeChallenge, codeChallengeMethod, state });
45
51
  }
46
52
  }
47
53
 
48
- function routerLogin(clientKey, redirectUri) {
54
+ // Router mode: Direct backend call
55
+ function routerLogin(clientKey, redirectUri, options = {}) {
49
56
  const { authBaseUrl } = getConfig();
50
- const backendLoginUrl = `${authBaseUrl}/login/${clientKey}?redirect_uri=${encodeURIComponent(redirectUri)}`;
57
+ const { codeChallenge, codeChallengeMethod, state } = options;
51
58
 
52
- console.log('🏭 Router Login:', backendLoginUrl);
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
- function clientLogin(clientKey, redirectUri) {
86
+ // Client mode: Centralized login
87
+ function clientLogin(clientKey, redirectUri, options = {}) {
57
88
  const { accountUiUrl } = getConfig();
58
- const centralizedLoginUrl = `${accountUiUrl}/login?client=${clientKey}&redirect_uri=${encodeURIComponent(redirectUri)}`;
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
+ });
59
96
 
60
- console.log('🔄 Client Login:', centralizedLoginUrl);
97
+ if (codeChallenge) {
98
+ params.append('code_challenge', codeChallenge);
99
+ params.append('code_challenge_method', codeChallengeMethod || 'S256');
100
+ }
101
+
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: 'POST',
146
+ method: 'GET',
92
147
  credentials: 'include',
93
148
  headers: {
94
149
  'Authorization': token ? `Bearer ${token}` : '',
@@ -131,42 +186,83 @@ 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
- hasRefreshToken: !!refreshToken,
140
- error
194
+ error,
195
+ hasState: !!state
141
196
  });
142
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
143
225
  if (callbackProcessed) {
144
226
  const existingToken = getToken();
145
- 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;
146
233
  }
147
234
 
148
235
  callbackProcessed = true;
149
236
  sessionStorage.removeItem('originalApp');
150
237
  sessionStorage.removeItem('returnUrl');
238
+ sessionStorage.removeItem('pkce_code_verifier'); // Clear PKCE verifier after use
151
239
 
152
240
  if (error) {
153
- throw new Error(`Authentication failed: ${error}`);
241
+ const errorDescription = params.get('error_description') || error;
242
+ throw new Error(`Authentication failed: ${errorDescription}`);
154
243
  }
155
244
 
156
245
  if (accessToken) {
157
246
  setToken(accessToken);
158
-
159
- if (refreshToken) {
160
- setRefreshToken(refreshToken);
161
- 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
162
254
  }
163
255
 
164
256
  const url = new URL(window.location);
165
257
  url.searchParams.delete('access_token');
166
258
  url.searchParams.delete('refresh_token');
259
+ url.searchParams.delete('refresh_token');
167
260
  url.searchParams.delete('state');
261
+ url.searchParams.delete('error');
262
+ url.searchParams.delete('error_description');
168
263
  window.history.replaceState({}, '', url);
169
264
 
265
+ console.log('✅ Callback processed successfully, token stored');
170
266
  return accessToken;
171
267
  }
172
268
 
@@ -177,51 +273,61 @@ export function resetCallbackState() {
177
273
  callbackProcessed = false;
178
274
  }
179
275
 
276
+ // ✅ Add refresh lock to prevent concurrent refresh calls
277
+ let refreshInProgress = false;
278
+ let refreshPromise = null;
279
+
180
280
  export async function refreshToken() {
181
281
  const { clientKey, authBaseUrl } = getConfig();
182
- const refreshTokenValue = getRefreshToken();
183
-
184
- console.log('🔄 Refreshing token');
185
-
186
- if (!refreshTokenValue) {
187
- console.warn('⚠️ No refresh token available');
188
- clearToken();
189
- 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;
190
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
+ }
191
307
 
192
- try {
193
- const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, {
194
- method: 'POST',
195
- credentials: 'include',
196
- headers: {
197
- 'Content-Type': 'application/json',
198
- 'X-Refresh-Token': refreshTokenValue
199
- },
200
- body: JSON.stringify({
201
- refreshToken: refreshTokenValue
202
- })
203
- });
204
-
205
- if (!response.ok) {
206
- throw new Error('Refresh failed');
207
- }
208
-
209
- const { access_token, refresh_token: new_refresh_token } = await response.json();
210
-
211
- setToken(access_token);
212
-
213
- if (new_refresh_token) {
214
- 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;
215
327
  }
216
-
217
- console.log('✅ Token refresh successful');
218
- return access_token;
219
- } catch (err) {
220
- console.error('❌ Token refresh failed:', err);
221
- clearToken();
222
- clearRefreshToken();
223
- throw err;
224
- }
328
+ })();
329
+
330
+ return refreshPromise;
225
331
  }
226
332
 
227
333
  export async function validateCurrentSession() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spidy092/auth-client",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
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
@@ -46,44 +46,44 @@ function readAccessToken() {
46
46
  }
47
47
 
48
48
  // ========== REFRESH TOKEN (KEEP SIMPLE) ==========
49
- export function setRefreshToken(token) {
50
- if (!token) {
51
- clearRefreshToken();
52
- return;
53
- }
49
+ // export function setRefreshToken(token) {
50
+ // if (!token) {
51
+ // clearRefreshToken();
52
+ // return;
53
+ // }
54
54
 
55
- const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);
55
+ // const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);
56
56
 
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
- }
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
- try {
66
- const match = document.cookie
67
- ?.split('; ')
68
- ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));
64
+ // export function getRefreshToken() {
65
+ // try {
66
+ // const match = document.cookie
67
+ // ?.split('; ')
68
+ // ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));
69
69
 
70
- if (match) {
71
- return decodeURIComponent(match.split('=')[1]);
72
- }
73
- } catch (err) {
74
- console.warn('Could not read refresh token:', err);
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
- return null;
78
- }
77
+ // return null;
78
+ // }
79
79
 
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
- }
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');