@axa-fr/oidc-client 7.27.16 → 7.27.17
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/dist/index.js +154 -130
- package/dist/index.umd.cjs +2 -2
- package/dist/logout.d.ts +14 -0
- package/dist/logout.d.ts.map +1 -1
- package/dist/oidc.d.ts +20 -0
- package/dist/oidc.d.ts.map +1 -1
- package/dist/oidcClient.d.ts +20 -0
- package/dist/oidcClient.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/logout.spec.ts +208 -1
- package/src/logout.ts +149 -80
- package/src/oidc.ts +30 -1
- package/src/oidcClient.ts +26 -0
- package/src/version.ts +1 -1
package/src/logout.spec.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
5
|
+
import { eventNames } from './events';
|
|
5
6
|
import { ILOidcLocation } from './location';
|
|
6
|
-
import { logoutAsync } from './logout';
|
|
7
|
+
import { clearSessionAsync, logoutAsync } from './logout';
|
|
7
8
|
|
|
8
9
|
describe('Logout test suite', () => {
|
|
9
10
|
const expectedFinalUrl =
|
|
@@ -138,4 +139,210 @@ describe('Logout test suite', () => {
|
|
|
138
139
|
expect(finalUrl).toBe(expectedFinalUrl);
|
|
139
140
|
},
|
|
140
141
|
);
|
|
142
|
+
|
|
143
|
+
it('navigates to end_session_endpoint before clearing the local session (issue #1677)', async () => {
|
|
144
|
+
// This is the regression test for the race described in issue #1677.
|
|
145
|
+
// `OidcProvider`/`OidcSecure` watches `oidc.tokens`; if `destroyAsync`
|
|
146
|
+
// runs before `oicLocation.open`, the React tree can briefly observe a
|
|
147
|
+
// null token state and kick off a new auth flow before the navigation
|
|
148
|
+
// to the IdP's end-session endpoint commits.
|
|
149
|
+
const configuration = {
|
|
150
|
+
client_id: 'interactive.public.short',
|
|
151
|
+
authority: 'http://api',
|
|
152
|
+
logout_tokens_to_invalidate: ['access_token', 'refresh_token'],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const callOrder: string[] = [];
|
|
156
|
+
|
|
157
|
+
const mockFetchFn = vi.fn().mockImplementation(() => {
|
|
158
|
+
callOrder.push('revoke');
|
|
159
|
+
return Promise.resolve({ status: 200 });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const oidc: any = {
|
|
163
|
+
configuration,
|
|
164
|
+
tokens: {
|
|
165
|
+
idToken: 'abcd',
|
|
166
|
+
accessToken: 'abcd',
|
|
167
|
+
refreshToken: 'abdc',
|
|
168
|
+
idTokenPayload: { sub: 'sub-123' },
|
|
169
|
+
},
|
|
170
|
+
isLoggingOut: false,
|
|
171
|
+
initAsync: () =>
|
|
172
|
+
Promise.resolve({
|
|
173
|
+
revocationEndpoint: 'http://api/connect/revocation',
|
|
174
|
+
endSessionEndpoint: 'http://api/connect/endsession',
|
|
175
|
+
}),
|
|
176
|
+
destroyAsync: vi.fn().mockImplementation(() => {
|
|
177
|
+
callOrder.push('destroyAsync');
|
|
178
|
+
oidc.tokens = null;
|
|
179
|
+
return Promise.resolve();
|
|
180
|
+
}),
|
|
181
|
+
logoutSameTabAsync: () => Promise.resolve(),
|
|
182
|
+
publishEvent: (name: string) => callOrder.push(`publishEvent:${name}`),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const oidcDatabase = { default: oidc };
|
|
186
|
+
|
|
187
|
+
let navigatedUrl = '';
|
|
188
|
+
class OidcLocationMock implements ILOidcLocation {
|
|
189
|
+
open(url: string): void {
|
|
190
|
+
callOrder.push('open');
|
|
191
|
+
navigatedUrl = url;
|
|
192
|
+
}
|
|
193
|
+
getCurrentHref() {
|
|
194
|
+
return '';
|
|
195
|
+
}
|
|
196
|
+
getPath() {
|
|
197
|
+
return '';
|
|
198
|
+
}
|
|
199
|
+
reload() {
|
|
200
|
+
callOrder.push('reload');
|
|
201
|
+
}
|
|
202
|
+
getOrigin() {
|
|
203
|
+
return 'http://localhost:4200';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await logoutAsync(
|
|
208
|
+
oidc,
|
|
209
|
+
oidcDatabase,
|
|
210
|
+
mockFetchFn,
|
|
211
|
+
console,
|
|
212
|
+
new OidcLocationMock(),
|
|
213
|
+
)('/logged_out', null);
|
|
214
|
+
|
|
215
|
+
// Revocation comes first (so tokens are still valid when revoked),
|
|
216
|
+
// navigation comes second (page starts unloading), and only then we
|
|
217
|
+
// clear local state and broadcast `logout_from_same_tab`.
|
|
218
|
+
expect(callOrder[0]).toBe('revoke');
|
|
219
|
+
expect(callOrder[1]).toBe('revoke');
|
|
220
|
+
expect(callOrder[2]).toBe('open');
|
|
221
|
+
expect(callOrder.indexOf('destroyAsync')).toBeGreaterThan(callOrder.indexOf('open'));
|
|
222
|
+
expect(callOrder.indexOf(`publishEvent:${eventNames.logout_from_same_tab}`)).toBeGreaterThan(
|
|
223
|
+
callOrder.indexOf('open'),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// The id_token_hint must still be present on the navigation URL, even
|
|
227
|
+
// though local tokens have been cleared by `destroyAsync` afterwards.
|
|
228
|
+
expect(navigatedUrl).toContain('id_token_hint=abcd');
|
|
229
|
+
expect(navigatedUrl).toContain(
|
|
230
|
+
'post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Flogged_out',
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// The flag stays set after the call returns: the page is expected to be
|
|
234
|
+
// unloading and any UI re-render that briefly observes `tokens === null`
|
|
235
|
+
// should skip starting a new login flow.
|
|
236
|
+
expect(oidc.isLoggingOut).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('resets isLoggingOut and does not navigate when no_reload is requested', async () => {
|
|
240
|
+
const configuration = {
|
|
241
|
+
client_id: 'interactive.public.short',
|
|
242
|
+
authority: 'http://api',
|
|
243
|
+
logout_tokens_to_invalidate: [],
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const events: string[] = [];
|
|
247
|
+
let navigated = false;
|
|
248
|
+
const oidc: any = {
|
|
249
|
+
configuration,
|
|
250
|
+
tokens: {
|
|
251
|
+
idToken: 'abcd',
|
|
252
|
+
accessToken: 'abcd',
|
|
253
|
+
refreshToken: 'abdc',
|
|
254
|
+
idTokenPayload: { sub: 'sub-123' },
|
|
255
|
+
},
|
|
256
|
+
isLoggingOut: false,
|
|
257
|
+
initAsync: () =>
|
|
258
|
+
Promise.resolve({
|
|
259
|
+
revocationEndpoint: 'http://api/connect/revocation',
|
|
260
|
+
endSessionEndpoint: 'http://api/connect/endsession',
|
|
261
|
+
}),
|
|
262
|
+
destroyAsync: () => Promise.resolve(),
|
|
263
|
+
logoutSameTabAsync: () => Promise.resolve(),
|
|
264
|
+
publishEvent: (name: string) => events.push(name),
|
|
265
|
+
};
|
|
266
|
+
const oidcDatabase = { default: oidc };
|
|
267
|
+
|
|
268
|
+
class OidcLocationMock implements ILOidcLocation {
|
|
269
|
+
open() {
|
|
270
|
+
navigated = true;
|
|
271
|
+
}
|
|
272
|
+
getCurrentHref() {
|
|
273
|
+
return '';
|
|
274
|
+
}
|
|
275
|
+
getPath() {
|
|
276
|
+
return '';
|
|
277
|
+
}
|
|
278
|
+
reload() {
|
|
279
|
+
navigated = true;
|
|
280
|
+
}
|
|
281
|
+
getOrigin() {
|
|
282
|
+
return 'http://localhost:4200';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await logoutAsync(
|
|
287
|
+
oidc,
|
|
288
|
+
oidcDatabase,
|
|
289
|
+
vi.fn(),
|
|
290
|
+
console,
|
|
291
|
+
new OidcLocationMock(),
|
|
292
|
+
)('/logged_out', { 'no_reload:oidc': 'true' });
|
|
293
|
+
|
|
294
|
+
expect(navigated).toBe(false);
|
|
295
|
+
expect(events).toContain(eventNames.logout_from_same_tab);
|
|
296
|
+
expect(oidc.isLoggingOut).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe('clearSessionAsync', () => {
|
|
300
|
+
it('clears the local session and emits logout_from_same_tab without contacting the IdP', async () => {
|
|
301
|
+
const events: { name: string; data: unknown }[] = [];
|
|
302
|
+
const oidc: any = {
|
|
303
|
+
configuration: { client_id: 'interactive.public.short' },
|
|
304
|
+
tokens: {
|
|
305
|
+
idToken: 'abcd',
|
|
306
|
+
accessToken: 'abcd',
|
|
307
|
+
refreshToken: 'abdc',
|
|
308
|
+
idTokenPayload: { sub: 'sub-123' },
|
|
309
|
+
},
|
|
310
|
+
destroyAsync: vi.fn().mockImplementation(status => {
|
|
311
|
+
oidc.tokens = null;
|
|
312
|
+
events.push({ name: 'destroyAsync', data: status });
|
|
313
|
+
return Promise.resolve();
|
|
314
|
+
}),
|
|
315
|
+
logoutSameTabAsync: vi.fn().mockResolvedValue(undefined),
|
|
316
|
+
publishEvent: (name: string, data: unknown) => events.push({ name, data }),
|
|
317
|
+
};
|
|
318
|
+
const oidcDatabase = { default: oidc };
|
|
319
|
+
|
|
320
|
+
await clearSessionAsync(oidc, oidcDatabase)();
|
|
321
|
+
|
|
322
|
+
expect(oidc.destroyAsync).toHaveBeenCalledWith('LOGGED_OUT');
|
|
323
|
+
expect(oidc.tokens).toBeNull();
|
|
324
|
+
expect(events.some(e => e.name === eventNames.logout_from_same_tab)).toBe(true);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('calls logoutSameTabAsync for sibling OIDC clients registered in the same tab', async () => {
|
|
328
|
+
const oidc: any = {
|
|
329
|
+
configuration: { client_id: 'interactive.public.short' },
|
|
330
|
+
tokens: {
|
|
331
|
+
idToken: 'abcd',
|
|
332
|
+
accessToken: 'abcd',
|
|
333
|
+
refreshToken: 'abdc',
|
|
334
|
+
idTokenPayload: { sub: 'sub-123' },
|
|
335
|
+
},
|
|
336
|
+
destroyAsync: () => Promise.resolve(),
|
|
337
|
+
logoutSameTabAsync: vi.fn().mockResolvedValue(undefined),
|
|
338
|
+
publishEvent: () => undefined,
|
|
339
|
+
};
|
|
340
|
+
const sibling: any = { configuration: { client_id: 'other' } };
|
|
341
|
+
const oidcDatabase = { default: oidc, other: sibling };
|
|
342
|
+
|
|
343
|
+
await clearSessionAsync(oidc, oidcDatabase)();
|
|
344
|
+
|
|
345
|
+
expect(oidc.logoutSameTabAsync).toHaveBeenCalledWith('interactive.public.short', 'sub-123');
|
|
346
|
+
});
|
|
347
|
+
});
|
|
141
348
|
});
|
package/src/logout.ts
CHANGED
|
@@ -59,6 +59,58 @@ export const destroyAsync = oidc => async status => {
|
|
|
59
59
|
oidc.userInfo = null;
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Clears the local OIDC session (tokens, user info, service-worker storage)
|
|
64
|
+
* and broadcasts `logout_from_same_tab` to any other OIDC clients registered
|
|
65
|
+
* in the same tab.
|
|
66
|
+
*
|
|
67
|
+
* It is intentionally decoupled from `logoutAsync`: callers that want to drop
|
|
68
|
+
* the local session without contacting the identity provider — for example a
|
|
69
|
+
* service-worker-only flow, a SPA-only logout, or an error-recovery path —
|
|
70
|
+
* can use this helper directly. `logoutAsync` itself calls it as the very
|
|
71
|
+
* last step, after the browser navigation to `end_session_endpoint` has been
|
|
72
|
+
* scheduled, so that the React tree never observes a transient "no tokens"
|
|
73
|
+
* state before the page is unloaded.
|
|
74
|
+
*/
|
|
75
|
+
export const clearSessionAsync = (oidc, oidcDatabase) => async () => {
|
|
76
|
+
const sub = oidc.tokens?.idTokenPayload?.sub ?? null;
|
|
77
|
+
await oidc.destroyAsync('LOGGED_OUT');
|
|
78
|
+
for (const [, itemOidc] of Object.entries(oidcDatabase)) {
|
|
79
|
+
if (itemOidc !== oidc) {
|
|
80
|
+
// @ts-ignore
|
|
81
|
+
await oidc.logoutSameTabAsync(oidc.configuration.client_id, sub);
|
|
82
|
+
} else {
|
|
83
|
+
oidc.publishEvent(eventNames.logout_from_same_tab, {});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const buildEndSessionUrl = (
|
|
89
|
+
endSessionEndpoint: string,
|
|
90
|
+
endPointExtras: StringMap,
|
|
91
|
+
idToken: string,
|
|
92
|
+
postLogoutRedirectUri: string | null,
|
|
93
|
+
): string => {
|
|
94
|
+
if (!('id_token_hint' in endPointExtras)) {
|
|
95
|
+
endPointExtras['id_token_hint'] = idToken;
|
|
96
|
+
}
|
|
97
|
+
if (!('post_logout_redirect_uri' in endPointExtras) && postLogoutRedirectUri !== null) {
|
|
98
|
+
endPointExtras['post_logout_redirect_uri'] = postLogoutRedirectUri;
|
|
99
|
+
}
|
|
100
|
+
let queryString = '';
|
|
101
|
+
for (const [key, value] of Object.entries(endPointExtras)) {
|
|
102
|
+
if (value !== null && value !== undefined) {
|
|
103
|
+
if (queryString === '') {
|
|
104
|
+
queryString += '?';
|
|
105
|
+
} else {
|
|
106
|
+
queryString += '&';
|
|
107
|
+
}
|
|
108
|
+
queryString += `${key}=${encodeURIComponent(value)}`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return `${endSessionEndpoint}${queryString}`;
|
|
112
|
+
};
|
|
113
|
+
|
|
62
114
|
export const logoutAsync =
|
|
63
115
|
(oidc, oidcDatabase, fetch, console, oicLocation: ILOidcLocation) =>
|
|
64
116
|
async (callbackPathOrUrl: string | null | undefined = undefined, extras: StringMap = null) => {
|
|
@@ -79,94 +131,111 @@ export const logoutAsync =
|
|
|
79
131
|
if (callbackPathOrUrl) {
|
|
80
132
|
isUri = callbackPathOrUrl.includes('https://') || callbackPathOrUrl.includes('http://');
|
|
81
133
|
}
|
|
82
|
-
const
|
|
134
|
+
const postLogoutRedirectUri =
|
|
135
|
+
callbackPathOrUrl === null
|
|
136
|
+
? null
|
|
137
|
+
: isUri
|
|
138
|
+
? callbackPathOrUrl
|
|
139
|
+
: oicLocation.getOrigin() + path;
|
|
140
|
+
// Capture identifiers from the live session *before* any clear happens, so the
|
|
141
|
+
// values stay valid no matter when we drop local state.
|
|
83
142
|
// @ts-ignore
|
|
84
143
|
const idToken = oidc.tokens ? oidc.tokens.idToken : '';
|
|
144
|
+
|
|
145
|
+
// Mark the instance as "logout in progress" so consumers (OidcSecure, route
|
|
146
|
+
// guards, silent renew, 401 retry interceptors, …) can back off from
|
|
147
|
+
// triggering a new auth flow during the window between us clearing the
|
|
148
|
+
// local session and the browser actually navigating away.
|
|
149
|
+
oidc.isLoggingOut = true;
|
|
150
|
+
|
|
85
151
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
152
|
+
try {
|
|
153
|
+
const revocationEndpoint = oidcServerConfiguration.revocationEndpoint;
|
|
154
|
+
if (revocationEndpoint) {
|
|
155
|
+
const promises = [];
|
|
156
|
+
const accessToken = oidc.tokens ? oidc.tokens.accessToken : null;
|
|
157
|
+
if (
|
|
158
|
+
accessToken &&
|
|
159
|
+
configuration.logout_tokens_to_invalidate.includes(oidcLogoutTokens.access_token)
|
|
160
|
+
) {
|
|
161
|
+
const revokeAccessTokenExtras = extractExtras(extras, ':revoke_access_token');
|
|
162
|
+
const revokeAccessTokenPromise = performRevocationRequestAsync(fetch)(
|
|
163
|
+
revocationEndpoint,
|
|
164
|
+
accessToken,
|
|
165
|
+
TOKEN_TYPE.access_token,
|
|
166
|
+
configuration.client_id,
|
|
167
|
+
revokeAccessTokenExtras,
|
|
168
|
+
);
|
|
169
|
+
promises.push(revokeAccessTokenPromise);
|
|
170
|
+
}
|
|
171
|
+
const refreshToken = oidc.tokens ? oidc.tokens.refreshToken : null;
|
|
172
|
+
if (
|
|
173
|
+
refreshToken &&
|
|
174
|
+
configuration.logout_tokens_to_invalidate.includes(oidcLogoutTokens.refresh_token)
|
|
175
|
+
) {
|
|
176
|
+
const revokeAccessTokenExtras = extractExtras(extras, ':revoke_refresh_token');
|
|
177
|
+
const revokeRefreshTokenPromise = performRevocationRequestAsync(fetch)(
|
|
178
|
+
revocationEndpoint,
|
|
179
|
+
refreshToken,
|
|
180
|
+
TOKEN_TYPE.refresh_token,
|
|
181
|
+
configuration.client_id,
|
|
182
|
+
revokeAccessTokenExtras,
|
|
183
|
+
);
|
|
184
|
+
promises.push(revokeRefreshTokenPromise);
|
|
185
|
+
}
|
|
186
|
+
if (promises.length > 0) {
|
|
187
|
+
// Revocation must be awaited *before* navigation, so a cancelled
|
|
188
|
+
// navigation can never leave valid tokens behind both in storage
|
|
189
|
+
// and on the authorization server.
|
|
190
|
+
await Promise.all(promises);
|
|
191
|
+
}
|
|
121
192
|
}
|
|
193
|
+
} catch (exception) {
|
|
194
|
+
console.warn(
|
|
195
|
+
'logoutAsync: error when revoking tokens, if the error persist, you ay configure property logout_tokens_to_invalidate from configuration to avoid this error',
|
|
196
|
+
);
|
|
197
|
+
console.warn(exception);
|
|
122
198
|
}
|
|
123
|
-
} catch (exception) {
|
|
124
|
-
console.warn(
|
|
125
|
-
'logoutAsync: error when revoking tokens, if the error persist, you ay configure property logout_tokens_to_invalidate from configuration to avoid this error',
|
|
126
|
-
);
|
|
127
|
-
console.warn(exception);
|
|
128
|
-
}
|
|
129
|
-
const sub = oidc.tokens?.idTokenPayload?.sub ?? null;
|
|
130
|
-
|
|
131
|
-
await oidc.destroyAsync('LOGGED_OUT');
|
|
132
|
-
for (const [, itemOidc] of Object.entries(oidcDatabase)) {
|
|
133
|
-
if (itemOidc !== oidc) {
|
|
134
|
-
// @ts-ignore
|
|
135
|
-
await oidc.logoutSameTabAsync(oidc.configuration.client_id, sub);
|
|
136
|
-
} else {
|
|
137
|
-
oidc.publishEvent(eventNames.logout_from_same_tab, {});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const oidcExtras = extractExtras(extras, ':oidc');
|
|
142
|
-
const noReload = oidcExtras && oidcExtras['no_reload'] === 'true';
|
|
143
|
-
|
|
144
|
-
if (noReload) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
199
|
|
|
148
|
-
|
|
200
|
+
const oidcExtras = extractExtras(extras, ':oidc');
|
|
201
|
+
const noReload = oidcExtras && oidcExtras['no_reload'] === 'true';
|
|
149
202
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
203
|
+
if (noReload) {
|
|
204
|
+
// No navigation happens here: this branch is essentially a "clear local
|
|
205
|
+
// session" call dressed as a logout. We can drop state immediately and
|
|
206
|
+
// reset the flag since the call returns normally to the caller.
|
|
207
|
+
await clearSessionAsync(oidc, oidcDatabase)();
|
|
208
|
+
oidc.isLoggingOut = false;
|
|
209
|
+
return;
|
|
156
210
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
211
|
+
|
|
212
|
+
// Navigate to the end-session endpoint (or reload) *before* clearing the
|
|
213
|
+
// local session. This closes the race where `OidcProvider` /
|
|
214
|
+
// `OidcSecure` / silent-renew timers observe a null `tokens` and kick
|
|
215
|
+
// off a new auth flow in the window between local clear and navigation.
|
|
216
|
+
const endPointExtras = keepExtras(extras);
|
|
217
|
+
if (oidcServerConfiguration.endSessionEndpoint) {
|
|
218
|
+
const endSessionUrl = buildEndSessionUrl(
|
|
219
|
+
oidcServerConfiguration.endSessionEndpoint,
|
|
220
|
+
endPointExtras,
|
|
221
|
+
idToken,
|
|
222
|
+
postLogoutRedirectUri,
|
|
223
|
+
);
|
|
224
|
+
oicLocation.open(endSessionUrl);
|
|
225
|
+
} else {
|
|
226
|
+
oicLocation.reload();
|
|
167
227
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
228
|
+
|
|
229
|
+
// Now that navigation has been scheduled, drop the local session. By the
|
|
230
|
+
// time React re-renders against the null tokens the page is already
|
|
231
|
+
// unloading; if for any reason it is not (e.g. navigation cancelled by a
|
|
232
|
+
// `beforeunload` handler) the `isLoggingOut` flag stays set so guards
|
|
233
|
+
// still know not to start a fresh auth flow.
|
|
234
|
+
await clearSessionAsync(oidc, oidcDatabase)();
|
|
235
|
+
} catch (exception) {
|
|
236
|
+
// If anything went wrong, reset the flag so the app is not stuck in a
|
|
237
|
+
// "logging out forever" state.
|
|
238
|
+
oidc.isLoggingOut = false;
|
|
239
|
+
throw exception;
|
|
171
240
|
}
|
|
172
241
|
};
|
package/src/oidc.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { tryKeepSessionAsync } from './keepSession';
|
|
13
13
|
import { ILOidcLocation, OidcLocation } from './location';
|
|
14
14
|
import { defaultLoginAsync, loginCallbackAsync } from './login.js';
|
|
15
|
-
import { destroyAsync, logoutAsync } from './logout.js';
|
|
15
|
+
import { clearSessionAsync, destroyAsync, logoutAsync } from './logout.js';
|
|
16
16
|
import { TokenRenewMode, Tokens } from './parseTokens.js';
|
|
17
17
|
import { autoRenewTokens, renewTokensAndStartTimerAsync } from './renewTokens.js';
|
|
18
18
|
import { fetchFromIssuer } from './requests.js';
|
|
@@ -99,6 +99,14 @@ export class Oidc {
|
|
|
99
99
|
public checkSessionIFrame: CheckSessionIFrame;
|
|
100
100
|
public getFetch: () => Fetch;
|
|
101
101
|
public location: ILOidcLocation;
|
|
102
|
+
/**
|
|
103
|
+
* `true` while {@link logoutAsync} is executing or has scheduled a
|
|
104
|
+
* navigation to the identity provider's end-session endpoint that has not
|
|
105
|
+
* yet committed. Consumers (UI guards, silent-renew handlers, 401 retry
|
|
106
|
+
* interceptors, …) should check this flag and skip starting a new auth
|
|
107
|
+
* flow when it is set, even if `tokens` is null.
|
|
108
|
+
*/
|
|
109
|
+
public isLoggingOut = false;
|
|
102
110
|
constructor(
|
|
103
111
|
configuration: OidcConfiguration,
|
|
104
112
|
configurationName = 'default',
|
|
@@ -477,6 +485,27 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
477
485
|
return await destroyAsync(this)(status);
|
|
478
486
|
}
|
|
479
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Drops the local OIDC session (tokens, user info, service-worker storage)
|
|
490
|
+
* and broadcasts `logout_from_same_tab`, without contacting the identity
|
|
491
|
+
* provider's `end_session_endpoint` and without revoking tokens.
|
|
492
|
+
*
|
|
493
|
+
* Use this for SPA-only logouts, service-worker-only flows, or
|
|
494
|
+
* error-recovery paths where a full IdP logout is not needed or not
|
|
495
|
+
* desirable. For a standard OIDC RP-initiated logout use
|
|
496
|
+
* {@link logoutAsync} instead.
|
|
497
|
+
*/
|
|
498
|
+
clearSessionPromise: Promise<void> = null;
|
|
499
|
+
async clearSessionAsync(): Promise<void> {
|
|
500
|
+
if (this.clearSessionPromise) {
|
|
501
|
+
return this.clearSessionPromise;
|
|
502
|
+
}
|
|
503
|
+
this.clearSessionPromise = clearSessionAsync(this, oidcDatabase)();
|
|
504
|
+
return this.clearSessionPromise.finally(() => {
|
|
505
|
+
this.clearSessionPromise = null;
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
480
509
|
async logoutSameTabAsync(clientId: string, sub: any) {
|
|
481
510
|
// @ts-ignore
|
|
482
511
|
if (
|
package/src/oidcClient.ts
CHANGED
|
@@ -84,6 +84,32 @@ export class OidcClient {
|
|
|
84
84
|
return this._oidc.logoutAsync(callbackPathOrUrl, extras);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Drops the local OIDC session (tokens, user info, service-worker storage)
|
|
89
|
+
* and notifies same-tab listeners via the `logout_from_same_tab` event,
|
|
90
|
+
* without contacting the identity provider's `end_session_endpoint` and
|
|
91
|
+
* without revoking tokens.
|
|
92
|
+
*
|
|
93
|
+
* Use this for SPA-only logouts, service-worker-only flows, or
|
|
94
|
+
* error-recovery paths. For a standard OIDC RP-initiated logout (with
|
|
95
|
+
* token revocation and navigation to the IdP's end-session endpoint) use
|
|
96
|
+
* {@link logoutAsync} instead.
|
|
97
|
+
*/
|
|
98
|
+
clearSessionAsync(): Promise<void> {
|
|
99
|
+
return this._oidc.clearSessionAsync();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* `true` while a logout flow is in progress: between the moment
|
|
104
|
+
* {@link logoutAsync} starts and the moment the browser navigates away to
|
|
105
|
+
* the identity provider's end-session endpoint. UI guards and silent-renew
|
|
106
|
+
* handlers should check this flag to avoid kicking off a new auth flow
|
|
107
|
+
* during that window.
|
|
108
|
+
*/
|
|
109
|
+
get isLoggingOut(): boolean {
|
|
110
|
+
return this._oidc.isLoggingOut === true;
|
|
111
|
+
}
|
|
112
|
+
|
|
87
113
|
silentLoginCallbackAsync(): Promise<void> {
|
|
88
114
|
return this._oidc.silentLoginCallbackAsync();
|
|
89
115
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default '7.27.
|
|
1
|
+
export default '7.27.17';
|