@spidy092/auth-client 1.0.0
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/Readme.md +112 -0
- package/api.js +38 -0
- package/config.js +26 -0
- package/core.js +109 -0
- package/index.js +48 -0
- package/package.json +59 -0
- package/react/AuthProvider.jsx +79 -0
- package/react/useAuth.js +10 -0
- package/token.js +31 -0
- package/utils/jwt.js +18 -0
package/Readme.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# auth-client SDK
|
|
2
|
+
|
|
3
|
+
A lightweight, framework-agnostic authentication client SDK designed for scalable React (and non-React) apps using centralized login via Keycloak + Auth Service.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ๐ฆ Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install auth-client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ๐ง Setup
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { auth } from 'auth-client';
|
|
19
|
+
|
|
20
|
+
auth.setConfig({
|
|
21
|
+
clientKey: 'admin-ui',
|
|
22
|
+
authBaseUrl: 'http://auth.localhost:4000/auth',
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## ๐ Usage
|
|
29
|
+
|
|
30
|
+
### Login
|
|
31
|
+
```js
|
|
32
|
+
auth.login();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Handle Callback
|
|
36
|
+
```js
|
|
37
|
+
auth.handleCallback(); // Call this on /callback page
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Logout
|
|
41
|
+
```js
|
|
42
|
+
auth.logout();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Get Token
|
|
46
|
+
```js
|
|
47
|
+
const token = auth.getToken();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## ๐ง React Integration
|
|
53
|
+
|
|
54
|
+
### Provider
|
|
55
|
+
```jsx
|
|
56
|
+
import { AuthProvider } from 'auth-client/react/AuthProvider';
|
|
57
|
+
|
|
58
|
+
<AuthProvider>
|
|
59
|
+
<App />
|
|
60
|
+
</AuthProvider>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Hook
|
|
64
|
+
```jsx
|
|
65
|
+
import { useAuth } from 'auth-client/react/useAuth';
|
|
66
|
+
|
|
67
|
+
const { user, token, login, logout } = useAuth();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## ๐ก Authenticated API
|
|
73
|
+
```js
|
|
74
|
+
import api from 'auth-client/api';
|
|
75
|
+
|
|
76
|
+
api.get('/me'); // sends Authorization header
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## ๐งช Utilities
|
|
82
|
+
```js
|
|
83
|
+
import { decodeToken, isTokenExpired } from 'auth-client/utils/jwt';
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## โ
Built-in Features
|
|
89
|
+
- Token handling (in-memory + localStorage)
|
|
90
|
+
- CSRF-safe login with state param
|
|
91
|
+
- Auto API auth header via Axios
|
|
92
|
+
- React support via context and hooks
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## ๐ Security
|
|
97
|
+
- No HttpOnly cookies โ safe from XSS if you sandbox `localStorage`
|
|
98
|
+
- Handles CSRF via `state`
|
|
99
|
+
- Designed for refresh via backend `/refresh`
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## ๐ฆ To Publish Locally
|
|
104
|
+
```bash
|
|
105
|
+
npm pack
|
|
106
|
+
npm install ../auth-client-1.0.0.tgz
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## ๐ License
|
|
112
|
+
MIT
|
package/api.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// auth-client/api.js
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { getToken } from './token';
|
|
4
|
+
import { getConfig } from './config';
|
|
5
|
+
|
|
6
|
+
// โ
Fixed: Create instance without baseURL initially
|
|
7
|
+
const api = axios.create({
|
|
8
|
+
withCredentials: true,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// โ
Fixed: Set baseURL dynamically in interceptor
|
|
12
|
+
api.interceptors.request.use((config) => {
|
|
13
|
+
// Set baseURL dynamically each time
|
|
14
|
+
if (!config.baseURL) {
|
|
15
|
+
const authConfig = getConfig();
|
|
16
|
+
config.baseURL = authConfig?.authBaseUrl || 'http://localhost:4000';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const token = getToken();
|
|
20
|
+
if (token) {
|
|
21
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
22
|
+
}
|
|
23
|
+
return config;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// โ
Added: Response interceptor for token refresh/error handling
|
|
27
|
+
api.interceptors.response.use(
|
|
28
|
+
(response) => response,
|
|
29
|
+
(error) => {
|
|
30
|
+
if (error.response?.status === 401) {
|
|
31
|
+
console.warn('API request failed with 401, token may be expired');
|
|
32
|
+
// You could trigger token refresh or logout here
|
|
33
|
+
}
|
|
34
|
+
return Promise.reject(error);
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export default api;
|
package/config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
let config = {
|
|
2
|
+
clientKey: null,
|
|
3
|
+
authBaseUrl: null,
|
|
4
|
+
redirectUri: null,
|
|
5
|
+
usePkce: false, // optional future
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function setConfig(customConfig = {}) {
|
|
9
|
+
if (!customConfig.clientKey || !customConfig.authBaseUrl) {
|
|
10
|
+
throw new Error('Missing required config: clientKey and authBaseUrl are required');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
config = {
|
|
14
|
+
...config,
|
|
15
|
+
...customConfig,
|
|
16
|
+
redirectUri: customConfig.redirectUri || window.location.origin + '/callback',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getConfig() {
|
|
21
|
+
return { ...config };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// pass client key and authbaseurl and redirectUri
|
package/core.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
|
|
2
|
+
import { setToken, clearToken, getToken } from './token';
|
|
3
|
+
import { getConfig } from './config';
|
|
4
|
+
|
|
5
|
+
export function login(clientKeyArg, redirectUriArg, stateArg) {
|
|
6
|
+
const {
|
|
7
|
+
clientKey: defaultClientKey,
|
|
8
|
+
authBaseUrl,
|
|
9
|
+
redirectUri: defaultRedirectUri,
|
|
10
|
+
accountUiUrl
|
|
11
|
+
} = getConfig();
|
|
12
|
+
|
|
13
|
+
const clientKey = clientKeyArg || defaultClientKey;
|
|
14
|
+
const redirectUri = redirectUriArg || defaultRedirectUri;
|
|
15
|
+
const state = stateArg || crypto.randomUUID();
|
|
16
|
+
|
|
17
|
+
if (!clientKey || !redirectUri) {
|
|
18
|
+
throw new Error('Missing clientKey or redirectUri');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Store original app info for return after auth
|
|
22
|
+
sessionStorage.setItem('authState', state);
|
|
23
|
+
sessionStorage.setItem('originalApp', clientKey);
|
|
24
|
+
sessionStorage.setItem('returnUrl', redirectUri);
|
|
25
|
+
|
|
26
|
+
// Redirect to centralized Account UI instead of direct auth service
|
|
27
|
+
const accountLoginUrl = `${accountUiUrl}/login?` + new URLSearchParams({
|
|
28
|
+
client: clientKey,
|
|
29
|
+
redirect_uri: redirectUri,
|
|
30
|
+
state: state
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
console.log('Redirecting to Account UI:', accountLoginUrl);
|
|
34
|
+
window.location.href = accountLoginUrl;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function logout() {
|
|
38
|
+
const { clientKey, authBaseUrl, accountUiUrl } = getConfig();
|
|
39
|
+
const token = getToken();
|
|
40
|
+
|
|
41
|
+
if (!token) {
|
|
42
|
+
window.location.href = `${accountUiUrl}/login`;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
clearToken();
|
|
47
|
+
|
|
48
|
+
// Call logout endpoint
|
|
49
|
+
fetch(`${authBaseUrl}/logout/${clientKey}`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
credentials: 'include',
|
|
52
|
+
headers: {
|
|
53
|
+
'Authorization': `Bearer ${token}`
|
|
54
|
+
}
|
|
55
|
+
}).catch(console.error);
|
|
56
|
+
|
|
57
|
+
// Redirect to Account UI logout page
|
|
58
|
+
window.location.href = `${accountUiUrl}/logout?client=${clientKey}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function handleCallback() {
|
|
62
|
+
const params = new URLSearchParams(window.location.search);
|
|
63
|
+
const accessToken = params.get('access_token');
|
|
64
|
+
const error = params.get('error');
|
|
65
|
+
const state = params.get('state');
|
|
66
|
+
const storedState = sessionStorage.getItem('authState');
|
|
67
|
+
|
|
68
|
+
// Validate state
|
|
69
|
+
if (state && storedState && state !== storedState) {
|
|
70
|
+
throw new Error('Invalid state. Possible CSRF attack.');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
sessionStorage.removeItem('authState');
|
|
74
|
+
sessionStorage.removeItem('originalApp');
|
|
75
|
+
sessionStorage.removeItem('returnUrl');
|
|
76
|
+
|
|
77
|
+
if (error) {
|
|
78
|
+
throw new Error(`Authentication failed: ${error}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (accessToken) {
|
|
82
|
+
setToken(accessToken);
|
|
83
|
+
return accessToken;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new Error('No access token found in callback URL');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function refreshToken() {
|
|
90
|
+
const { clientKey, authBaseUrl } = getConfig();
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
credentials: 'include',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
throw new Error('Refresh failed');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { access_token } = await response.json();
|
|
103
|
+
setToken(access_token);
|
|
104
|
+
return access_token;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
clearToken();
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { setConfig, getConfig } from './config';
|
|
2
|
+
import { login, logout, handleCallback, refreshToken } from './core';
|
|
3
|
+
import { getToken, setToken, clearToken } from './token';
|
|
4
|
+
import api from './api';
|
|
5
|
+
import { decodeToken, isTokenExpired } from './utils/jwt';
|
|
6
|
+
|
|
7
|
+
export const auth = {
|
|
8
|
+
// ๐ง Config
|
|
9
|
+
setConfig,
|
|
10
|
+
getConfig,
|
|
11
|
+
|
|
12
|
+
// ๐ Core flows
|
|
13
|
+
login,
|
|
14
|
+
logout,
|
|
15
|
+
handleCallback,
|
|
16
|
+
refreshToken,
|
|
17
|
+
|
|
18
|
+
// ๐ Token management
|
|
19
|
+
getToken,
|
|
20
|
+
setToken,
|
|
21
|
+
clearToken,
|
|
22
|
+
|
|
23
|
+
// ๐ Authenticated API client
|
|
24
|
+
api,
|
|
25
|
+
|
|
26
|
+
// ๐งช Utilities
|
|
27
|
+
decodeToken,
|
|
28
|
+
isTokenExpired,
|
|
29
|
+
|
|
30
|
+
// ๐ Auto-refresh setup
|
|
31
|
+
startTokenRefresh: () => {
|
|
32
|
+
const interval = setInterval(async () => {
|
|
33
|
+
const token = getToken();
|
|
34
|
+
if (token && isTokenExpired(token, 300)) { // Refresh 5 min before expiry
|
|
35
|
+
try {
|
|
36
|
+
await refreshToken();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error('Auto-refresh failed:', err);
|
|
39
|
+
clearInterval(interval);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, 60000); // Check every minute
|
|
43
|
+
return interval;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export { AuthProvider } from './react/AuthProvider';
|
|
48
|
+
export { useAuth } from './react/useAuth';
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spidy092/auth-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scalable frontend auth SDK for centralized login using Keycloak + Auth Service.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.js",
|
|
10
|
+
"./react/AuthProvider": "./react/AuthProvider.jsx",
|
|
11
|
+
"./react/useAuth": "./react/useAuth.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"*.js",
|
|
15
|
+
"react/",
|
|
16
|
+
"utils/",
|
|
17
|
+
"token.js",
|
|
18
|
+
"core.js",
|
|
19
|
+
"config.js",
|
|
20
|
+
"api.js",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"auth",
|
|
25
|
+
"sdk",
|
|
26
|
+
"keycloak",
|
|
27
|
+
"react",
|
|
28
|
+
"access-token",
|
|
29
|
+
"centralized-login"
|
|
30
|
+
],
|
|
31
|
+
"author": {
|
|
32
|
+
"name": "Spiddyy",
|
|
33
|
+
"url": "https://github.com/Spidy092"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/Spidy092/auth-client.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/Spidy092/auth-client/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/Spidy092/auth-client#readme",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"axios": "^1.6.0",
|
|
46
|
+
"jwt-decode": "^4.0.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"react": ">=17.0.0"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=14.0.0",
|
|
53
|
+
"npm": ">=6.0.0"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
57
|
+
"prepublishOnly": "echo 'Run build scripts here if needed'"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// auth-client/react/AuthProvider.jsx
|
|
2
|
+
import React, { createContext, useState, useEffect } from 'react';
|
|
3
|
+
import { getToken, setToken, clearToken } from '../token';
|
|
4
|
+
import { getConfig } from '../config';
|
|
5
|
+
import { login as coreLogin, logout as coreLogout } from '../core';
|
|
6
|
+
|
|
7
|
+
export const AuthContext = createContext();
|
|
8
|
+
|
|
9
|
+
export function AuthProvider({ children }) {
|
|
10
|
+
const [token, setTokenState] = useState(getToken());
|
|
11
|
+
const [user, setUser] = useState(null);
|
|
12
|
+
const [loading, setLoading] = useState(!!token); // Loading if we have a token to validate
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!token) {
|
|
16
|
+
setLoading(false);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { authBaseUrl } = getConfig();
|
|
21
|
+
if (!authBaseUrl) {
|
|
22
|
+
console.warn('AuthProvider: No authBaseUrl configured');
|
|
23
|
+
setLoading(false);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fetch(`${authBaseUrl}/me`, {
|
|
28
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
29
|
+
credentials: 'include',
|
|
30
|
+
})
|
|
31
|
+
.then(res => {
|
|
32
|
+
if (!res.ok) throw new Error('Failed to fetch user');
|
|
33
|
+
return res.json();
|
|
34
|
+
})
|
|
35
|
+
.then(userData => {
|
|
36
|
+
setUser(userData);
|
|
37
|
+
setLoading(false);
|
|
38
|
+
})
|
|
39
|
+
.catch(err => {
|
|
40
|
+
console.error('Fetch user error:', err);
|
|
41
|
+
clearToken();
|
|
42
|
+
setTokenState(null);
|
|
43
|
+
setUser(null);
|
|
44
|
+
setLoading(false);
|
|
45
|
+
});
|
|
46
|
+
}, [token]);
|
|
47
|
+
|
|
48
|
+
const login = (clientKey, redirectUri, state) => {
|
|
49
|
+
coreLogin(clientKey, redirectUri, state);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const logout = () => {
|
|
53
|
+
coreLogout();
|
|
54
|
+
setUser(null);
|
|
55
|
+
setTokenState(null);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const value = {
|
|
59
|
+
token,
|
|
60
|
+
user,
|
|
61
|
+
loading,
|
|
62
|
+
login,
|
|
63
|
+
logout,
|
|
64
|
+
isAuthenticated: !!token && !!user,
|
|
65
|
+
setUser,
|
|
66
|
+
setToken: (newToken) => {
|
|
67
|
+
setToken(newToken);
|
|
68
|
+
setTokenState(newToken);
|
|
69
|
+
},
|
|
70
|
+
clearToken: () => {
|
|
71
|
+
clearToken();
|
|
72
|
+
setTokenState(null);
|
|
73
|
+
setUser(null);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
78
|
+
}
|
|
79
|
+
|
package/react/useAuth.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { AuthContext } from './AuthProvider';
|
|
3
|
+
|
|
4
|
+
export function useAuth() {
|
|
5
|
+
const context = useContext(AuthContext);
|
|
6
|
+
if (!context) {
|
|
7
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
8
|
+
}
|
|
9
|
+
return context;
|
|
10
|
+
}
|
package/token.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
let memoryToken = null;
|
|
2
|
+
|
|
3
|
+
export function setToken(token) {
|
|
4
|
+
memoryToken = token;
|
|
5
|
+
try {
|
|
6
|
+
localStorage.setItem('authToken', token);
|
|
7
|
+
} catch (err) {
|
|
8
|
+
console.warn('Could not write token to localStorage:', err);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getToken() {
|
|
13
|
+
if (memoryToken) return memoryToken;
|
|
14
|
+
try {
|
|
15
|
+
const stored = localStorage.getItem('authToken');
|
|
16
|
+
memoryToken = stored;
|
|
17
|
+
return stored;
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.warn('Could not read token from localStorage:', err);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function clearToken() {
|
|
25
|
+
memoryToken = null;
|
|
26
|
+
try {
|
|
27
|
+
localStorage.removeItem('authToken');
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.warn('Could not clear token from localStorage:', err);
|
|
30
|
+
}
|
|
31
|
+
}
|
package/utils/jwt.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// auth-client/utils/jwt.js
|
|
2
|
+
import { jwtDecode } from 'jwt-decode';
|
|
3
|
+
|
|
4
|
+
export function decodeToken(token) {
|
|
5
|
+
try {
|
|
6
|
+
return jwtDecode(token);
|
|
7
|
+
} catch (err) {
|
|
8
|
+
console.warn('Failed to decode JWT:', err);
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isTokenExpired(token, bufferSeconds = 60) {
|
|
14
|
+
const decoded = decodeToken(token);
|
|
15
|
+
if (!decoded || !decoded.exp) return true;
|
|
16
|
+
const currentTime = Date.now() / 1000;
|
|
17
|
+
return decoded.exp < currentTime + bufferSeconds;
|
|
18
|
+
}
|