Skip to content

Commit be134fd

Browse files
committed
feat(oidc-client): add no redirect support for response type none
1 parent af2e859 commit be134fd

10 files changed

Lines changed: 459 additions & 51 deletions

File tree

e2e/oidc-app/src/ping-am/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ <h1>OIDC App | PingAM Login</h1>
2121
<button id="logout">Logout</button>
2222
<button id="user-info-btn">User Info</button>
2323
<button id="revoke">Revoke Token</button>
24-
<button id="session-check-btn">Session Check (none)</button>
24+
<button id="session-check-btn">Session Check (none, iframe)</button>
25+
<button id="session-check-no-redirect-btn">Session Check (none, no redirect)</button>
2526
<button id="session-check-id-token-btn">Session Check (id_token)</button>
2627
<a href="/ping-am/">Start Over</a>
2728
</div>

e2e/oidc-app/src/ping-one/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ <h1>OIDC App | P1 Login</h1>
2121
<button id="logout">Logout</button>
2222
<button id="user-info-btn">User Info</button>
2323
<button id="revoke">Revoke Token</button>
24-
<button id="session-check-btn">Session Check (none)</button>
24+
<button id="session-check-btn">Session Check (none, iframe)</button>
25+
<button id="session-check-no-redirect-btn">Session Check (none, no redirect)</button>
2526
<button id="session-check-id-token-btn">Session Check (id_token)</button>
2627
<a href="/ping-one/">Start Over</a>
2728
</div>

e2e/oidc-app/src/utils/oidc-app.ts

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,14 @@ let tokenIndex = 0;
2121

2222
function displayError(error: unknown) {
2323
const errorEl = document.createElement('div');
24-
errorEl.innerHTML = `<p><strong>Error:</strong> <span class="error">${JSON.stringify(error, null, 2)}</span></p>`;
24+
const p = document.createElement('p');
25+
const strong = document.createElement('strong');
26+
strong.textContent = 'Error:';
27+
const span = document.createElement('span');
28+
span.className = 'error';
29+
span.textContent = JSON.stringify(error, null, 2);
30+
p.append(strong, document.createTextNode(' '), span);
31+
errorEl.appendChild(p);
2532
document.body.appendChild(errorEl);
2633
}
2734

@@ -53,7 +60,14 @@ function displayTokenResponse(
5360
}
5461

