diff --git a/.changeset/better-towns-teach.md b/.changeset/better-towns-teach.md new file mode 100644 index 000000000..3ddd6c113 --- /dev/null +++ b/.changeset/better-towns-teach.md @@ -0,0 +1,6 @@ +--- +'@asgardeo/javascript': minor +'@asgardeo/react': minor +--- + +Expose OIDC discovery response from `useAsgardeo` diff --git a/packages/javascript/src/AsgardeoJavaScriptClient.ts b/packages/javascript/src/AsgardeoJavaScriptClient.ts index d1038ac81..c1d3e17f8 100644 --- a/packages/javascript/src/AsgardeoJavaScriptClient.ts +++ b/packages/javascript/src/AsgardeoJavaScriptClient.ts @@ -38,6 +38,7 @@ import { EmbeddedSignInFlowInitiateResponse, EmbeddedSignInFlowStatus, } from './models/embedded-signin-flow'; +import {OIDCDiscoveryApiResponse} from './models/oidc-discovery'; import {AllOrganizationsApiResponse, Organization} from './models/organization'; import {Storage} from './models/store'; import {TokenExchangeRequestConfig, TokenResponse} from './models/token'; @@ -68,6 +69,14 @@ class AsgardeoJavaScriptClient implements AsgardeoClient { this.baseURL = config?.baseUrl ?? ''; } + public async getDiscoveryResponse(): Promise { + if (!this.storageManager) { + return null; + } + + return this.storageManager.loadOpenIDProviderConfiguration(); + } + /* eslint-disable class-methods-use-this, @typescript-eslint/no-unused-vars */ switchOrganization(_organization: Organization, _sessionId?: string): Promise { throw new Error('Method not implemented.'); diff --git a/packages/javascript/src/__legacy__/client.ts b/packages/javascript/src/__legacy__/client.ts index f3c7968e4..113867f19 100644 --- a/packages/javascript/src/__legacy__/client.ts +++ b/packages/javascript/src/__legacy__/client.ts @@ -21,12 +21,14 @@ import {AuthClientConfig, StrictAuthClientConfig} from './models'; import OIDCDiscoveryConstants from '../constants/OIDCDiscoveryConstants'; import OIDCRequestConstants from '../constants/OIDCRequestConstants'; import PKCEConstants from '../constants/PKCEConstants'; +import OIDCDiscoveryConstantsV2 from '../constants/v2/OIDCDiscoveryConstants'; import {AsgardeoAuthException} from '../errors/exception'; import {IsomorphicCrypto} from '../IsomorphicCrypto'; import {Crypto} from '../models/crypto'; import {ExtendedAuthorizeRequestUrlParams} from '../models/oauth-request'; import {OIDCDiscoveryApiResponse} from '../models/oidc-discovery'; import {OIDCEndpoints} from '../models/oidc-endpoints'; +import {Platform} from '../models/platforms'; import {SessionData, UserSession} from '../models/session'; import {Storage, TemporaryStore} from '../models/store'; import {TokenResponse, IdToken, TokenExchangeRequestConfig} from '../models/token'; @@ -149,7 +151,8 @@ export class AsgardeoAuthClient { // Ref: https://github.com/asgardeo/asgardeo-auth-js-core/pull/205 AsgardeoAuthClient.authHelperInstance = this.authHelper; - let {applicationId} = config; + const {applicationId, platform, endpoints} = config; + let resolvedApplicationId: string | undefined = applicationId; if (applicationId) { await this.storageManager.setPersistedData({ @@ -159,14 +162,23 @@ export class AsgardeoAuthClient { const persistedData: TemporaryStore = await this.storageManager.getPersistedData(); if (persistedData['applicationId']) { - applicationId = persistedData['applicationId'] as string; + resolvedApplicationId = persistedData['applicationId'] as string; + } + } + + const resolvedEndpoints: Partial = endpoints || {}; + + if (platform === Platform.AsgardeoV2) { + if (!resolvedEndpoints['wellKnown']) { + resolvedEndpoints['wellKnown'] = OIDCDiscoveryConstantsV2.Endpoints.WELL_KNOWN; } } await this.storageManager.setConfigData({ ...DefaultConfig, ...config, - applicationId, + applicationId: resolvedApplicationId, + endpoints: resolvedEndpoints, scope: processOpenIDScopes(config.scopes), }); } @@ -434,13 +446,19 @@ export class AsgardeoAuthClient { return Promise.resolve(); } - const {wellKnownEndpoint} = configData as any; + const {wellKnownEndpoint, platform, discovery, baseUrl, endpoints} = configData as any; + + const resolvedWellKnownEndpoint: string | undefined = + wellKnownEndpoint || + (platform === Platform.AsgardeoV2 && discovery?.wellKnown?.enabled + ? `${baseUrl}${endpoints?.wellKnown ?? '/.well-known/openid-configuration'}` + : undefined); - if (wellKnownEndpoint) { + if (resolvedWellKnownEndpoint) { let response: Response; try { - response = await fetch(wellKnownEndpoint); + response = await fetch(resolvedWellKnownEndpoint); if (response.status !== 200 || !response.ok) { throw new Error(); } diff --git a/packages/javascript/src/__legacy__/models/client-config.ts b/packages/javascript/src/__legacy__/models/client-config.ts index 4dcaca187..ea6ff7bb0 100644 --- a/packages/javascript/src/__legacy__/models/client-config.ts +++ b/packages/javascript/src/__legacy__/models/client-config.ts @@ -18,6 +18,7 @@ import {OAuthResponseMode} from '../../models/oauth-response'; import {OIDCEndpoints} from '../../models/oidc-endpoints'; +import {Platform} from '../../models/platforms'; export interface DefaultAuthClientConfig { afterSignInUrl: string; @@ -42,6 +43,12 @@ export interface DefaultAuthClientConfig { */ targetOrganizationId?: string; }; + /** + * Optional platform where the application is running. + * This helps the SDK to optimize its behavior based on the platform. + * If not provided, the SDK will attempt to auto-detect the platform. + */ + platform?: keyof typeof Platform; prompt?: string; responseMode?: OAuthResponseMode; scopes?: string | string[] | undefined; diff --git a/packages/javascript/src/constants/v2/OIDCDiscoveryConstants.ts b/packages/javascript/src/constants/v2/OIDCDiscoveryConstants.ts new file mode 100644 index 000000000..c8c82e9ad --- /dev/null +++ b/packages/javascript/src/constants/v2/OIDCDiscoveryConstants.ts @@ -0,0 +1,54 @@ +/** + * 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. + */ + +/** + * Constants related to OpenID Connect (OIDC) metadata and endpoints. + * This object contains all the standard OIDC endpoints and storage keys + * used throughout the application for authentication and authorization. + * + * @remarks + * The constants are organized into two main sections: + * 1. Endpoints - Contains all OIDC standard endpoint paths + * 2. Storage - Contains keys used for storing OIDC-related data + * + * @example + * ```typescript + * // Using an endpoint + * const wellKnownEndpoint = OIDCDiscoveryConstants.Endpoints.WELL_KNOWN; + * ``` + */ +const OIDCDiscoveryConstants: { + readonly Endpoints: { + readonly WELL_KNOWN: string; + }; +} = { + /** + * Collection of standard OIDC endpoint paths used for authentication flows. + * These endpoints are relative paths that should be appended to the base URL + * of your identity provider. + */ + Endpoints: { + /** + * OpenID Connect discovery document endpoint. + * Used to fetch provider metadata from the authorization server. + */ + WELL_KNOWN: '/.well-known/openid-configuration', + }, +} as const; + +export default OIDCDiscoveryConstants; diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index 6dbd2ffc2..28538e3f8 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -165,6 +165,7 @@ export { ExtendedAuthorizeRequestUrlParams, } from './models/oauth-request'; export {OIDCEndpoints} from './models/oidc-endpoints'; +export {OIDCDiscoveryApiResponse} from './models/oidc-discovery'; export {Storage, TemporaryStore} from './models/store'; export {User, UserProfile} from './models/user'; export {SessionData} from './models/session'; diff --git a/packages/javascript/src/models/config.ts b/packages/javascript/src/models/config.ts index 970487545..4957a33af 100644 --- a/packages/javascript/src/models/config.ts +++ b/packages/javascript/src/models/config.ts @@ -123,6 +123,86 @@ export interface BaseConfig extends WithPreferences { */ clientSecret?: string | undefined; + /** + * OpenID Connect discovery configuration. + * Controls how the SDK resolves endpoint URLs from the authorization server. + * Each discovery mechanism is independently configurable. + * + * @example + * // Use a custom well-known discovery document URL + * discovery: { wellKnown: { endpoint: "https://custom.example.com/.well-known/openid-configuration" } } + * + * @example + * // Disable well-known discovery entirely + * discovery: { wellKnown: { enabled: false } } + */ + discovery?: { + /** + * Configuration for OpenID Connect Discovery via the well-known endpoint (RFC 8414). + * The SDK fetches `{baseUrl}/oauth2/token/.well-known/openid-configuration` by default. + */ + wellKnown?: { + /** + * Whether to fetch and use the well-known discovery document to resolve endpoint URLs. + * @default true + */ + enabled?: boolean; + }; + }; + + /** + * Optional overrides for the OIDC protocol endpoints. + * By default, the SDK derives all endpoint URLs from the well-known discovery document + * located at `{baseUrl}/oauth2/token/.well-known/openid-configuration`. + * Use this when your authorization server exposes endpoints at non-standard paths, + * or when a custom domain differs from `baseUrl`. + * + * Individual overrides take precedence over values resolved from the discovery document. + * + * @example + * endpoints: { + * wellKnown: "https://custom-domain.example.com/.well-known/openid-configuration", + * authorization: "https://custom-domain.example.com/oauth2/authorize", + * } + */ + endpoints?: { + /** + * The authorization endpoint URL. + * If not provided, resolved from the well-known discovery document. + */ + authorization?: string; + /** + * The end-session (logout) endpoint URL. + * If not provided, resolved from the well-known discovery document. + */ + endSession?: string; + /** + * The introspection endpoint URL. + * If not provided, resolved from the well-known discovery document. + */ + introspection?: string; + /** + * The JSON Web Key Set (JWKS) endpoint URL used to fetch public keys for token verification. + * If not provided, resolved from the well-known discovery document. + */ + jwks?: string; + /** + * The token endpoint URL. + * If not provided, resolved from the well-known discovery document. + */ + token?: string; + /** + * The userinfo endpoint URL. + * If not provided, resolved from the well-known discovery document. + */ + userInfo?: string; + /** + * The OpenID Connect discovery document URL. + * Defaults to `{baseUrl}/oauth2/token/.well-known/openid-configuration`. + */ + wellKnown?: string; + }; + /** * Optional instance ID for multi-auth context support. * Use this when you need multiple authentication contexts in the same application. @@ -226,6 +306,7 @@ export interface BaseConfig extends WithPreferences { * @see {@link https://openid.net/specs/openid-connect-session-management-1_0.html#IframeBasedSessionManagement} */ syncSession?: boolean; + /** * Token validation configuration. * This allows you to configure how the SDK validates tokens received from the authorization server. diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index fa90b5848..994bc3876 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -51,6 +51,7 @@ import { EmbeddedSignInFlowResponseV2, executeEmbeddedSignUpFlowV2, EmbeddedSignInFlowStatusV2, + OIDCDiscoveryApiResponse, } from '@asgardeo/browser'; import AuthAPI from './__temp__/api'; import getAllOrganizations from './api/getAllOrganizations'; @@ -124,9 +125,11 @@ class AsgardeoReactClient e resolvedOrganizationHandle = deriveOrganizationHandleFromBaseUrl(config?.baseUrl); } - return this.withLoading(async () => - this.asgardeo.init({...config, organizationHandle: resolvedOrganizationHandle} as any), - ); + return this.withLoading(async () => { + this.initializeConfig = {...config, organizationHandle: resolvedOrganizationHandle}; + + return this.asgardeo.init(this.initializeConfig as any); + }); } override reInitialize(config: Partial): Promise { @@ -134,7 +137,7 @@ class AsgardeoReactClient e let isInitialized: boolean; try { - await this.asgardeo.reInitialize(config); + await this.asgardeo.reInitialize(config as any); isInitialized = true; } catch (error) { @@ -522,6 +525,12 @@ class AsgardeoReactClient e return undefined; } + override async getDiscoveryResponse(): Promise { + const storageManager: any = await this.asgardeo.getStorageManager(); + + return storageManager.loadOpenIDProviderConfiguration(); + } + async request(requestConfig?: HttpRequestConfig): Promise> { return this.asgardeo.httpRequest(requestConfig); } diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts index 9ba884f85..ca3ac440e 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts +++ b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts @@ -21,6 +21,7 @@ import { HttpRequestConfig, HttpResponse, IdToken, + OIDCDiscoveryApiResponse, Organization, SignInOptions, TokenExchangeRequestConfig, @@ -38,6 +39,17 @@ export type AsgardeoContextProps = { applicationId: string | undefined; baseUrl: string | undefined; clientId: string | undefined; + /** + * OIDC discovery data. + */ + discovery: { + /** + * The response from the `.well-known/openid-configuration` endpoint. + * Contains server capabilities, supported endpoints, and metadata. + * `null` while loading or when discovery has not been fetched. + */ + wellKnown: OIDCDiscoveryApiResponse | null; + }; /** * Swaps the current access token with a new one based on the provided configuration (with a grant type). * @param config - Configuration for the token exchange request. @@ -186,6 +198,9 @@ const AsgardeoContext: Context = createContext {}, clientId: undefined, + discovery: { + wellKnown: null, + }, exchangeToken: null, getAccessToken: null, getDecodedIdToken: null, diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index bff38a327..228c52597 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -20,6 +20,7 @@ import { AllOrganizationsApiResponse, AsgardeoRuntimeError, generateFlattenedUserProfile, + OIDCDiscoveryApiResponse, Organization, SignInOptions, User, @@ -100,6 +101,7 @@ const AsgardeoProvider: FC> = ({ }); const [isUpdatingSession, setIsUpdatingSession] = useState(false); + const [wellKnown, setWellKnown] = useState(null); // Branding state const [brandingPreference, setBrandingPreference] = useState(null); @@ -122,6 +124,7 @@ const AsgardeoProvider: FC> = ({ await asgardeo.initialize(config); const initializedConfig: AsgardeoReactConfig = await asgardeo.getConfiguration(); setConfig(initializedConfig); + setWellKnown(await asgardeo.getDiscoveryResponse()); })(); }, []); @@ -559,6 +562,9 @@ const AsgardeoProvider: FC> = ({ baseUrl, clearSession, clientId, + discovery: { + wellKnown, + }, exchangeToken, getAccessToken, getDecodedIdToken, @@ -595,6 +601,7 @@ const AsgardeoProvider: FC> = ({ afterSignInUrl, baseUrl, clientId, + wellKnown, isInitializedSync, isLoadingSync, isSignedInSync,