@equinor/fusion-framework-react-app 7.0.1 → 8.0.0-next.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.
@@ -1 +1 @@
1
- export declare const version = "7.0.1";
1
+ export declare const version = "8.0.0-next.1";
package/docs/msal.md ADDED
@@ -0,0 +1,291 @@
1
+ # MSAL Authentication
2
+
3
+ This package includes React hooks for Microsoft authentication using MSAL v4.
4
+
5
+ > [!CAUTION]
6
+ > **Applications should NOT configure the MSAL module themselves.**
7
+ >
8
+ > The MSAL module **must be configured by the host/portal application**, not by individual apps. This is required for module hoisting, which allows sharing authentication state across all applications in a portal.
9
+ >
10
+ > - ✅ **Host/Portal:** Configure MSAL using `enableMSAL()` in the portal's configuration
11
+ > - ❌ **App:** Do NOT call `enableMSAL()` or configure the auth module
12
+ > - ✅ **App:** Just use the hooks to access the already-configured MSAL module
13
+
14
+ > [!IMPORTANT]
15
+ > `@equinor/fusion-framework-module-msal` must be installed to make MSAL hooks available
16
+
17
+ ## Overview
18
+
19
+ The MSAL authentication hooks provide a simple way to integrate Microsoft authentication into your React applications. These hooks are built on top of `@equinor/fusion-framework-module-msal` and provide React-friendly access to authentication state and token acquisition.
20
+
21
+ Since the MSAL module is configured by your host application, you don't need to worry about configuration—just import and use the hooks!
22
+
23
+ The hooks use a simplified API that internally handles the MSAL v4 format, so you can use the simple `{ scopes: string[] }` format without worrying about the v4 nested request structure.
24
+
25
+ ## Available Hooks
26
+
27
+ ### useCurrentAccount
28
+
29
+ Returns the current authenticated user account information.
30
+
31
+ **Signature:**
32
+ ```typescript
33
+ useCurrentAccount(): AccountInfo | undefined
34
+ ```
35
+
36
+ **Example:**
37
+ ```tsx
38
+ import { useCurrentAccount } from '@equinor/fusion-framework-react-app/msal';
39
+
40
+ const UserProfile = () => {
41
+ const account = useCurrentAccount();
42
+
43
+ if (!account) {
44
+ return <span>Not authenticated</span>;
45
+ }
46
+
47
+ return (
48
+ <div>
49
+ <p>Username: {account.username}</p>
50
+ <p>Name: {account.name}</p>
51
+ <p>Tenant ID: {account.tenantId}</p>
52
+ </div>
53
+ );
54
+ };
55
+ ```
56
+
57
+ **Returns:** `AccountInfo | undefined`
58
+
59
+ ### useAccessToken
60
+
61
+ Returns just the access token string for making authenticated API calls.
62
+
63
+ **Signature:**
64
+ ```typescript
65
+ useAccessToken(req: { scopes: string[] }): { token?: string; pending: boolean; error: unknown }
66
+ ```
67
+
68
+ **Example:**
69
+ ```tsx
70
+ import { useAccessToken } from '@equinor/fusion-framework-react-app/msal';
71
+ import { useMemo } from 'react';
72
+
73
+ const ProtectedComponent = () => {
74
+ const { token, pending, error } = useAccessToken(
75
+ useMemo(() => ({ scopes: ['User.Read'] }), [])
76
+ );
77
+
78
+ if (pending) return <span>Loading token...</span>;
79
+ if (error) return <span>Error: {String(error)}</span>;
80
+
81
+ return token ? <span>Token acquired</span> : <span>No token</span>;
82
+ };
83
+ ```
84
+
85
+ **Parameters:**
86
+ - `req`: Object with `scopes` property - Array of OAuth scopes to request
87
+
88
+ **Returns:**
89
+ - `token?: string` - The access token string if available
90
+ - `pending: boolean` - Whether the token request is in progress
91
+ - `error: unknown` - Any error that occurred during token acquisition
92
+
93
+ ### useToken
94
+
95
+ Returns the full authentication result object with complete token information.
96
+
97
+ **Signature:**
98
+ ```typescript
99
+ useToken(req: { scopes: string[] }): { token?: AuthenticationResult; pending: boolean; error: unknown }
100
+ ```
101
+
102
+ **Example:**
103
+ ```tsx
104
+ import { useToken } from '@equinor/fusion-framework-react-app/msal';
105
+ import { useMemo } from 'react';
106
+
107
+ const TokenInfo = () => {
108
+ const { token, pending, error } = useToken(
109
+ useMemo(() => ({ scopes: ['User.Read', 'api.read'] }), [])
110
+ );
111
+
112
+ if (pending) return <span>Loading...</span>;
113
+ if (error) return <span>Error: {String(error)}</span>;
114
+ if (!token) return <span>No token</span>;
115
+
116
+ return (
117
+ <div>
118
+ <p>Access Token: {token.accessToken.substring(0, 20)}...</p>
119
+ <p>Expires: {new Date(token.expiresOn).toLocaleString()}</p>
120
+ <p>Token Type: {token.tokenType}</p>
121
+ <p>Scope: {token.scopes.join(', ')}</p>
122
+ </div>
123
+ );
124
+ };
125
+ ```
126
+
127
+ **Parameters:**
128
+ - `req`: Object with `scopes` property - Array of OAuth scopes to request
129
+
130
+ **Returns:**
131
+ - `token?: AuthenticationResult` - Full authentication result containing accessToken, account info, expiresOn, etc.
132
+ - `pending: boolean` - Whether the token request is in progress
133
+ - `error: unknown` - Any error that occurred during token acquisition
134
+
135
+ **Note:** The `AuthenticationResult` type includes:
136
+ - `accessToken: string`
137
+ - `account: AccountInfo`
138
+ - `expiresOn: Date`
139
+ - `tokenType: string`
140
+ - `scopes: string[]`
141
+ - And more...
142
+
143
+ ## How It Works
144
+
145
+ Internally, these hooks call the MSAL provider's methods which support both legacy and modern formats:
146
+
147
+ ```typescript
148
+ // The hook receives simple format:
149
+ useToken({ scopes: ['User.Read'] })
150
+
151
+ // Internally converts to MSAL v4 format:
152
+ msalProvider.acquireToken({ request: { scopes: ['User.Read'] } })
153
+ ```
154
+
155
+ This means you can use the simpler API without dealing with the v4 nested structure, while still benefiting from MSAL v4 features under the hood.
156
+
157
+ ## Complete Example
158
+
159
+ ```tsx
160
+ import { useCurrentAccount, useAccessToken } from '@equinor/fusion-framework-react-app/msal';
161
+ import { useMemo, useState } from 'react';
162
+
163
+ const App = () => {
164
+ const account = useCurrentAccount();
165
+
166
+ // Get scopes from somewhere (e.g., service discovery)
167
+ const [scopes] = useState(['User.Read', 'api.read']);
168
+
169
+ const { token, pending, error } = useAccessToken(
170
+ useMemo(() => ({ scopes }), [scopes])
171
+ );
172
+
173
+ return (
174
+ <div>
175
+ {!account && <p>Please log in</p>}
176
+
177
+ {account && (
178
+ <>
179
+ <h1>Welcome, {account.name}!</h1>
180
+ <p>Username: {account.username}</p>
181
+
182
+ {pending && <p>Loading token...</p>}
183
+ {error && <p>Error: {String(error)}</p>}
184
+ {token && (
185
+ <div>
186
+ <p>Token acquired successfully</p>
187
+ <pre>{token.substring(0, 50)}...</pre>
188
+ </div>
189
+ )}
190
+ </>
191
+ )}
192
+ </div>
193
+ );
194
+ };
195
+ ```
196
+
197
+ ## Advanced Usage
198
+
199
+ ### Dynamic Scopes
200
+
201
+ ```tsx
202
+ const Component = () => {
203
+ const [scopes, setScopes] = useState(['User.Read']);
204
+
205
+ const { token, pending } = useAccessToken(
206
+ useMemo(() => ({ scopes }), [scopes])
207
+ );
208
+
209
+ return (
210
+ <div>
211
+ <button onClick={() => setScopes(['User.Read', 'api.write'])}>
212
+ Request More Permissions
213
+ </button>
214
+ {token && <p>Token ready</p>}
215
+ </div>
216
+ );
217
+ };
218
+ ```
219
+
220
+ ### Error Handling
221
+
222
+ ```tsx
223
+ const Component = () => {
224
+ const { token, error, pending } = useAccessToken(
225
+ useMemo(() => ({ scopes: ['User.Read'] }), [])
226
+ );
227
+
228
+ useEffect(() => {
229
+ if (error) {
230
+ console.error('Token acquisition failed:', error);
231
+ // Handle error appropriately
232
+ }
233
+ }, [error]);
234
+
235
+ return pending ? <Loading /> : token ? <Content /> : <Error />;
236
+ };
237
+ ```
238
+
239
+ ## Performance Considerations
240
+
241
+ 1. **Memoize request objects**: Always wrap the scopes object in `useMemo` to prevent unnecessary re-renders
242
+ ```tsx
243
+ // Bad - creates new object on every render
244
+ const { token } = useAccessToken({ scopes: ['User.Read'] });
245
+
246
+ // Good - stable reference
247
+ const { token } = useAccessToken(
248
+ useMemo(() => ({ scopes: ['User.Read'] }), [])
249
+ );
250
+ ```
251
+
252
+ 2. **Dependent scopes**: Include scopes in the dependency array
253
+ ```tsx
254
+ const { token } = useAccessToken(
255
+ useMemo(() => ({ scopes }), [scopes])
256
+ );
257
+ ```
258
+
259
+ ## MSAL v4 Under the Hood
260
+
261
+ While the hooks use a simple API, they internally use MSAL v4:
262
+
263
+ - Hooks accept `{ scopes: string[] }` for simplicity
264
+ - Internally converts to `{ request: { scopes: string[] } }` format
265
+ - The provider supports both legacy and modern formats
266
+ - All benefits of MSAL v4 are available (better security, performance, etc.)
267
+
268
+ ## Troubleshooting
269
+
270
+ ### Hook Returns No Account
271
+
272
+ - Ensure user is logged in
273
+ - Check that MSAL module is properly configured
274
+ - Verify `useCurrentAccount()` hook is wrapped in a component within the framework provider
275
+
276
+ ### Token Acquisition Fails
277
+
278
+ - Verify scopes are properly configured in Azure AD
279
+ - Check user has necessary permissions for requested scopes
280
+ - Review browser console for MSAL errors
281
+
282
+ ### Performance Issues
283
+
284
+ - Ensure scopes object is memoized with `useMemo`
285
+ - Check for unnecessary re-renders of parent components
286
+
287
+ ## Related Documentation
288
+
289
+ - [MSAL Module Documentation](https://equinor.github.io/fusion-framework/modules/auth/msal/) - Core MSAL provider
290
+ - [Microsoft MSAL Browser Docs](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser) - Official MSAL documentation
291
+ - [Azure AD App Registration Guide](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) - App registration guide
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@equinor/fusion-framework-react-app",
3
- "version": "7.0.1",
3
+ "version": "8.0.0-next.1",
4
4
  "description": "",
5
5
  "main": "./dist/esm/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -106,13 +106,13 @@
106
106
  "directory": "packages/react"
107
107
  },