5562
const tokenInfoEl = document.createElement('div');
56-
tokenInfoEl.innerHTML = `<p><strong>Access Token:</strong> <span id="accessToken-${tokenIndex}">${response.accessToken}</span></p>`;
63+
const tokenP = document.createElement('p');
64+
const tokenStrong = document.createElement('strong');
65+
tokenStrong.textContent = 'Access Token:';
66+
const tokenSpan = document.createElement('span');
67+
tokenSpan.id = `accessToken-${tokenIndex}`;
68+
tokenSpan.textContent = response.accessToken;
69+
tokenP.append(tokenStrong, document.createTextNode(' '), tokenSpan);
70+
tokenInfoEl.appendChild(tokenP);
5771
appEl?.appendChild(tokenInfoEl);
5872
tokenIndex++;
5973
}
@@ -162,7 +176,14 @@ export async function oidcApp({
162176

163177
const appEl = document.getElementById('app');
164178
const userInfoEl = document.createElement('div');
165-
userInfoEl.innerHTML = `<p><strong>User Info:</strong> <span id="userInfo">${JSON.stringify(userInfo, null, 2)}</span></p>`;
179+
const userInfoP = document.createElement('p');
180+
const userInfoStrong = document.createElement('strong');
181+
userInfoStrong.textContent = 'User Info:';
182+
const userInfoSpan = document.createElement('span');
183+
userInfoSpan.id = 'userInfo';
184+
userInfoSpan.textContent = JSON.stringify(userInfo, null, 2);
185+
userInfoP.append(userInfoStrong, document.createTextNode(' '), userInfoSpan);
186+
userInfoEl.appendChild(userInfoP);
166187
appEl?.appendChild(userInfoEl);
167188
}
168189
});
@@ -178,7 +199,9 @@ export async function oidcApp({
178199
} else {
179200
const appEl = document.getElementById('app');
180201
const revokeEl = document.createElement('div');
181-
revokeEl.innerHTML = `<p>Token successfully revoked</p>`;
202+
const revokeP = document.createElement('p');
203+
revokeP.textContent = 'Token successfully revoked';
204+
revokeEl.appendChild(revokeP);
182205
appEl?.appendChild(revokeEl);
183206
}
184207
});
@@ -218,7 +241,30 @@ export async function oidcApp({
218241
const result = await oidcClient.user?.session();
219242
const appEl = document.getElementById('app');
220243
const el = document.createElement('div');
221-
el.innerHTML = `<p><strong>Session Check (none):</strong></p><pre id="session-check-result">${JSON.stringify(result, null, 2)}</pre>`;
244+
const title = document.createElement('p');
245+
const titleStrong = document.createElement('strong');
246+
titleStrong.textContent = 'Session Check (none, iframe):';
247+
title.appendChild(titleStrong);
248+
const pre = document.createElement('pre');
249+
pre.id = 'session-check-result';
250+
pre.textContent = JSON.stringify(result, null, 2);
251+
el.append(title, pre);
252+
appEl?.appendChild(el);
253+
});
254+
255+
document.getElementById('session-check-no-redirect-btn')?.addEventListener('click', async () => {
256+
const options: SessionCheckOptions = { redirectUri: '' };
257+
const result = await oidcClient.user?.session(options);
258+
const appEl = document.getElementById('app');
259+
const el = document.createElement('div');
260+
const title = document.createElement('p');
261+
const titleStrong = document.createElement('strong');
262+
titleStrong.textContent = 'Session Check (none, no redirect):';
263+
title.appendChild(titleStrong);
264+
const pre = document.createElement('pre');
265+
pre.id = 'session-check-no-redirect-result';
266+
pre.textContent = JSON.stringify(result, null, 2);
267+
el.append(title, pre);
222268
appEl?.appendChild(el);
223269
});
224270

@@ -227,7 +273,14 @@ export async function oidcApp({
227273
const result = await oidcClient.user?.session(options);
228274
const appEl = document.getElementById('app');
229275
const el = document.createElement('div');
230-
el.innerHTML = `<p><strong>Session Check (id_token):</strong></p><pre id="session-check-id-token-result">${JSON.stringify(result, null, 2)}</pre>`;
276+
const title = document.createElement('p');
277+
const titleStrong = document.createElement('strong');
278+
titleStrong.textContent = 'Session Check (id_token):';
279+
title.appendChild(titleStrong);
280+
const pre = document.createElement('pre');
281+
pre.id = 'session-check-id-token-result';
282+
pre.textContent = JSON.stringify(result, null, 2);
283+
el.append(title, pre);
231284
appEl?.appendChild(el);
232285
});
233286

