authscape 1.0.766 → 1.0.772

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": "authscape",
3
- "version": "1.0.766",
3
+ "version": "1.0.772",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -4,7 +4,7 @@ import Head from "next/head";
4
4
 
5
5
  // Re-export toast and transitions so pages can import from authscape
6
6
  export { toast, Bounce, Slide, Zoom, Flip };
7
- import { useSearchParams, usePathname } from "next/navigation";
7
+ import { useRouter } from "next/router";
8
8
  import axios from "axios";
9
9
  import querystring from "query-string";
10
10
  import Router from "next/router";
@@ -17,18 +17,33 @@ import { HubConnectionBuilder, LogLevel, HttpTransportType } from '@microsoft/si
17
17
  import Cookies from 'js-cookie';
18
18
 
19
19
  // ============================================================================
20
- // Cookie utility function
20
+ // Auth Redirect Circuit Breaker
21
21
  // ============================================================================
22
- const setCookie = (name, value, options = {}) => {
23
- return new Promise((resolve) => {
24
- let cookieString = `${name}=${value};`;
25
- if (options.maxAge) cookieString += `max-age=${options.maxAge};`;
26
- if (options.path) cookieString += `path=${options.path};`;
27
- if (options.domain) cookieString += `domain=${options.domain};`;
28
- if (options.secure) cookieString += `secure;`;
29
- document.cookie = cookieString;
30
- resolve();
31
- });
22
+ const AUTH_REDIRECT_KEY = 'authscape_redirect_count';
23
+ const AUTH_REDIRECT_TS_KEY = 'authscape_redirect_ts';
24
+ const AUTH_MAX_REDIRECTS = 3;
25
+ const AUTH_REDIRECT_WINDOW_MS = 30000; // 30 seconds
26
+
27
+ const checkAndIncrementRedirect = () => {
28
+ if (typeof window === 'undefined') return false;
29
+ const now = Date.now();
30
+ const storedTs = parseInt(sessionStorage.getItem(AUTH_REDIRECT_TS_KEY) || '0', 10);
31
+ let count = parseInt(sessionStorage.getItem(AUTH_REDIRECT_KEY) || '0', 10);
32
+
33
+ if (now - storedTs > AUTH_REDIRECT_WINDOW_MS) {
34
+ count = 0;
35
+ sessionStorage.setItem(AUTH_REDIRECT_TS_KEY, String(now));
36
+ }
37
+
38
+ count += 1;
39
+ sessionStorage.setItem(AUTH_REDIRECT_KEY, String(count));
40
+ return count <= AUTH_MAX_REDIRECTS;
41
+ };
42
+
43
+ const resetRedirectCounter = () => {
44
+ if (typeof window === 'undefined') return;
45
+ sessionStorage.removeItem(AUTH_REDIRECT_KEY);
46
+ sessionStorage.removeItem(AUTH_REDIRECT_TS_KEY);
32
47
  };
33
48
 
34
49
  // ============================================================================
@@ -484,10 +499,24 @@ export function AuthScapeApp({
484
499
  const queryCodeUsed = useRef(null);
485
500
  const ga4React = useRef(null);
486
501
  const errorTrackingInitializedRef = useRef(false);
487
-
488
- const searchParams = useSearchParams();
489
- const queryCode = searchParams?.get("code") ?? null;
490
- const pathname = usePathname();
502
+ const loginRedirectPending = useRef(false);
503
+
504
+ // Use Pages Router's `useRouter` instead of `useSearchParams`/`usePathname`
505
+ // from `next/navigation`. The `next/navigation` hooks don't hydrate query
506
+ // params reliably on the 404 page in Webkit/Safari — when the IDP redirects
507
+ // back to /signin-oidc (a URL with no Next.js page in the consumer project),
508
+ // useSearchParams() returns null and the PKCE code is never picked up.
509
+ // The Pages Router router parses query from `asPath` and works on 404 too.
510
+ const router = useRouter();
511
+ const rawQueryCode = router.isReady ? router.query.code : null;
512
+ // router.query values can be string | string[] — coerce to a single string.
513
+ const queryCode = typeof rawQueryCode === "string"
514
+ ? rawQueryCode
515
+ : (Array.isArray(rawQueryCode) ? rawQueryCode[0] : null);
516
+ // Pages Router's `router.pathname` returns the route file path (e.g. "/_error"
517
+ // for the 404 page). The auth-redirect guard below compares against
518
+ // "/signin-oidc", so we derive the actual URL path from `asPath` instead.
519
+ const pathname = (router.asPath || "").split("?")[0].split("#")[0];
491
520
 
492
521
  const signInValidator = async (codeFromQuery) => {
493
522
  if (queryCodeUsed.current === codeFromQuery) return;
@@ -500,7 +529,8 @@ export function AuthScapeApp({
500
529
  const codeVerifier = window.localStorage.getItem("verifier");
501
530
  if (!codeFromQuery || !codeVerifier) {
502
531
  window.localStorage.clear();
503
- module.exports.authService().login();
532
+ setIsSigningIn(false);
533
+ setFrontEndLoadedState(true);
504
534
  return;
505
535
  }
506
536
 
@@ -526,34 +556,72 @@ export function AuthScapeApp({
526
556
 
527
557
  window.localStorage.removeItem("verifier");
528
558
 
529
- await setCookie("access_token", response.data.access_token, {
530
- maxAge: 60 * 60 * 24 * 365,
559
+ Cookies.set("access_token", response.data.access_token, {
560
+ expires: 365,
531
561
  path: "/",
532
562
  domain: domainHost,
533
563
  secure: true,
534
564
  });
535
- await setCookie("expires_in", response.data.expires_in, {
536
- maxAge: 60 * 60 * 24 * 365,
565
+ Cookies.set("expires_in", String(response.data.expires_in), {
566
+ expires: 365,
537
567
  path: "/",
538
568
  domain: domainHost,
539
569
  secure: true,
540
570
  });
541
- await setCookie("refresh_token", response.data.refresh_token, {
542
- maxAge: 60 * 60 * 24 * 365,
571
+ Cookies.set("refresh_token", response.data.refresh_token, {
572
+ expires: 365,
543
573
  path: "/",
544
574
  domain: domainHost,
545
575
  secure: true,
546
576
  });
547
577
 
578
+ resetRedirectCounter();
579
+
548
580
  const redirectUri = window.localStorage.getItem("redirectUri") || "/";
549
581
  window.localStorage.clear();
550
582
 
551
- window.location.href = redirectUri;
583
+ // Pre-load user while spinner is still showing — eliminates the
584
+ // second GetCurrentUser call (and resulting re-renders) on the destination page.
585
+ let usr = null;
586
+ try {
587
+ usr = await module.exports.apiService().GetCurrentUser();
588
+ } catch (fetchErr) {
589
+ console.warn("[AuthScape] GetCurrentUser failed after token exchange:", fetchErr);
590
+ }
591
+ const enrichedUser = ensureUserHelpers(usr);
592
+
593
+ signedInUser.current = enrichedUser;
594
+ setSignedInUserState(enrichedUser);
595
+ setFrontEndLoadedState(true);
596
+
597
+ // Prevent the useEffect from calling GetCurrentUser again when queryCode → null.
598
+ loadingAuth.current = true;
599
+
600
+ if (enableErrorTracking && enrichedUser && !errorTrackingInitializedRef.current) {
601
+ initializeErrorTracking(enrichedUser);
602
+ errorTrackingInitializedRef.current = true;
603
+ }
604
+
605
+ if (onUserLoaded && enrichedUser) {
606
+ onUserLoaded(enrichedUser);
607
+ }
608
+
609
+ // Dismiss spinner before navigating so destination renders logged-in UI on first paint.
610
+ setIsSigningIn(false);
611
+
612
+ // Client-side navigation preserves the React component tree and all ref values,
613
+ // eliminating the hard-reload → remount → re-render chain.
614
+ // Fall back to hard navigation for absolute external URLs.
615
+ if (redirectUri.startsWith("http://") || redirectUri.startsWith("https://")) {
616
+ window.location.href = redirectUri;
617
+ } else {
618
+ Router.push(redirectUri);
619
+ }
552
620
  } catch (exp) {
553
621
  console.error("PKCE sign-in failed", exp);
554
622
  window.localStorage.clear();
555
623
  setIsSigningIn(false);
556
- module.exports.authService().login();
624
+ setFrontEndLoadedState(true);
557
625
  }
558
626
  };
559
627
 
@@ -653,8 +721,14 @@ export function AuthScapeApp({
653
721
  enforceLoggedIn &&
654
722
  pathname !== "/signin-oidc" &&
655
723
  frontEndLoadedState &&
656
- !signedInUserState
724
+ !signedInUserState &&
725
+ !loginRedirectPending.current
657
726
  ) {
727
+ if (!checkAndIncrementRedirect()) {
728
+ console.warn('[AuthScape] Auth redirect loop detected — halting redirects.');
729
+ return;
730
+ }
731
+ loginRedirectPending.current = true;
658
732
  module.exports.authService().login();
659
733
  }
660
734
  }, [signedInUserState, enforceLoggedIn, frontEndLoadedState, pathname]);
@@ -780,3 +854,8 @@ export function AuthScapeApp({
780
854
  </>
781
855
  );
782
856
  }
857
+
858
+ // AuthScapeProvider is the umbrella component that bundles the three always-on AuthScape
859
+ // features (auth, error tracking, analytics) plus optional in-app notifications. Wrap your
860
+ // NextJS _app.js return value with it. Existing call sites can continue using AuthScapeApp.
861
+ export const AuthScapeProvider = AuthScapeApp;
@@ -3,19 +3,6 @@ import querystring from 'query-string';
3
3
  import fileDownload from 'js-file-download';
4
4
  import Cookies from 'js-cookie';
5
5
 
6
- // Cookie utility function
7
- const setCookie = (name, value, options = {}) => {
8
- return new Promise((resolve) => {
9
- let cookieString = `${name}=${value};`;
10
- if (options.maxAge) cookieString += `max-age=${options.maxAge};`;
11
- if (options.path) cookieString += `path=${options.path};`;
12
- if (options.domain) cookieString += `domain=${options.domain};`;
13
- if (options.secure) cookieString += `secure;`;
14
- document.cookie = cookieString;
15
- resolve();
16
- });
17
- };
18
-
19
6
  const setupDefaultOptions = async (ctx = null) => {
20
7
  let defaultOptions = {};
21
8
  if (ctx == null) {
@@ -42,46 +29,58 @@ const setupDefaultOptions = async (ctx = null) => {
42
29
  }
43
30
 
44
31
  const RefreshToken = async (originalRequest, instance) => {
45
- let accessToken = Cookies.get('access_token') || '';
46
- let refreshToken = Cookies.get('refresh_token') || '';
47
-
48
- let response = await instance.post(process.env.authorityUri + "/connect/token",
49
- querystring.stringify({
50
- grant_type: 'refresh_token',
51
- client_id: process.env.client_id,
52
- client_secret: process.env.client_secret,
53
- refresh_token: refreshToken
54
- }), {
55
- headers: {
56
- "Content-Type": "application/x-www-form-urlencoded",
57
- "Authorization": "Bearer " + accessToken
58
- }
59
- });
32
+ try {
33
+ let accessToken = Cookies.get('access_token') || '';
34
+ let refreshToken = Cookies.get('refresh_token') || '';
60
35
 
61
- if (response != null && response.status == 200) {
62
- let domainHost = window.location.hostname.split('.').slice(-2).join('.');
63
- originalRequest.headers['Authorization'] = 'Bearer ' + response.data.access_token;
36
+ if (!refreshToken) {
37
+ return false;
38
+ }
64
39
 
65
- await setCookie('access_token', response.data.access_token, {
66
- maxAge: 60 * 60 * 24 * 365,
67
- path: '/',
68
- domain: domainHost,
69
- secure: true
40
+ let response = await instance.post(process.env.authorityUri + "/connect/token",
41
+ querystring.stringify({
42
+ grant_type: 'refresh_token',
43
+ client_id: process.env.client_id,
44
+ client_secret: process.env.client_secret,
45
+ refresh_token: refreshToken
46
+ }), {
47
+ headers: {
48
+ "Content-Type": "application/x-www-form-urlencoded",
49
+ "Authorization": "Bearer " + accessToken
50
+ }
70
51
  });
71
52
 
72
- await setCookie('expires_in', response.data.expires_in, {
73
- maxAge: 60 * 60 * 24 * 365,
74
- path: '/',
75
- domain: domainHost,
76
- secure: true
77
- });
53
+ if (response != null && response.status == 200) {
54
+ let domainHost = window.location.hostname.split('.').slice(-2).join('.');
55
+ originalRequest.headers['Authorization'] = 'Bearer ' + response.data.access_token;
56
+
57
+ Cookies.set('access_token', response.data.access_token, {
58
+ expires: 365,
59
+ path: '/',
60
+ domain: domainHost,
61
+ secure: true
62
+ });
63
+
64
+ Cookies.set('expires_in', String(response.data.expires_in), {
65
+ expires: 365,
66
+ path: '/',
67
+ domain: domainHost,
68
+ secure: true
69
+ });
70
+
71
+ Cookies.set('refresh_token', response.data.refresh_token, {
72
+ expires: 365,
73
+ path: '/',
74
+ domain: domainHost,
75
+ secure: true
76
+ });
77
+
78
+ return true;
79
+ }
78
80
 
79
- await setCookie('refresh_token', response.data.refresh_token, {
80
- maxAge: 60 * 60 * 24 * 365,
81
- path: '/',
82
- domain: domainHost,
83
- secure: true
84
- });
81
+ return false;
82
+ } catch (err) {
83
+ return false;
85
84
  }
86
85
  }
87
86
 
@@ -107,16 +106,19 @@ export const apiService = (ctx = null) => {
107
106
  if (error.response) {
108
107
  if (error.response.status === 401 && !originalConfig._retry) {
109
108
  originalConfig._retry = true;
110
- await RefreshToken(originalConfig, instance);
111
- return instance.request(originalConfig);
109
+ const refreshed = await RefreshToken(originalConfig, instance);
110
+ if (refreshed) {
111
+ return instance.request(originalConfig);
112
+ }
113
+ return Promise.reject(error);
112
114
  }
113
115
 
114
116
  if (error.response.status === 400) {
115
117
  if (error.response.config.url.includes("/connect/token")) {
116
118
  let domainHost = window.location.hostname.split('.').slice(-2).join('.');
117
- Cookies.remove('access_token', { path: '/', domain: domainHost });
118
- Cookies.remove('refresh_token', { path: '/', domain: domainHost });
119
- Cookies.remove('expires_in', { path: '/', domain: domainHost });
119
+ Cookies.remove('access_token', { path: '/', domain: domainHost, secure: true });
120
+ Cookies.remove('refresh_token', { path: '/', domain: domainHost, secure: true });
121
+ Cookies.remove('expires_in', { path: '/', domain: domainHost, secure: true });
120
122
  }
121
123
  return Promise.reject(error);
122
124
  }
@@ -5,7 +5,7 @@ export const authService = () => {
5
5
  return {
6
6
 
7
7
  dec2hex: (dec) => {
8
- return ('0' + dec.toString(16)).substr(-2)
8
+ return ('0' + dec.toString(16)).slice(-2)
9
9
  },
10
10
  generateRandomString: () => {
11
11
  var array = new Uint32Array(56/2);
@@ -64,9 +64,10 @@ export const authService = () => {
64
64
 
65
65
  return response;
66
66
  },
67
- login: async (redirectUserUri = null, dnsRecord = null, deviceId = null) => {
67
+ login: async (redirectUserUri = null, deviceId = null) => {
68
+
69
+ let state = authService().generateRandomString();
68
70
 
69
- let state = "1234";
70
71
  if (redirectUserUri != null)
71
72
  {
72
73
  localStorage.setItem("redirectUri", redirectUserUri);
@@ -75,18 +76,8 @@ export const authService = () => {
75
76
  let verifier = authService().generateRandomString();
76
77
  var challenge = await authService().challenge_from_verifier(verifier);
77
78
 
78
- console.log('[PKCE] Login initiated');
79
- console.log('[PKCE] Verifier generated | length:', verifier.length, '| preview:', verifier.substring(0, 10) + '...');
80
- console.log('[PKCE] Challenge generated | value:', challenge);
81
- console.log('[PKCE] authorityUri:', process.env.authorityUri);
82
- console.log('[PKCE] client_id:', process.env.client_id);
83
-
84
79
  window.localStorage.setItem("verifier", verifier);
85
80
 
86
- const storedVerifier = window.localStorage.getItem("verifier");
87
- console.log('[PKCE] Verifier stored successfully:', storedVerifier === verifier);
88
- console.log('[PKCE] Stored verifier length:', storedVerifier?.length);
89
-
90
81
  let redirectUri = window.location.origin + "/signin-oidc";
91
82
  let loginUri = process.env.authorityUri + "/connect/authorize?response_type=code&state=" + state + "&client_id=" + process.env.client_id + "&scope=email%20openid%20offline_access%20profile%20api1&redirect_uri=" + redirectUri + "&code_challenge=" + challenge + "&code_challenge_method=S256";
92
83
 
@@ -95,7 +86,6 @@ export const authService = () => {
95
86
  loginUri += "&deviceId=" + deviceId; // will be for chrome extention and mobile apps later
96
87
  }
97
88
 
98
- console.log('[PKCE] Redirecting to:', loginUri);
99
89
  window.location.href = loginUri;
100
90
  },
101
91
  signUp: (redirectUrl = null) => {
@@ -126,40 +116,18 @@ export const authService = () => {
126
116
  let domainHost = window.location.hostname.split('.').slice(-2).join('.');
127
117
  let AuthUri = process.env.authorityUri;
128
118
 
119
+ Cookies.remove('access_token', { path: '/', domain: domainHost, secure: true });
120
+ Cookies.remove('refresh_token', { path: '/', domain: domainHost, secure: true });
121
+ Cookies.remove('expires_in', { path: '/', domain: domainHost, secure: true });
129
122
 
130
- Cookies.remove('access_token', { path: '/', domain: domainHost });
131
- Cookies.remove('refresh_token', { path: '/', domain: domainHost });
132
- Cookies.remove('expires_in', { path: '/', domain: domainHost });
133
-
134
-
135
- // destroyCookie({}, "access_token", {
136
- // maxAge: 2147483647,
137
- // path: '/',
138
- // domain: domainHost
139
- // });
140
-
141
- // destroyCookie({}, "refresh_token", {
142
- // maxAge: 2147483647,
143
- // path: '/',
144
- // domain: domainHost
145
- // });
146
-
147
- // destroyCookie({}, "expires_in", {
148
- // maxAge: 2147483647,
149
- // path: '/',
150
- // domain: domainHost
151
- // });
152
-
153
- setTimeout(() => {
154
- if (redirectUri == null)
155
- {
156
- window.location.href = AuthUri + "/connect/logout?redirect=" + window.location.href;
157
- }
158
- else
159
- {
160
- window.location.href = AuthUri + "/connect/logout?redirect=" + redirectUri;
161
- }
162
- }, 500);
123
+ if (redirectUri == null)
124
+ {
125
+ window.location.href = AuthUri + "/connect/logout?redirect=" + window.location.href;
126
+ }
127
+ else
128
+ {
129
+ window.location.href = AuthUri + "/connect/logout?redirect=" + redirectUri;
130
+ }
163
131
 
164
132
  },
165
133
  }
@@ -1,117 +0,0 @@
1
- import React, { useEffect, useState } from 'react';
2
- import axios from 'axios';
3
- import querystring from "query-string";
4
- // import Cookies from 'js-cookie';
5
-
6
- export const signInValidator = async (queryCode) => {
7
-
8
- let codeVerifier = window.localStorage.getItem("verifier");
9
-
10
- console.log('[PKCE] signInValidator called');
11
- console.log('[PKCE] queryCode present:', !!queryCode, '| value:', queryCode);
12
- console.log('[PKCE] codeVerifier present:', !!codeVerifier, '| length:', codeVerifier?.length);
13
-
14
- if (queryCode != null && codeVerifier != null)
15
- {
16
- const headers = {'Content-Type': 'application/x-www-form-urlencoded'}
17
-
18
- const tokenParams = {
19
- code: queryCode,
20
- grant_type: "authorization_code",
21
- redirect_uri: window.location.origin + "/signin-oidc",
22
- client_id: process.env.client_id,
23
- client_secret: process.env.client_secret,
24
- code_verifier: codeVerifier
25
- };
26
-
27
- console.log('[PKCE] Token request params:', {
28
- ...tokenParams,
29
- client_secret: tokenParams.client_secret ? '***' : '(missing!)',
30
- code: tokenParams.code?.substring(0, 10) + '...',
31
- code_verifier: tokenParams.code_verifier?.substring(0, 10) + '... (length: ' + tokenParams.code_verifier?.length + ')',
32
- });
33
- console.log('[PKCE] Token endpoint:', process.env.authorityUri + '/connect/token');
34
- console.log('[PKCE] redirect_uri:', tokenParams.redirect_uri);
35
-
36
- let queryString = querystring.stringify(tokenParams);
37
-
38
- let response;
39
- try {
40
- response = await axios.post(process.env.authorityUri + '/connect/token', queryString, {
41
- headers: headers
42
- });
43
- console.log('[PKCE] Token response status:', response.status);
44
- console.log('[PKCE] Token response has access_token:', !!response.data?.access_token);
45
- console.log('[PKCE] Token response has refresh_token:', !!response.data?.refresh_token);
46
- } catch (err) {
47
- console.error('[PKCE] Token request FAILED');
48
- console.error('[PKCE] Status:', err.response?.status);
49
- console.error('[PKCE] Error data:', JSON.stringify(err.response?.data));
50
- console.error('[PKCE] Error message:', err.message);
51
- throw err;
52
- }
53
-
54
- let domainHost = window.location.hostname.split('.').slice(-2).join('.');
55
- window.localStorage.removeItem("verifier");
56
-
57
-
58
-
59
- await setCookie('access_token', response.data.access_token, {
60
- maxAge: 60 * 60 * 24 * 365, // 1 year,
61
- path: '/',
62
- domain: domainHost,
63
- secure: true
64
- });
65
-
66
- await setCookie('expires_in', response.data.expires_in, {
67
- maxAge: 60 * 60 * 24 * 365, // 1 year,
68
- path: '/',
69
- domain: domainHost,
70
- secure: true
71
- });
72
-
73
- await setCookie('refresh_token', response.data.refresh_token, {
74
- maxAge: 60 * 60 * 24 * 365, // 1 year,
75
- path: '/',
76
- domain: domainHost,
77
- secure: true
78
- });
79
-
80
-
81
- // await setCookie(null, "access_token", response.data.access_token,
82
- // {
83
- // maxAge: 2147483647,
84
- // path: '/',
85
- // domain: domainHost,
86
- // secure: true
87
- // });
88
-
89
- // await setCookie(null, "expires_in", response.data.expires_in,
90
- // {
91
- // maxAge: 2147483647,
92
- // path: '/',
93
- // domain: domainHost,
94
- // secure: true
95
- // });
96
-
97
- // await setCookie(null, "refresh_token", response.data.refresh_token,
98
- // {
99
- // maxAge: 2147483647,
100
- // path: '/',
101
- // domain: domainHost,
102
- // secure: true
103
- // });
104
-
105
-
106
- let redirectUri = localStorage.getItem("redirectUri")
107
- localStorage.clear();
108
- if (redirectUri != null)
109
- {
110
- window.location.href = redirectUri;
111
- }
112
- else
113
- {
114
- window.location.href = "/";
115
- }
116
- }
117
- }