@loka-sms/sso 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,193 @@
1
+ # @loka-sms/sso
2
+
3
+ SSO utilities, hooks, and components for Loka SMS modules.
4
+
5
+ **Cookie-first auth** for same-domain apps. **OAuth2 PKCE** for third-party/external apps.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @loka-sms/sso
11
+ ```
12
+
13
+ ## Quick Start — Same-Domain App (Cookie-First)
14
+
15
+ App runs on same domain as Gateway (e.g. `10.7.1.82`). Cookie `sms_ac_token` shared across all ports.
16
+
17
+ ### 1. Setup API client
18
+
19
+ ```ts
20
+ // src/api/client.ts
21
+ import axios from 'axios';
22
+ import { createAuthInterceptor } from '@loka-sms/sso';
23
+
24
+ const api = axios.create({
25
+ baseURL: import.meta.env.VITE_API_URL || '/api',
26
+ withCredentials: true,
27
+ });
28
+
29
+ api.interceptors.request.use(createAuthInterceptor());
30
+ export default api;
31
+ ```
32
+
33
+ Headers automatically attached:
34
+
35
+ | Header | Source |
36
+ |--------|--------|
37
+ | `Authorization: Bearer <token>` | Cookie `sms_ac_token` → localStorage fallback |
38
+ | `X-School-ID` | localStorage `school_sms_id` |
39
+ | `X-User-ID` | localStorage `user_id` |
40
+ | `X-User-Role` | localStorage `user_role` |
41
+ | `X-Device-ID` | localStorage `device_sms_id` |
42
+ | `X-Request-ID` | `crypto.randomUUID()` |
43
+
44
+ ### 2. Setup Auth Store
45
+
46
+ ```ts
47
+ // src/stores/authStore.ts
48
+ function getCookie(name: string): string {
49
+ const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
50
+ return match ? decodeURIComponent(match[2]) : '';
51
+ }
52
+
53
+ function rehydrateUser() {
54
+ const cached = localStorage.getItem('my_user');
55
+ if (cached) return JSON.parse(cached);
56
+
57
+ const token = getCookie('sms_ac_token');
58
+ if (!token) return null;
59
+
60
+ const payload = JSON.parse(atob(token.split('.')[1]));
61
+ const user = {
62
+ sub: payload.sub, email: payload.email, fullName: payload.fullName,
63
+ role: payload.role, schoolId: payload.schoolId || '',
64
+ };
65
+ localStorage.setItem('my_user', JSON.stringify(user));
66
+ return user;
67
+ }
68
+ ```
69
+
70
+ ### 3. Add routes
71
+
72
+ ```tsx
73
+ import { OAuthCallback } from '@loka-sms/sso';
74
+ import SignIn from './pages/SignIn';
75
+
76
+ <Route path="/signin" element={<SignIn />} />
77
+ <Route path="/auth/callback" element={<OAuthCallback clientId="my-module" />} />
78
+ ```
79
+
80
+ ### 4. Listen for cross-app logout
81
+
82
+ ```tsx
83
+ import { useCrossAppLogout } from '@loka-sms/sso';
84
+
85
+ function App() {
86
+ useCrossAppLogout(); // listens on BroadcastChannel 'loka-sso-logout'
87
+ // ...
88
+ }
89
+ ```
90
+
91
+ ## Quick Start — Third-Party App (OAuth2 PKCE)
92
+
93
+ App on different domain from Gateway. Full PKCE flow required.
94
+
95
+ ### 1. Redirect user to authorize
96
+
97
+ ```ts
98
+ import { generateCodeVerifier, generateCodeChallenge, generateState } from '@loka-sms/sso';
99
+
100
+ const verifier = generateCodeVerifier();
101
+ const state = generateState();
102
+ sessionStorage.setItem(`oauth_verifier_${state}`, verifier);
103
+
104
+ const challenge = await generateCodeChallenge(verifier);
105
+ const params = new URLSearchParams({
106
+ client_id: 'my-app',
107
+ redirect_uri: 'https://my-app.com/auth/callback',
108
+ response_type: 'code',
109
+ code_challenge: challenge,
110
+ code_challenge_method: 'S256',
111
+ state,
112
+ });
113
+ window.location.href = `https://gateway.loka.id/api/oauth/authorize?${params}`;
114
+ ```
115
+
116
+ ### 2. Handle callback
117
+
118
+ ```tsx
119
+ import { OAuthCallback } from '@loka-sms/sso';
120
+
121
+ <Route path="/auth/callback" element={<OAuthCallback clientId="my-app" />} />
122
+ ```
123
+
124
+ Or use the hook directly:
125
+
126
+ ```ts
127
+ import { useOAuthCallback } from '@loka-sms/sso';
128
+
129
+ function AuthCallback() {
130
+ const { error, loading, phase } = useOAuthCallback({
131
+ clientId: 'my-app',
132
+ apiBase: 'https://gateway.loka.id/api',
133
+ redirectPath: '/dashboard',
134
+ });
135
+ // ...
136
+ }
137
+ ```
138
+
139
+ ## API Reference
140
+
141
+ ### Interceptor
142
+
143
+ | Export | Description |
144
+ |--------|-------------|
145
+ | `createAuthInterceptor()` | Axios request interceptor. Reads token cookie-first, attaches Authorization + context headers. |
146
+
147
+ ### Hooks
148
+
149
+ | Export | Description |
150
+ |--------|-------------|
151
+ | `useCrossAppLogout()` | Listens on BroadcastChannel `loka-sso-logout`. Clears auth state and redirects to `/signin`. |
152
+ | `useOAuthCallback(input)` | Handles OAuth2 PKCE code exchange. Returns `{ error, loading, phase }`. |
153
+
154
+ **`useOAuthCallback` input:**
155
+
156
+ | Field | Type | Default | Description |
157
+ |-------|------|---------|-------------|
158
+ | `clientId` | `string` | required | OAuth2 client ID |
159
+ | `apiBase` | `string` | `'/api'` | Gateway API base URL |
160
+ | `redirectPath` | `string` | `'/'` | Redirect path after success |
161
+
162
+ ### Components
163
+
164
+ | Export | Description |
165
+ |--------|-------------|
166
+ | `<OAuthCallback clientId />` | Drop-in OAuth2 PKCE callback page. Wraps `useOAuthCallback`. |
167
+ | `<OAuthTransfer />` | **Deprecated.** Legacy token-in-URL handler. Use cookie-first auth instead. |
168
+
169
+ ### PKCE Utilities
170
+
171
+ | Export | Description |
172
+ |--------|-------------|
173
+ | `generateCodeVerifier()` | Generate cryptographically random code verifier |
174
+ | `generateCodeChallenge(verifier)` | SHA-256 hash → base64url encode |
175
+ | `generateState()` | Generate random state string for CSRF protection |
176
+ | `sha256(input)` | Pure JS SHA-256 implementation |
177
+
178
+ ### Constants
179
+
180
+ | Export | Description |
181
+ |--------|-------------|
182
+ | `SSO_STORAGE_KEYS` | Standard localStorage key names |
183
+ | `SSO_CHANNELS` | BroadcastChannel names (`loka-sso-logout`, `loka-sso-login`, `loka-school-change`) |
184
+ | `API_HEADERS` | Standard API header names |
185
+
186
+ ## Architecture
187
+
188
+ ```
189
+ Same-domain apps (10.7.1.82:*) → cookie sms_ac_token shared → auto-login
190
+ Third-party apps (external domain) → OAuth2 PKCE → code exchange → token
191
+ ```
192
+
193
+ Cookie `sms_ac_token` is the single source of truth for authentication on same domain. `localStorage` is cache only. See [docs/flow_baru.md](../../docs/flow_baru.md) for full architecture.
@@ -0,0 +1,10 @@
1
+ import type { OAuthCallbackInput } from '../hooks/useOAuthCallback';
2
+ export interface OAuthCallbackProps extends Omit<OAuthCallbackInput, 'apiBase' | 'redirectPath'> {
3
+ apiBase?: string;
4
+ redirectPath?: string;
5
+ loadingText?: string;
6
+ errorText?: string;
7
+ loginLinkText?: string;
8
+ loginPath?: string;
9
+ }
10
+ export declare function OAuthCallback({ clientId, apiBase, redirectPath, loadingText, errorText, loginLinkText, loginPath, }: OAuthCallbackProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OAuthCallback = OAuthCallback;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const useOAuthCallback_1 = require("../hooks/useOAuthCallback");
6
+ function OAuthCallback({ clientId, apiBase, redirectPath, loadingText = 'Menyelesaikan login...', errorText, loginLinkText = 'Kembali ke login', loginPath = '/signin', }) {
7
+ const { error, loading, phase } = (0, useOAuthCallback_1.useOAuthCallback)({ clientId, apiBase, redirectPath });
8
+ if (error) {
9
+ return ((0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-center min-h-screen", children: (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-red-500 mb-4", children: errorText || error }), (0, jsx_runtime_1.jsx)("a", { href: loginPath, className: "text-brand-500 hover:text-brand-600", children: loginLinkText })] }) }));
10
+ }
11
+ return ((0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-center min-h-screen", children: (0, jsx_runtime_1.jsx)("p", { className: "text-gray-500", children: loadingText }) }));
12
+ }
@@ -0,0 +1,6 @@
1
+ export interface OAuthTransferProps {
2
+ apiBase?: string;
3
+ redirectPath?: string;
4
+ loginPath?: string;
5
+ }
6
+ export declare function OAuthTransfer({ apiBase, redirectPath, loginPath, }: OAuthTransferProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OAuthTransfer = OAuthTransfer;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ // DEPRECATED: Cookie-based auth replaces token-in-URL transfer.
6
+ // Kept for backward compatibility with external SSO clients.
7
+ const react_1 = require("react");
8
+ function OAuthTransfer({ apiBase, redirectPath = '/', loginPath = '/signin', }) {
9
+ const [error, setError] = (0, react_1.useState)('');
10
+ (0, react_1.useEffect)(() => {
11
+ const params = new URLSearchParams(window.location.search);
12
+ const token = params.get('token');
13
+ const refreshToken = params.get('refreshToken');
14
+ const base = apiBase || '/api';
15
+ if (!token) {
16
+ setError('Token tidak ditemukan.');
17
+ return;
18
+ }
19
+ // 1. Save JWT immediately (no network needed)
20
+ localStorage.setItem('sms_ac_token', token);
21
+ if (refreshToken)
22
+ localStorage.setItem('sms_refresh_token', refreshToken);
23
+ try {
24
+ const payload = JSON.parse(atob(token.split('.')[1]));
25
+ const userObj = {
26
+ sub: payload.sub, email: payload.email, fullName: payload.fullName,
27
+ name: payload.fullName, role: payload.role, schoolId: payload.schoolId,
28
+ features: payload.features || [], accessibleSchools: payload.accessibleSchools || [],
29
+ };
30
+ localStorage.setItem('lms_user', JSON.stringify(userObj));
31
+ localStorage.setItem('user_role', (payload.role || '').toLowerCase());
32
+ localStorage.setItem('user_id', payload.sub || '');
33
+ if (payload.schoolId)
34
+ localStorage.setItem('school_sms_id', payload.schoolId);
35
+ }
36
+ catch { /* decode non-critical */ }
37
+ // 2. Redirect immediately
38
+ window.location.href = redirectPath;
39
+ // 3. API calls — fire-and-forget
40
+ fetch(`${base}/auth/set-cookie`, {
41
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
42
+ credentials: 'include',
43
+ body: JSON.stringify({ token, refreshToken }),
44
+ }).catch(() => { });
45
+ fetch(`${base}/auth/me`, {
46
+ headers: { Authorization: `Bearer ${token}` },
47
+ credentials: 'include',
48
+ }).then(r => r.json()).then(d => {
49
+ const p = d?.data ?? d;
50
+ if (p) {
51
+ localStorage.setItem('sms_user_profile', JSON.stringify(p));
52
+ document.cookie = `sms_user_profile=${encodeURIComponent(JSON.stringify(p))}; path=/; SameSite=Lax`;
53
+ }
54
+ }).catch(() => { });
55
+ }, []);
56
+ if (error) {
57
+ return ((0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-center min-h-screen", children: (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-red-500 mb-4", children: error }), (0, jsx_runtime_1.jsx)("a", { href: loginPath, children: "Kembali ke login" })] }) }));
58
+ }
59
+ return ((0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-center min-h-screen", children: (0, jsx_runtime_1.jsx)("p", { className: "text-gray-500", children: "Menyelesaikan login..." }) }));
60
+ }
@@ -0,0 +1,22 @@
1
+ export declare const SSO_STORAGE_KEYS: {
2
+ readonly ACCESS_TOKEN: "sms_ac_token";
3
+ readonly REFRESH_TOKEN: "sms_refresh_token";
4
+ readonly USER_PROFILE: "sms_user_profile";
5
+ readonly SCHOOL_ID: "school_sms_id";
6
+ readonly USER_ROLE: "user_role";
7
+ readonly USER_ID: "user_id";
8
+ readonly DEVICE_ID: "device_sms_id";
9
+ };
10
+ export declare const SSO_CHANNELS: {
11
+ readonly LOGOUT: "loka-sso-logout";
12
+ readonly LOGIN: "loka-sso-login";
13
+ readonly SCHOOL_CHANGE: "loka-school-change";
14
+ };
15
+ export declare const API_HEADERS: {
16
+ readonly AUTHORIZATION: "Authorization";
17
+ readonly SCHOOL_ID: "X-School-ID";
18
+ readonly USER_ROLE: "X-User-Role";
19
+ readonly USER_ID: "X-User-Id";
20
+ readonly DEVICE_ID: "X-Device-ID";
21
+ readonly REQUEST_ID: "X-Request-ID";
22
+ };
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.API_HEADERS = exports.SSO_CHANNELS = exports.SSO_STORAGE_KEYS = void 0;
4
+ exports.SSO_STORAGE_KEYS = {
5
+ ACCESS_TOKEN: 'sms_ac_token',
6
+ REFRESH_TOKEN: 'sms_refresh_token',
7
+ USER_PROFILE: 'sms_user_profile',
8
+ SCHOOL_ID: 'school_sms_id',
9
+ USER_ROLE: 'user_role',
10
+ USER_ID: 'user_id',
11
+ DEVICE_ID: 'device_sms_id',
12
+ };
13
+ exports.SSO_CHANNELS = {
14
+ LOGOUT: 'loka-sso-logout',
15
+ LOGIN: 'loka-sso-login',
16
+ SCHOOL_CHANGE: 'loka-school-change',
17
+ };
18
+ exports.API_HEADERS = {
19
+ AUTHORIZATION: 'Authorization',
20
+ SCHOOL_ID: 'X-School-ID',
21
+ USER_ROLE: 'X-User-Role',
22
+ USER_ID: 'X-User-Id',
23
+ DEVICE_ID: 'X-Device-ID',
24
+ REQUEST_ID: 'X-Request-ID',
25
+ };
@@ -0,0 +1,6 @@
1
+ interface CrossAppLogoutOptions {
2
+ onLogout?: () => void;
3
+ redirectPath?: string;
4
+ }
5
+ export declare function useCrossAppLogout(options?: CrossAppLogoutOptions): void;
6
+ export {};
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useCrossAppLogout = useCrossAppLogout;
4
+ const react_1 = require("react");
5
+ function useCrossAppLogout(options) {
6
+ const redirectPath = options?.redirectPath || '/signin';
7
+ (0, react_1.useEffect)(() => {
8
+ const cleanup = [];
9
+ const bc = new BroadcastChannel('loka-sso-logout');
10
+ bc.onmessage = () => {
11
+ options?.onLogout?.();
12
+ window.location.href = redirectPath;
13
+ };
14
+ cleanup.push(() => bc.close());
15
+ const handler = (event) => {
16
+ if (event.key === 'sms_ac_token' && !event.newValue) {
17
+ options?.onLogout?.();
18
+ window.location.href = redirectPath;
19
+ }
20
+ };
21
+ window.addEventListener('storage', handler);
22
+ cleanup.push(() => window.removeEventListener('storage', handler));
23
+ return () => cleanup.forEach((fn) => fn());
24
+ }, [redirectPath]);
25
+ }
@@ -0,0 +1,11 @@
1
+ export interface OAuthCallbackInput {
2
+ clientId: string;
3
+ apiBase?: string;
4
+ redirectPath?: string;
5
+ }
6
+ export interface OAuthCallbackState {
7
+ error: string | null;
8
+ loading: boolean;
9
+ phase: 'init' | 'exchange' | 'legacy' | 'done' | 'error';
10
+ }
11
+ export declare function useOAuthCallback(input: OAuthCallbackInput): OAuthCallbackState;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useOAuthCallback = useOAuthCallback;
4
+ const react_1 = require("react");
5
+ const processedMarkers = new Set();
6
+ function saveJwt(token, refreshToken) {
7
+ localStorage.setItem('sms_ac_token', token);
8
+ if (refreshToken)
9
+ localStorage.setItem('sms_refresh_token', refreshToken);
10
+ try {
11
+ const payload = JSON.parse(atob(token.split('.')[1]));
12
+ const userObj = {
13
+ sub: payload.sub, email: payload.email, fullName: payload.fullName,
14
+ name: payload.fullName, role: payload.role, schoolId: payload.schoolId,
15
+ features: payload.features || [], accessibleSchools: payload.accessibleSchools || [],
16
+ };
17
+ localStorage.setItem('lms_user', JSON.stringify(userObj));
18
+ localStorage.setItem('user_role', (payload.role || '').toLowerCase());
19
+ localStorage.setItem('user_id', payload.sub || '');
20
+ if (payload.schoolId)
21
+ localStorage.setItem('school_sms_id', payload.schoolId);
22
+ }
23
+ catch { /* decode non-critical */ }
24
+ }
25
+ function useOAuthCallback(input) {
26
+ const [state, setState] = (0, react_1.useState)({ error: null, loading: true, phase: 'init' });
27
+ (0, react_1.useEffect)(() => {
28
+ const params = new URLSearchParams(window.location.search);
29
+ const code = params.get('code');
30
+ const oauthState = params.get('state');
31
+ const token = params.get('token');
32
+ const refreshToken = params.get('refresh_token') || params.get('refreshToken');
33
+ const apiBase = input.apiBase || '/api';
34
+ const marker = code || token || '';
35
+ if (processedMarkers.has(marker))
36
+ return;
37
+ processedMarkers.add(marker);
38
+ // init=1 auto-PKCE trigger removed — cross-app auth now uses shared cookie.
39
+ // OAuth2 PKCE flow is still available for external/third-party apps via manual redirect.
40
+ if (!code && !token) {
41
+ setState({ error: 'Token tidak ditemukan.', loading: false, phase: 'error' });
42
+ return;
43
+ }
44
+ // Phase 2 & 3 — Save JWT FIRST, then redirect (API calls fire-and-forget)
45
+ (async () => {
46
+ let accessToken = token || '';
47
+ let refreshTok = refreshToken || '';
48
+ try {
49
+ if (code) {
50
+ setState({ error: null, loading: true, phase: 'exchange' });
51
+ const codeVerifier = sessionStorage.getItem(`oauth_verifier_${oauthState || ''}`) || '';
52
+ sessionStorage.removeItem(`oauth_verifier_${oauthState || ''}`);
53
+ let res = await fetch(`${apiBase}/oauth/token`, {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ credentials: 'include',
57
+ body: JSON.stringify({ grant_type: 'authorization_code', code, code_verifier: codeVerifier }),
58
+ });
59
+ // Development fallback: retry without code_verifier if PKCE fails
60
+ if (!res.ok && !codeVerifier) {
61
+ res = await fetch(`${apiBase}/oauth/token`, {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/json' },
64
+ credentials: 'include',
65
+ body: JSON.stringify({ grant_type: 'authorization_code', code }),
66
+ });
67
+ }
68
+ if (!res.ok)
69
+ throw new Error('Token exchange failed');
70
+ const d = await res.json();
71
+ accessToken = d.access_token;
72
+ refreshTok = d.refresh_token || '';
73
+ }
74
+ else {
75
+ setState({ error: null, loading: true, phase: 'legacy' });
76
+ accessToken = token;
77
+ refreshTok = refreshToken || '';
78
+ }
79
+ // 1. Save token from JWT decode FIRST — no network needed
80
+ saveJwt(accessToken, refreshTok);
81
+ // 2. Redirect immediately — don't wait for API calls
82
+ setState({ error: null, loading: false, phase: 'done' });
83
+ window.location.href = input.redirectPath || '/';
84
+ }
85
+ catch {
86
+ // Even if token exchange fails, saved token from JWT decode might still exist
87
+ if (accessToken)
88
+ saveJwt(accessToken, refreshTok);
89
+ setState({ error: 'Gagal menyelesaikan login. Silakan coba lagi.', loading: false, phase: 'error' });
90
+ }
91
+ // 3. API calls — fire-and-forget (non-blocking)
92
+ if (accessToken) {
93
+ fetch(`${apiBase}/auth/set-cookie`, {
94
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
95
+ credentials: 'include',
96
+ body: JSON.stringify({ token: accessToken, refreshToken: refreshTok }),
97
+ }).catch(() => { });
98
+ fetch(`${apiBase}/auth/me`, {
99
+ headers: { Authorization: `Bearer ${accessToken}` },
100
+ credentials: 'include',
101
+ }).then(r => r.json()).then(d => {
102
+ const p = d?.data ?? d;
103
+ if (p) {
104
+ localStorage.setItem('sms_user_profile', JSON.stringify(p));
105
+ document.cookie = `sms_user_profile=${encodeURIComponent(JSON.stringify(p))}; path=/; SameSite=Lax`;
106
+ }
107
+ }).catch(() => { });
108
+ }
109
+ })();
110
+ }, []);
111
+ return state;
112
+ }
@@ -0,0 +1,10 @@
1
+ export { sha256 } from './sha256';
2
+ export { generateCodeVerifier, generateCodeChallenge, generateState, } from './pkce';
3
+ export { SSO_STORAGE_KEYS, SSO_CHANNELS, API_HEADERS, } from './constants';
4
+ export { createAuthInterceptor } from './interceptor';
5
+ export { useOAuthCallback } from './hooks/useOAuthCallback';
6
+ export { useCrossAppLogout } from './hooks/useCrossAppLogout';
7
+ export { OAuthCallback } from './components/OAuthCallback';
8
+ export type { OAuthCallbackProps } from './components/OAuthCallback';
9
+ export { OAuthTransfer } from './components/OAuthTransfer';
10
+ export type { OAuthTransferProps } from './components/OAuthTransfer';
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OAuthTransfer = exports.OAuthCallback = exports.useCrossAppLogout = exports.useOAuthCallback = exports.createAuthInterceptor = exports.API_HEADERS = exports.SSO_CHANNELS = exports.SSO_STORAGE_KEYS = exports.generateState = exports.generateCodeChallenge = exports.generateCodeVerifier = exports.sha256 = void 0;
4
+ var sha256_1 = require("./sha256");
5
+ Object.defineProperty(exports, "sha256", { enumerable: true, get: function () { return sha256_1.sha256; } });
6
+ var pkce_1 = require("./pkce");
7
+ Object.defineProperty(exports, "generateCodeVerifier", { enumerable: true, get: function () { return pkce_1.generateCodeVerifier; } });
8
+ Object.defineProperty(exports, "generateCodeChallenge", { enumerable: true, get: function () { return pkce_1.generateCodeChallenge; } });
9
+ Object.defineProperty(exports, "generateState", { enumerable: true, get: function () { return pkce_1.generateState; } });
10
+ var constants_1 = require("./constants");
11
+ Object.defineProperty(exports, "SSO_STORAGE_KEYS", { enumerable: true, get: function () { return constants_1.SSO_STORAGE_KEYS; } });
12
+ Object.defineProperty(exports, "SSO_CHANNELS", { enumerable: true, get: function () { return constants_1.SSO_CHANNELS; } });
13
+ Object.defineProperty(exports, "API_HEADERS", { enumerable: true, get: function () { return constants_1.API_HEADERS; } });
14
+ var interceptor_1 = require("./interceptor");
15
+ Object.defineProperty(exports, "createAuthInterceptor", { enumerable: true, get: function () { return interceptor_1.createAuthInterceptor; } });
16
+ var useOAuthCallback_1 = require("./hooks/useOAuthCallback");
17
+ Object.defineProperty(exports, "useOAuthCallback", { enumerable: true, get: function () { return useOAuthCallback_1.useOAuthCallback; } });
18
+ var useCrossAppLogout_1 = require("./hooks/useCrossAppLogout");
19
+ Object.defineProperty(exports, "useCrossAppLogout", { enumerable: true, get: function () { return useCrossAppLogout_1.useCrossAppLogout; } });
20
+ var OAuthCallback_1 = require("./components/OAuthCallback");
21
+ Object.defineProperty(exports, "OAuthCallback", { enumerable: true, get: function () { return OAuthCallback_1.OAuthCallback; } });
22
+ var OAuthTransfer_1 = require("./components/OAuthTransfer");
23
+ Object.defineProperty(exports, "OAuthTransfer", { enumerable: true, get: function () { return OAuthTransfer_1.OAuthTransfer; } });
@@ -0,0 +1 @@
1
+ export declare function createAuthInterceptor(): (config: any) => any;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAuthInterceptor = createAuthInterceptor;
4
+ const constants_1 = require("./constants");
5
+ function getCookie(name) {
6
+ const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
7
+ return match ? decodeURIComponent(match[2]) : '';
8
+ }
9
+ function createAuthInterceptor() {
10
+ return (config) => {
11
+ const token = getCookie(constants_1.SSO_STORAGE_KEYS.ACCESS_TOKEN) || localStorage.getItem(constants_1.SSO_STORAGE_KEYS.ACCESS_TOKEN);
12
+ if (token) {
13
+ config.headers[constants_1.API_HEADERS.AUTHORIZATION] = `Bearer ${token}`;
14
+ }
15
+ const schoolId = localStorage.getItem(constants_1.SSO_STORAGE_KEYS.SCHOOL_ID) || getCookie('sms_school_id');
16
+ if (schoolId) {
17
+ config.headers[constants_1.API_HEADERS.SCHOOL_ID] = schoolId;
18
+ }
19
+ config.headers[constants_1.API_HEADERS.USER_ID] = localStorage.getItem(constants_1.SSO_STORAGE_KEYS.USER_ID) || '';
20
+ config.headers[constants_1.API_HEADERS.USER_ROLE] = localStorage.getItem(constants_1.SSO_STORAGE_KEYS.USER_ROLE) || '';
21
+ config.headers[constants_1.API_HEADERS.DEVICE_ID] = localStorage.getItem(constants_1.SSO_STORAGE_KEYS.DEVICE_ID) || 'web-unknown';
22
+ config.headers[constants_1.API_HEADERS.REQUEST_ID] = crypto.randomUUID();
23
+ return config;
24
+ };
25
+ }
package/dist/pkce.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function generateCodeVerifier(): string;
2
+ export declare function generateCodeChallenge(verifier: string): Promise<string>;
3
+ export declare function generateState(): string;
package/dist/pkce.js ADDED
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateCodeVerifier = generateCodeVerifier;
4
+ exports.generateCodeChallenge = generateCodeChallenge;
5
+ exports.generateState = generateState;
6
+ const sha256_1 = require("./sha256");
7
+ function base64url(buf) {
8
+ return btoa(String.fromCharCode(...new Uint8Array(buf)))
9
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
10
+ }
11
+ function generateCodeVerifier() {
12
+ const array = new Uint8Array(64);
13
+ crypto.getRandomValues(array);
14
+ return base64url(array.buffer);
15
+ }
16
+ async function generateCodeChallenge(verifier) {
17
+ const digest = await (0, sha256_1.sha256)(verifier);
18
+ return base64url(digest);
19
+ }
20
+ function generateState() {
21
+ const array = new Uint8Array(16);
22
+ crypto.getRandomValues(array);
23
+ return Array.from(array, b => b.toString(36).padStart(2, '0')).join('');
24
+ }
@@ -0,0 +1 @@
1
+ export declare function sha256(message: string): Promise<ArrayBuffer>;
package/dist/sha256.js ADDED
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sha256 = sha256;
4
+ // Pure JS SHA-256 implementation — verified against FIPS-180-4 test vectors
5
+ async function sha256(message) {
6
+ const data = new TextEncoder().encode(message);
7
+ if (typeof crypto !== 'undefined' && crypto.subtle) {
8
+ return crypto.subtle.digest('SHA-256', data);
9
+ }
10
+ return pureJsSha256(data);
11
+ }
12
+ /* ---------- Pure JS SHA-256 (RFC 6234) ---------- */
13
+ const K = new Uint32Array([
14
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
15
+ 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
16
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
17
+ 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
18
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
19
+ 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
20
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
21
+ 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
22
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
23
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
24
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
25
+ 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
26
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
27
+ 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
28
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
29
+ 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
30
+ ]);
31
+ function safeAdd(x, y) {
32
+ return (x + y) | 0;
33
+ }
34
+ function sigma0(x) {
35
+ return ((x >>> 7) | (x << 25)) ^ ((x >>> 18) | (x << 14)) ^ (x >>> 3);
36
+ }
37
+ function sigma1(x) {
38
+ return ((x >>> 17) | (x << 15)) ^ ((x >>> 19) | (x << 13)) ^ (x >>> 10);
39
+ }
40
+ function Sig0(x) {
41
+ return ((x >>> 2) | (x << 30)) ^ ((x >>> 13) | (x << 19)) ^ ((x >>> 22) | (x << 10));
42
+ }
43
+ function Sig1(x) {
44
+ return ((x >>> 6) | (x << 26)) ^ ((x >>> 11) | (x << 21)) ^ ((x >>> 25) | (x << 7));
45
+ }
46
+ function Ch(x, y, z) {
47
+ return (x & y) ^ (~x & z);
48
+ }
49
+ function Maj(x, y, z) {
50
+ return (x & y) ^ (x & z) ^ (y & z);
51
+ }
52
+ function pureJsSha256(data) {
53
+ // Message padding
54
+ const bitLen = data.length * 8;
55
+ const padLen = (((data.length + 9 + 63) >>> 6) << 6);
56
+ const pad = new Uint8Array(padLen);
57
+ pad.set(data);
58
+ pad[data.length] = 0x80;
59
+ const dv = new DataView(pad.buffer);
60
+ dv.setUint32(padLen - 4, bitLen >>> 0, false);
61
+ dv.setUint32(padLen - 8, Math.floor(bitLen / 0x100000000), false);
62
+ // Initial hash values
63
+ let H0 = 0x6a09e667, H1 = 0xbb67ae85, H2 = 0x3c6ef372, H3 = 0xa54ff53a;
64
+ let H4 = 0x510e527f, H5 = 0x9b05688c, H6 = 0x1f83d9ab, H7 = 0x5be0cd19;
65
+ const W = new Uint32Array(64);
66
+ for (let block = 0; block < padLen; block += 64) {
67
+ for (let t = 0; t < 16; t++) {
68
+ W[t] = dv.getUint32(block + t * 4, false);
69
+ }
70
+ for (let t = 16; t < 64; t++) {
71
+ W[t] = safeAdd(safeAdd(safeAdd(sigma1(W[t - 2]), W[t - 7]), sigma0(W[t - 15])), W[t - 16]);
72
+ }
73
+ let a = H0, b = H1, c = H2, d = H3;
74
+ let e = H4, f = H5, g = H6, h = H7;
75
+ for (let t = 0; t < 64; t++) {
76
+ const T1 = safeAdd(safeAdd(safeAdd(safeAdd(h, Sig1(e)), Ch(e, f, g)), K[t]), W[t]);
77
+ const T2 = safeAdd(Sig0(a), Maj(a, b, c));
78
+ h = g;
79
+ g = f;
80
+ f = e;
81
+ e = safeAdd(d, T1);
82
+ d = c;
83
+ c = b;
84
+ b = a;
85
+ a = safeAdd(T1, T2);
86
+ }
87
+ H0 = safeAdd(H0, a);
88
+ H1 = safeAdd(H1, b);
89
+ H2 = safeAdd(H2, c);
90
+ H3 = safeAdd(H3, d);
91
+ H4 = safeAdd(H4, e);
92
+ H5 = safeAdd(H5, f);
93
+ H6 = safeAdd(H6, g);
94
+ H7 = safeAdd(H7, h);
95
+ }
96
+ const result = new Uint8Array(32);
97
+ const resView = new DataView(result.buffer);
98
+ resView.setUint32(0, H0 >>> 0, false);
99
+ resView.setUint32(4, H1 >>> 0, false);
100
+ resView.setUint32(8, H2 >>> 0, false);
101
+ resView.setUint32(12, H3 >>> 0, false);
102
+ resView.setUint32(16, H4 >>> 0, false);
103
+ resView.setUint32(20, H5 >>> 0, false);
104
+ resView.setUint32(24, H6 >>> 0, false);
105
+ resView.setUint32(28, H7 >>> 0, false);
106
+ return result.buffer;
107
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@loka-sms/sso",
3
+ "version": "1.0.0",
4
+ "description": "SSO utilities, hooks, and components for Loka SMS modules (OAuth2 PKCE, cross-app logout)",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": [
10
+ "dist/"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "peerDependencies": {
17
+ "axios": "^1.0.0",
18
+ "react": "^18.0.0 || ^19.0.0",
19
+ "react-router": "^7.0.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/react": "^19.2.14",
23
+ "typescript": "^5.9.3"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://pd2-git-immersive.erlangga.id/team-immersive-pd2/project-sms/packages/loka-sms-sso.git"
28
+ },
29
+ "keywords": [
30
+ "loka",
31
+ "sso",
32
+ "oauth2",
33
+ "pkce",
34
+ "react"
35
+ ],
36
+ "dependencies": {}
37
+ }