基于T3 Stack+NextAuth的无ID自定义用户模型认证问题
问题
使用T3 Stack(App Router、TRPC、Prisma、NextAuth)开发项目时,自定义Prisma的User模型,移除默认ID字段,将username设为主键,仅保留username、email、password、first_name、last_name字段。完成数据库迁移后,在NextAuth配置中使用Credentials Provider时,authorize函数出现类型错误——NextAuth期望User对象包含必填的id字段,现有文档及渠道未找到解决方法,求实现无ID用户认证的可行方案。
src/server/auth.ts
/** * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` * object and keep type safety. * * @see https://next-auth.js.org/getting-started/typescript#module-augmentation */ declare module "next-auth" { interface Session extends DefaultSession { user: { username: string; email: string; first_name: string; last_name: string; } & DefaultSession["user"]; } interface User { username: string; email: string; password: string; first_name: string; last_name: string; } } /** * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. * * @see https://next-auth.js.org/configuration/options */ export const authOptions: NextAuthOptions = { callbacks: { session: ({ session, user }) => ({ ...session, user: { ...session.user, username: user.username, email: user.email, first_name: user.first_name, last_name: user.last_name, }, }), }, // adapter: PrismaAdapter(db) as Adapter, providers: [ CredentialsProvider({ // The name to display on the sign in form (e.g. 'Sign in with...') name: "Credentials", // The credentials is used to generate a suitable form on the sign in page. // You can specify whatever fields you are expecting to be submitted. // e.g. domain, username, password, 2FA token, etc. // You can pass any HTML attribute to the <input> tag through the object. credentials: { username: { label: "Username", type: "text", placeholder: "jsmith" }, password: { label: "Password", type: "password" }, }, async authorize(credentials, req) { const creds = await z .object({ username: z.string(), password: z.string().min(6), }) .parseAsync(credentials); // Find user by username const user = await db.user.findUnique({ where: { username: creds.username }, }); // If user not found, return null if (!user) return null; // Verify password const valid = await verifyPassword(creds.password, user.password); // If password is invalid, return null if (!valid) return null; // If user is found and password is valid, return user return user; }, }), /** * ...add more providers here. * * Most other providers require a bit more work than the Discord provider. For example, the * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account * model. Refer to the NextAuth.js docs for the provider you want to use. Example: * * @see https://next-auth.js.org/providers/github */ ], };
Prisma Schema
// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" } generator zod { provider = "zod-prisma-types" } datasource db { provider = "postgresql" // NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below // Further reading: // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string url = env("DATABASE_URL") } model Session { id String @id @default(cuid()) sessionToken String @unique usernameId String expires DateTime user User @relation(fields: [usernameId], references: [username], onDelete: Cascade) } model User { username String @unique email String @unique password String first_name String last_name String sessions Session[] } model VerificationToken { identifier String token String @unique expires DateTime @@unique([identifier, token]) }
错误信息
Type '(credentials: Record<"username" | "password", string> | undefined, req: Pick<RequestInternal, "body" | "query" | "headers" | "method">) => Promise<...>' is not assignable to type '(credentials: Record<"username" | "password", string> | undefined, req: Pick<RequestInternal, "body" | "query" | "headers" | "method">) => Awaitable<...>'. Type 'Promise<{ username: string; email: string; password: string; first_name: string; last_name: string; } | null>' is not assignable to type 'Awaitable<User | null>'. Type 'Promise<{ username: string; email: string; password: string; first_name: string; last_name: string; } | null>' is not assignable to type 'PromiseLike<User | null>'. Types of property 'then' are incompatible. Type '<TResult1 = { username: string; email: string; password: string; first_name: string; last_name: string; } | null, TResult2 = never>(onfulfilled?: ((value: { username: string; email: string; password: string; first_name: string; last_name: string; } | null) => TResult1 | PromiseLike<...>) | null | undefined, onreject...' is not assignable to type '<TResult1 = User | null, TResult2 = never>(onfulfilled?: ((value: User | null) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<...>) | null | undefined) => PromiseLike<...>'. Types of parameters 'onfulfilled' and 'onfulfilled' are incompatible. Types of parameters 'value' and 'value' are incompatible. Type '{ username: string; email: string; password: string; first_name: string; last_name: string; } | null' is not assignable to type 'User | null'. Property 'id' is missing in type '{ username: string; email: string; password: string; first_name: string; last_name: string; }' but required in type 'User'.ts(2322) types.d.ts(504, 5): 'id' is declared here. credentials.d.ts(13, 5): The expected type comes from property 'authorize' which is declared here on type 'UserCredentialsConfig<{ username: { label: string; type: string; placeholder: string; }; password: { label: string; type: string; }; }>' (property) authorize: (credentials: Record<"username" | "password", string> | undefined, req: Pick<RequestInternal, "body" | "query" | "headers" | "method">) => Awaitable<User | null>
解决方案
NextAuth核心逻辑依赖User对象存在唯一标识字段id,但不强制要求该字段是数据库自增ID,只要是唯一值即可。通过以下两步解决类型错误:
1. 修正类型扩展,明确包含id字段
在src/server/auth.ts的模块扩展部分,给自定义的User接口添加id字段,对齐NextAuth默认类型:
declare module "next-auth" { interface Session extends DefaultSession { user: { username: string; email: string; first_name: string; last_name: string; } & DefaultSession["user"]; } interface User { id: string; // 添加该行,匹配NextAuth类型要求 username: string; email: string; password: string; first_name: string; last_name: string; } }
2. 在authorize函数中返回带id的用户对象
找到authorize函数中返回用户的代码,将username赋值给id字段后返回,既满足类型要求,又无需修改数据库结构:
// If user is found and password is valid, return user with id set to username return { ...user, id: user.username };
修改后,NextAuth可正确识别用户对象,类型错误消失,同时保留自定义的User模型结构。
内容的提问来源于stack exchange,提问作者Ash Gharibyan
相关产品推荐
相关产品推荐

