diff --git a/packages/javascript/src/ThunderIDJavaScriptClient.ts b/packages/javascript/src/ThunderIDJavaScriptClient.ts index f53eac6..9da6787 100644 --- a/packages/javascript/src/ThunderIDJavaScriptClient.ts +++ b/packages/javascript/src/ThunderIDJavaScriptClient.ts @@ -45,6 +45,7 @@ import deepMerge from './utils/deepMerge'; import extractPkceStorageKeyFromState from './utils/extractPkceStorageKeyFromState'; import generatePkceStorageKey from './utils/generatePkceStorageKey'; import getAuthorizeRequestUrlParams from './utils/getAuthorizeRequestUrlParams'; +import logger from './utils/logger'; import processOpenIDScopes from './utils/processOpenIDScopes'; const WELL_KNOWN_PATH = '/.well-known/openid-configuration'; @@ -358,9 +359,18 @@ class ThunderIDJavaScriptClient implements ThunderIDClient { try { response = await fetch(resolvedWellKnownEndpoint); if (!response.ok || response.status !== 200) { + logger.warn( + `ThunderIDJavaScriptClient: Discovery request to ${resolvedWellKnownEndpoint} returned ` + + `${response.status}; falling back to endpoints derived from the base URL.`, + ); response = undefined; } - } catch { + } catch (error: unknown) { + logger.warn( + `ThunderIDJavaScriptClient: Discovery request to ${resolvedWellKnownEndpoint} failed; ` + + 'falling back to endpoints derived from the base URL.', + error, + ); response = undefined; } @@ -371,8 +381,13 @@ class ThunderIDJavaScriptClient implements ThunderIDClient { await this.authHelper.resolveEndpoints(await response.json()), ); discoveryResolved = true; - } catch { + } catch (error: unknown) { // Parsing or endpoint resolution failed; fall through to baseUrl fallback. + logger.warn( + `ThunderIDJavaScriptClient: Could not resolve endpoints from the discovery response of ` + + `${resolvedWellKnownEndpoint}; falling back to endpoints derived from the base URL.`, + error, + ); } if (!discoveryResolved) { diff --git a/packages/javascript/src/__tests__/AuthenticationHelper.test.ts b/packages/javascript/src/__tests__/AuthenticationHelper.test.ts new file mode 100644 index 0000000..ff040a0 --- /dev/null +++ b/packages/javascript/src/__tests__/AuthenticationHelper.test.ts @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {describe, expect, it, beforeEach} from 'vitest'; +import type {IsomorphicCrypto} from '../IsomorphicCrypto'; +import type {AuthClientConfig} from '../models/config'; +import type {Storage} from '../models/store'; +import StorageManager from '../StorageManager'; +import AuthenticationHelper from '../utils/AuthenticationHelper'; + +class MemoryStore implements Storage { + private store = new Map(); + + getData(key: string): Promise { + return Promise.resolve(this.store.get(key) ?? null!); + } + + setData(key: string, value: string): Promise { + this.store.set(key, value); + + return Promise.resolve(); + } + + removeData(key: string): Promise { + this.store.delete(key); + + return Promise.resolve(); + } +} + +const BASE_URL = 'https://localhost:8090'; + +async function initHelper(config: Partial>): Promise> { + const storageManager = new StorageManager('test-instance', new MemoryStore()); + await storageManager.setConfigData(config); + + return new AuthenticationHelper(storageManager, {} as unknown as IsomorphicCrypto); +} + +describe('AuthenticationHelper', () => { + let helper: AuthenticationHelper; + + beforeEach(async () => { + helper = await initHelper({baseUrl: BASE_URL, clientId: 'test-client'}); + }); + + describe('resolveEndpointsByBaseURL()', () => { + it('derives every endpoint under the base URL', async () => { + const endpoints = await helper.resolveEndpointsByBaseURL(); + + expect(endpoints.authorization_endpoint).toBe(`${BASE_URL}/oauth2/authorize`); + expect(endpoints.token_endpoint).toBe(`${BASE_URL}/oauth2/token`); + expect(endpoints.jwks_uri).toBe(`${BASE_URL}/oauth2/jwks`); + expect(endpoints.revocation_endpoint).toBe(`${BASE_URL}/oauth2/revoke`); + expect(endpoints.userinfo_endpoint).toBe(`${BASE_URL}/oauth2/userinfo`); + expect(endpoints.issuer).toBe(BASE_URL); + }); + + it('derives the end session endpoint on the path the server actually serves', async () => { + const endpoints = await helper.resolveEndpointsByBaseURL(); + + expect(endpoints.end_session_endpoint).toBe(`${BASE_URL}/oauth2/logout`); + }); + + it('lets explicitly configured endpoints override the derived defaults', async () => { + const configuredHelper = await initHelper({ + baseUrl: BASE_URL, + clientId: 'test-client', + endpoints: {endSessionEndpoint: 'https://idp.example.com/custom/logout'}, + }); + + const endpoints = await configuredHelper.resolveEndpointsByBaseURL(); + + expect(endpoints.end_session_endpoint).toBe('https://idp.example.com/custom/logout'); + expect(endpoints.token_endpoint).toBe(`${BASE_URL}/oauth2/token`); + }); + + it('throws when no base URL is configured', async () => { + const helperWithoutBaseUrl = await initHelper({clientId: 'test-client'}); + + await expect(helperWithoutBaseUrl.resolveEndpointsByBaseURL()).rejects.toMatchObject({ + code: 'JS-AUTH_HELPER_REBO-NF01', + }); + }); + }); +}); diff --git a/packages/javascript/src/constants/OIDCDiscoveryConstants.ts b/packages/javascript/src/constants/OIDCDiscoveryConstants.ts index e16e609..2ae3f2f 100644 --- a/packages/javascript/src/constants/OIDCDiscoveryConstants.ts +++ b/packages/javascript/src/constants/OIDCDiscoveryConstants.ts @@ -79,7 +79,7 @@ const OIDCDiscoveryConstants: { * End session endpoint for logout functionality. * Used to terminate the user's session and perform logout operations. */ - END_SESSION: '/oidc/logout', + END_SESSION: '/oauth2/logout', /** * Token issuer endpoint.