Skip to content

Commit 6a3775b

Browse files
Merge pull request #43 from madurangasiriwardena/fix-default-end-session-endpoint
Derive the end session endpoint from the server's logout path
2 parents 191076d + a658a51 commit 6a3775b

3 files changed

Lines changed: 119 additions & 3 deletions

File tree

‎packages/javascript/src/ThunderIDJavaScriptClient.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import deepMerge from './utils/deepMerge';
4545
import extractPkceStorageKeyFromState from './utils/extractPkceStorageKeyFromState';
4646
import generatePkceStorageKey from './utils/generatePkceStorageKey';
4747
import getAuthorizeRequestUrlParams from './utils/getAuthorizeRequestUrlParams';
48+
import logger from './utils/logger';
4849
import processOpenIDScopes from './utils/processOpenIDScopes';
4950

5051
const WELL_KNOWN_PATH = '/.well-known/openid-configuration';
@@ -358,9 +359,18 @@ class ThunderIDJavaScriptClient<T = Config> implements ThunderIDClient<T> {
358359
try {
359360
response = await fetch(resolvedWellKnownEndpoint);
360361
if (!response.ok || response.status !== 200) {
362+
logger.warn(
363+
`ThunderIDJavaScriptClient: Discovery request to ${resolvedWellKnownEndpoint} returned ` +
364+
`${response.status}; falling back to endpoints derived from the base URL.`,
365+
);
361366
response = undefined;
362367
}
363-
} catch {
368+
} catch (error: unknown) {
369+
logger.warn(
370+
`ThunderIDJavaScriptClient: Discovery request to ${resolvedWellKnownEndpoint} failed; ` +
371+
'falling back to endpoints derived from the base URL.',
372+
error,
373+
);
364374
response = undefined;
365375
}
366376

@@ -371,8 +381,13 @@ class ThunderIDJavaScriptClient<T = Config> implements ThunderIDClient<T> {
371381
await this.authHelper.resolveEndpoints(await response.json()),
372382
);
373383
discoveryResolved = true;
374-
} catch {
384+
} catch (error: unknown) {
375385
// Parsing or endpoint resolution failed; fall through to baseUrl fallback.
386+
logger.warn(
387+
`ThunderIDJavaScriptClient: Could not resolve endpoints from the discovery response of ` +
388+
`${resolvedWellKnownEndpoint}; falling back to endpoints derived from the base URL.`,
389+
error,
390+
);
376391
}
377392

378393
if (!discoveryResolved) {
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
import {describe, expect, it, beforeEach} from 'vitest';
20+
import type {IsomorphicCrypto} from '../IsomorphicCrypto';
21+
import type {AuthClientConfig} from '../models/config';
22+
import type {Storage} from '../models/store';
23+
import StorageManager from '../StorageManager';
24+
import AuthenticationHelper from '../utils/AuthenticationHelper';
25+
26+
class MemoryStore implements Storage {
27+
private store = new Map<string, string>();
28+
29+
getData(key: string): Promise<string> {
30+
return Promise.resolve(this.store.get(key) ?? null!);
31+
}
32+
33+
setData(key: string, value: string): Promise<void> {
34+
this.store.set(key, value);
35+
36+
return Promise.resolve();
37+
}
38+
39+
removeData(key: string): Promise<void> {
40+
this.store.delete(key);
41+
42+
return Promise.resolve();
43+
}
44+
}
45+
46+
const BASE_URL = 'https://localhost:8090';
47+
48+
async function initHelper(config: Partial<AuthClientConfig<unknown>>): Promise<AuthenticationHelper<unknown>> {
49+
const storageManager = new StorageManager<unknown>('test-instance', new MemoryStore());
50+
await storageManager.setConfigData(config);
51+
52+
return new AuthenticationHelper<unknown>(storageManager, {} as unknown as IsomorphicCrypto<unknown>);
53+
}
54+
55+
describe('AuthenticationHelper', () => {
56+
let helper: AuthenticationHelper<unknown>;
57+
58+
beforeEach(async () => {
59+
helper = await initHelper({baseUrl: BASE_URL, clientId: 'test-client'});
60+
});
61+
62+
describe('resolveEndpointsByBaseURL()', () => {
63+
it('derives every endpoint under the base URL', async () => {
64+
const endpoints = await helper.resolveEndpointsByBaseURL();
65+
66+
expect(endpoints.authorization_endpoint).toBe(`${BASE_URL}/oauth2/authorize`);
67+
expect(endpoints.token_endpoint).toBe(`${BASE_URL}/oauth2/token`);
68+
expect(endpoints.jwks_uri).toBe(`${BASE_URL}/oauth2/jwks`);
69+
expect(endpoints.revocation_endpoint).toBe(`${BASE_URL}/oauth2/revoke`);
70+
expect(endpoints.userinfo_endpoint).toBe(`${BASE_URL}/oauth2/userinfo`);
71+
expect(endpoints.issuer).toBe(BASE_URL);
72+
});
73+
74+
it('derives the end session endpoint on the path the server actually serves', async () => {
75+
const endpoints = await helper.resolveEndpointsByBaseURL();
76+
77+
expect(endpoints.end_session_endpoint).toBe(`${BASE_URL}/oauth2/logout`);
78+
});
79+
80+
it('lets explicitly configured endpoints override the derived defaults', async () => {
81+
const configuredHelper = await initHelper({
82+
baseUrl: BASE_URL,
83+
clientId: 'test-client',
84+
endpoints: {endSessionEndpoint: 'https://idp.example.com/custom/logout'},
85+
});
86+
87+
const endpoints = await configuredHelper.resolveEndpointsByBaseURL();
88+
89+
expect(endpoints.end_session_endpoint).toBe('https://idp.example.com/custom/logout');
90+
expect(endpoints.token_endpoint).toBe(`${BASE_URL}/oauth2/token`);
91+
});
92+
93+
it('throws when no base URL is configured', async () => {
94+
const helperWithoutBaseUrl = await initHelper({clientId: 'test-client'});
95+
96+
await expect(helperWithoutBaseUrl.resolveEndpointsByBaseURL()).rejects.toMatchObject({
97+
code: 'JS-AUTH_HELPER_REBO-NF01',
98+
});
99+
});
100+
});
101+
});

‎packages/javascript/src/constants/OIDCDiscoveryConstants.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const OIDCDiscoveryConstants: {
7979
* End session endpoint for logout functionality.
8080
* Used to terminate the user's session and perform logout operations.
8181
*/
82-
END_SESSION: '/oidc/logout',
82+
END_SESSION: '/oauth2/logout',
8383

8484
/**
8585
* Token issuer endpoint.

0 commit comments

Comments
 (0)