nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
24.87k stars 3.51k forks source link

Next Auth V5 Augment Typescript User Interface doesnt work #9253

Closed cexra closed 10 months ago

cexra commented 11 months ago

What is the improvement or update you wish to see?

next auth js provides documentation that does not work to augment the User interface in the @auth/core module.

Is there any context that might help us understand?

from the existing documentation we can augment the User interface like the method below

next-auth.d.ts

import { JWT } from '@auth/core/jwt'
import { type DefaultSession } from 'next-auth'

declare module '@auth/core' {
  interface Session {
    user: {
      id: string
      identifier: string
      role: 'USER' | 'ADMIN'
      name: string
    } & DefaultSession['user']
  }

  interface User {
    id: string
    identifier: string
    name: string
    role: 'USER' | 'ADMIN'
  }
}

declare module '@auth/core/jwt' {
  interface JWT {
    identifier: string
    name: string
    role: 'USER' | 'ADMIN'
  }
}

augment Session interface and JWT interface it works. but augment User interface does not work. my purpose of augmenting the User interface is to persist the role attribute on the User object, so that I can store it on the Session object.

auth.config.ts

import type { NextAuthConfig } from 'next-auth'

export const authConfig = {
  callbacks: {
    jwt({ token, user }) {
      if (user) token.role = user.role // Property 'role' does not exist on type 'User | AdapterUser'. Property 'role' does not exist on type 'User'.ts(2339)
      return token
    },

    session({ session, token }) {
      session.user.role = token.role
      return session
    },
  },
} satisfies NextAuthConfig

Does the docs page already exist? Please link to it.

https://authjs.dev/getting-started/typescript?frameworks=next

judewang commented 11 months ago

@cexra Try using @auth/core/types rather than @auth/core, that worked for me.

cexra commented 11 months ago

yes the solution works but only partially, the solution solves the problem on jwt and session callback. but why user.name is still a string | null | undefined when I think user.name should be a string only. can anyone help me how to augment this type

Constantiner commented 11 months ago
import "next-auth";

declare module "next-auth" {
    interface User {
        role: "SUPER" | "USER";
    }

    interface Session {
        user?: User;
    }
}

declare module "@auth/core/jwt" {
    interface JWT {
        role: "SUPER" | "USER";
    }
}

This works for me.

KrisTemmerman commented 11 months ago

I tried both ways, and i run into the same issue: Property 'id' does not exist on type '{ name?: string | null | undefined; email?: string | null | undefined; image?: string | null | undefined; }'.

import NextAuth from "next-auth";
import Discord from "next-auth/providers/discord";
import type { NextAuthConfig } from "next-auth";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "@/app/db";
import { pgTable } from "./db/schema";
import { env } from "@/env.mjs";

enum UserRole {
  Admin = "admin",
  User = "user",
  Free = "free",
}

declare module "@auth/core/types" {
  interface Session {
    user: {
      id: string;
      // ...other properties
      role: UserRole;
    } & DefaultSession["user"];
  }

  interface User {
    // ...other properties

    role: UserRole;
  }
}

export const config = {
  theme: {
    logo: "https://next-auth.js.org/img/logo/logo-sm.png",
  },
  providers: [
    Discord({
      clientId: env.DISCORD_CLIENT_ID,
      clientSecret: env.DISCORD_CLIENT_SECRET,
    }),
  ],
  adapter: DrizzleAdapter(db, pgTable),

  callbacks: {
    session: ({ session, user }) => ({
      ...session,
      user: {
        ...session.user,
        id: user.id,
        role:session.user?.role
      },
    }),
  },
} satisfies NextAuthConfig;

export const { handlers, auth } = NextAuth(config);
Constantiner commented 11 months ago

Have you tried to put your augmented types into a separate .d.ts file?

KrisTemmerman commented 11 months ago

I needed to update my package to next-auth": "5.0.0-beta.4 and drizzle-adapter:"^0.3.9", and could just run it inline. Thanks all for confirming it was me and not NextAuth.

