@chemmangat/msal-next 5.3.3 → 5.3.4
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/CHANGELOG.md +23 -0
- package/dist/index.js +11 -6
- package/dist/index.mjs +36 -31
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [5.3.4] - 2026-05-06
|
|
6
|
+
|
|
7
|
+
### 🐛 Bug Fix
|
|
8
|
+
|
|
9
|
+
#### `useTokenRefresh` — `expiresIn` and `isExpiringSoon` are now reactive
|
|
10
|
+
|
|
11
|
+
`expiresIn` and `isExpiringSoon` were previously derived from `useRef` values, which never trigger a re-render. Any component calling `useTokenRefresh()` to display a session-expiry warning would always see the initial `null` / `false` values and never update.
|
|
12
|
+
|
|
13
|
+
Both values are now backed by `useState`, so components re-render correctly when the token expiry changes.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
// This now works as expected — the warning will actually appear
|
|
17
|
+
const { isExpiringSoon, expiresIn } = useTokenRefresh({ refreshBeforeExpiry: 300 });
|
|
18
|
+
|
|
19
|
+
if (isExpiringSoon) {
|
|
20
|
+
return <div>⚠️ Your session expires in {Math.round(expiresIn!)} seconds</div>;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`lastRefresh` is also now a state value, so it updates in the UI after each refresh.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
5
28
|
## [5.3.0] - 2026-04-07
|
|
6
29
|
|
|
7
30
|
### 🐛 Bug Fixes
|
package/dist/index.js
CHANGED
|
@@ -857,6 +857,8 @@ function useTokenRefresh(options = {}) {
|
|
|
857
857
|
const intervalRef = (0, import_react2.useRef)(null);
|
|
858
858
|
const lastRefreshRef = (0, import_react2.useRef)(null);
|
|
859
859
|
const expiresInRef = (0, import_react2.useRef)(null);
|
|
860
|
+
const [expiresIn, setExpiresIn] = (0, import_react2.useState)(null);
|
|
861
|
+
const [lastRefresh, setLastRefresh] = (0, import_react2.useState)(null);
|
|
860
862
|
const refresh = (0, import_react2.useCallback)(async () => {
|
|
861
863
|
if (!isAuthenticated || !account) {
|
|
862
864
|
return;
|
|
@@ -868,9 +870,11 @@ function useTokenRefresh(options = {}) {
|
|
|
868
870
|
forceRefresh: false
|
|
869
871
|
});
|
|
870
872
|
lastRefreshRef.current = /* @__PURE__ */ new Date();
|
|
871
|
-
const
|
|
872
|
-
expiresInRef.current =
|
|
873
|
-
|
|
873
|
+
const newExpiresIn = response.expiresOn ? Math.max(0, response.expiresOn.getTime() / 1e3 - Date.now() / 1e3) : 3600;
|
|
874
|
+
expiresInRef.current = newExpiresIn;
|
|
875
|
+
setExpiresIn(newExpiresIn);
|
|
876
|
+
setLastRefresh(lastRefreshRef.current);
|
|
877
|
+
onRefresh?.(newExpiresIn);
|
|
874
878
|
} catch (error) {
|
|
875
879
|
console.error("[TokenRefresh] Failed to refresh token:", error);
|
|
876
880
|
onError?.(error);
|
|
@@ -888,6 +892,7 @@ function useTokenRefresh(options = {}) {
|
|
|
888
892
|
const timeSinceRefresh = lastRefreshRef.current ? (Date.now() - lastRefreshRef.current.getTime()) / 1e3 : 0;
|
|
889
893
|
const remainingTime = expiresInRef.current - timeSinceRefresh;
|
|
890
894
|
expiresInRef.current = Math.max(0, remainingTime);
|
|
895
|
+
setExpiresIn(expiresInRef.current);
|
|
891
896
|
if (remainingTime <= refreshBeforeExpiry && remainingTime > 0) {
|
|
892
897
|
refresh();
|
|
893
898
|
}
|
|
@@ -898,12 +903,12 @@ function useTokenRefresh(options = {}) {
|
|
|
898
903
|
}
|
|
899
904
|
};
|
|
900
905
|
}, [enabled, isAuthenticated, refreshBeforeExpiry, refresh]);
|
|
901
|
-
const isExpiringSoon =
|
|
906
|
+
const isExpiringSoon = expiresIn !== null && expiresIn <= refreshBeforeExpiry;
|
|
902
907
|
return {
|
|
903
|
-
expiresIn
|
|
908
|
+
expiresIn,
|
|
904
909
|
isExpiringSoon,
|
|
905
910
|
refresh,
|
|
906
|
-
lastRefresh
|
|
911
|
+
lastRefresh
|
|
907
912
|
};
|
|
908
913
|
}
|
|
909
914
|
|
package/dist/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
9
9
|
// src/components/MsalAuthProvider.tsx
|
|
10
10
|
import { MsalProvider } from "@azure/msal-react";
|
|
11
11
|
import { PublicClientApplication, EventType } from "@azure/msal-browser";
|
|
12
|
-
import { useEffect as useEffect2, useState, useRef as useRef2 } from "react";
|
|
12
|
+
import { useEffect as useEffect2, useState as useState2, useRef as useRef2 } from "react";
|
|
13
13
|
|
|
14
14
|
// src/utils/createMsalConfig.ts
|
|
15
15
|
import { LogLevel } from "@azure/msal-browser";
|
|
@@ -613,7 +613,7 @@ Note: Environment variables starting with NEXT_PUBLIC_ are exposed to the browse
|
|
|
613
613
|
}
|
|
614
614
|
|
|
615
615
|
// src/hooks/useTokenRefresh.ts
|
|
616
|
-
import { useEffect, useRef, useCallback as useCallback2 } from "react";
|
|
616
|
+
import { useEffect, useRef, useCallback as useCallback2, useState } from "react";
|
|
617
617
|
import { useMsal as useMsal2 } from "@azure/msal-react";
|
|
618
618
|
|
|
619
619
|
// src/hooks/useMsalAuth.ts
|
|
@@ -798,6 +798,8 @@ function useTokenRefresh(options = {}) {
|
|
|
798
798
|
const intervalRef = useRef(null);
|
|
799
799
|
const lastRefreshRef = useRef(null);
|
|
800
800
|
const expiresInRef = useRef(null);
|
|
801
|
+
const [expiresIn, setExpiresIn] = useState(null);
|
|
802
|
+
const [lastRefresh, setLastRefresh] = useState(null);
|
|
801
803
|
const refresh = useCallback2(async () => {
|
|
802
804
|
if (!isAuthenticated || !account) {
|
|
803
805
|
return;
|
|
@@ -809,9 +811,11 @@ function useTokenRefresh(options = {}) {
|
|
|
809
811
|
forceRefresh: false
|
|
810
812
|
});
|
|
811
813
|
lastRefreshRef.current = /* @__PURE__ */ new Date();
|
|
812
|
-
const
|
|
813
|
-
expiresInRef.current =
|
|
814
|
-
|
|
814
|
+
const newExpiresIn = response.expiresOn ? Math.max(0, response.expiresOn.getTime() / 1e3 - Date.now() / 1e3) : 3600;
|
|
815
|
+
expiresInRef.current = newExpiresIn;
|
|
816
|
+
setExpiresIn(newExpiresIn);
|
|
817
|
+
setLastRefresh(lastRefreshRef.current);
|
|
818
|
+
onRefresh?.(newExpiresIn);
|
|
815
819
|
} catch (error) {
|
|
816
820
|
console.error("[TokenRefresh] Failed to refresh token:", error);
|
|
817
821
|
onError?.(error);
|
|
@@ -829,6 +833,7 @@ function useTokenRefresh(options = {}) {
|
|
|
829
833
|
const timeSinceRefresh = lastRefreshRef.current ? (Date.now() - lastRefreshRef.current.getTime()) / 1e3 : 0;
|
|
830
834
|
const remainingTime = expiresInRef.current - timeSinceRefresh;
|
|
831
835
|
expiresInRef.current = Math.max(0, remainingTime);
|
|
836
|
+
setExpiresIn(expiresInRef.current);
|
|
832
837
|
if (remainingTime <= refreshBeforeExpiry && remainingTime > 0) {
|
|
833
838
|
refresh();
|
|
834
839
|
}
|
|
@@ -839,12 +844,12 @@ function useTokenRefresh(options = {}) {
|
|
|
839
844
|
}
|
|
840
845
|
};
|
|
841
846
|
}, [enabled, isAuthenticated, refreshBeforeExpiry, refresh]);
|
|
842
|
-
const isExpiringSoon =
|
|
847
|
+
const isExpiringSoon = expiresIn !== null && expiresIn <= refreshBeforeExpiry;
|
|
843
848
|
return {
|
|
844
|
-
expiresIn
|
|
849
|
+
expiresIn,
|
|
845
850
|
isExpiringSoon,
|
|
846
851
|
refresh,
|
|
847
|
-
lastRefresh
|
|
852
|
+
lastRefresh
|
|
848
853
|
};
|
|
849
854
|
}
|
|
850
855
|
|
|
@@ -985,7 +990,7 @@ function MsalAuthProvider({
|
|
|
985
990
|
onTenantDenied,
|
|
986
991
|
...config
|
|
987
992
|
}) {
|
|
988
|
-
const [msalInstance, setMsalInstance] =
|
|
993
|
+
const [msalInstance, setMsalInstance] = useState2(null);
|
|
989
994
|
const instanceRef = useRef2(null);
|
|
990
995
|
const { scopes = ["User.Read"], enableLogging = false } = config;
|
|
991
996
|
useEffect2(() => {
|
|
@@ -1155,7 +1160,7 @@ function MSALProvider({ children, protection, onTenantDenied, ...props }) {
|
|
|
1155
1160
|
}
|
|
1156
1161
|
|
|
1157
1162
|
// src/components/MicrosoftSignInButton.tsx
|
|
1158
|
-
import { useState as
|
|
1163
|
+
import { useState as useState3, useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
1159
1164
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1160
1165
|
function MicrosoftSignInButton({
|
|
1161
1166
|
text = "Sign in with Microsoft",
|
|
@@ -1168,7 +1173,7 @@ function MicrosoftSignInButton({
|
|
|
1168
1173
|
onError
|
|
1169
1174
|
}) {
|
|
1170
1175
|
const { loginRedirect, inProgress, isAuthenticated } = useMsalAuth();
|
|
1171
|
-
const [isLoading, setIsLoading] =
|
|
1176
|
+
const [isLoading, setIsLoading] = useState3(false);
|
|
1172
1177
|
const timeoutRef = useRef3(null);
|
|
1173
1178
|
useEffect3(() => {
|
|
1174
1179
|
return () => {
|
|
@@ -1356,10 +1361,10 @@ function MicrosoftLogo2() {
|
|
|
1356
1361
|
}
|
|
1357
1362
|
|
|
1358
1363
|
// src/components/UserAvatar.tsx
|
|
1359
|
-
import { useEffect as useEffect5, useState as
|
|
1364
|
+
import { useEffect as useEffect5, useState as useState5 } from "react";
|
|
1360
1365
|
|
|
1361
1366
|
// src/hooks/useUserProfile.ts
|
|
1362
|
-
import { useState as
|
|
1367
|
+
import { useState as useState4, useEffect as useEffect4, useCallback as useCallback4 } from "react";
|
|
1363
1368
|
|
|
1364
1369
|
// src/hooks/useGraphApi.ts
|
|
1365
1370
|
import { useCallback as useCallback3 } from "react";
|
|
@@ -1490,9 +1495,9 @@ function enforceCacheLimit() {
|
|
|
1490
1495
|
function useUserProfile() {
|
|
1491
1496
|
const { isAuthenticated, account } = useMsalAuth();
|
|
1492
1497
|
const graph = useGraphApi();
|
|
1493
|
-
const [profile, setProfile] =
|
|
1494
|
-
const [loading, setLoading] =
|
|
1495
|
-
const [error, setError] =
|
|
1498
|
+
const [profile, setProfile] = useState4(null);
|
|
1499
|
+
const [loading, setLoading] = useState4(false);
|
|
1500
|
+
const [error, setError] = useState4(null);
|
|
1496
1501
|
const fetchProfile = useCallback4(async () => {
|
|
1497
1502
|
if (!isAuthenticated || !account) {
|
|
1498
1503
|
setProfile(null);
|
|
@@ -1626,8 +1631,8 @@ function UserAvatar({
|
|
|
1626
1631
|
fallbackImage
|
|
1627
1632
|
}) {
|
|
1628
1633
|
const { profile, loading } = useUserProfile();
|
|
1629
|
-
const [photoUrl, setPhotoUrl] =
|
|
1630
|
-
const [photoError, setPhotoError] =
|
|
1634
|
+
const [photoUrl, setPhotoUrl] = useState5(null);
|
|
1635
|
+
const [photoError, setPhotoError] = useState5(false);
|
|
1631
1636
|
useEffect5(() => {
|
|
1632
1637
|
if (profile?.photo) {
|
|
1633
1638
|
setPhotoUrl(profile.photo);
|
|
@@ -1907,10 +1912,10 @@ var ErrorBoundary = class extends Component {
|
|
|
1907
1912
|
// src/hooks/useMultiAccount.ts
|
|
1908
1913
|
import { useMsal as useMsal3 } from "@azure/msal-react";
|
|
1909
1914
|
import { InteractionStatus as InteractionStatus2 } from "@azure/msal-browser";
|
|
1910
|
-
import { useCallback as useCallback5, useMemo as useMemo2, useState as
|
|
1915
|
+
import { useCallback as useCallback5, useMemo as useMemo2, useState as useState6, useEffect as useEffect7 } from "react";
|
|
1911
1916
|
function useMultiAccount(defaultScopes = ["User.Read"]) {
|
|
1912
1917
|
const { instance, accounts, inProgress } = useMsal3();
|
|
1913
|
-
const [activeAccount, setActiveAccount] =
|
|
1918
|
+
const [activeAccount, setActiveAccount] = useState6(
|
|
1914
1919
|
instance.getActiveAccount()
|
|
1915
1920
|
);
|
|
1916
1921
|
useEffect7(() => {
|
|
@@ -2060,7 +2065,7 @@ function useMultiAccount(defaultScopes = ["User.Read"]) {
|
|
|
2060
2065
|
}
|
|
2061
2066
|
|
|
2062
2067
|
// src/components/AccountSwitcher.tsx
|
|
2063
|
-
import { useState as
|
|
2068
|
+
import { useState as useState7 } from "react";
|
|
2064
2069
|
import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2065
2070
|
function AccountSwitcher({
|
|
2066
2071
|
showAvatars = true,
|
|
@@ -2084,8 +2089,8 @@ function AccountSwitcher({
|
|
|
2084
2089
|
isActiveAccount,
|
|
2085
2090
|
accountCount
|
|
2086
2091
|
} = useMultiAccount();
|
|
2087
|
-
const [isOpen, setIsOpen] =
|
|
2088
|
-
const [removingAccount, setRemovingAccount] =
|
|
2092
|
+
const [isOpen, setIsOpen] = useState7(false);
|
|
2093
|
+
const [removingAccount, setRemovingAccount] = useState7(null);
|
|
2089
2094
|
const handleSwitch = (account) => {
|
|
2090
2095
|
switchAccount(account);
|
|
2091
2096
|
setIsOpen(false);
|
|
@@ -2614,7 +2619,7 @@ function useTenant() {
|
|
|
2614
2619
|
}
|
|
2615
2620
|
|
|
2616
2621
|
// src/hooks/useRoles.ts
|
|
2617
|
-
import { useState as
|
|
2622
|
+
import { useState as useState8, useEffect as useEffect8, useCallback as useCallback6 } from "react";
|
|
2618
2623
|
var rolesCache = /* @__PURE__ */ new Map();
|
|
2619
2624
|
var CACHE_DURATION2 = 5 * 60 * 1e3;
|
|
2620
2625
|
var MAX_CACHE_SIZE2 = 100;
|
|
@@ -2636,10 +2641,10 @@ function enforceCacheLimit2() {
|
|
|
2636
2641
|
function useRoles() {
|
|
2637
2642
|
const { isAuthenticated, account } = useMsalAuth();
|
|
2638
2643
|
const graph = useGraphApi();
|
|
2639
|
-
const [roles, setRoles] =
|
|
2640
|
-
const [groups, setGroups] =
|
|
2641
|
-
const [loading, setLoading] =
|
|
2642
|
-
const [error, setError] =
|
|
2644
|
+
const [roles, setRoles] = useState8([]);
|
|
2645
|
+
const [groups, setGroups] = useState8([]);
|
|
2646
|
+
const [loading, setLoading] = useState8(false);
|
|
2647
|
+
const [error, setError] = useState8(null);
|
|
2643
2648
|
const fetchRolesAndGroups = useCallback6(async () => {
|
|
2644
2649
|
if (!isAuthenticated || !account) {
|
|
2645
2650
|
setRoles([]);
|
|
@@ -3005,7 +3010,7 @@ function createScopedLogger(scope, config) {
|
|
|
3005
3010
|
}
|
|
3006
3011
|
|
|
3007
3012
|
// src/protection/ProtectedPage.tsx
|
|
3008
|
-
import { useEffect as useEffect9, useState as
|
|
3013
|
+
import { useEffect as useEffect9, useState as useState9 } from "react";
|
|
3009
3014
|
import { useRouter } from "next/navigation";
|
|
3010
3015
|
import { Fragment as Fragment6, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3011
3016
|
function ProtectedPage({
|
|
@@ -3019,8 +3024,8 @@ function ProtectedPage({
|
|
|
3019
3024
|
const router = useRouter();
|
|
3020
3025
|
const { isAuthenticated, account, inProgress } = useMsalAuth();
|
|
3021
3026
|
const tenantInfo = useTenant();
|
|
3022
|
-
const [isValidating, setIsValidating] =
|
|
3023
|
-
const [isAuthorized, setIsAuthorized] =
|
|
3027
|
+
const [isValidating, setIsValidating] = useState9(true);
|
|
3028
|
+
const [isAuthorized, setIsAuthorized] = useState9(false);
|
|
3024
3029
|
useEffect9(() => {
|
|
3025
3030
|
async function checkAuth() {
|
|
3026
3031
|
if (debug) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chemmangat/msal-next",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.4",
|
|
4
4
|
"description": "Production-ready Microsoft/Azure AD authentication for Next.js App Router. Zero-config setup, TypeScript-first, multi-account support, auto token refresh. The easiest way to add Microsoft login to your Next.js app.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|