Skip to content

feat(auth): Next.js App Router support #2106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bd063ed
feat(auth): Next.js App Router support
hsynlms Dec 20, 2023
2c697a9
fix(auth): `isValidSignature` parameter name
hsynlms Dec 20, 2023
f7f39b5
Merge branch 'main' into feat-next-app-router-support
hsynlms Dec 20, 2023
8502757
tweak(auth): request object is not mandatory for `getUser` method
hsynlms Dec 20, 2023
d7aea09
Merge branch 'main' into feat-next-app-router-support
hsynlms Dec 20, 2023
e54a192
chore(auth): Next type cleanup
hsynlms Dec 22, 2023
25a6233
fix(auth): `getUser` arguments order
hsynlms Dec 22, 2023
9e25aaf
tweak(auth): `onUser` callback type
hsynlms Dec 22, 2023
097f5f4
Merge branch 'main' into feat-next-app-router-support
hsynlms Dec 22, 2023
37003b4
Merge branch 'main' into feat-next-app-router-support
hsynlms Dec 29, 2023
c0a8606
Merge branch 'main' into feat-next-app-router-support
hsynlms Jan 3, 2024
270d67b
fix(auth): Next.js functions return types
hsynlms Jan 3, 2024
78712b9
tweak(auth): `onUser` function signature
hsynlms Jan 5, 2024
ca8b90f
Merge branch 'main' into feat-next-app-router-support
hsynlms Jan 7, 2024
de040d9
Merge branch 'main' into feat-next-app-router-support
hsynlms Jan 9, 2024
4c62904
Merge branch 'main' into feat-next-app-router-support
hsynlms Jan 10, 2024
803a1c4
chore(auth): minor tweaks
hsynlms Jan 11, 2024
c07c9db
chore(auth): no need to request object
hsynlms Jan 11, 2024
e6d24a1
feat(auth): Next.js Pages Router compatibility
hsynlms Jan 12, 2024
4df73e4
Add changeset
adam-maj Jan 23, 2024
aad99bb
fix: pnpm lock file conflict
hsynlms Jan 24, 2024
628d1c7
revert: pnpm lock file
hsynlms Jan 24, 2024
677989d
Merge branch 'main' into feat-next-app-router-support
hsynlms Jan 24, 2024
14ba584
fix: incorrect Next.js version
hsynlms Jan 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/five-hairs-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@thirdweb-dev/auth": patch
---

Add support for Next.js App Router
2 changes: 1 addition & 1 deletion packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
"express": "^4.18.1",
"fastify": "^4.24.2",
"mocha": "^10.2.0",
"next": "^12.3.4",
"next": "^13.4",
"next-auth": "^4.22.3",
"prettier": "^3.1.1",
"typescript": "^5.3.3"
Expand Down
62 changes: 62 additions & 0 deletions packages/auth/src/next/common/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { GenericAuthWallet } from "@thirdweb-dev/wallets";
import { z } from "zod";

import { Json, LoginPayloadOutputSchema, User } from "../../core";

export const PayloadBodySchema = z.object({
address: z.string(),
chainId: z.string().optional(),
});

export const ActiveBodySchema = z.object({
address: z.string(),
});

export const LoginPayloadBodySchema = z.object({
payload: LoginPayloadOutputSchema,
});

export type ThirdwebAuthRoute =
| "payload"
| "login"
| "logout"
| "user"
| "switch-account";

export type ThirdwebAuthUser<
TData extends Json = Json,
TSession extends Json = Json,
> = User<TSession> & {
data?: TData;
};

export type ThirdwebAuthConfigShared = {
domain: string;
wallet: GenericAuthWallet;
authOptions?: {
statement?: string;
uri?: string;
version?: string;
chainId?: string;
resources?: string[];
validateNonce?:
| ((nonce: string) => void)
| ((nonce: string) => Promise<void>);
validateTokenId?:
| ((tokenId: string) => void)
| ((tokenId: string) => Promise<void>);
loginPayloadDurationInSeconds?: number;
tokenDurationInSeconds?: number;
refreshIntervalInSeconds?: number;
};
cookieOptions?: {
domain?: string;
path?: string;
sameSite?: "lax" | "strict" | "none";
secure?: boolean;
};
};

