Skip to content

Commit 3a87ffa

Browse files
committed
fix: persist resource metadata URL across OAuth redirects
In browser OAuth flows, when the user is redirected to the authorization server and back, the resourceMetadataUrl discovered from the WWW-Authenticate header was lost. This caused token exchange to fail because the SDK couldn't locate the correct token endpoint. This commit adds two optional methods to OAuthClientProvider: - saveResourceMetadataUrl(url): Saves the URL before redirect - resourceMetadataUrl(): Loads the saved URL after redirect The SDK now: 1. Loads resourceMetadataUrl from provider if not passed in options 2. Saves resourceMetadataUrl before calling redirectToAuthorization() This change is fully backwards-compatible as both methods are optional. Providers that don't implement them will continue to work as before. Fixes #1234
1 parent 6b4d99f commit 3a87ffa

File tree

3 files changed

+423
-2
lines changed

3 files changed

+423
-2
lines changed

examples/client/src/simpleOAuthClientProvider.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export class InMemoryOAuthClientProvider implements OAuthClientProvider {
88
private _clientInformation?: OAuthClientInformationMixed;
99
private _tokens?: OAuthTokens;
1010
private _codeVerifier?: string;
11+
private _resourceMetadataUrl?: URL;
1112

1213
constructor(
1314
private readonly _redirectUrl: string | URL,
@@ -62,4 +63,20 @@ export class InMemoryOAuthClientProvider implements OAuthClientProvider {
6263
}
6364
return this._codeVerifier;
6465
}
66+
67+
/**
68+
* Saves the resource metadata URL discovered during the initial OAuth challenge.
69+
* In browser environments, you should persist this to sessionStorage.
70+
*/
71+
saveResourceMetadataUrl(url: URL): void {
72+
this._resourceMetadataUrl = url;
73+
}
74+
75+
/**
76+
* Loads a previously saved resource metadata URL.
77+
* This is called when exchanging the authorization code for tokens after redirect.
78+
*/
79+
resourceMetadataUrl(): URL | undefined {
80+
return this._resourceMetadataUrl;
81+
}
6582
}

packages/client/src/client/auth.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,45 @@ export interface OAuthClientProvider {
188188
* }
189189
*/
190190
prepareTokenRequest?(scope?: string): URLSearchParams | Promise<URLSearchParams | undefined> | undefined;
191+
192+
/**
193+
* Saves the resource metadata URL discovered during the initial OAuth challenge.
194+
*
195+
* This optional method allows browser-based applications to persist the resource
196+
* metadata URL (from the WWW-Authenticate header) before redirecting to the
197+
* authorization server. This is necessary because the URL would otherwise be
198+
* lost during the redirect, causing token exchange to fail.
199+
*
200+
* In browser environments, implementations should persist this to sessionStorage
201+
* or a similar mechanism that survives page navigation.
202+
*
203+
* @param url - The resource metadata URL discovered from the WWW-Authenticate header
204+
*
205+
* @example
206+
* // Browser implementation using sessionStorage:
207+
* saveResourceMetadataUrl(url) {
208+
* sessionStorage.setItem('oauth_resource_metadata_url', url.toString());
209+
* }
210+
*/
211+
saveResourceMetadataUrl?(url: URL): void | Promise<void>;
212+
213+
/**
214+
* Loads a previously saved resource metadata URL.
215+
*
216+
* This optional method retrieves the resource metadata URL that was saved
217+
* before the OAuth redirect. It's called when exchanging the authorization
218+
* code for tokens, allowing the SDK to locate the correct token endpoint.
219+
*
220+
* @returns The saved resource metadata URL, or undefined if none was saved
221+
*
222+
* @example
223+
* // Browser implementation using sessionStorage:
224+
* resourceMetadataUrl() {
225+
* const url = sessionStorage.getItem('oauth_resource_metadata_url');
226+
* return url ? new URL(url) : undefined;
227+
* }
228+
*/
229+
resourceMetadataUrl?(): URL | undefined | Promise<URL | undefined>;
191230
}
192231

193232
export type AuthResult = 'AUTHORIZED' | 'REDIRECT';
@@ -399,8 +438,19 @@ async function authInternal(
399438
let resourceMetadata: OAuthProtectedResourceMetadata | undefined;
400439
let authorizationServerUrl: string | URL | undefined;
401440

441+
// If resourceMetadataUrl is not provided, try to load it from the provider
442+
// This handles the case where the URL was saved before a browser redirect
443+
let effectiveResourceMetadataUrl = resourceMetadataUrl;
444+
if (!effectiveResourceMetadataUrl && provider.resourceMetadataUrl) {
445+
effectiveResourceMetadataUrl = await Promise.resolve(provider.resourceMetadataUrl());
446+
}
447+
402448
try {
403-
resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl }, fetchFn);
449+
resourceMetadata = await discoverOAuthProtectedResourceMetadata(
450+
serverUrl,
451+
{ resourceMetadataUrl: effectiveResourceMetadataUrl },
452+
fetchFn
453+
);
404454
if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) {
405455
authorizationServerUrl = resourceMetadata.authorization_servers[0];
406456
}
@@ -509,6 +559,12 @@ async function authInternal(
509559

510560
const state = provider.state ? await provider.state() : undefined;
511561

562+
// Save the resource metadata URL before redirect so it can be restored after
563+
// This is essential for browser environments where the page navigates away
564+
if (effectiveResourceMetadataUrl && provider.saveResourceMetadataUrl) {
565+
await provider.saveResourceMetadataUrl(effectiveResourceMetadataUrl);
566+
}
567+
512568
// Start new authorization flow
513569
const { authorizationUrl, codeVerifier } = await startAuthorization(authorizationServerUrl, {
514570
metadata,

0 commit comments

Comments
 (0)