Skip to content

Commit 6f8fbb2

Browse files
feat: add my-account and my-org proxy
1 parent 76e635d commit 6f8fbb2

File tree

2 files changed

+154
-0
lines changed

2 files changed

+154
-0
lines changed

src/server/auth-client.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
SUBJECT_TOKEN_TYPES
2121
} from "../types/index.js";
2222
import { DEFAULT_SCOPES } from "../utils/constants.js";
23+
import { generateDpopKeyPair } from "../utils/dpopUtils.js";
2324
import { AuthClient } from "./auth-client.js";
2425
import { decrypt, encrypt } from "./cookies.js";
2526
import { StatefulSessionStore } from "./session/stateful-session-store.js";
@@ -6423,6 +6424,77 @@ ca/T0LLtgmbMmxSv/MmzIg==
64236424
});
64246425
});
64256426

6427+
describe("handleMyAccount", async () => {
6428+
it("should rewrite to my account", async () => {
6429+
const currentAccessToken = DEFAULT.accessToken;
6430+
const secret = await generateSecret(32);
6431+
const transactionStore = new TransactionStore({
6432+
secret
6433+
});
6434+
const sessionStore = new StatelessSessionStore({
6435+
secret
6436+
});
6437+
6438+
const dpopKeyPair = await generateDpopKeyPair();
6439+
const authClient = new AuthClient({
6440+
transactionStore,
6441+
sessionStore,
6442+
6443+
domain: DEFAULT.domain,
6444+
clientId: DEFAULT.clientId,
6445+
clientSecret: DEFAULT.clientSecret,
6446+
6447+
secret,
6448+
appBaseUrl: DEFAULT.appBaseUrl,
6449+
6450+
routes: getDefaultRoutes(),
6451+
6452+
fetch: getMockAuthorizationServer(),
6453+
useDPoP: true,
6454+
dpopKeyPair: dpopKeyPair
6455+
});
6456+
const expiresAt = Math.floor(Date.now() / 1000) - 10 * 24 * 60 * 60; // expired 10 days ago
6457+
const session: SessionData = {
6458+
user: {
6459+
sub: DEFAULT.sub,
6460+
name: "John Doe",
6461+
6462+
picture: "https://example.com/john.jpg"
6463+
},
6464+
tokenSet: {
6465+
accessToken: currentAccessToken,
6466+
scope: "openid profile email",
6467+
refreshToken: DEFAULT.refreshToken,
6468+
expiresAt
6469+
},
6470+
internal: {
6471+
sid: DEFAULT.sid,
6472+
createdAt: Math.floor(Date.now() / 1000)
6473+
}
6474+
};
6475+
const maxAge = 60 * 60; // 1 hour
6476+
const expiration = Math.floor(Date.now() / 1000 + maxAge);
6477+
const sessionCookie = await encrypt(session, secret, expiration);
6478+
const headers = new Headers();
6479+
headers.append("cookie", `__session=${sessionCookie}`);
6480+
headers.append("auth0-scope", "foo:bar");
6481+
const request = new NextRequest(
6482+
new URL("/me/foo-bar/12", DEFAULT.appBaseUrl),
6483+
{
6484+
method: "GET",
6485+
headers
6486+
}
6487+
);
6488+
6489+
const response = await authClient.handleMyAccount(request);
6490+
expect(response.status).toEqual(200);
6491+
expect(response.headers.get("authorization")).toEqual("DPoP at_123");
6492+
expect(response.headers.get("auth0-scope")).toEqual("foo:bar");
6493+
expect(response.headers.get("x-middleware-rewrite")).toEqual("https://guabu.us.auth0.com/me/v1/foo-bar/12");
6494+
6495+
});
6496+
});
6497+
64266498
describe("getTokenSet", async () => {
64276499
it("should return the access token if it has not expired", async () => {
64286500
const secret = await generateSecret(32);

src/server/auth-client.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,17 @@ export type RoutesOptions = Partial<
157157
>
158158
>;
159159

160+
// We are using an internal method of DPoPHandle.
161+
// We should look for a way to achieve this without relying on internal methods.
162+
type DPoPHandle = oauth.DPoPHandle & {
163+
addProof: (
164+
url: URL,
165+
headers: Headers,
166+
htm: string,
167+
accessToken?: string
168+
) => Promise<void>;
169+
};
170+
160171
export interface AuthClientOptions {
161172
transactionStore: TransactionStore;
162173
sessionStore: AbstractSessionStore;
@@ -399,6 +410,10 @@ export class AuthClient {
399410
this.enableConnectAccountEndpoint
400411
) {
401412
return this.handleConnectAccount(req);
413+
} else if (sanitizedPathname.startsWith("/me")) {
414+
return this.handleMyAccount(req);
415+
} else if (sanitizedPathname.startsWith("/my-org")) {
416+
return this.handleMyOrg(req);
402417
} else {
403418
// no auth handler found, simply touch the sessions
404419
// TODO: this should only happen if rolling sessions are enabled. Also, we should
@@ -1073,6 +1088,73 @@ export class AuthClient {
10731088
return connectAccountResponse;
10741089
}
10751090

1091+
async handleMyAccount(req: NextRequest): Promise<NextResponse> {
1092+
return this.handleProxy(req, {
1093+
proxyPath: "/me",
1094+
targetBaseUrl: `${this.issuer}/me/v1`,
1095+
audience: `${this.issuer}/me/v1/`
1096+
});
1097+
}
1098+
1099+
async handleMyOrg(req: NextRequest): Promise<NextResponse> {
1100+
return this.handleProxy(req, {
1101+
proxyPath: "/my-org",
1102+
targetBaseUrl: `${this.issuer}/my-org`,
1103+
audience: `${this.issuer}/my-org/`
1104+
});
1105+
}
1106+
1107+
async handleProxy(
1108+
req: NextRequest,
1109+
options: {
1110+
proxyPath: string;
1111+
targetBaseUrl: string;
1112+
audience: string;
1113+
}
1114+
): Promise<NextResponse> {
1115+
const session = await this.sessionStore.get(req.cookies);
1116+
if (!session) {
1117+
return new NextResponse("The user does not have an active session.", {
1118+
status: 401
1119+
});
1120+
}
1121+
1122+
const targetBaseUrl = options.targetBaseUrl;
1123+
const targetUrl = new URL(
1124+
req.nextUrl.pathname.replace(options.proxyPath, targetBaseUrl.toString())
1125+
);
1126+
1127+
const [error, token] = await this.getTokenSet(session, {
1128+
audience: targetBaseUrl.toString(),
1129+
scope: req.headers.get("auth0-scope")
1130+
});
1131+
1132+
if (error) {
1133+
throw new Error(
1134+
`Failed to retrieve access token for My Account: ${error.message}`
1135+
);
1136+
}
1137+
1138+
const headers = new Headers(req.headers);
1139+
1140+
if (token.tokenSet.token_type?.toLowerCase() === "bearer") {
1141+
headers.set("Authorization", `Bearer ${token.tokenSet.accessToken}`);
1142+
} else {
1143+
const dpopHandle = oauth.DPoP(
1144+
this.clientMetadata,
1145+
this.dpopKeyPair!
1146+
) as DPoPHandle;
1147+
1148+
dpopHandle.addProof(targetUrl, headers, req.method);
1149+
1150+
headers.set("Authorization", `DPoP ${token?.tokenSet.accessToken}`);
1151+
}
1152+
1153+
return NextResponse.rewrite(targetUrl, {
1154+
request: { headers }
1155+
});
1156+
}
1157+
10761158
/**
10771159
* Retrieves the token set from the session data, considering optional audience and scope parameters.
10781160
* When audience and scope are provided, it checks if they match the global ones defined in the authorization parameters.

0 commit comments

Comments
 (0)