@cas-system/react-cas-client 1.0.0 → 1.0.1
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/dist/CasProvider.d.ts +63 -0
- package/dist/CasProvider.d.ts.map +1 -0
- package/dist/CasProvider.js +133 -0
- package/dist/CasProvider.js.map +1 -0
- package/dist/cas-client.d.ts +161 -0
- package/dist/cas-client.d.ts.map +1 -0
- package/dist/cas-client.js +309 -0
- package/dist/cas-client.js.map +1 -0
- package/dist/components/CasLoginButton.d.ts +40 -0
- package/dist/components/CasLoginButton.d.ts.map +1 -0
- package/dist/components/CasLoginButton.js +26 -0
- package/dist/components/CasLoginButton.js.map +1 -0
- package/dist/components/CasProtectedRoute.d.ts +53 -0
- package/dist/components/CasProtectedRoute.d.ts.map +1 -0
- package/dist/components/CasProtectedRoute.js +54 -0
- package/dist/components/CasProtectedRoute.js.map +1 -0
- package/dist/hooks/useCasAuth.d.ts +34 -0
- package/dist/hooks/useCasAuth.d.ts.map +1 -0
- package/dist/hooks/useCasAuth.js +42 -0
- package/dist/hooks/useCasAuth.js.map +1 -0
- package/dist/hooks/useCasUser.d.ts +58 -0
- package/dist/hooks/useCasUser.d.ts.map +1 -0
- package/dist/hooks/useCasUser.js +50 -0
- package/dist/hooks/useCasUser.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +103 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +10 -8
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { CasConfig, CasContextValue, CasUser } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* React Context for CAS authentication state and actions.
|
|
5
|
+
*
|
|
6
|
+
* Access this through the {@link useCasAuth} or {@link useCasUser} hooks
|
|
7
|
+
* rather than consuming it directly.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CasContext: import("react").Context<CasContextValue | null>;
|
|
10
|
+
/**
|
|
11
|
+
* Props for the {@link CasProvider} component.
|
|
12
|
+
*/
|
|
13
|
+
export interface CasProviderProps {
|
|
14
|
+
/** CAS configuration (server URL, client ID, etc.). */
|
|
15
|
+
config: CasConfig;
|
|
16
|
+
/** Child components that need access to CAS auth state. */
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
/**
|
|
19
|
+
* Callback invoked after successful authentication.
|
|
20
|
+
* Useful for analytics, redirects, or syncing with app state.
|
|
21
|
+
*/
|
|
22
|
+
onAuthSuccess?: (user: CasUser) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Callback invoked when an authentication error occurs.
|
|
25
|
+
* Useful for error reporting or showing notifications.
|
|
26
|
+
*/
|
|
27
|
+
onAuthError?: (error: string) => void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* CAS Authentication Provider.
|
|
31
|
+
*
|
|
32
|
+
* Wrap your application (or a subtree) in `<CasProvider>` to enable CAS
|
|
33
|
+
* authentication. On mount the provider will:
|
|
34
|
+
*
|
|
35
|
+
* 1. Check `sessionStorage` for an existing user session.
|
|
36
|
+
* 2. If the URL contains a `?token=` parameter (i.e. the CAS redirect
|
|
37
|
+
* callback), automatically validate it via the backend and establish
|
|
38
|
+
* a session.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* import { CasProvider } from '@cas-system/react-cas-client';
|
|
43
|
+
*
|
|
44
|
+
* function App() {
|
|
45
|
+
* return (
|
|
46
|
+
* <CasProvider
|
|
47
|
+
* config={{
|
|
48
|
+
* serverUrl: 'https://cas.example.com',
|
|
49
|
+
* clientId: 'my-app',
|
|
50
|
+
* callbackUrl: 'https://myapp.com/auth/callback',
|
|
51
|
+
* backendValidateUrl: '/api/auth/validate',
|
|
52
|
+
* }}
|
|
53
|
+
* onAuthSuccess={(user) => console.log('Logged in:', user.username)}
|
|
54
|
+
* onAuthError={(err) => console.error('Auth failed:', err)}
|
|
55
|
+
* >
|
|
56
|
+
* <YourApp />
|
|
57
|
+
* </CasProvider>
|
|
58
|
+
* );
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare function CasProvider({ config, children, onAuthSuccess, onAuthError, }: CasProviderProps): import("react").JSX.Element;
|
|
63
|
+
//# sourceMappingURL=CasProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CasProvider.d.ts","sourceRoot":"","sources":["../src/CasProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,UAAU,iDAA8C,CAAC;AAGtE;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,MAAM,EAAE,SAAS,CAAC;IAElB,2DAA2D;IAC3D,QAAQ,EAAE,SAAS,CAAC;IAEpB;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAExC;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,QAAQ,EACR,aAAa,EACb,WAAW,GACZ,EAAE,gBAAgB,+BAqHlB"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { CasClient } from './cas-client';
|
|
4
|
+
/**
|
|
5
|
+
* React Context for CAS authentication state and actions.
|
|
6
|
+
*
|
|
7
|
+
* Access this through the {@link useCasAuth} or {@link useCasUser} hooks
|
|
8
|
+
* rather than consuming it directly.
|
|
9
|
+
*/
|
|
10
|
+
export const CasContext = createContext(null);
|
|
11
|
+
CasContext.displayName = 'CasContext';
|
|
12
|
+
/**
|
|
13
|
+
* CAS Authentication Provider.
|
|
14
|
+
*
|
|
15
|
+
* Wrap your application (or a subtree) in `<CasProvider>` to enable CAS
|
|
16
|
+
* authentication. On mount the provider will:
|
|
17
|
+
*
|
|
18
|
+
* 1. Check `sessionStorage` for an existing user session.
|
|
19
|
+
* 2. If the URL contains a `?token=` parameter (i.e. the CAS redirect
|
|
20
|
+
* callback), automatically validate it via the backend and establish
|
|
21
|
+
* a session.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* import { CasProvider } from '@cas-system/react-cas-client';
|
|
26
|
+
*
|
|
27
|
+
* function App() {
|
|
28
|
+
* return (
|
|
29
|
+
* <CasProvider
|
|
30
|
+
* config={{
|
|
31
|
+
* serverUrl: 'https://cas.example.com',
|
|
32
|
+
* clientId: 'my-app',
|
|
33
|
+
* callbackUrl: 'https://myapp.com/auth/callback',
|
|
34
|
+
* backendValidateUrl: '/api/auth/validate',
|
|
35
|
+
* }}
|
|
36
|
+
* onAuthSuccess={(user) => console.log('Logged in:', user.username)}
|
|
37
|
+
* onAuthError={(err) => console.error('Auth failed:', err)}
|
|
38
|
+
* >
|
|
39
|
+
* <YourApp />
|
|
40
|
+
* </CasProvider>
|
|
41
|
+
* );
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function CasProvider({ config, children, onAuthSuccess, onAuthError, }) {
|
|
46
|
+
const [user, setUser] = useState(null);
|
|
47
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
48
|
+
const [error, setError] = useState(null);
|
|
49
|
+
// Stable reference to the CAS client — recreated only when config changes
|
|
50
|
+
const client = useMemo(() => new CasClient(config), [config]);
|
|
51
|
+
// Guard to ensure the callback is handled at most once (React strict-mode safe)
|
|
52
|
+
const callbackHandled = useRef(false);
|
|
53
|
+
// -------------------------------------------------------------------------
|
|
54
|
+
// Initialisation
|
|
55
|
+
// -------------------------------------------------------------------------
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
let cancelled = false;
|
|
58
|
+
async function init() {
|
|
59
|
+
try {
|
|
60
|
+
// 1. If there is a ?token= in the URL, handle the callback
|
|
61
|
+
const token = client.extractTokenFromUrl();
|
|
62
|
+
if (token && !callbackHandled.current) {
|
|
63
|
+
callbackHandled.current = true;
|
|
64
|
+
const validatedUser = await client.handleCallback();
|
|
65
|
+
if (!cancelled && validatedUser) {
|
|
66
|
+
setUser(validatedUser);
|
|
67
|
+
setError(null);
|
|
68
|
+
onAuthSuccess?.(validatedUser);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// 2. Otherwise, try to restore a session from sessionStorage
|
|
73
|
+
const existingUser = client.getUser();
|
|
74
|
+
if (!cancelled && existingUser) {
|
|
75
|
+
setUser(existingUser);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (!cancelled) {
|
|
81
|
+
const message = err instanceof Error ? err.message : 'Authentication failed';
|
|
82
|
+
setError(message);
|
|
83
|
+
setUser(null);
|
|
84
|
+
onAuthError?.(message);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
if (!cancelled) {
|
|
89
|
+
setIsLoading(false);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
init();
|
|
94
|
+
return () => {
|
|
95
|
+
cancelled = true;
|
|
96
|
+
};
|
|
97
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
98
|
+
}, [client]);
|
|
99
|
+
// -------------------------------------------------------------------------
|
|
100
|
+
// Actions
|
|
101
|
+
// -------------------------------------------------------------------------
|
|
102
|
+
const login = useCallback((returnUrl) => {
|
|
103
|
+
client.login(returnUrl);
|
|
104
|
+
}, [client]);
|
|
105
|
+
const logout = useCallback(async (redirectUrl) => {
|
|
106
|
+
await client.logout(redirectUrl);
|
|
107
|
+
setUser(null);
|
|
108
|
+
setError(null);
|
|
109
|
+
}, [client]);
|
|
110
|
+
const hasRole = useCallback((role) => {
|
|
111
|
+
return user?.roles?.includes(role) ?? false;
|
|
112
|
+
}, [user]);
|
|
113
|
+
const hasAnyRole = useCallback((roles) => {
|
|
114
|
+
if (!user?.roles)
|
|
115
|
+
return false;
|
|
116
|
+
return roles.some((role) => user.roles.includes(role));
|
|
117
|
+
}, [user]);
|
|
118
|
+
// -------------------------------------------------------------------------
|
|
119
|
+
// Context value
|
|
120
|
+
// -------------------------------------------------------------------------
|
|
121
|
+
const value = useMemo(() => ({
|
|
122
|
+
user,
|
|
123
|
+
isAuthenticated: user !== null,
|
|
124
|
+
isLoading,
|
|
125
|
+
error,
|
|
126
|
+
login,
|
|
127
|
+
logout,
|
|
128
|
+
hasRole,
|
|
129
|
+
hasAnyRole,
|
|
130
|
+
}), [user, isLoading, error, login, logout, hasRole, hasAnyRole]);
|
|
131
|
+
return _jsx(CasContext.Provider, { value: value, children: children });
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=CasProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CasProvider.js","sourceRoot":"","sources":["../src/CasProvider.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { CasConfig, CasUser } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Core CAS (Central Authentication System) client for browser-side operations.
|
|
4
|
+
*
|
|
5
|
+
* This class handles the SSO redirect flow, token extraction from callback URLs,
|
|
6
|
+
* session management via `sessionStorage`, and role-based access checks.
|
|
7
|
+
*
|
|
8
|
+
* **Security note:** Token validation is ALWAYS delegated to your backend.
|
|
9
|
+
* This client never sends `client_secret` or validates JWTs in the browser.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { CasClient } from '@cas-system/react-cas-client';
|
|
14
|
+
*
|
|
15
|
+
* const client = new CasClient({
|
|
16
|
+
* serverUrl: 'https://cas.example.com',
|
|
17
|
+
* clientId: 'my-app',
|
|
18
|
+
* callbackUrl: 'https://myapp.com/auth/callback',
|
|
19
|
+
* backendValidateUrl: '/api/auth/validate',
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Redirect to CAS login
|
|
23
|
+
* client.login();
|
|
24
|
+
*
|
|
25
|
+
* // Handle the callback (after redirect back)
|
|
26
|
+
* const user = await client.handleCallback();
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare class CasClient {
|
|
30
|
+
private readonly config;
|
|
31
|
+
/**
|
|
32
|
+
* Create a new CAS client instance.
|
|
33
|
+
* @param config - The CAS configuration object.
|
|
34
|
+
*/
|
|
35
|
+
constructor(config: CasConfig);
|
|
36
|
+
/**
|
|
37
|
+
* Build the full CAS SSO login URL.
|
|
38
|
+
*
|
|
39
|
+
* @param returnUrl - Optional URL to return to after authentication.
|
|
40
|
+
* Falls back to `config.callbackUrl`, then to `window.location.href`.
|
|
41
|
+
* @returns The complete login URL including query parameters.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* const url = client.getLoginUrl('/dashboard');
|
|
46
|
+
* // => "https://cas.example.com/sso/login?client_id=my-app&response_type=token&redirect_uri=https://myapp.com/dashboard"
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
getLoginUrl(returnUrl?: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Redirect the browser to the CAS SSO login page.
|
|
52
|
+
*
|
|
53
|
+
* @param returnUrl - Optional URL to return to after authentication.
|
|
54
|
+
*/
|
|
55
|
+
login(returnUrl?: string): void;
|
|
56
|
+
/**
|
|
57
|
+
* Extract the JWT token from the current URL's `?token=` query parameter.
|
|
58
|
+
*
|
|
59
|
+
* After the CAS server authenticates the user, it redirects back to
|
|
60
|
+
* `callbackUrl?token=JWT_TOKEN`. This method reads that parameter.
|
|
61
|
+
*
|
|
62
|
+
* @returns The JWT token string, or `null` if not present.
|
|
63
|
+
*/
|
|
64
|
+
extractTokenFromUrl(): string | null;
|
|
65
|
+
/**
|
|
66
|
+
* Send the token to your backend for validation.
|
|
67
|
+
*
|
|
68
|
+
* Your backend should forward the token to
|
|
69
|
+
* `POST {casServerUrl}/api/validate-token` along with `client_id` and
|
|
70
|
+
* `client_secret` — values that must **never** be exposed to the browser.
|
|
71
|
+
*
|
|
72
|
+
* @param token - The JWT token to validate.
|
|
73
|
+
* @returns The validated {@link CasUser} object.
|
|
74
|
+
* @throws {Error} If `backendValidateUrl` is not configured or the request fails.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* const user = await client.validateTokenViaBackend(token);
|
|
79
|
+
* console.log(user.username); // 'john_doe'
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
validateTokenViaBackend(token: string): Promise<CasUser>;
|
|
83
|
+
/**
|
|
84
|
+
* Full callback handler: extract the token from the URL, validate it via the
|
|
85
|
+
* backend, persist the user in `sessionStorage`, and clean the URL.
|
|
86
|
+
*
|
|
87
|
+
* Call this on the page the CAS server redirects to after login.
|
|
88
|
+
*
|
|
89
|
+
* @returns The validated {@link CasUser}, or `null` if no `?token=` param is present.
|
|
90
|
+
* @throws {Error} If token validation fails.
|
|
91
|
+
*/
|
|
92
|
+
handleCallback(): Promise<CasUser | null>;
|
|
93
|
+
/**
|
|
94
|
+
* Retrieve the currently stored user from `sessionStorage`.
|
|
95
|
+
*
|
|
96
|
+
* @returns The stored {@link CasUser}, or `null` if no session exists.
|
|
97
|
+
*/
|
|
98
|
+
getUser(): CasUser | null;
|
|
99
|
+
/**
|
|
100
|
+
* Retrieve the currently stored JWT token from `sessionStorage`.
|
|
101
|
+
*
|
|
102
|
+
* @returns The stored token string, or `null` if no session exists.
|
|
103
|
+
*/
|
|
104
|
+
getToken(): string | null;
|
|
105
|
+
/**
|
|
106
|
+
* Check whether a user session currently exists.
|
|
107
|
+
*
|
|
108
|
+
* @returns `true` if a user is stored in `sessionStorage`.
|
|
109
|
+
*/
|
|
110
|
+
isAuthenticated(): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Persist a {@link CasUser} in `sessionStorage`.
|
|
113
|
+
* @internal
|
|
114
|
+
*/
|
|
115
|
+
private setUser;
|
|
116
|
+
/**
|
|
117
|
+
* Persist a JWT token in `sessionStorage`.
|
|
118
|
+
* @internal
|
|
119
|
+
*/
|
|
120
|
+
private setToken;
|
|
121
|
+
/**
|
|
122
|
+
* Clear all CAS-related data from `sessionStorage`.
|
|
123
|
+
* @internal
|
|
124
|
+
*/
|
|
125
|
+
private clearSession;
|
|
126
|
+
/**
|
|
127
|
+
* Log the user out by clearing the local session, calling the CAS server's
|
|
128
|
+
* logout endpoint, and optionally redirecting.
|
|
129
|
+
*
|
|
130
|
+
* @param redirectUrl - URL to navigate to after logout. If omitted, the
|
|
131
|
+
* page is reloaded at the current location.
|
|
132
|
+
*/
|
|
133
|
+
logout(redirectUrl?: string): Promise<void>;
|
|
134
|
+
/**
|
|
135
|
+
* Check whether the current user has a specific role.
|
|
136
|
+
*
|
|
137
|
+
* @param role - The role to check (case-sensitive).
|
|
138
|
+
* @returns `true` if the stored user has the given role.
|
|
139
|
+
*/
|
|
140
|
+
userHasRole(role: string): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Check whether the current user has **at least one** of the given roles.
|
|
143
|
+
*
|
|
144
|
+
* @param roles - The roles to check.
|
|
145
|
+
* @returns `true` if the user has any of the specified roles.
|
|
146
|
+
*/
|
|
147
|
+
userHasAnyRole(roles: string[]): boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Check whether the current user has **all** of the given roles.
|
|
150
|
+
*
|
|
151
|
+
* @param roles - The roles to check.
|
|
152
|
+
* @returns `true` if the user has every specified role.
|
|
153
|
+
*/
|
|
154
|
+
userHasAllRoles(roles: string[]): boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Remove the `token` query parameter from the browser URL without reloading.
|
|
157
|
+
* @internal
|
|
158
|
+
*/
|
|
159
|
+
private cleanUrl;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=cas-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cas-client.d.ts","sourceRoot":"","sources":["../src/cas-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAQlD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IAEnC;;;OAGG;gBACS,MAAM,EAAE,SAAS;IAQ7B;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM;IAiBvC;;;;OAIG;IACH,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAU/B;;;;;;;OAOG;IACH,mBAAmB,IAAI,MAAM,GAAG,IAAI;IAOpC;;;;;;;;;;;;;;;;OAgBG;IACG,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA4B9D;;;;;;;;OAQG;IACG,cAAc,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAoB/C;;;;OAIG;IACH,OAAO,IAAI,OAAO,GAAG,IAAI;IAWzB;;;;OAIG;IACH,QAAQ,IAAI,MAAM,GAAG,IAAI;IAUzB;;;;OAIG;IACH,eAAe,IAAI,OAAO;IAI1B;;;OAGG;IACH,OAAO,CAAC,OAAO;IAKf;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAKhB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAUpB;;;;;;OAMG;IACG,MAAM,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BjD;;;;;OAKG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAKlC;;;;;OAKG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO;IAMxC;;;;;OAKG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO;IAUzC;;;OAGG;IACH,OAAO,CAAC,QAAQ;CASjB"}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/** Key used to persist user data in sessionStorage. */
|
|
2
|
+
const STORAGE_KEY = 'cas_user';
|
|
3
|
+
/** Key used to persist the JWT token in sessionStorage. */
|
|
4
|
+
const TOKEN_STORAGE_KEY = 'cas_token';
|
|
5
|
+
/**
|
|
6
|
+
* Core CAS (Central Authentication System) client for browser-side operations.
|
|
7
|
+
*
|
|
8
|
+
* This class handles the SSO redirect flow, token extraction from callback URLs,
|
|
9
|
+
* session management via `sessionStorage`, and role-based access checks.
|
|
10
|
+
*
|
|
11
|
+
* **Security note:** Token validation is ALWAYS delegated to your backend.
|
|
12
|
+
* This client never sends `client_secret` or validates JWTs in the browser.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { CasClient } from '@cas-system/react-cas-client';
|
|
17
|
+
*
|
|
18
|
+
* const client = new CasClient({
|
|
19
|
+
* serverUrl: 'https://cas.example.com',
|
|
20
|
+
* clientId: 'my-app',
|
|
21
|
+
* callbackUrl: 'https://myapp.com/auth/callback',
|
|
22
|
+
* backendValidateUrl: '/api/auth/validate',
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Redirect to CAS login
|
|
26
|
+
* client.login();
|
|
27
|
+
*
|
|
28
|
+
* // Handle the callback (after redirect back)
|
|
29
|
+
* const user = await client.handleCallback();
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class CasClient {
|
|
33
|
+
/**
|
|
34
|
+
* Create a new CAS client instance.
|
|
35
|
+
* @param config - The CAS configuration object.
|
|
36
|
+
*/
|
|
37
|
+
constructor(config) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
}
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Login / Redirect
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
/**
|
|
44
|
+
* Build the full CAS SSO login URL.
|
|
45
|
+
*
|
|
46
|
+
* @param returnUrl - Optional URL to return to after authentication.
|
|
47
|
+
* Falls back to `config.callbackUrl`, then to `window.location.href`.
|
|
48
|
+
* @returns The complete login URL including query parameters.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* const url = client.getLoginUrl('/dashboard');
|
|
53
|
+
* // => "https://cas.example.com/sso/login?client_id=my-app&response_type=token&redirect_uri=https://myapp.com/dashboard"
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
getLoginUrl(returnUrl) {
|
|
57
|
+
const redirectUri = returnUrl ??
|
|
58
|
+
this.config.callbackUrl ??
|
|
59
|
+
(typeof window !== 'undefined'
|
|
60
|
+
? window.location.href
|
|
61
|
+
: '');
|
|
62
|
+
const params = new URLSearchParams({
|
|
63
|
+
client_id: this.config.clientId,
|
|
64
|
+
response_type: 'token',
|
|
65
|
+
redirect_uri: redirectUri,
|
|
66
|
+
});
|
|
67
|
+
return `${this.config.serverUrl}/sso/login?${params.toString()}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Redirect the browser to the CAS SSO login page.
|
|
71
|
+
*
|
|
72
|
+
* @param returnUrl - Optional URL to return to after authentication.
|
|
73
|
+
*/
|
|
74
|
+
login(returnUrl) {
|
|
75
|
+
if (typeof window !== 'undefined') {
|
|
76
|
+
window.location.href = this.getLoginUrl(returnUrl);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Token Handling
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
/**
|
|
83
|
+
* Extract the JWT token from the current URL's `?token=` query parameter.
|
|
84
|
+
*
|
|
85
|
+
* After the CAS server authenticates the user, it redirects back to
|
|
86
|
+
* `callbackUrl?token=JWT_TOKEN`. This method reads that parameter.
|
|
87
|
+
*
|
|
88
|
+
* @returns The JWT token string, or `null` if not present.
|
|
89
|
+
*/
|
|
90
|
+
extractTokenFromUrl() {
|
|
91
|
+
if (typeof window === 'undefined')
|
|
92
|
+
return null;
|
|
93
|
+
const params = new URLSearchParams(window.location.search);
|
|
94
|
+
return params.get('token');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Send the token to your backend for validation.
|
|
98
|
+
*
|
|
99
|
+
* Your backend should forward the token to
|
|
100
|
+
* `POST {casServerUrl}/api/validate-token` along with `client_id` and
|
|
101
|
+
* `client_secret` — values that must **never** be exposed to the browser.
|
|
102
|
+
*
|
|
103
|
+
* @param token - The JWT token to validate.
|
|
104
|
+
* @returns The validated {@link CasUser} object.
|
|
105
|
+
* @throws {Error} If `backendValidateUrl` is not configured or the request fails.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* const user = await client.validateTokenViaBackend(token);
|
|
110
|
+
* console.log(user.username); // 'john_doe'
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
async validateTokenViaBackend(token) {
|
|
114
|
+
const { backendValidateUrl } = this.config;
|
|
115
|
+
if (!backendValidateUrl) {
|
|
116
|
+
throw new Error('[CAS Client] backendValidateUrl is not configured. ' +
|
|
117
|
+
'Token validation must be performed by your backend server.');
|
|
118
|
+
}
|
|
119
|
+
const response = await fetch(backendValidateUrl, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Content-Type': 'application/json' },
|
|
122
|
+
body: JSON.stringify({ token }),
|
|
123
|
+
credentials: 'include',
|
|
124
|
+
});
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
127
|
+
throw new Error(`[CAS Client] Token validation failed (${response.status}): ${errorText}`);
|
|
128
|
+
}
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
return data;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Full callback handler: extract the token from the URL, validate it via the
|
|
134
|
+
* backend, persist the user in `sessionStorage`, and clean the URL.
|
|
135
|
+
*
|
|
136
|
+
* Call this on the page the CAS server redirects to after login.
|
|
137
|
+
*
|
|
138
|
+
* @returns The validated {@link CasUser}, or `null` if no `?token=` param is present.
|
|
139
|
+
* @throws {Error} If token validation fails.
|
|
140
|
+
*/
|
|
141
|
+
async handleCallback() {
|
|
142
|
+
const token = this.extractTokenFromUrl();
|
|
143
|
+
if (!token)
|
|
144
|
+
return null;
|
|
145
|
+
const user = await this.validateTokenViaBackend(token);
|
|
146
|
+
// Persist user and token in sessionStorage
|
|
147
|
+
this.setUser(user);
|
|
148
|
+
this.setToken(token);
|
|
149
|
+
// Remove the token from the URL to prevent accidental leakage
|
|
150
|
+
this.cleanUrl();
|
|
151
|
+
return user;
|
|
152
|
+
}
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Session Management
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
/**
|
|
157
|
+
* Retrieve the currently stored user from `sessionStorage`.
|
|
158
|
+
*
|
|
159
|
+
* @returns The stored {@link CasUser}, or `null` if no session exists.
|
|
160
|
+
*/
|
|
161
|
+
getUser() {
|
|
162
|
+
if (typeof window === 'undefined')
|
|
163
|
+
return null;
|
|
164
|
+
try {
|
|
165
|
+
const raw = sessionStorage.getItem(STORAGE_KEY);
|
|
166
|
+
return raw ? JSON.parse(raw) : null;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Retrieve the currently stored JWT token from `sessionStorage`.
|
|
174
|
+
*
|
|
175
|
+
* @returns The stored token string, or `null` if no session exists.
|
|
176
|
+
*/
|
|
177
|
+
getToken() {
|
|
178
|
+
if (typeof window === 'undefined')
|
|
179
|
+
return null;
|
|
180
|
+
try {
|
|
181
|
+
return sessionStorage.getItem(TOKEN_STORAGE_KEY);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Check whether a user session currently exists.
|
|
189
|
+
*
|
|
190
|
+
* @returns `true` if a user is stored in `sessionStorage`.
|
|
191
|
+
*/
|
|
192
|
+
isAuthenticated() {
|
|
193
|
+
return this.getUser() !== null;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Persist a {@link CasUser} in `sessionStorage`.
|
|
197
|
+
* @internal
|
|
198
|
+
*/
|
|
199
|
+
setUser(user) {
|
|
200
|
+
if (typeof window === 'undefined')
|
|
201
|
+
return;
|
|
202
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(user));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Persist a JWT token in `sessionStorage`.
|
|
206
|
+
* @internal
|
|
207
|
+
*/
|
|
208
|
+
setToken(token) {
|
|
209
|
+
if (typeof window === 'undefined')
|
|
210
|
+
return;
|
|
211
|
+
sessionStorage.setItem(TOKEN_STORAGE_KEY, token);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Clear all CAS-related data from `sessionStorage`.
|
|
215
|
+
* @internal
|
|
216
|
+
*/
|
|
217
|
+
clearSession() {
|
|
218
|
+
if (typeof window === 'undefined')
|
|
219
|
+
return;
|
|
220
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
221
|
+
sessionStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
222
|
+
}
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Logout
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
/**
|
|
227
|
+
* Log the user out by clearing the local session, calling the CAS server's
|
|
228
|
+
* logout endpoint, and optionally redirecting.
|
|
229
|
+
*
|
|
230
|
+
* @param redirectUrl - URL to navigate to after logout. If omitted, the
|
|
231
|
+
* page is reloaded at the current location.
|
|
232
|
+
*/
|
|
233
|
+
async logout(redirectUrl) {
|
|
234
|
+
// Clear local session first
|
|
235
|
+
this.clearSession();
|
|
236
|
+
// Call the CAS server logout endpoint (fire-and-forget)
|
|
237
|
+
try {
|
|
238
|
+
await fetch(`${this.config.serverUrl}/api/logout`, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
credentials: 'include',
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Swallow network errors — the local session is already cleared
|
|
245
|
+
}
|
|
246
|
+
// Redirect
|
|
247
|
+
if (typeof window !== 'undefined') {
|
|
248
|
+
if (redirectUrl) {
|
|
249
|
+
window.location.href = redirectUrl;
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
window.location.reload();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// Role Helpers
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
/**
|
|
260
|
+
* Check whether the current user has a specific role.
|
|
261
|
+
*
|
|
262
|
+
* @param role - The role to check (case-sensitive).
|
|
263
|
+
* @returns `true` if the stored user has the given role.
|
|
264
|
+
*/
|
|
265
|
+
userHasRole(role) {
|
|
266
|
+
const user = this.getUser();
|
|
267
|
+
return user?.roles?.includes(role) ?? false;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Check whether the current user has **at least one** of the given roles.
|
|
271
|
+
*
|
|
272
|
+
* @param roles - The roles to check.
|
|
273
|
+
* @returns `true` if the user has any of the specified roles.
|
|
274
|
+
*/
|
|
275
|
+
userHasAnyRole(roles) {
|
|
276
|
+
const user = this.getUser();
|
|
277
|
+
if (!user?.roles)
|
|
278
|
+
return false;
|
|
279
|
+
return roles.some((role) => user.roles.includes(role));
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Check whether the current user has **all** of the given roles.
|
|
283
|
+
*
|
|
284
|
+
* @param roles - The roles to check.
|
|
285
|
+
* @returns `true` if the user has every specified role.
|
|
286
|
+
*/
|
|
287
|
+
userHasAllRoles(roles) {
|
|
288
|
+
const user = this.getUser();
|
|
289
|
+
if (!user?.roles)
|
|
290
|
+
return false;
|
|
291
|
+
return roles.every((role) => user.roles.includes(role));
|
|
292
|
+
}
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// Internal Helpers
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
/**
|
|
297
|
+
* Remove the `token` query parameter from the browser URL without reloading.
|
|
298
|
+
* @internal
|
|
299
|
+
*/
|
|
300
|
+
cleanUrl() {
|
|
301
|
+
if (typeof window === 'undefined')
|
|
302
|
+
return;
|
|
303
|
+
const url = new URL(window.location.href);
|
|
304
|
+
url.searchParams.delete('token');
|
|
305
|
+
const cleanedUrl = url.pathname + (url.search || '') + url.hash;
|
|
306
|
+
window.history.replaceState({}, document.title, cleanedUrl);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
//# sourceMappingURL=cas-client.js.map
|