packages/oidc-client/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,53 @@ Logs out the user by revoking tokens and clearing the storage. Uses the end sess
180180
const logoutResponse = await oidcClient.user.logout();
181181
```
182182

183+
#### `user.session(options?)`
184+
185+
Checks whether the user has an active session at the AS without prompting for login (`prompt=none`).
186+
187+
- **Parameters**: `SessionCheckOptions` (optional)
188+
- **Returns**: `Promise<SessionCheckSuccess | GenericError>`
189+
190+
```js
191+
const session = await oidcClient.user.session();
192+
if ('error' in session) {
193+
// No active session
194+
}
195+
```
196+
197+
##### `responseType: 'none'` (default)
198+
199+
Use when you only need to know if a session is alive — no claims are returned.
200+
201+
Requires a stored `id_token` to send as `id_token_hint`. Fails immediately with `no_id_token_hint` if storage is empty.
202+
203+
How the check runs depends on `redirectUri`:
204+
205+
- **With `redirectUri`** (iframe): a hidden iframe loads the authorization URL. The AS redirects to `redirectUri` on success or appends error params on failure. The `redirectUri` must be same-origin — the browser blocks cross-origin iframe content access.
206+
- **Without `redirectUri`** (fetch): a plain GET to the authorization endpoint — no iframe. The AS returns `204` on success or `400` on failure. Use this when no same-origin callback page is available.
207+
208+
Returns `{ mode: 'none' }` on success.
209+
210+
##### `responseType: 'id_token'`
211+
212+
Use when you need the user's claims back from the session. The AS issues a fresh `id_token` whose decoded claims are returned on success.
213+
214+
Always uses the iframe path — `redirectUri` is required. A stored `id_token` is sent as `id_token_hint` if available but is not required.
215+
216+
`state` and `nonce` are validated before claims are returned. If `subject` is provided, the `sub` claim must also match — otherwise any active session's claims are returned.
217+
218+
```js
219+
const session = await oidcClient.user.session({
220+
responseType: 'id_token',
221+
subject: knownUserId, // optional — omit to get claims for whoever is logged in
222+
});
223+
if (!('error' in session)) {
224+
console.log(session.claims); // JWTPayload
225+
}
226+
```
227+
228+
Returns `{ mode: 'id_token', claims: JWTPayload }` on success.
229+
183230
## Usage Examples
184231

185232
### Redirect-Based Login (`authorize.url()`)

packages/oidc-client/api-report/oidc-client.api.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ responseType: SessionCheckResponseType;
135135
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, {
136136
params: Record<string, string>;
137137
}, "oidc", unknown>;
138+
sessionCheckFetch: MutationDefinition< {
139+
url: string;
140+
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, {
141+
status: 204;
142+
}, "oidc", unknown>;
138143
authorizeIframe: MutationDefinition< {
139144
url: string;
140145
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, AuthorizationSuccess, "oidc", unknown>;
@@ -177,6 +182,11 @@ responseType: SessionCheckResponseType;
177182
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, {
178183
params: Record<string, string>;
179184
}, "oidc", unknown>;
185+
sessionCheckFetch: MutationDefinition< {
186+
url: string;
187+
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, {
188+
status: 204;
189+
}, "oidc", unknown>;
180190
authorizeIframe: MutationDefinition< {
181191
url: string;
182192
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, AuthorizationSuccess, "oidc", unknown>;

packages/oidc-client/api-report/oidc-client.types.api.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ responseType: SessionCheckResponseType;
135135
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, {
136136
params: Record<string, string>;
137137
}, "oidc", unknown>;
138+
sessionCheckFetch: MutationDefinition< {
139+
url: string;
140+
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, {
141+
status: 204;
142+
}, "oidc", unknown>;
138143
authorizeIframe: MutationDefinition< {
139144
url: string;
140145
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, AuthorizationSuccess, "oidc", unknown>;
@@ -177,6 +182,11 @@ responseType: SessionCheckResponseType;
177182
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, {
178183
params: Record<string, string>;
179184
}, "oidc", unknown>;
185+
sessionCheckFetch: MutationDefinition< {
186+
url: string;
187+
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, {
188+
status: 204;
189+
}, "oidc", unknown>;
180190
authorizeIframe: MutationDefinition< {
181191
url: string;
182192
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, AuthorizationSuccess, "oidc", unknown>;

packages/oidc-client/src/lib/client.store.test.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -734,9 +734,8 @@ describe('user.session()', async () => {
734734
customStorage.remove(storageKey);
735735
});
736736

737-
it('returns a GenericError when no tokens are stored (best-effort: dispatch is attempted)', async () => {
738-
// No tokens in storage — best-effort: id_token_hint is omitted, dispatch still runs.
739-
// In Vitest (no real DOM), the iframe manager cannot run and surfaces a session_check_error.
737+
it('returns a GenericError when no tokens are stored', async () => {
738+
// response_type=none requires a stored id_token; failing before dispatch.
740739
const oidcClient = await oidc({ config, storage: customStorageConfig });
741740
if ('error' in oidcClient) throw new Error('Error creating OIDC Client');
742741

@@ -745,8 +744,8 @@ describe('user.session()', async () => {
745744
if (!('error' in result)) {
746745
expect.fail('Expected SessionCheckError, got success');
747746
}
748-
expect(result.error).toBe('session_check_error');
749-
expect(result.type).toBe('network_error');
747+
expect(result.error).toBe('no_id_token_hint');
748+
expect(result.type).toBe('argument_error');
750749
});
751750

752751
it('returns wellknown_error when authorization_endpoint is missing from the wellknown config', async () => {

packages/oidc-client/src/lib/oidc.api.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ export const oidcApi = createApi({
192192

193193
const request: FetchArgs = { url };
194194

195-
logger.debug('OIDC session check iframe request', request);
195+
logger.debug('OIDC session check iframe request', { responseType });
196196

197197
const response = await initQuery(request, 'authorize')
198198
.applyMiddleware(requestMiddleware)
@@ -265,14 +265,82 @@ export const oidcApi = createApi({
265265
>;
266266
}
267267

268-
logger.debug('OIDC session check iframe response', response);
268+
logger.debug('OIDC session check iframe response received');
269269
return response as QueryReturnValue<
270270
{ params: Record<string, string> },
271271
FetchBaseQueryError,
272272
FetchBaseQueryMeta
273273
>;
274274
},
275275
}),
276+
sessionCheckFetch: builder.mutation<{ status: 204 }, { url: string }>({
277+
queryFn: async ({ url }, api, _, baseQuery) => {
278+
const { requestMiddleware, logger } = api.extra as Extras;
279+
280+
const request: FetchArgs = {
281+
url,
282+
method: 'GET',
283+
credentials: 'include',
284+
headers: {
285+
Accept: 'application/json',
286+
},
287+
};
288+
289+
logger.debug('OIDC session check fetch request');
290+
291+
const response = await initQuery(request, 'authorize')
292+
.applyMiddleware(requestMiddleware)
293+
.applyQuery(async (req: FetchArgs) => await baseQuery(req));
294+
295+
if (response.error) {
296+
const responseError = response.error;
297+
const isNetworkError = typeof responseError.status === 'string';
298+
const errorData = responseError.data;
299+
const message =
300+
!isNetworkError &&
301+
errorData &&
302+
typeof errorData === 'object' &&
303+
'error_description' in errorData &&
304+
typeof errorData.error_description === 'string'
305+
? errorData.error_description
306+
: 'The request requires login.';
307+
308+
logger.error('Error in session check fetch', responseError);
309+
310+
return {
311+
error: {
312+
status: 400,
313+
statusText: 'SESSION_CHECK_ERROR',
314+
data: {
315+
error: isNetworkError ? 'session_check_error' : 'login_required',
316+
message,
317+
type: isNetworkError ? 'network_error' : 'auth_error',
318+
} satisfies GenericError,
319+
} as FetchBaseQueryError,
320+
};
321+
}
322+
323+
if (response.meta?.response?.status !== 204) {
324+
logger.error('Unexpected status in session check fetch', {
325+
status: response.meta?.response?.status,
326+
});
327+
return {
328+
error: {
329+
status: 'CUSTOM_ERROR',
330+
error: 'SESSION_CHECK_ERROR',
331+
data: {
332+
error: 'session_check_error',
333+
message: `Unexpected response status: ${response.meta?.response?.status}`,
334+
type: 'network_error',
335+
} satisfies GenericError,
336+
} as FetchBaseQueryError,
337+
};
338+
}
339+
340+
logger.debug('OIDC session check fetch response received');
341+
return { data: { status: 204 } };
342+
},
343+
}),
276344
authorizeIframe: builder.mutation<AuthorizationSuccess, { url: string }>({
277345
queryFn: async ({ url }, api) => {
278346
const { requestMiddleware, logger } = api.extra as Extras;

0 commit comments

Comments
 (0)