@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 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
+ });
96
+
97
+ if (codeChallenge) {
98
+ params.append('code_challenge', codeChallenge);
99
+ params.append('code_challenge_method', codeChallengeMethod || 'S256');
100
+ }
59
101
 
60
- console.log('🔄 Client Login:', centralizedLoginUrl);
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,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
- hasRefreshToken: !!refreshToken,
140
- error
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) return 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
- throw new Error(`Authentication failed: ${error}`);
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
- if (refreshToken) {
160
- setRefreshToken(refreshToken);
161
- console.log('✅ Refresh token persisted');
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
- 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');
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
- 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);
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
- 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
- }
311
+ })();
312
+
313
+ return refreshPromise;
225
314
  }
226
315
 
227
316
  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.7",
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');