Skip to content

Commit 6c4b824

Browse files
committed
feat(auth): support resource indicators in auth flow
1 parent 1fd6265 commit 6c4b824

File tree

2 files changed

+96
-9
lines changed

2 files changed

+96
-9
lines changed

src/client/auth.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,49 @@ describe("OAuth Authorization", () => {
202202
expect(authorizationUrl.searchParams.has("scope")).toBe(false);
203203
});
204204

205+
it("includes resource parameter when provided", async () => {
206+
const { authorizationUrl } = await startAuthorization(
207+
"https://auth.example.com",
208+
{
209+
clientInformation: validClientInfo,
210+
redirectUrl: "http://localhost:3000/callback",
211+
resources: ["https://api.example.com/resource"],
212+
}
213+
);
214+
215+
expect(authorizationUrl.searchParams.get("resource")).toBe(
216+
"https://api.example.com/resource"
217+
);
218+
});
219+
220+
it("includes multiple resource parameters when provided", async () => {
221+
const { authorizationUrl } = await startAuthorization(
222+
"https://auth.example.com",
223+
{
224+
clientInformation: validClientInfo,
225+
redirectUrl: "http://localhost:3000/callback",
226+
resources: ["https://api.example.com/resource1", "https://api.example.com/resource2"],
227+
}
228+
);
229+
230+
expect(authorizationUrl.searchParams.getAll("resource")).toEqual([
231+
"https://api.example.com/resource1",
232+
"https://api.example.com/resource2",
233+
]);
234+
});
235+
236+
it("excludes resource parameter when not provided", async () => {
237+
const { authorizationUrl } = await startAuthorization(
238+
"https://auth.example.com",
239+
{
240+
clientInformation: validClientInfo,
241+
redirectUrl: "http://localhost:3000/callback",
242+
}
243+
);
244+
245+
expect(authorizationUrl.searchParams.has("resource")).toBe(false);
246+
});
247+
205248
it("uses metadata authorization_endpoint when provided", async () => {
206249
const { authorizationUrl } = await startAuthorization(
207250
"https://auth.example.com",

src/client/auth.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ export interface OAuthClientProvider {
6666
* the authorization result.
6767
*/
6868
codeVerifier(): string | Promise<string>;
69+
70+
/**
71+
* The resource to be used for the current session.
72+
*
73+
* Implements RFC 8707 Resource Indicators.
74+
*
75+
* This is placed in the provider to ensure the strong binding between tokens
76+
* and their intended resource throughout the authorization session.
77+
*
78+
* This method is optional and only needs to be implemented if using
79+
* Resource Indicators (RFC 8707).
80+
*/
81+
resource?(): string | undefined;
6982
}
7083

7184
export type AuthResult = "AUTHORIZED" | "REDIRECT";
@@ -123,6 +136,7 @@ export async function auth(
123136
authorizationCode,
124137
codeVerifier,
125138
redirectUri: provider.redirectUrl,
139+
resource: provider.resource?.(),
126140
});
127141

128142
await provider.saveTokens(tokens);
@@ -139,6 +153,7 @@ export async function auth(
139153
metadata,
140154
clientInformation,
141155
refreshToken: tokens.refresh_token,
156+
resource: provider.resource?.(),
142157
});
143158

144159
await provider.saveTokens(newTokens);
@@ -149,12 +164,22 @@ export async function auth(
149164
}
150165

151166
// Start new authorization flow
152-
const { authorizationUrl, codeVerifier } = await startAuthorization(serverUrl, {
153-
metadata,
154-
clientInformation,
155-
redirectUrl: provider.redirectUrl,
156-
scope: scope || provider.clientMetadata.scope,
157-
});
167+
const resource = provider.resource?.();
168+
const { authorizationUrl, codeVerifier } = await startAuthorization(
169+
serverUrl,
170+
{
171+
metadata,
172+
clientInformation,
173+
redirectUrl: provider.redirectUrl,
174+
scope: scope || provider.clientMetadata.scope,
175+
/**
176+
* Although RFC 8707 supports multiple resources, we currently only support
177+
* a single resource per auth session to maintain a 1:1 token-resource binding
178+
* based on current auth flow implementation
179+
*/
180+
resources: resource ? [resource] : undefined,
181+
}
182+
);
158183

159184
await provider.saveCodeVerifier(codeVerifier);
160185
await provider.redirectToAuthorization(authorizationUrl);
@@ -211,12 +236,19 @@ export async function startAuthorization(
211236
clientInformation,
212237
redirectUrl,
213238
scope,
239+
resources,
214240
}: {
215241
metadata?: OAuthMetadata;
216242
clientInformation: OAuthClientInformation;
217243
redirectUrl: string | URL;
218244
scope?: string;
219-
},
245+
/**
246+
* Array type to align with RFC 8707 which supports multiple resources,
247+
* making it easier to extend for multiple resource indicators in the future
248+
* (though current implementation only uses a single resource)
249+
*/
250+
resources?: string[];
251+
}
220252
): Promise<{ authorizationUrl: URL; codeVerifier: string }> {
221253
const responseType = "code";
222254
const codeChallengeMethod = "S256";
@@ -261,6 +293,12 @@ export async function startAuthorization(
261293
authorizationUrl.searchParams.set("scope", scope);
262294
}
263295

296+
if (resources?.length) {
297+
for (const resource of resources) {
298+
authorizationUrl.searchParams.append("resource", resource);
299+
}
300+
}
301+
264302
return { authorizationUrl, codeVerifier };
265303
}
266304

@@ -275,13 +313,15 @@ export async function exchangeAuthorization(
275313
authorizationCode,
276314
codeVerifier,
277315
redirectUri,
316+
resource,
278317
}: {
279318
metadata?: OAuthMetadata;
280319
clientInformation: OAuthClientInformation;
281320
authorizationCode: string;
282321
codeVerifier: string;
283322
redirectUri: string | URL;
284-
},
323+
resource?: string;
324+
}
285325
): Promise<OAuthTokens> {
286326
const grantType = "authorization_code";
287327

@@ -308,6 +348,7 @@ export async function exchangeAuthorization(
308348
code: authorizationCode,
309349
code_verifier: codeVerifier,
310350
redirect_uri: String(redirectUri),
351+
...(resource ? { resource } : {}),
311352
});
312353

313354
if (clientInformation.client_secret) {
@@ -338,11 +379,13 @@ export async function refreshAuthorization(
338379
metadata,
339380
clientInformation,
340381
refreshToken,
382+
resource,
341383
}: {
342384
metadata?: OAuthMetadata;
343385
clientInformation: OAuthClientInformation;
344386
refreshToken: string;
345-
},
387+
resource?: string;
388+
}
346389
): Promise<OAuthTokens> {
347390
const grantType = "refresh_token";
348391

@@ -367,6 +410,7 @@ export async function refreshAuthorization(
367410
grant_type: grantType,
368411
client_id: clientInformation.client_id,
369412
refresh_token: refreshToken,
413+
...(resource ? { resource } : {}),
370414
});
371415

372416
if (clientInformation.client_secret) {

0 commit comments

Comments
 (0)