import NextAuth from "next-auth";
import Discord from "next-auth/providers/discord";
import type { NextAuthConfig } from "next-auth";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "@/app/db";
import { pgTable } from "./db/schema";
import { env } from "@/env.mjs";

enum UserRole {
  Admin = "admin",
  User = "user",
  Free = "free",
}

declare module "@auth/core/types" {
  interface Session {
    user: {
      id: string;
      // ...other properties
      role: UserRole;
    } 
  }

  interface User {
    // ...other properties

    role: UserRole;
  }
}

export const config = {
  theme: {
    logo: "https://next-auth.js.org/img/logo/logo-sm.png",
  },
  providers: [
    Discord({
      clientId: env.DISCORD_CLIENT_ID,
      clientSecret: env.DISCORD_CLIENT_SECRET,
    }),
  ],
  adapter: DrizzleAdapter(db, pgTable),

  callbacks: {
    session: ({ session, user }) => ({
      ...session,
      user: {
        ...session.user,
        id: user.id,
        role: session.user?.role,
      },
    }),
  },
} satisfies NextAuthConfig;

export const { handlers, auth } = NextAuth(config);
andreluis-oliveira commented 11 months ago

@cexra Try using @auth/core/types rather than @auth/core, that worked for me.

thanks bro

ypanagidis commented 11 months ago

I gave this a go today and no luck still unfortunately. How come this works for you guys when this is still open? https://github.com/nextauthjs/next-auth/pull/8561

chungweileong94 commented 10 months ago

So I spend quite some time debugging this, as both @auth/core & @auth/core/types doesn't work for me, and I don't think adding the /types is really necessary.

I managed to solve it by making the user optional, which make sense as the user can be undefined when user is not login.

declare module "next-auth" {
  interface Session {
    user?: {
      id: string;
      // ...other properties
      role: UserRole;
    } 
  }
}

Notice that it also works with next-auth in my case, so I believe it should also works with @auth/core & @auth/core/types.

rvndev commented 8 months ago

I've tested various releases of next-auth@beta, however, I am still unable to overwrite the default values in the User interface. I saved the following into next-auth.d.ts, however, I am still getting an error when I try to assign a Number to id in authorize().

declare module "next-auth" {
  interface User {
    id: number;
  }

  interface Session {
    user?: User;
  }
}

declare module "@auth/core" {
  interface User {
    id: number;
  }
}

declare module "@auth/core/types" {
  interface User {
    id: number;
  }
}
ndom91 commented 8 months ago

@rvndev did you include your next-auth.d.ts in your tsconfig.json types?

I've added this snippet in my auth.config.ts and it seemed to work:


declare module "next-auth" {
  interface Session {
    user: {
      address: string
    } & User
  }

  interface User {
    foo?: string
  }
}

Check this out for more details: https://authjs.dev/getting-started/typescript

dir commented 3 months ago

Extending the native Auth.js type + actually passing those extended values to the session is way more of a nightmare than it should be. Crazy this wasn't improved from v4 to v5, in fact, it's actually worse. Why is this issue closed?

dir commented 3 months ago

Following the documentation perfectly on the TypeScript section throws errors and fails.

types.d.ts

import NextAuth, { type DefaultSession } from "next-auth"

declare module "next-auth" {
  /**
   * Returned by `auth`, `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
   */
  interface Session {
    user: {
      /** The user's postal address. */
      address: string
      /**
       * By default, TypeScript merges new interface properties and overwrites existing ones.
       * In this case, the default session user properties will be overwritten,
       * with the new ones defined above. To keep the default session user properties,
       * you need to add them back into the newly declared interface.
       */
    } & DefaultSession["user"]
  }
}

