Skip to content

Commit 1eda94e

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

10 files changed

Lines changed: 361 additions & 45 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: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,16 @@ export async function oidcApp({
218218
const result = await oidcClient.user?.session();
219219
const appEl = document.getElementById('app');
220220
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>`;
221+
el.innerHTML = `<p><strong>Session Check (none, iframe):</strong></p><pre id="session-check-result">${JSON.stringify(result, null, 2)}</pre>`;
222+
appEl?.appendChild(el);
223+
});
224+
225+
document.getElementById('session-check-no-redirect-btn')?.addEventListener('click', async () => {
226+
const options: SessionCheckOptions = { redirectUri: '' };
227+
const result = await oidcClient.user?.session(options);
228+
const appEl = document.getElementById('app');
229+
const el = document.createElement('div');
230+
el.innerHTML = `<p><strong>Session Check (none, no redirect):</strong></p><pre id="session-check-no-redirect-result">${JSON.stringify(result, null, 2)}</pre>`;
222231
appEl?.appendChild(el);
223232
});
224233

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: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,74 @@ export const oidcApi = createApi({
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', 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', response);
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)