108
108
  "dependencies": {
109
- "@equinor/fusion-framework-app": "^10.1.1",
110
- "@equinor/fusion-framework-module": "^5.0.4",
111
- "@equinor/fusion-framework-module-app": "^7.0.2",
112
- "@equinor/fusion-framework-module-navigation": "^6.0.0",
113
- "@equinor/fusion-framework-react": "^7.4.18",
114
- "@equinor/fusion-framework-react-module": "^3.1.13",
115
- "@equinor/fusion-framework-react-module-http": "^10.0.0"
109
+ "@equinor/fusion-framework-app": "^10.1.2-next.1",
110
+ "@equinor/fusion-framework-module-app": "^7.0.4-next.0",
111
+ "@equinor/fusion-framework-react": "^7.4.19-next.1",
112
+ "@equinor/fusion-framework-module": "^5.0.6-next.0",
113
+ "@equinor/fusion-framework-module-navigation": "^6.0.1-next.0",
114
+ "@equinor/fusion-framework-react-module": "^3.1.14-next.0",
115
+ "@equinor/fusion-framework-react-module-http": "^10.0.1-next.1"
116
116
  },
117
117
  "devDependencies": {
118
118
  "@types/react": "^18.2.50",
@@ -121,20 +121,20 @@
121
121
  "react-dom": "^18.2.0",
122
122
  "rxjs": "^7.8.1",
123
123
  "typescript": "^5.8.2",
124
- "@equinor/fusion-framework-module-ag-grid": "^34.2.1",
125
- "@equinor/fusion-framework-module-event": "^4.4.0",
126
- "@equinor/fusion-framework-module-feature-flag": "^1.1.25",
127
- "@equinor/fusion-framework-module-msal": "^5.1.1",
128
- "@equinor/fusion-framework-react-module-bookmark": "^5.0.1",
129
- "@equinor/fusion-framework-react-module-context": "^6.2.33",
130
- "@equinor/fusion-observable": "^8.5.5"
124
+ "@equinor/fusion-framework-module-ag-grid": "^34.2.3-next.0",
125
+ "@equinor/fusion-framework-module-event": "^4.4.1-next.0",
126
+ "@equinor/fusion-framework-module-feature-flag": "^1.1.27-next.0",
127
+ "@equinor/fusion-framework-module-msal": "^6.0.0-next.1",
128
+ "@equinor/fusion-framework-react-module-bookmark": "^5.0.2-next.1",
129
+ "@equinor/fusion-framework-react-module-context": "^6.2.34-next.0",
130
+ "@equinor/fusion-observable": "^8.5.7-next.0"
131
131
  },