auth.ts

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [redacted],
  session: {
    strategy: "jwt",
    maxAge: 48 * 60 * 60, // 48 hours
  },
  callbacks: {
    session({ session, token, user }) {
      // `session.user.address` is now a valid property, and will be type-checked
      // in places like `useSession().data.user` or `auth().user`
      return {
        ...session,
        user: {
          ...session.user,
          address: user.address,
        },
      };
    },
  },
});

The above example is taken directly from the documentation, and yields:

Unsafe assignment of an error typed value.eslint[@typescript-eslint/no-unsafe-assignment](https://typescript-eslint.io/rules/no-unsafe-assignment)
Property 'address' does not exist on type 'AdapterUser'.ts(2339)

This issue needs to be re-opened, at a minimum the docs need to be fixed.

ypanagidis commented 3 months ago

Following the documentation perfectly on the TypeScript section throws errors and fails.

types.d.ts

import NextAuth, { type DefaultSession } from "next-auth"

declare module "next-auth" {
  /**
   * Returned by `auth`, `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
   */
  interface Session {
    user: {
      /** The user's postal address. */
      address: string
      /**
       * By default, TypeScript merges new interface properties and overwrites existing ones.
       * In this case, the default session user properties will be overwritten,
       * with the new ones defined above. To keep the default session user properties,
       * you need to add them back into the newly declared interface.
       */
    } & DefaultSession["user"]
  }
}

auth.ts

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [redacted],
  session: {
    strategy: "jwt",
    maxAge: 48 * 60 * 60, // 48 hours
  },
  callbacks: {
    session({ session, token, user }) {
      // `session.user.address` is now a valid property, and will be type-checked
      // in places like `useSession().data.user` or `auth().user`
      return {
        ...session,
        user: {
          ...session.user,
          address: user.address,
        },
      };
    },
  },
});

The above example is taken directly from the documentation, and yields:

