Skip to content

Commit 0b60cb3

Browse files
committed
feat(auth): support resource indicators in auth flow
1 parent 8b9aa20 commit 0b60cb3

File tree

2 files changed

+90
-4
lines changed

2 files changed

+90
-4
lines changed

src/client/auth.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,50 @@ describe("OAuth Authorization", () => {
391391
expect(authorizationUrl.searchParams.has("state")).toBe(false);
392392
});
393393

394+
395+
it("includes resource parameter when provided", async () => {
396+
const { authorizationUrl } = await startAuthorization(
397+
"https://auth.example.com",
398+
{
399+
clientInformation: validClientInfo,
400+
redirectUrl: "http://localhost:3000/callback",
401+
resources: ["https://api.example.com/resource"],
402+
}
403+
);
404+
405+
expect(authorizationUrl.searchParams.get("resource")).toBe(
406+
"https://api.example.com/resource"
407+
);
408+
});
409+
410+
it("includes multiple resource parameters when provided", async () => {
411+
const { authorizationUrl } = await startAuthorization(
412+
"https://auth.example.com",
413+
{
414+
clientInformation: validClientInfo,
415+
redirectUrl: "http://localhost:3000/callback",
416+
resources: ["https://api.example.com/resource1", "https://api.example.com/resource2"],
417+
}
418+
);
419+
420+
expect(authorizationUrl.searchParams.getAll("resource")).toEqual([
421+
"https://api.example.com/resource1",
422+
"https://api.example.com/resource2",
423+
]);
424+
});
425+
426+
it("excludes resource parameter when not provided", async () => {
427+
const { authorizationUrl } = await startAuthorization(
428+
"https://auth.example.com",
429+
{
430+
clientInformation: validClientInfo,
431+
redirectUrl: "http://localhost:3000/callback",
432+
}
433+
);
434+
435+
expect(authorizationUrl.searchParams.has("resource")).toBe(false);
436+
});
437+
394438
it("uses metadata authorization_endpoint when provided", async () => {
395439
const { authorizationUrl } = await startAuthorization(
396440
"https://auth.example.com",

src/client/auth.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,19 @@ export interface OAuthClientProvider {
7171
* the authorization result.
7272
*/
7373
codeVerifier(): string | Promise<string>;
74+
75+
/**
76+
* The resource to be used for the current session.
77+
*
78+
* Implements RFC 8707 Resource Indicators.
79+
*
80+
* This is placed in the provider to ensure the strong binding between tokens
81+
* and their intended resource throughout the authorization session.
82+
*
83+
* This method is optional and only needs to be implemented if using
84+
* Resource Indicators (RFC 8707).
85+
*/
86+
resource?(): string | undefined;
7487
}
7588

7689
export type AuthResult = "AUTHORIZED" | "REDIRECT";
@@ -142,6 +155,7 @@ export async function auth(
142155
authorizationCode,
143156
codeVerifier,
144157
redirectUri: provider.redirectUrl,
158+
resource: provider.resource?.(),
145159
});
146160

147161
await provider.saveTokens(tokens);
@@ -158,6 +172,7 @@ export async function auth(
158172
metadata,
159173
clientInformation,
160174
refreshToken: tokens.refresh_token,
175+
resource: provider.resource?.(),
161176
});
162177

163178
await provider.saveTokens(newTokens);
@@ -170,12 +185,20 @@ export async function auth(
170185
const state = provider.state ? await provider.state() : undefined;
171186

172187
// Start new authorization flow
173-
const { authorizationUrl, codeVerifier } = await startAuthorization(authorizationServerUrl, {
188+
189+
const resource = provider.resource?.();
190+
const { authorizationUrl, codeVerifier } = await startAuthorization(authorizationServerUrl, {
174191
metadata,
175192
clientInformation,
176193
state,
177194
redirectUrl: provider.redirectUrl,
178195
scope: scope || provider.clientMetadata.scope,
196+
/**
197+
* Although RFC 8707 supports multiple resources, we currently only support
198+
* a single resource per auth session to maintain a 1:1 token-resource binding
199+
* based on current auth flow implementation
200+
*/
201+
resources: resource ? [resource] : undefined,
179202
});
180203

181204
await provider.saveCodeVerifier(codeVerifier);
@@ -310,13 +333,20 @@ export async function startAuthorization(
310333
redirectUrl,
311334
scope,
312335
state,
336+
resources,
313337
}: {
314338
metadata?: OAuthMetadata;
315339
clientInformation: OAuthClientInformation;
316340
redirectUrl: string | URL;
317341
scope?: string;
318342
state?: string;
319-
},
343+
/**
344+
* Array type to align with RFC 8707 which supports multiple resources,
345+
* making it easier to extend for multiple resource indicators in the future
346+
* (though current implementation only uses a single resource)
347+
*/
348+
resources?: string[];
349+
}
320350
): Promise<{ authorizationUrl: URL; codeVerifier: string }> {
321351
const responseType = "code";
322352
const codeChallengeMethod = "S256";
@@ -365,6 +395,12 @@ export async function startAuthorization(
365395
authorizationUrl.searchParams.set("scope", scope);
366396
}
367397

398+
if (resources?.length) {
399+
for (const resource of resources) {
400+
authorizationUrl.searchParams.append("resource", resource);
401+
}
402+
}
403+
368404
return { authorizationUrl, codeVerifier };
369405
}
370406

@@ -379,13 +415,15 @@ export async function exchangeAuthorization(
379415
authorizationCode,
380416
codeVerifier,
381417
redirectUri,
418+
resource,
382419
}: {
383420
metadata?: OAuthMetadata;
384421
clientInformation: OAuthClientInformation;
385422
authorizationCode: string;
386423
codeVerifier: string;
387424
redirectUri: string | URL;
388-
},
425+
resource?: string;
426+
}
389427
): Promise<OAuthTokens> {
390428
const grantType = "authorization_code";
391429

@@ -412,6 +450,7 @@ export async function exchangeAuthorization(
412450
code: authorizationCode,
413451
code_verifier: codeVerifier,
414452
redirect_uri: String(redirectUri),
453+
...(resource ? { resource } : {}),
415454
});
416455

417456
if (clientInformation.client_secret) {
@@ -442,11 +481,13 @@ export async function refreshAuthorization(
442481
metadata,
443482
clientInformation,
444483
refreshToken,
484+
resource,
445485
}: {
446486
metadata?: OAuthMetadata;
447487
clientInformation: OAuthClientInformation;
448488
refreshToken: string;
449-
},
489+
resource?: string;
490+
}
450491
): Promise<OAuthTokens> {
451492
const grantType = "refresh_token";
452493

@@ -471,6 +512,7 @@ export async function refreshAuthorization(
471512
grant_type: grantType,
472513
client_id: clientInformation.client_id,
473514
refresh_token: refreshToken,
515+
...(resource ? { resource } : {}),
474516
});
475517

476518
if (clientInformation.client_secret) {

0 commit comments

Comments
 (0)