132
132
  "peerDependencies": {
133
133
  "@types/react": "^17.0.0 || ^18.0.0",
134
134
  "react": "^17.0.0 || ^18.0.0",
135
135
  "react-dom": "^17.0.0 || ^18.0.0",
136
136
  "rxjs": "^7.8.1",
137
- "@equinor/fusion-framework-module-msal": "^5.1.1"
137
+ "@equinor/fusion-framework-module-msal": "^6.0.0-next.1"
138
138
  },
139
139
  "peerDependenciesMeta": {
140
140
  "@equinor/fusion-framework-react-module-ag-grid": {
@@ -7,5 +7,5 @@ import useAppModule from '../useAppModule';
7
7
  */
8
8
  export const useCurrentAccount = (): AccountInfo | undefined => {
9
9
  const msalProvider = useAppModule('auth');
10
- return msalProvider.defaultAccount;
10
+ return msalProvider.account || undefined;
11
11
  };
@@ -20,9 +20,12 @@ export const useToken = (req: {
20
20
  setPending(true);
21
21
  setToken(undefined);
22
22
  msalProvider
23
- .acquireToken(req)
24
- // biome-ignore lint/suspicious/noConfusingVoidType: this method returns a void type when no token is acquired
25
- .then((token: AuthenticationResult | void) => token && setToken(token))
23
+ .acquireToken({ request: req })
24
+ .then((result) => {
25
+ if (result) {
26
+ setToken(result);
27
+ }
28
+ })
26
29
  .catch(setError)
27
30
  .finally(() => setPending(false));
28
31
  }, [msalProvider, req]);
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '7.0.1';
2
+ export const version = '8.0.0-next.1';