@svadmin/sso 0.8.4 → 0.8.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/sso",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "type": "module",
5
5
  "description": "OIDC/OAuth2 SSO AuthProvider plugin for svadmin — supports Okta, Azure AD, Cognito, Keycloak, Google",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "src"
32
32
  ],
33
33
  "peerDependencies": {
34
- "@svadmin/core": "^0.19.4"
34
+ "@svadmin/core": "^0.22.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "typescript": "^5.7.0"
@@ -16,6 +16,8 @@
16
16
  * ```ts
17
17
  * import { createSSOAuthProvider } from '@svadmin/sso';
18
18
  * import { setAuthProvider } from '@svadmin/core';
19
+
20
+
19
21
  *
20
22
  * const authProvider = createSSOAuthProvider({
21
23
  * issuer: 'https://your-tenant.okta.com',
@@ -29,6 +31,19 @@
29
31
 
30
32
  import type { AuthProvider, Identity } from '@svadmin/core';
31
33
 
34
+ // Safe base64url decode for JWT payloads
35
+ function decodeJwtPayload(token: string): any {
36
+ if (!token) return null;
37
+ const parts = token.split('.');
38
+ if (parts.length < 2) return null;
39
+ const payloadStr = parts[1];
40
+ const padded = payloadStr + '='.repeat((4 - (payloadStr.length % 4)) % 4);
41
+ const binString = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));
42
+ const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0)!);
43
+ return JSON.parse(new TextDecoder().decode(bytes));
44
+ }
45
+
46
+
32
47
  // ─── Types ────────────────────────────────────────────────────
33
48
 
34
49
  export interface SSOConfig {
@@ -116,11 +131,10 @@ const STORAGE_PREFIX = 'svadmin_sso_';
116
131
 
117
132
  function getStorage(config: SSOConfig): TokenStorage {
118
133
  if (config.storage && typeof config.storage === 'object') return config.storage;
119
- const backend = config.storage === 'session' ? sessionStorage : localStorage;
120
134
  return {
121
- getItem: (key) => backend.getItem(key),
122
- setItem: (key, value) => backend.setItem(key, value),
123
- removeItem: (key) => backend.removeItem(key),
135
+ getItem: (key) => typeof window !== 'undefined' ? (config.storage === 'session' ? sessionStorage : localStorage).getItem(key) : null,
136
+ setItem: (key, value) => { if (typeof window !== 'undefined') (config.storage === 'session' ? sessionStorage : localStorage).setItem(key, value); },
137
+ removeItem: (key) => { if (typeof window !== 'undefined') (config.storage === 'session' ? sessionStorage : localStorage).removeItem(key); },
124
138
  };
125
139
  }
126
140
 
@@ -264,6 +278,9 @@ export function createSSOAuthProvider(config: SSOConfig): AuthProvider {
264
278
 
265
279
  return {
266
280
  async login() {
281
+ if (typeof window === 'undefined') {
282
+ return { success: false, error: { message: 'SSO login requires a browser environment' } };
283
+ }
267
284
  const cfg = await discover();
268
285
  const { verifier, challenge } = await createPKCEChallenge();
269
286
  const state = generateRandomString(32);
@@ -293,7 +310,7 @@ export function createSSOAuthProvider(config: SSOConfig): AuthProvider {
293
310
  const tokens = getTokens();
294
311
  clearTokens();
295
312
 
296
- if (cfg.end_session_endpoint && tokens?.id_token) {
313
+ if (typeof window !== 'undefined' && cfg.end_session_endpoint && tokens?.id_token) {
297
314
  const params = new URLSearchParams({
298
315
  id_token_hint: tokens.id_token,
299
316
  ...(config.postLogoutRedirectUri
@@ -311,6 +328,15 @@ export function createSSOAuthProvider(config: SSOConfig): AuthProvider {
311
328
  },
312
329
 
313
330
  async check() {
331
+ // SSR: no tokens, no callback — return unauthenticated
332
+ if (typeof window === 'undefined') {
333
+ const tokens = getTokens();
334
+ if (tokens && (!tokens.expires_at || tokens.expires_at > Math.floor(Date.now() / 1000))) {
335
+ return { authenticated: true };
336
+ }
337
+ return { authenticated: false, logout: true };
338
+ }
339
+
314
340
  // Handle callback — exchange authorization code for tokens
315
341
  const url = new URL(window.location.href);
316
342
  const code = url.searchParams.get('code');
@@ -382,7 +408,7 @@ export function createSSOAuthProvider(config: SSOConfig): AuthProvider {
382
408
  // Try to extract roles/permissions from the ID token
383
409
  if (tokens.id_token) {
384
410
  try {
385
- const payload = JSON.parse(atob(tokens.id_token.split('.')[1]));
411
+ const payload = decodeJwtPayload(tokens.id_token);
386
412
  return payload.roles ?? payload.groups ?? payload.permissions ?? null;
387
413
  } catch {
388
414
  return null;