@authu/react 1.0.65 → 1.0.66
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 +50 -0
- package/dist/AuthUProvider.d.ts.map +1 -1
- package/dist/AuthUProvider.js +194 -183
- package/dist/pkce.d.ts.map +1 -1
- package/dist/pkce.js +8 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,6 +128,55 @@ function ApiComponent() {
|
|
|
128
128
|
}
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
## Token Storage
|
|
132
|
+
|
|
133
|
+
**Tokens never touch `localStorage`, `sessionStorage` or any other persistent
|
|
134
|
+
store.** The access token and the refresh token live in memory for the lifetime
|
|
135
|
+
of the page: an XSS on the host application can still act as the user while the
|
|
136
|
+
page is open, but it can no longer walk away with a 30-day refresh token and
|
|
137
|
+
replay it from anywhere, later.
|
|
138
|
+
|
|
139
|
+
The trade-off is that a reload loses the tokens. The session itself is not
|
|
140
|
+
lost, though: it lives in AuthU's own cookie, and the SDK re-derives the tokens
|
|
141
|
+
from it on every page load with one silent round trip.
|
|
142
|
+
|
|
143
|
+
### Silent renewal (`prompt=none`)
|
|
144
|
+
|
|
145
|
+
On mount, when no code is coming back in the URL, the SDK redirects to
|
|
146
|
+
`/authorize?prompt=none`. AuthU answers without ever showing a page:
|
|
147
|
+
|
|
148
|
+
- session cookie still valid → it redirects straight back with a fresh
|
|
149
|
+
authorization code, which the SDK exchanges for tokens. The visitor sees a
|
|
150
|
+
single redirect and stays signed in.
|
|
151
|
+
- no session → it redirects back with `error=login_required`. The SDK reads
|
|
152
|
+
that as "anonymous", renders accordingly, and records the attempt in
|
|
153
|
+
`sessionStorage` so the tab never loops through AuthU again.
|
|
154
|
+
|
|
155
|
+
The URL the visitor asked for is saved before the round trip and restored
|
|
156
|
+
afterwards, so landing on `redirectUri` is invisible.
|
|
157
|
+
|
|
158
|
+
Set `silentAuth: false` to opt out. The session then does not survive a reload
|
|
159
|
+
at all: every load starts signed out until `login()` is called.
|
|
160
|
+
|
|
161
|
+
### Logging out
|
|
162
|
+
|
|
163
|
+
`logout()` now navigates to AuthU's `/logout` rather than only clearing memory.
|
|
164
|
+
It has to: the session lives in the AuthU cookie, so a purely local logout
|
|
165
|
+
would let the next silent renewal sign the visitor right back in. The browser
|
|
166
|
+
returns to `redirectUri`, or to `options.returnTo` when given - which must be a
|
|
167
|
+
URI registered for the tenant, or on the tenant's home origin, otherwise AuthU
|
|
168
|
+
falls back to its own host.
|
|
169
|
+
|
|
170
|
+
### Upgrading from 1.0.x
|
|
171
|
+
|
|
172
|
+
Two behaviour changes to expect in an app that upgrades:
|
|
173
|
+
|
|
174
|
+
- any token left in `localStorage` under `authu_tokens` by a previous version
|
|
175
|
+
is deleted on the first load;
|
|
176
|
+
- `logout()` performs a full navigation. Code that expected it to be
|
|
177
|
+
synchronous and stay on the page must move whatever it did afterwards into
|
|
178
|
+
`options.returnTo`.
|
|
179
|
+
|
|
131
180
|
## API Reference
|
|
132
181
|
|
|
133
182
|
### AuthUProvider Props
|
|
@@ -139,6 +188,7 @@ function ApiComponent() {
|
|
|
139
188
|
| `redirectUri` | `string` | Yes | Callback URL after login |
|
|
140
189
|
| `scope` | `string` | No | OAuth2 scopes (default: `openid profile email`) |
|
|
141
190
|
| `audience` | `string` | No | API audience for access tokens |
|
|
191
|
+
| `silentAuth` | `boolean` | No | Silent session renewal (default: `true`, see below) |
|
|
142
192
|
|
|
143
193
|
### useAuthU Returns
|
|
144
194
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthUProvider.d.ts","sourceRoot":"","sources":["../src/AuthUProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,
|
|
1
|
+
{"version":3,"file":"AuthUProvider.d.ts","sourceRoot":"","sources":["../src/AuthUProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AAef,OAAO,KAAK,EAEV,WAAW,EACX,iBAAiB,EAIlB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,YAAY,mDAAgD,CAAC;AAO1E,UAAU,kBAAkB;IAC1B,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAuDD,wBAAgB,aAAa,CAAC,EAAC,MAAM,EAAE,QAAQ,EAAC,EAAE,kBAAkB,2CAwYnE"}
|
package/dist/AuthUProvider.js
CHANGED
|
@@ -1,23 +1,77 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { createContext, useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { clearCodeVerifier, clearNonce, clearState, generateCodeChallenge, generateCodeVerifier, generateNonce, generateState, getCodeVerifier, getStoredState, storeCodeVerifier, storeNonce, storeState } from './pkce.js';
|
|
4
4
|
export const AuthUContext = createContext(null);
|
|
5
5
|
function buildBaseUrl(domain) {
|
|
6
6
|
const protocol = domain.startsWith('localhost') ? 'http' : 'https';
|
|
7
7
|
return `${protocol}://${domain}`;
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Tokens are never written to disk. Up to 1.0.x they lived in localStorage
|
|
11
|
+
* under this key, where any XSS on the host app could read them and replay a
|
|
12
|
+
* 30-day refresh token from anywhere; the key is only kept here so an app
|
|
13
|
+
* upgrading to this version wipes what the previous one left behind.
|
|
14
|
+
*/
|
|
15
|
+
const LEGACY_TOKEN_STORAGE_KEY = 'authu_tokens';
|
|
16
|
+
/**
|
|
17
|
+
* Holding the tokens in memory means a reload loses them, so the session is
|
|
18
|
+
* re-derived from the AuthU session cookie instead: one `prompt=none` round
|
|
19
|
+
* trip through /authorize, which returns a fresh code without showing a page
|
|
20
|
+
* when the cookie is still valid, and `error=login_required` when it is not.
|
|
21
|
+
* The attempt is recorded per tab so a signed-out visitor bounces once and
|
|
22
|
+
* then renders anonymously rather than looping through AuthU forever.
|
|
23
|
+
*/
|
|
24
|
+
const SILENT_ATTEMPT_KEY = 'authu_silent_attempted';
|
|
25
|
+
const RETURN_TO_KEY = 'authu_return_to';
|
|
26
|
+
const EMPTY_STATE = {
|
|
27
|
+
isLoading: false,
|
|
28
|
+
isAuthenticated: false,
|
|
29
|
+
user: null,
|
|
30
|
+
accessToken: null,
|
|
31
|
+
refreshToken: null,
|
|
32
|
+
expiresAt: null,
|
|
33
|
+
error: null
|
|
34
|
+
};
|
|
35
|
+
function currentLocation() {
|
|
36
|
+
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Only a same-origin path is ever restored: `//evil.example` is a valid
|
|
40
|
+
* relative URL for the browser but a different origin, so a poisoned entry
|
|
41
|
+
* cannot turn the silent round trip into an open redirect.
|
|
42
|
+
*/
|
|
43
|
+
function readReturnTo() {
|
|
44
|
+
const stored = sessionStorage.getItem(RETURN_TO_KEY);
|
|
45
|
+
sessionStorage.removeItem(RETURN_TO_KEY);
|
|
46
|
+
if (!stored || !stored.startsWith('/') || stored.startsWith('//')) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return stored;
|
|
50
|
+
}
|
|
10
51
|
export function AuthUProvider({ config, children }) {
|
|
11
52
|
const [state, setState] = useState({
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
user: null,
|
|
15
|
-
accessToken: null,
|
|
16
|
-
refreshToken: null,
|
|
17
|
-
expiresAt: null,
|
|
18
|
-
error: null
|
|
53
|
+
...EMPTY_STATE,
|
|
54
|
+
isLoading: true
|
|
19
55
|
});
|
|
20
|
-
|
|
56
|
+
// The tokens also live in a ref so getAccessToken() and refreshTokens() read
|
|
57
|
+
// the current pair rather than the one captured when the callback was built:
|
|
58
|
+
// two concurrent API calls racing on an expiring token would otherwise both
|
|
59
|
+
// rotate the refresh token, and the loser is now treated as a replay.
|
|
60
|
+
const tokensRef = useRef(null);
|
|
61
|
+
const refreshInFlight = useRef(null);
|
|
62
|
+
const setTokens = useCallback((tokens, user) => {
|
|
63
|
+
tokensRef.current = tokens;
|
|
64
|
+
setState(prev => ({
|
|
65
|
+
isLoading: false,
|
|
66
|
+
isAuthenticated: tokens !== null,
|
|
67
|
+
user: tokens === null ? null : user !== undefined ? user : prev.user,
|
|
68
|
+
accessToken: tokens?.accessToken ?? null,
|
|
69
|
+
refreshToken: tokens?.refreshToken ?? null,
|
|
70
|
+
expiresAt: tokens?.expiresAt ?? null,
|
|
71
|
+
error: null
|
|
72
|
+
}));
|
|
73
|
+
}, []);
|
|
74
|
+
const buildAuthorizationUrl = useCallback(async (options) => {
|
|
21
75
|
const codeVerifier = generateCodeVerifier();
|
|
22
76
|
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
23
77
|
const stateValue = generateState();
|
|
@@ -44,6 +98,9 @@ export function AuthUProvider({ config, children }) {
|
|
|
44
98
|
if (options?.screenHint) {
|
|
45
99
|
params.set('screen_hint', options.screenHint);
|
|
46
100
|
}
|
|
101
|
+
if (options?.prompt) {
|
|
102
|
+
params.set('prompt', options.prompt);
|
|
103
|
+
}
|
|
47
104
|
return `${buildBaseUrl(config.domain)}/authorize?${params.toString()}`;
|
|
48
105
|
}, [config]);
|
|
49
106
|
const exchangeCodeForTokensWithVerifier = useCallback(async (code, codeVerifier) => {
|
|
@@ -79,22 +136,31 @@ export function AuthUProvider({ config, children }) {
|
|
|
79
136
|
};
|
|
80
137
|
}, []);
|
|
81
138
|
const login = useCallback(async (options) => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
139
|
+
sessionStorage.removeItem(SILENT_ATTEMPT_KEY);
|
|
140
|
+
if (options?.returnTo) {
|
|
141
|
+
sessionStorage.setItem(RETURN_TO_KEY, options.returnTo);
|
|
142
|
+
}
|
|
143
|
+
window.location.href = await buildAuthorizationUrl(options);
|
|
144
|
+
}, [buildAuthorizationUrl]);
|
|
145
|
+
/**
|
|
146
|
+
* A local logout is no longer enough: the session now lives in AuthU's own
|
|
147
|
+
* cookie, so clearing memory alone would let the next silent renewal sign
|
|
148
|
+
* the visitor straight back in. The browser is sent to AuthU /logout, which
|
|
149
|
+
* kills the session and bounces back to the app.
|
|
150
|
+
*/
|
|
151
|
+
const logout = useCallback((options) => {
|
|
152
|
+
tokensRef.current = null;
|
|
153
|
+
sessionStorage.setItem(SILENT_ATTEMPT_KEY, '1');
|
|
154
|
+
setState(EMPTY_STATE);
|
|
155
|
+
// The destination has to survive AuthU's own check, which only accepts a
|
|
156
|
+
// URI the tenant registered or its home origin: the configured
|
|
157
|
+
// redirectUri is registered by construction, so it is the safe default.
|
|
158
|
+
const params = new URLSearchParams({
|
|
159
|
+
client_id: config.clientId,
|
|
160
|
+
redirect_uri: options?.returnTo || config.redirectUri
|
|
96
161
|
});
|
|
97
|
-
|
|
162
|
+
window.location.href = `${buildBaseUrl(config.domain)}/logout?${params.toString()}`;
|
|
163
|
+
}, [config.domain, config.clientId, config.redirectUri]);
|
|
98
164
|
const fetchUserInfo = useCallback(async (accessToken) => {
|
|
99
165
|
try {
|
|
100
166
|
const response = await fetch(`${buildBaseUrl(config.domain)}/oauth/userinfo`, {
|
|
@@ -116,206 +182,151 @@ export function AuthUProvider({ config, children }) {
|
|
|
116
182
|
}
|
|
117
183
|
}, [config.domain]);
|
|
118
184
|
const refreshTokens = useCallback(async () => {
|
|
119
|
-
|
|
185
|
+
const refreshToken = tokensRef.current?.refreshToken;
|
|
186
|
+
if (!refreshToken)
|
|
120
187
|
return false;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
188
|
+
if (refreshInFlight.current)
|
|
189
|
+
return refreshInFlight.current;
|
|
190
|
+
const run = async () => {
|
|
191
|
+
try {
|
|
192
|
+
const response = await fetch(`${buildBaseUrl(config.domain)}/oauth/token`, {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: { 'Content-Type': 'application/json' },
|
|
195
|
+
body: JSON.stringify({
|
|
196
|
+
grant_type: 'refresh_token',
|
|
197
|
+
refresh_token: refreshToken,
|
|
198
|
+
client_id: config.clientId
|
|
199
|
+
})
|
|
200
|
+
});
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
tokensRef.current = null;
|
|
203
|
+
setState(EMPTY_STATE);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const tokens = await response.json();
|
|
207
|
+
setTokens({
|
|
208
|
+
accessToken: tokens.access_token,
|
|
209
|
+
refreshToken: tokens.refresh_token ?? null,
|
|
210
|
+
expiresAt: Date.now() + tokens.expires_in * 1000
|
|
211
|
+
}, tokens.id_token ? parseIdToken(tokens.id_token) : undefined);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
tokensRef.current = null;
|
|
216
|
+
setState(EMPTY_STATE);
|
|
133
217
|
return false;
|
|
134
218
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}));
|
|
143
|
-
setState(prev => ({
|
|
144
|
-
...prev,
|
|
145
|
-
accessToken: tokens.access_token,
|
|
146
|
-
refreshToken: tokens.refresh_token,
|
|
147
|
-
expiresAt,
|
|
148
|
-
user: user || prev.user
|
|
149
|
-
}));
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
logout();
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
}, [
|
|
157
|
-
state.refreshToken,
|
|
158
|
-
state.user,
|
|
159
|
-
config.domain,
|
|
160
|
-
config.clientId,
|
|
161
|
-
logout,
|
|
162
|
-
parseIdToken
|
|
163
|
-
]);
|
|
219
|
+
};
|
|
220
|
+
const pending = run().finally(() => {
|
|
221
|
+
refreshInFlight.current = null;
|
|
222
|
+
});
|
|
223
|
+
refreshInFlight.current = pending;
|
|
224
|
+
return pending;
|
|
225
|
+
}, [config.domain, config.clientId, parseIdToken, setTokens]);
|
|
164
226
|
const getAccessToken = useCallback(async () => {
|
|
165
|
-
|
|
227
|
+
const tokens = tokensRef.current;
|
|
228
|
+
if (!tokens)
|
|
166
229
|
return null;
|
|
167
|
-
if (
|
|
230
|
+
if (Date.now() >= tokens.expiresAt - 60000) {
|
|
168
231
|
const refreshed = await refreshTokens();
|
|
169
232
|
if (!refreshed)
|
|
170
233
|
return null;
|
|
171
234
|
}
|
|
172
|
-
return
|
|
173
|
-
}, [
|
|
235
|
+
return tokensRef.current?.accessToken ?? null;
|
|
236
|
+
}, [refreshTokens]);
|
|
237
|
+
const configRef = useRef(config);
|
|
238
|
+
configRef.current = config;
|
|
239
|
+
const bootstrapped = useRef(false);
|
|
174
240
|
useEffect(() => {
|
|
175
|
-
|
|
176
|
-
|
|
241
|
+
if (bootstrapped.current)
|
|
242
|
+
return;
|
|
243
|
+
bootstrapped.current = true;
|
|
244
|
+
// Silent renewal lands on redirectUri, which for most apps is a bare
|
|
245
|
+
// /callback route: the URL is rewritten back to the page the visitor
|
|
246
|
+
// actually asked for. replaceState alone would leave a client-side router
|
|
247
|
+
// still rendering /callback, so a popstate is dispatched to make it
|
|
248
|
+
// re-read the location.
|
|
249
|
+
const restoreUrl = () => {
|
|
250
|
+
const returnTo = readReturnTo();
|
|
251
|
+
window.history.replaceState({}, document.title, returnTo || window.location.pathname);
|
|
252
|
+
if (returnTo) {
|
|
253
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const bootstrap = async () => {
|
|
257
|
+
localStorage.removeItem(LEGACY_TOKEN_STORAGE_KEY);
|
|
177
258
|
const params = new URLSearchParams(window.location.search);
|
|
178
259
|
const code = params.get('code');
|
|
179
260
|
const returnedState = params.get('state');
|
|
180
261
|
const error = params.get('error');
|
|
181
|
-
console.log('[AuthU SDK] handleCallback params - code:', !!code, 'state:', returnedState, 'error:', error);
|
|
182
262
|
if (error) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
263
|
+
const description = params.get('error_description') ||
|
|
264
|
+
params.get('errorDescription') ||
|
|
265
|
+
error;
|
|
266
|
+
restoreUrl();
|
|
267
|
+
// A refused silent renewal is the expected answer for a visitor who is
|
|
268
|
+
// simply not signed in: it is an anonymous render, not an error.
|
|
269
|
+
if (error === 'login_required' || error === 'interaction_required') {
|
|
270
|
+
sessionStorage.setItem(SILENT_ATTEMPT_KEY, '1');
|
|
271
|
+
setState(EMPTY_STATE);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
setState({ ...EMPTY_STATE, error: new Error(description) });
|
|
188
275
|
return;
|
|
189
276
|
}
|
|
190
277
|
if (code) {
|
|
191
278
|
const storedState = getStoredState();
|
|
192
279
|
if (returnedState !== storedState) {
|
|
193
|
-
setState(
|
|
194
|
-
...
|
|
195
|
-
isLoading: false,
|
|
280
|
+
setState({
|
|
281
|
+
...EMPTY_STATE,
|
|
196
282
|
error: new Error('Invalid state parameter')
|
|
197
|
-
})
|
|
283
|
+
});
|
|
198
284
|
return;
|
|
199
285
|
}
|
|
200
|
-
//
|
|
201
|
-
//
|
|
286
|
+
// Read and clear the verifier in one go: React StrictMode mounts this
|
|
287
|
+
// effect twice, and the second pass must not replay the same code.
|
|
202
288
|
const codeVerifier = getCodeVerifier();
|
|
203
|
-
if (!codeVerifier)
|
|
204
|
-
console.log('[AuthU SDK] No code verifier found, skipping duplicate callback');
|
|
289
|
+
if (!codeVerifier)
|
|
205
290
|
return;
|
|
206
|
-
}
|
|
207
291
|
clearCodeVerifier();
|
|
208
292
|
clearState();
|
|
209
293
|
clearNonce();
|
|
210
|
-
|
|
211
|
-
window.history.replaceState({}, document.title, window.location.pathname);
|
|
294
|
+
restoreUrl();
|
|
212
295
|
try {
|
|
213
296
|
const tokens = await exchangeCodeForTokensWithVerifier(code, codeVerifier);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify({
|
|
217
|
-
accessToken: tokens.access_token,
|
|
218
|
-
refreshToken: tokens.refresh_token,
|
|
219
|
-
expiresAt
|
|
220
|
-
}));
|
|
221
|
-
setState({
|
|
222
|
-
isLoading: false,
|
|
223
|
-
isAuthenticated: true,
|
|
224
|
-
user,
|
|
297
|
+
sessionStorage.removeItem(SILENT_ATTEMPT_KEY);
|
|
298
|
+
setTokens({
|
|
225
299
|
accessToken: tokens.access_token,
|
|
226
|
-
refreshToken: tokens.refresh_token,
|
|
227
|
-
expiresAt
|
|
228
|
-
|
|
229
|
-
|
|
300
|
+
refreshToken: tokens.refresh_token ?? null,
|
|
301
|
+
expiresAt: Date.now() + tokens.expires_in * 1000
|
|
302
|
+
}, tokens.id_token
|
|
303
|
+
? parseIdToken(tokens.id_token)
|
|
304
|
+
: await fetchUserInfo(tokens.access_token));
|
|
230
305
|
}
|
|
231
306
|
catch (err) {
|
|
232
|
-
setState(
|
|
233
|
-
...
|
|
234
|
-
isLoading: false,
|
|
307
|
+
setState({
|
|
308
|
+
...EMPTY_STATE,
|
|
235
309
|
error: err instanceof Error ? err : new Error('Authentication failed')
|
|
236
|
-
})
|
|
310
|
+
});
|
|
237
311
|
}
|
|
238
312
|
return;
|
|
239
313
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const tokens = JSON.parse(stored);
|
|
245
|
-
console.log('[AuthU SDK] Found tokens, expiresAt:', tokens.expiresAt, 'now:', Date.now(), 'valid:', tokens.expiresAt > Date.now());
|
|
246
|
-
if (tokens.expiresAt > Date.now()) {
|
|
247
|
-
const user = await fetchUserInfo(tokens.accessToken);
|
|
248
|
-
console.log('[AuthU SDK] Tokens valid, user:', user);
|
|
249
|
-
setState({
|
|
250
|
-
isLoading: false,
|
|
251
|
-
isAuthenticated: true,
|
|
252
|
-
user,
|
|
253
|
-
accessToken: tokens.accessToken,
|
|
254
|
-
refreshToken: tokens.refreshToken,
|
|
255
|
-
expiresAt: tokens.expiresAt,
|
|
256
|
-
error: null
|
|
257
|
-
});
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
if (tokens.refreshToken) {
|
|
261
|
-
console.log('[AuthU SDK] Access token expired, attempting refresh...');
|
|
262
|
-
try {
|
|
263
|
-
const response = await fetch(`${buildBaseUrl(config.domain)}/oauth/token`, {
|
|
264
|
-
method: 'POST',
|
|
265
|
-
headers: { 'Content-Type': 'application/json' },
|
|
266
|
-
body: JSON.stringify({
|
|
267
|
-
grant_type: 'refresh_token',
|
|
268
|
-
refresh_token: tokens.refreshToken,
|
|
269
|
-
client_id: config.clientId
|
|
270
|
-
})
|
|
271
|
-
});
|
|
272
|
-
if (response.ok) {
|
|
273
|
-
const newTokens = await response.json();
|
|
274
|
-
const expiresAt = Date.now() + newTokens.expires_in * 1000;
|
|
275
|
-
const user = newTokens.id_token
|
|
276
|
-
? parseIdToken(newTokens.id_token)
|
|
277
|
-
: await fetchUserInfo(newTokens.access_token);
|
|
278
|
-
localStorage.setItem(TOKEN_STORAGE_KEY, JSON.stringify({
|
|
279
|
-
accessToken: newTokens.access_token,
|
|
280
|
-
refreshToken: newTokens.refresh_token,
|
|
281
|
-
expiresAt
|
|
282
|
-
}));
|
|
283
|
-
console.log('[AuthU SDK] Token refresh successful, user:', user);
|
|
284
|
-
setState({
|
|
285
|
-
isLoading: false,
|
|
286
|
-
isAuthenticated: true,
|
|
287
|
-
user,
|
|
288
|
-
accessToken: newTokens.access_token,
|
|
289
|
-
refreshToken: newTokens.refresh_token,
|
|
290
|
-
expiresAt,
|
|
291
|
-
error: null
|
|
292
|
-
});
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
console.log('[AuthU SDK] Token refresh failed, clearing tokens');
|
|
296
|
-
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
297
|
-
}
|
|
298
|
-
catch {
|
|
299
|
-
console.log('[AuthU SDK] Token refresh error, clearing tokens');
|
|
300
|
-
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
catch {
|
|
305
|
-
console.log('[AuthU SDK] Error parsing stored tokens, removing');
|
|
306
|
-
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
307
|
-
}
|
|
314
|
+
if (configRef.current.silentAuth === false ||
|
|
315
|
+
sessionStorage.getItem(SILENT_ATTEMPT_KEY)) {
|
|
316
|
+
setState(EMPTY_STATE);
|
|
317
|
+
return;
|
|
308
318
|
}
|
|
309
|
-
|
|
310
|
-
|
|
319
|
+
sessionStorage.setItem(SILENT_ATTEMPT_KEY, '1');
|
|
320
|
+
sessionStorage.setItem(RETURN_TO_KEY, currentLocation());
|
|
321
|
+
window.location.replace(await buildAuthorizationUrl({ prompt: 'none' }));
|
|
311
322
|
};
|
|
312
|
-
|
|
323
|
+
bootstrap();
|
|
313
324
|
}, [
|
|
325
|
+
buildAuthorizationUrl,
|
|
314
326
|
exchangeCodeForTokensWithVerifier,
|
|
315
327
|
fetchUserInfo,
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
parseIdToken
|
|
328
|
+
parseIdToken,
|
|
329
|
+
setTokens
|
|
319
330
|
]);
|
|
320
331
|
const checkEmailExists = useCallback(async (email) => {
|
|
321
332
|
try {
|
package/dist/pkce.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":"AA2BA,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG7E;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;
|
|
1
|
+
{"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":"AA2BA,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG7E;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAcD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAExD;AAED,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAE/C;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE9C;AAED,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAE9C;AAED,wBAAgB,UAAU,IAAI,IAAI,CAEjC;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE9C;AAED,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAE9C;AAED,wBAAgB,UAAU,IAAI,IAAI,CAEjC"}
|
package/dist/pkce.js
CHANGED
|
@@ -34,6 +34,14 @@ export function generateState() {
|
|
|
34
34
|
export function generateNonce() {
|
|
35
35
|
return generateRandomString(32);
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The verifier, the state and the nonce have to outlive a full navigation to
|
|
39
|
+
* AuthU and back. They stay in localStorage rather than sessionStorage on
|
|
40
|
+
* purpose: 1.0.10 moved them there after sessionStorage lost them across the
|
|
41
|
+
* cross-domain round trip. They are single-use, consumed within seconds, and
|
|
42
|
+
* cleared as soon as the callback is handled - unlike the tokens, which never
|
|
43
|
+
* touch any storage at all.
|
|
44
|
+
*/
|
|
37
45
|
const STORAGE_KEY_VERIFIER = 'authu_code_verifier';
|
|
38
46
|
const STORAGE_KEY_STATE = 'authu_state';
|
|
39
47
|
const STORAGE_KEY_NONCE = 'authu_nonce';
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,13 @@ export interface AuthUConfig {
|
|
|
4
4
|
redirectUri: string;
|
|
5
5
|
scope?: string;
|
|
6
6
|
audience?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Silent session renewal through `/authorize?prompt=none`, on by default.
|
|
9
|
+
* Tokens are held in memory only, so this round trip is what carries a
|
|
10
|
+
* session across a page reload. Turning it off means every reload starts
|
|
11
|
+
* signed out.
|
|
12
|
+
*/
|
|
13
|
+
silentAuth?: boolean;
|
|
7
14
|
}
|
|
8
15
|
export interface AuthUUser {
|
|
9
16
|
sub: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB"}
|