export type ThirdwebNextContext = {
params?: Record<string, string | string[]>
};
90 changes: 17 additions & 73 deletions packages/auth/src/next/index.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,21 @@
import { Json, ThirdwebAuth as ThirdwebAuthSDK } from "../core";
import { getUser } from "./helpers/user";
import payloadHandler from "./routes/payload";
import loginHandler from "./routes/login";
import logoutHandler from "./routes/logout";
import userHandler from "./routes/user";
import switchAccountHandler from "./routes/switch-account";
import {
// Next.js Pages Router support
// default and backward compatible
export { ThirdwebAuth } from './router-pages'
export type {
ThirdwebAuthConfig,
ThirdwebAuthContext,
ThirdwebAuthRoute,
} from "./types";
import { NextRequest } from "next/server";
import {
GetServerSidePropsContext,
NextApiRequest,
NextApiResponse,
} from "next/types";

export * from "./types";

async function ThirdwebAuthRouter(
req: NextApiRequest,
res: NextApiResponse,
ctx: ThirdwebAuthContext,
) {
// Catch-all route must be named with [...thirdweb]
const { thirdweb } = req.query;
const action = thirdweb?.[0] as ThirdwebAuthRoute;

switch (action) {
case "payload":
return await payloadHandler(req, res, ctx);
case "login":
return await loginHandler(req, res, ctx);
case "user":
return await userHandler(req, res, ctx);
case "logout":
return await logoutHandler(req, res, ctx);
case "switch-account":
return await switchAccountHandler(req, res, ctx);
default:
return res.status(400).json({
message: "Invalid route for authentication.",
});
}
}
} from './router-pages/types'

export function ThirdwebAuth<
TData extends Json = Json,
TSession extends Json = Json,
>(cfg: ThirdwebAuthConfig<TData, TSession>) {
const ctx = {
...cfg,
auth: new ThirdwebAuthSDK(cfg.wallet, cfg.domain),
};
// Next.js App Router support
export { ThirdwebAuth as ThirdwebAuthAppRouter } from './router-app'
export type {
ThirdwebAuthConfig as ThirdwebAuthAppRouterConfig,
ThirdwebAuthContext as ThirdwebAuthAppRouterContext,
} from './router-app/types'

function ThirdwebAuthHandler(
...args: [] | [NextApiRequest, NextApiResponse]
) {
if (args.length === 0) {
return async (req: NextApiRequest, res: NextApiResponse) =>
await ThirdwebAuthRouter(req, res, ctx as ThirdwebAuthContext);
}

return ThirdwebAuthRouter(args[0], args[1], ctx as ThirdwebAuthContext);
}

return {
ThirdwebAuthHandler,
getUser: (
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
) => {
return getUser<TData, TSession>(req, ctx);
},
};
}
// common types
export type {
ThirdwebAuthRoute,
ThirdwebAuthUser,
ThirdwebNextContext,
} from './common/types'
75 changes: 75 additions & 0 deletions packages/auth/src/next/router-app/helpers/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { cookies, headers } from "next/headers";

import {
THIRDWEB_AUTH_ACTIVE_ACCOUNT_COOKIE,
THIRDWEB_AUTH_TOKEN_COOKIE_PREFIX,
} from "../../../constants";
import type { Json } from "../../../core";
import type { ThirdwebAuthUser } from "../../common/types";
import type { ThirdwebAuthContext } from "../types";

export function getCookie(cookie: string): string | undefined {
return cookies().get(cookie)?.value;
}

