Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions packages/javascript/src/ThunderIDJavaScriptClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -358,9 +359,18 @@ class ThunderIDJavaScriptClient<T = Config> implements ThunderIDClient<T> {
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,
);
Comment on lines +362 to +373

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not promise a base-URL fallback when baseUrl is absent.

These warnings always say “falling back,” but the subsequent branches throw JS-AUTH_CORE-GOPMD-HE01 when no baseUrl is configured. Make the message conditional or state that a fallback will be attempted only when available.

Also applies to: 384-390

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/javascript/src/ThunderIDJavaScriptClient.ts` around lines 362 - 373,
Update the warning messages in the discovery response-status and catch branches
of ThunderIDJavaScriptClient so they do not unconditionally promise a base-URL
fallback. Make the wording conditional on baseUrl being configured, or state
that fallback will be attempted only when available, while preserving the
existing fallback and error behavior.

response = undefined;
}

Expand All @@ -371,8 +381,13 @@ class ThunderIDJavaScriptClient<T = Config> implements ThunderIDClient<T> {
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) {
Expand Down
101 changes: 101 additions & 0 deletions packages/javascript/src/__tests__/AuthenticationHelper.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>();

getData(key: string): Promise<string> {
return Promise.resolve(this.store.get(key) ?? null!);
}

setData(key: string, value: string): Promise<void> {
this.store.set(key, value);

return Promise.resolve();
}

removeData(key: string): Promise<void> {
this.store.delete(key);

return Promise.resolve();
}
}

const BASE_URL = 'https://localhost:8090';

async function initHelper(config: Partial<AuthClientConfig<unknown>>): Promise<AuthenticationHelper<unknown>> {
const storageManager = new StorageManager<unknown>('test-instance', new MemoryStore());
await storageManager.setConfigData(config);

return new AuthenticationHelper<unknown>(storageManager, {} as unknown as IsomorphicCrypto<unknown>);
}

describe('AuthenticationHelper', () => {
let helper: AuthenticationHelper<unknown>;

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',
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading