Skip to content

Commit 193a786

Browse files
fix: ensure Connected Accounts use fetcher to properly use DPoP (#2366)
1 parent a0c120c commit 193a786

File tree

7 files changed

+176
-127
lines changed

7 files changed

+176
-127
lines changed

src/server/auth-client.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,11 @@ ca/T0LLtgmbMmxSv/MmzIg==
204204
// Connect Account
205205
if (url.pathname === "/me/v1/connected-accounts/connect") {
206206
if (onConnectAccountRequest) {
207-
await onConnectAccountRequest(new Request(input, init));
207+
// When body is send, new Request() requires duplex: 'half'
208+
// This appeared to only become neccessary once we started using the fetcher.
209+
await onConnectAccountRequest(
210+
new Request(input, { ...init, duplex: "half" } as RequestInit)
211+
);
208212
}
209213

210214
return Response.json(
@@ -224,7 +228,11 @@ ca/T0LLtgmbMmxSv/MmzIg==
224228
// Connect Account complete
225229
if (url.pathname === "/me/v1/connected-accounts/complete") {
226230
if (onCompleteConnectAccountRequest) {
227-
await onCompleteConnectAccountRequest(new Request(input, init));
231+
// When body is send, new Request() requires duplex: 'half'
232+
// This appeared to only become neccessary once we started using the fetcher.
233+
await onCompleteConnectAccountRequest(
234+
new Request(input, { ...init, duplex: "half" } as RequestInit)
235+
);
228236
}
229237

230238
if (completeConnectAccountErrorResponse) {
@@ -4738,6 +4746,7 @@ ca/T0LLtgmbMmxSv/MmzIg==
47384746
}
47394747
});
47404748

4749+
// Here is an issue
47414750
expect(mockOnCallback).toHaveBeenCalledWith(
47424751
null,
47434752
expectedContext,

src/server/auth-client.ts

Lines changed: 53 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import { NextResponse, type NextRequest } from "next/server.js";
22
import * as jose from "jose";
33
import * as oauth from "oauth4webapi";
4-
import {
5-
allowInsecureRequests,
6-
customFetch,
7-
protectedResourceRequest
8-
} from "oauth4webapi";
94
import * as client from "openid-client";
105

116
import packageJson from "../../package.json" with { type: "json" };
@@ -79,6 +74,7 @@ import {
7974
import { toSafeRedirect } from "../utils/url-helpers.js";
8075
import { addCacheControlHeadersForSession } from "./cookies.js";
8176
import {
77+
AccessTokenFactory,
8278
Fetcher,
8379
FetcherConfig,
8480
FetcherHooks,
@@ -691,7 +687,7 @@ export class AuthClient {
691687

692688
const [completeConnectAccountError, connectedAccount] =
693689
await this.completeConnectAccount({
694-
accessToken: tokenSetResponse.tokenSet.accessToken,
690+
tokenSet: tokenSetResponse.tokenSet,
695691
authSession: transactionState.authSession!,
696692
connectCode: req.nextUrl.searchParams.get("connect_code")!,
697693
redirectUri: createRouteUrl(
@@ -1035,7 +1031,7 @@ export class AuthClient {
10351031
const { tokenSet, idTokenClaims } = getTokenSetResponse;
10361032
const [connectAccountError, connectAccountResponse] =
10371033
await this.connectAccount({
1038-
accessToken: tokenSet.accessToken,
1034+
tokenSet: tokenSet,
10391035
connection,
10401036
authorizationParams,
10411037
returnTo
@@ -1842,7 +1838,7 @@ export class AuthClient {
18421838
* The user will be redirected to authorize the connection.
18431839
*/
18441840
async connectAccount(
1845-
options: ConnectAccountOptions & { accessToken: string }
1841+
options: ConnectAccountOptions & { tokenSet: TokenSet }
18461842
): Promise<[ConnectAccountError, null] | [null, NextResponse]> {
18471843
const redirectUri = createRouteUrl(this.routes.callback, this.appBaseUrl);
18481844
let returnTo = this.signInReturnToPath;
@@ -1871,7 +1867,7 @@ export class AuthClient {
18711867

18721868
const [error, connectAccountResponse] =
18731869
await this.createConnectAccountTicket({
1874-
accessToken: options.accessToken,
1870+
tokenSet: options.tokenSet,
18751871
connection: options.connection,
18761872
redirectUri: redirectUri.toString(),
18771873
state,
@@ -1910,38 +1906,37 @@ export class AuthClient {
19101906
this.issuer
19111907
);
19121908

1909+
const fetcher = await this.fetcherFactory({
1910+
useDPoP: this.useDPoP,
1911+
getAccessToken: async () => ({
1912+
accessToken: options.tokenSet.accessToken,
1913+
expiresAt: options.tokenSet.expiresAt || 0,
1914+
scope: options.tokenSet.scope,
1915+
token_type: options.tokenSet.token_type
1916+
}),
1917+
fetch: this.fetch
1918+
});
1919+
19131920
const httpOptions = this.httpOptions();
19141921
const headers = new Headers(httpOptions.headers);
19151922
headers.set("Content-Type", "application/json");
19161923

1917-
const requestBody = JSON.stringify({
1924+
const requestBody = {
19181925
connection: options.connection,
19191926
redirect_uri: options.redirectUri,
19201927
state: options.state,
19211928
code_challenge: options.codeChallenge,
19221929
code_challenge_method: options.codeChallengeMethod,
19231930
authorization_params: options.authorizationParams
1924-
});
1931+
};
19251932

1926-
const res = await protectedResourceRequest(
1927-
options.accessToken,
1928-
"POST",
1929-
connectAccountUrl,
1930-
headers,
1931-
requestBody,
1932-
{
1933-
...httpOptions,
1934-
[customFetch]: (url: string, requestOptions: any) => {
1935-
const tmpRequest = new Request(url, requestOptions);
1936-
return this.fetch(tmpRequest);
1937-
},
1938-
[allowInsecureRequests]: this.allowInsecureRequests || false,
1939-
...(this.useDPoP &&
1940-
this.dpopKeyPair && {
1941-
DPoP: oauth.DPoP(this.clientMetadata, this.dpopKeyPair!)
1942-
})
1943-
}
1944-
);
1933+
const res = await fetcher.fetchWithAuth(connectAccountUrl.toString(), {
1934+
method: "POST",
1935+
headers: {
1936+
"Content-Type": "application/json"
1937+
},
1938+
body: JSON.stringify(requestBody)
1939+
});
19451940

19461941
if (!res.ok) {
19471942
try {
@@ -1984,11 +1979,15 @@ export class AuthClient {
19841979
}
19851980
];
19861981
} catch (e: any) {
1982+
let message =
1983+
"An unexpected error occurred while trying to initiate the connect account flow.";
1984+
if (e instanceof DPoPError) {
1985+
message = e.message;
1986+
}
19871987
return [
19881988
new ConnectAccountError({
19891989
code: ConnectAccountErrorCodes.FAILED_TO_INITIATE,
1990-
message:
1991-
"An unexpected error occurred while trying to initiate the connect account flow."
1990+
message: message
19921991
}),
19931992
null
19941993
];
@@ -2008,32 +2007,31 @@ export class AuthClient {
20082007
const headers = new Headers(httpOptions.headers);
20092008
headers.set("Content-Type", "application/json");
20102009

2011-
const requestBody = JSON.stringify({
2010+
const fetcher = await this.fetcherFactory({
2011+
useDPoP: this.useDPoP,
2012+
getAccessToken: async () => ({
2013+
accessToken: options.tokenSet.accessToken,
2014+
expiresAt: options.tokenSet.expiresAt || 0,
2015+
scope: options.tokenSet.scope,
2016+
token_type: options.tokenSet.token_type
2017+
}),
2018+
fetch: this.fetch
2019+
});
2020+
2021+
const requestBody = {
20122022
auth_session: options.authSession,
20132023
connect_code: options.connectCode,
20142024
redirect_uri: options.redirectUri,
20152025
code_verifier: options.codeVerifier
2016-
});
2026+
};
20172027

2018-
const res = await protectedResourceRequest(
2019-
options.accessToken,
2020-
"POST",
2021-
completeConnectAccountUrl,
2022-
headers,
2023-
requestBody,
2024-
{
2025-
...httpOptions,
2026-
[customFetch]: (url: string, requestOptions: any) => {
2027-
const tmpRequest = new Request(url, requestOptions);
2028-
return this.fetch(tmpRequest);
2029-
},
2030-
[allowInsecureRequests]: this.allowInsecureRequests || false,
2031-
...(this.useDPoP &&
2032-
this.dpopKeyPair && {
2033-
DPoP: oauth.DPoP(this.clientMetadata, this.dpopKeyPair!)
2034-
})
2035-
}
2036-
);
2028+
const res = await fetcher.fetchWithAuth(completeConnectAccountUrl, {
2029+
method: "POST",
2030+
headers: {
2031+
"Content-Type": "application/json"
2032+
},
2033+
body: JSON.stringify(requestBody)
2034+
});
20372035

20382036
if (!res.ok) {
20392037
try {
@@ -2172,19 +2170,6 @@ export class AuthClient {
21722170
throw discoveryError;
21732171
}
21742172

2175-
const defaultAccessTokenFactory = async (
2176-
getAccessTokenOptions: GetAccessTokenOptions
2177-
) => {
2178-
const [error, getTokenSetResponse] = await this.getTokenSet(
2179-
options.session,
2180-
getAccessTokenOptions || {}
2181-
);
2182-
if (error) {
2183-
throw error;
2184-
}
2185-
return getTokenSetResponse.tokenSet;
2186-
};
2187-
21882173
const fetcherConfig: FetcherConfig<TOutput> = {
21892174
// Fetcher-scoped DPoP handle and nonce management
21902175
dpopHandle:
@@ -2200,7 +2185,7 @@ export class AuthClient {
22002185
};
22012186

22022187
const fetcherHooks: FetcherHooks = {
2203-
getAccessToken: defaultAccessTokenFactory,
2188+
getAccessToken: options.getAccessToken,
22042189
isDpopEnabled: () => options.useDPoP ?? this.useDPoP ?? false
22052190
};
22062191

@@ -2234,5 +2219,5 @@ type GetTokenSetResponse = {
22342219
*/
22352220
export type FetcherFactoryOptions<TOutput extends Response> = {
22362221
useDPoP?: boolean;
2237-
session: SessionData;
2222+
getAccessToken: AccessTokenFactory;
22382223
} & FetcherMinimalConfig<TOutput>;

src/server/client.ts

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -554,9 +554,13 @@ export class Auth0Client {
554554
* @param options Optional configuration for getting the access token.
555555
* @param options.refresh Force a refresh of the access token.
556556
*/
557-
async getAccessToken(
558-
options?: GetAccessTokenOptions
559-
): Promise<{ token: string; expiresAt: number; scope?: string }>;
557+
async getAccessToken(options?: GetAccessTokenOptions): Promise<{
558+
token: string;
559+
expiresAt: number;
560+
scope?: string;
561+
token_type?: string;
562+
audience?: string;
563+
}>;
560564

561565
/**
562566
* getAccessToken returns the access token.
@@ -572,7 +576,13 @@ export class Auth0Client {
572576
req: PagesRouterRequest | NextRequest,
573577
res: PagesRouterResponse | NextResponse,
574578
options?: GetAccessTokenOptions
575-
): Promise<{ token: string; expiresAt: number; scope?: string }>;
579+
): Promise<{
580+
token: string;
581+
expiresAt: number;
582+
scope?: string;
583+
token_type?: string;
584+
audience?: string;
585+
}>;
576586

577587
/**
578588
* getAccessToken returns the access token.
@@ -588,7 +598,13 @@ export class Auth0Client {
588598
arg1?: PagesRouterRequest | NextRequest | GetAccessTokenOptions,
589599
arg2?: PagesRouterResponse | NextResponse,
590600
arg3?: GetAccessTokenOptions
591-
): Promise<{ token: string; expiresAt: number; scope?: string }> {
601+
): Promise<{
602+
token: string;
603+
expiresAt: number;
604+
scope?: string;
605+
token_type?: string;
606+
audience?: string;
607+
}> {
592608
const defaultOptions: GetAccessTokenOptions = {
593609
refresh: false
594610
};
@@ -645,7 +661,13 @@ export class Auth0Client {
645661
req: PagesRouterRequest | NextRequest | undefined,
646662
res: PagesRouterResponse | NextResponse | undefined,
647663
options: GetAccessTokenOptions
648-
): Promise<{ token: string; expiresAt: number; scope?: string }> {
664+
): Promise<{
665+
token: string;
666+
expiresAt: number;
667+
scope?: string;
668+
token_type?: string;
669+
audience?: string;
670+
}> {
649671
const session: SessionData | null = req
650672
? await this.getSession(req)
651673
: await this.getSession();
@@ -698,7 +720,9 @@ export class Auth0Client {
698720
return {
699721
token: tokenSet.accessToken,
700722
scope: tokenSet.scope,
701-
expiresAt: tokenSet.expiresAt
723+
expiresAt: tokenSet.expiresAt,
724+
token_type: tokenSet.token_type,
725+
audience: tokenSet.audience
702726
};
703727
}
704728

@@ -982,7 +1006,12 @@ export class Auth0Client {
9821006
const [error, connectAccountResponse] =
9831007
await this.authClient.connectAccount({
9841008
...options,
985-
accessToken: accessToken.token
1009+
tokenSet: {
1010+
accessToken: accessToken.token,
1011+
expiresAt: accessToken.expiresAt,
1012+
scope: getMyAccountTokenOpts.scope,
1013+
audience: accessToken.audience
1014+
}
9861015
});
9871016

9881017
if (error) {
@@ -1219,9 +1248,22 @@ export class Auth0Client {
12191248
);
12201249
}
12211250

1251+
const getAccessToken = async (
1252+
getAccessTokenOptions: GetAccessTokenOptions
1253+
) => {
1254+
const [error, getTokenSetResponse] = await this.authClient.getTokenSet(
1255+
session,
1256+
getAccessTokenOptions || {}
1257+
);
1258+
if (error) {
1259+
throw error;
1260+
}
1261+
return getTokenSetResponse.tokenSet;
1262+
};
1263+
12221264
const fetcher: Fetcher<TOutput> = await this.authClient.fetcherFactory({
12231265
...options,
1224-
session
1266+
getAccessToken
12251267
});
12261268

12271269
return fetcher;

0 commit comments

Comments
 (0)