Unsafe assignment of an error typed value.eslint[@typescript-eslint/no-unsafe-assignment](https://typescript-eslint.io/rules/no-unsafe-assignment)
Property 'address' does not exist on type 'AdapterUser'.ts(2339)

This issue needs to be re-opened, at a minimum the docs need to be fixed.

@dir To fix this just augment the module "@auth/core/adapters".

You do need to install it though (pnpm add "@auth/core" and make sure it's the latest version by checking the npm releases or on github.)

Bellow see an example:

declare module "next-auth" {
  interface Session {
    user: {
      role: UserRoles;
      id: string;
      email: string;
      activated: UserActivated;

    } & DefaultSession["user"];
  }
  interface User {
    role: UserRoles;
    activated: UserActivated;
  }
}

//For the adaptor user objcet
//Here add any other fields you have in user table that are not part of the default schema.
declare module "@auth/core/adapters" {
  interface AdapterUser extends User {
    role: UserRoles;
    activated: UserActivated;
  }
}
dir commented 3 months ago

@ypanagidis I appreciate your help! I installed @auth/core at the latest version (0.34.1)

But... (after restarting my editor, clearing my modules folder, reinstalling, etc), I get

Invalid module name in augmentation, module '@auth/core/adapters' cannot be found.ts(2664)
⚠ Error (TS2664)  | 
Invalid module name in augmentation, module 
 cannot be found.

On a simple reproduction of your example:

declare module "@auth/core/adapters" {
  interface AdapterUser extends User {
    role: UserRoles;
    activated: UserActivated;
  }
}

This is nightmarish just to extend my session and add a couple of types. What a headache.

ypanagidis commented 3 months ago

@ypanagidis I appreciate your help! I installed @auth/core at the latest version (0.34.1)

But... (after restarting my editor, clearing my modules folder, reinstalling, etc), I get

Invalid module name in augmentation, module '@auth/core/adapters' cannot be found.ts(2664)
⚠ Error (TS2664)  | 
Invalid module name in augmentation, module 
 cannot be found.

On a simple reproduction of your example:

declare module "@auth/core/adapters" {
  interface AdapterUser extends User {
    role: UserRoles;
    activated: UserActivated;
  }
}

This is nightmarish just to extend my session and add a couple of types. What a headache.

@dir I had the same issue in a monorepo, make sure to import anything from '@auth/core" and the error should go away! I think it's getting treeshaken

andersonmendesdev commented 3 months ago

I tried everything above and I still couldn't overwrite the interfaces User and Session, any other suggestions?

package.json

{
    "name": "application",
    "version": "0.1.0",
    "private": true,
    "scripts": {
        "dev": "next dev",
        "build": "next build",
        "start": "next start",
        "lint": "next lint"
    },
    "dependencies": {
        "@emotion/is-prop-valid": "^1.2.2",
        "@emotion/react": "^11.11.4",
        "@emotion/styled": "^11.11.5",
        "@mui/icons-material": "^5.15.20",
        "@mui/material": "^5.15.20",
        "@next/third-parties": "^14.2.4",
        "@react-oauth/google": "^0.12.1",
        "@svgr/webpack": "^8.1.0",
        "axios": "^1.7.2",
        "date-fns": "^3.6.0",
        "dotenv": "^16.4.5",
        "firebase": "^10.12.3",
        "next": "^14.2.4",
        "next-auth": "^5.0.0-beta.20",
        "react": "^18",
        "react-apple-signin-auth": "^1.1.0",
        "react-datepicker": "^7.2.0",
        "react-dom": "^18",
        "react-icons": "^5.2.1",
        "react-input-mask": "3.0.0-alpha.2",
        "styled-components": "^6.1.11",
        "superstruct": "^2.0.2",
        "unstorage": "^1.10.2"
    },
    "devDependencies": {
        "@types/next-auth": "^3.15.0",
        "@types/node": "^20",
        "@types/react": "^18",
        "@types/react-dom": "^18",
        "@types/react-input-mask": "3.0.2",
        "@types/styled-components": "^5.1.34",
        "eslint": "^8",
        "eslint-config-next": "14.2.4",
        "typescript": "^5"
    }
}
MrOxMasTer commented 3 months ago

Can someone explain why when setting different types in next-auth.d.ts, I have all other types fail altogether The file has been added to tsconfig.json image image image

Fixes it if I move it to auth.ts are all types, but this file will be very huge anyway. I don't want to shove any more types in there.

andersonmendesdev commented 3 months ago

I downgraded to stable version 4.24.7.

Mheer91 commented 2 months ago

@ypanagidis I appreciate your help! I installed @auth/core at the latest version (0.34.1)

But... (after restarting my editor, clearing my modules folder, reinstalling, etc), I get

Invalid module name in augmentation, module '@auth/core/adapters' cannot be found.ts(2664)
⚠ Error (TS2664)  | 
Invalid module name in augmentation, module 
 cannot be found.

On a simple reproduction of your example:

declare module "@auth/core/adapters" {
  interface AdapterUser extends User {
    role: UserRoles;
    activated: UserActivated;
  }
}

This is nightmarish just to extend my session and add a couple of types. What a headache.

Have you come across a solution yet?

ypanagidis commented 2 months ago

@ypanagidis I appreciate your help! I installed @auth/core at the latest version (0.34.1)

But... (after restarting my editor, clearing my modules folder, reinstalling, etc), I get


Invalid module name in augmentation, module '@auth/core/adapters' cannot be found.ts(2664)

⚠ Error (TS2664)  | 

Invalid module name in augmentation, module 

 cannot be found.

On a simple reproduction of your example:


declare module "@auth/core/adapters" {

  interface AdapterUser extends User {

    role: UserRoles;

    activated: UserActivated;

  }

}

This is nightmarish just to extend my session and add a couple of types. What a headache.

Have you come across a solution yet?

I think it's been tree shaken away. Just import it and export the type

Rylxnd commented 2 months ago

Trying multiple different way to override the User types with 5.0.0-beta.20 below

import 'next-auth'
import '@auth/core/types'
import '@auth/core'

declare module '@auth/core' {
    // trying to override type of these field from being undefined | null
    interface User {
        id: string;
        name: string;
        image: string;
        email: string | null;
        emailVerified: Date | null;
    }

    /**
     * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
     */
    interface Session {
        user?: User;
    }

    interface Account {
        accessToken: string;
        refreshToken: string;
    }
}

declare module 'next-auth' {
    // trying to override type of these field from being undefined | null
    interface User {
        id: string;
        name: string;
        image: string;
        email: string | null;
        emailVerified: Date | null;
    }

    /**
     * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
     */
    interface Session {
        user?: User;
    }

    interface Account {
        accessToken: string;
        refreshToken: string;
    }
}

Neither solutions work. That farthest I made it was overriding the session user type. This beta is definitely not cross-compatible with version 4 when it comes to overriding the next-auth types. Still not sure why this issue is still closed.

The only solution is to downgrade to the stable v4 release until this gets patched or another solution arises.

vexra commented 2 months ago

for those of you who still have problems with augmentation, I have a project that successfully performs augmentation, you can see it https://github.com/prave-com/electra. As an illustration, I added role attributes to the session and user objects. You can see the project directly, and here I will provide some of the code for you to see and study, I hope this can help you.

as a reminder, I once read that we should only use the same config for all next auth which I saved in auth.config.ts then we use the that config in the auth.ts and middleware.ts files

package.json file

{
  "name": "electra",
  "version": "0.1.0",
  "private": true,
  "prisma": {
    "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
  },
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "format:check": "prettier --check --ignore-unknown .",
    "format": "prettier --write --ignore-unknown . && prisma format",
    "postinstall": "prisma generate"
  },
  "dependencies": {
    "@auth/prisma-adapter": "^2.4.2",
    "@emotion/cache": "^11.13.1",
    "@emotion/react": "^11.13.0",
    "@emotion/styled": "^11.13.0",
    "@hookform/resolvers": "^3.9.0",
    "@mui/icons-material": "^5.16.7",
    "@mui/material": "^5.16.7",
    "@mui/material-nextjs": "^5.16.6",
    "@prisma/client": "^5.18.0",
    "bcryptjs": "^2.4.3",
    "next": "14.2.5",
    "next-auth": "5.0.0-beta.20",
    "react": "^18",
    "react-dom": "^18",
    "react-hook-form": "^7.53.0",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@trivago/prettier-plugin-sort-imports": "^4.3.0",
    "@types/bcryptjs": "^2.4.6",
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "csv-parse": "^5.5.6",
    "eslint": "^8",
    "eslint-config-next": "14.2.5",
    "postcss": "^8",
    "prettier": "^3.3.3",
    "prettier-plugin-tailwindcss": "^0.6.6",
    "prisma": "^5.18.0",
    "tailwindcss": "^3.4.1",
    "ts-node": "^10.9.2",
    "typescript": "^5"
  }
}

next-auth.d.ts file

import { Role } from '@prisma/client'
import NextAuth, { DefaultSession } from 'next-auth'
import { JWT } from 'next-auth/jwt'

declare module 'next-auth' {
  interface Session {
    user: {
      role: Role
    } & DefaultSession['user']
  }
}

declare module 'next-auth/jwt' {
  interface JWT {
    role: Role
  }
}

auth.config.ts file

import { getUserById } from '@/data/user'
import { getUserByEmail } from '@/data/user'
import db from '@/lib/db'
import { LoginSchema } from '@/schema/login'
import { PrismaAdapter } from '@auth/prisma-adapter'
import bcrypt from 'bcryptjs'
import type { NextAuthConfig } from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import Google from 'next-auth/providers/google'

export default {
  providers: [
    Google,
    Credentials({
      name: 'Credentials',
      credentials: {
        username: { label: 'Username', type: 'text' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        const validatedFields = LoginSchema.safeParse(credentials)

        if (validatedFields.success) {
          const { email, password } = validatedFields.data

          const user = await getUserByEmail(email)
          if (!user || !user.password) return null

          const passwordMatch = await bcrypt.compare(password, user.password)
          if (passwordMatch) return user
        }

        return null
      },
    }),
  ],
  events: {
    async linkAccount({ user }) {
      await db.user.update({
        where: { id: user.id },
        data: { emailVerified: new Date() },
      })
    },
  },
  pages: {
    signIn: '/signin',
  },
  callbacks: {
    async session({ token, session }) {
      if (token.sub && session.user) {
        session.user.id = token.sub
      }

      if (token.role && session.user) {
        session.user.role = token.role
      }

      return session
    },
    async jwt({ token }) {
      if (!token.sub) return token

      const existingUser = await getUserById(token.sub)
      if (!existingUser) return token

      token.role = existingUser.role

      return token
    },
  },
  adapter: PrismaAdapter(db),
  session: { strategy: 'jwt' },
} satisfies NextAuthConfig

auth.ts file

import authConfig from '@/auth.config'
import NextAuth from 'next-auth'

export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth(authConfig)

middleware.ts file

import authConfig from '@/auth.config'
import {
  administratorRoutePrefix,
  apiAuthPrefix,
  authRoutes,
  operatorRoutePrefix,
  publicRoutes,
} from '@/routes'
import { Role } from '@prisma/client'
import NextAuth from 'next-auth'

const { auth } = NextAuth(authConfig)

export default auth((req) => {
  const { nextUrl } = req
  const isLoggedIn = !!req.auth

  const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix)
  const isPublicRoute = publicRoutes.includes(nextUrl.pathname)
  const isAuthRoute = authRoutes.includes(nextUrl.pathname)
  const isAdministratorRoute = nextUrl.pathname.startsWith(
    administratorRoutePrefix,
  )
  const isOperatorRoute = nextUrl.pathname.startsWith(operatorRoutePrefix)

  const isAdministrator = req.auth?.user.role === Role.ADMINISTRATOR
  const isOperator = req.auth?.user.role === Role.OPERATOR
  const isGuest = req.auth?.user.role === Role.GUEST

  if (isApiAuthRoute) return

  if (isAuthRoute) {
    if (isLoggedIn && isAdministrator) {
      return Response.redirect(new URL(administratorRoutePrefix, nextUrl))
    }

    if (isLoggedIn && isOperator) {
      return Response.redirect(new URL(operatorRoutePrefix, nextUrl))
    }

    if (isLoggedIn && isGuest) {
      return Response.redirect(new URL('/', nextUrl))
    }

    return
  }

  if (isLoggedIn) {
    if (isAdministratorRoute && !isAdministrator) {
      return Response.redirect(
        new URL('/signin?message=Not Authorized', nextUrl),
      )
    }

    if (isOperatorRoute && !isOperator) {
      return Response.redirect(
        new URL(`/signin?message=Not Authorized`, nextUrl),
      )
    }

    return
  }

  if (!isLoggedIn && !isPublicRoute) {
    let callbackUrl = nextUrl.pathname

    if (nextUrl.search) callbackUrl += nextUrl.search

    const encodedCallbackUrl = encodeURIComponent(callbackUrl)

    return Response.redirect(
      new URL(`/signin?callbackUrl=${encodedCallbackUrl}`, nextUrl),
    )
  }

  return
})

export const config = {
  matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/(api|trpc)(.*)'],
}
ndom91 commented 2 months ago

@vexra thanks for chiming in with another example!

I just want to clarify one thing - about the auth.config.ts and middleware split.

This was an old workaround required because many database adapters didn't work in edge runtimes yet (i.e. middlware).

So if you're not using a database, you don't need to do this split at all. In addition, many DB pkgs have been updated over the last months / year, for example prisma, and it may not be necessary when using prisma anymore either. Please check out our prisma docs here: https://authjs.dev/getting-started/adapters/prisma#edge-compatibility

sKopheK commented 1 week ago

I have issue making user.id number - change it from string defined in AdapterUser