export function getActiveCookie(): string | undefined {
const activeAccount = getCookie(THIRDWEB_AUTH_ACTIVE_ACCOUNT_COOKIE);
if (activeAccount) {
return `${THIRDWEB_AUTH_TOKEN_COOKIE_PREFIX}_${activeAccount}`;
}

// If active account is not present, then use the old default
return THIRDWEB_AUTH_TOKEN_COOKIE_PREFIX;
}

export function getToken(): string | undefined {
const headerList = headers();

if (headerList.has("authorization")) {
const authorizationHeader = headerList.get("authorization")?.split(" ");
if (authorizationHeader?.length === 2) {
return authorizationHeader[1];
}
}

const activeCookie = getActiveCookie();
if (!activeCookie) {
return undefined;
}

return getCookie(activeCookie);
}

export async function getUser<
TData extends Json = Json,
TSession extends Json = Json,
>(
ctx: ThirdwebAuthContext<TData, TSession>
): Promise<ThirdwebAuthUser<TData, TSession> | null> {
const token = getToken();
if (!token) {
return null;
}

let authenticatedUser: ThirdwebAuthUser<TData, TSession>;
try {
authenticatedUser = await ctx.auth.authenticate<TSession>(token, {
validateTokenId: async (tokenId: string) => {
if (ctx.authOptions?.validateTokenId) {
await ctx.authOptions?.validateTokenId(tokenId);
}
},
});
} catch (err) {
return null;
}

if (ctx.callbacks?.onUser) {
const data = await ctx.callbacks.onUser(authenticatedUser);
if (data) {
return { ...authenticatedUser, data };
}
}

return authenticatedUser;
}
76 changes: 76 additions & 0 deletions packages/auth/src/next/router-app/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { NextRequest } from "next/server";

import type { Json } from "../../core";
import { ThirdwebAuth as ThirdwebAuthSDK } from "../../core";
import { getUser } from "./helpers/user";
import payloadHandler from "./routes/payload";
import loginHandler from "./routes/login";
import logoutHandler from "./routes/logout";
import userHandler from "./routes/user";
import switchAccountHandler from "./routes/switch-account";
import type {
ThirdwebAuthConfig,
ThirdwebAuthContext,
} from "./types";
import type {
ThirdwebAuthRoute,
ThirdwebNextContext,
} from "../common/types";

export type {
ThirdwebAuthConfig,
ThirdwebAuthContext,
} from "./types";

export async function ThirdwebAuthRouter(
req: NextRequest,
ctx: ThirdwebNextContext,
authCtx: ThirdwebAuthContext,
) {
// Catch-all route must be named with [...thirdweb]
const action = ctx.params?.thirdweb?.[0] as ThirdwebAuthRoute;

switch (action) {
case "payload":
return await payloadHandler(req, authCtx);
case "login":
return await loginHandler(req, authCtx);
case "user":
return await userHandler(req, authCtx);
case "logout":
return await logoutHandler(req, authCtx);
case "switch-account":
return await switchAccountHandler(req, authCtx);
default:
return Response.json(
{ message: "Invalid route for authentication." },
{ status: 400},
);
}
}

export function ThirdwebAuth<
TData extends Json = Json,
TSession extends Json = Json,
>(cfg: ThirdwebAuthConfig<TData, TSession>) {
const authCtx = {
...cfg,
auth: new ThirdwebAuthSDK(cfg.wallet, cfg.domain),
};

async function ThirdwebAuthHandler(...args: [] | [NextRequest, ThirdwebNextContext]) {
if (args.length === 0) {
return async (req: NextRequest, ctx: ThirdwebNextContext): Promise<void | Response> =>
await ThirdwebAuthRouter(req, ctx, authCtx as ThirdwebAuthContext);
}

return ThirdwebAuthRouter(args[0], args[1], authCtx as ThirdwebAuthContext);
}

return {
ThirdwebAuthHandler,
getUser: async () => {
return await getUser<TData, TSession>(authCtx);
},
};
}
Loading