@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 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
+
@@ -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
+ }