Hebilicious / authjs-nuxt

AuthJS edge-compatible authentication Nuxt module.
https://authjs-nuxt.pages.dev/
MIT License
246 stars 30 forks source link

[Production Only] SignIn method calls api/auth/callback/credentials with error 403: CSRF Token Mismatch #166

Open beeGiaLe opened 4 months ago

beeGiaLe commented 4 months ago

Environment

Reproduction

  1. Url: https://admin.hexasync.com
  2. User & pwd: any user & password.
  3. Click Submit

Actual: the network console will returns 403: Forbidden because of CSRF Token Mismatch

I take a look at the networking behind the scene and saw that it always calls api/auth/callback/credentials? with undefined csrfToken.

Question: How can I set the csrfToken and how does the nuxt server api verify it? I don't see any setup for csrfToken in nuxt tutorial, neither this site's tutorial.

# request
Request URL:
https://admin.hexasync.com/api/auth/callback/credentials?
Request Method:
POST
Status Code:
403 Forbidden
Payload: 
- redirect: false
- username: team@beehexa.com
- password: abc123456
- csrfToken: undefined  // I don't know why this is undefined and how to fille its value.
- callbackUrl: https://admin.hexasync.com/login
# response
{
    "url": "/api/auth/callback/credentials?",
    "statusCode": 403,
    "statusMessage": "CSRF Token Mismatch",
    "message": "CSRF Token Mismatch",
    "stack": ""
}

Describe the bug

  1. There is no module related to csrf installed
  2. There is no security module installed
  3. The signIn() method always calls api/auth/callback/credentials and there is no way to set the csrfToken. Thus, the csrfToken always null/undefined.
  4. The nuxt server will not accept the request.

Below is the configurations

# nuxt.config.ts
import * as antd from 'ant-design-vue'
import { addComponent } from '@nuxt/kit'
import { resolve } from "node:path"

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  app: {
    // baseURL: '/profiles',
    head: {
      title: 'HexaSync Integration Platform',
      meta: [
        { name: 'description', content: 'HexaSync Admin' }
      ],
      link: [
        // { rel: 'stylesheet', href: 'https://fonts.googleapis.com/icon?family=Material+Icons' }
      ]
    }
  },
  devtools: { enabled: true },
  typescript: {
    shim: false,
    typeCheck: true
  },
  $production: {
    routeRules: {
      '/**': { isr: true }
    }
  },
  $development: {
    //
  },
  runtimeConfig: {
    // The private keys which are only available server-side
    // apiSecret: '123',
    // Keys within public are also exposed client-side
    authJs: {
      secret: process.env.NUXT_NEXTAUTH_SECRET, // You can generate one with `openssl rand -base64 32`
      guestRedirectTo: "/login",
      authenticatedRedirectTo: "/"
    },
    hexasync: {
      ssoSecret: process.env.SSO_SERVICE_SECRET,
      ssoUrl: process.env.SSO_SERVICE_URL,
      profileUrl: process.env.PROFILE_SERVICE_URL
    },
    // github: {

    // },
    public: {
      // apiBase: '/api'
      authJs: {
        baseUrl: process.env.NUXT_NEXTAUTH_URL,
        verifyClientOnEveryRequest: true,
      }
    }
  },
  nitro: {
    routeRules: {
      "/": { ssr: true, prerender: false },
      "/sso-proxy/**": { proxy: `${process.env.SSO_SERVICE_URL}/**` },
    }
  },
  vite: {
    vue: {
      customElement: true
    },
    vueJsx: {
      mergeProps: true
    }
  },
  plugins: [
  ],
  image: {
    inject: true,
    quality: 80
  },
  build: {
    transpile: ['lodash']
  },
  modules: [
    // '@nuxtjs/vuetify',
    // 'nuxt-vite',
    // '@nuxt/vite-builder',
    '@nuxt/image',
    async function (options, nuxt) {
      for (const key in antd) {
        if (['version', 'install'].includes(key)) continue
        await addComponent({
          filePath: 'ant-design-vue',
          name: `A${key}`,
          export: key
        })
      }
    },
    '@hebilicious/authjs-nuxt'
  ],
  components: {
    global: true,
    dirs: ['~/components']
  },
  alias: {
    cookie: resolve(__dirname, "node_modules/cookie")
  },
  css: [
    '~/assets/scss/main.scss'
  ]
})
# packages.json
{
  "name": "nuxt-app",
  "private": true,
  "type": "module",
  "lint": "eslint .",
  "lint:fix": "eslint . --fix",
  "scripts": {
    "build": "nuxt build --standalone",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "devDependencies": {
    "@nuxt/eslint-config": "^0.2.0",
    "@nuxt/vite-builder": "^3.10.0",
    "@types/jsonwebtoken": "^9.0.5",
    "@types/lodash": "^4.14.202",
    "@types/luxon": "^3.4.2",
    "@vitejs/plugin-vue": "^5.0.3",
    "@vitejs/plugin-vue-jsx": "^3.1.0",
    "@vue/babel-plugin-jsx": "^1.2.1",
    "eslint": "^8.56.0",
    "node-gyp": "^10.0.1",
    "nuxt": "^3.10.0",
    "nuxt-security": "^1.1.1",
    "sass": "^1.70.0",
    "typescript": "^5.3.3",
    "vue": "^3.4.15",
    "vue-router": "^4.2.5",
    "vue-tsc": "^1.8.27"
  },
  "dependencies": {
    "@ant-design/icons-vue": "^7.0.1",
    "@auth/core": "^0.17.0",
    "@hebilicious/authjs-nuxt": "^0.3.5",
    "@nuxt/image": "^1.3.0",
    "ant-design-vue": "^4.1.2",
    "chart.js": "^4.4.1",
    "chartjs-adapter-luxon": "^1.3.1",
    "jsonwebtoken": "^9.0.2",
    "lodash": "^4.17.21",
    "luxon": "^3.4.4",
    "node-addon-api": "^7.1.0",
    "vue-chartjs": "^5.3.0"
  }
}
# /server/api/auth/[...].ts
import CredentialsProvider from "@auth/core/providers/credentials"
import type { AuthConfig } from "@auth/core/types"

import { NuxtAuthHandler } from "#auth"
import { AccountService } from "~/services/accountService"
import { verifyToken } from "~/utils/jwt"

// The #auth virtual import comes from this module. You can use it on the client
// and server side, however not every export is universal. For example do not
// use sign-in and sign-out on the server side.

const runtimeConfig = useRuntimeConfig()

// Refer to Auth.js docs for more details

export const authOptions: AuthConfig = {
  // secret: runtimeConfig.authJs.secret,
  secret: process.env.NUXT_NEXTAUTH_SECRET,
  session: {
    strategy: 'jwt'
  },
  providers: [
    CredentialsProvider({
      id: 'credentials',
      type: 'credentials',
      name: 'credentials',
      credentials: {
        username: { label: "Username", type: "text", placeholder: "admin@beehexa.com" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        const accountService = new AccountService(runtimeConfig)
        const jwt = await accountService.login(credentials.username as string, credentials.password as string)
        const user = await accountService.me(jwt.accessToken)
        if (user?.email?.indexOf('beehexa.com') || user?.email?.indexOf('hexasync.com')) {
          return {...user, jwt: {...jwt}}
        }
        return null as any
      }
    })
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (!token) {
        return {}
      }
      // it is token, but it is not. It's a user info with jwt token
      let result = {...token} as any
      if (user) {
        result = {...result, ...user}
      }
      let {jwt} = result
      const accessToken = await verifyToken(jwt.accessToken)
      if (accessToken.isValid) {
        return {...result, claims: {...accessToken.decoded}}
      }
      const refreshToken = await verifyToken(jwt.refreshToken)
      if (!refreshToken.isValid) {
        return {}
      }

      const accountService = new AccountService(runtimeConfig)
      jwt = await accountService.refreshToken(jwt.refreshToken)
      const newAccessToken = await verifyToken(jwt.accessToken)

      return {
        ...result, 
        ...user,
        jwt: {...jwt},
        claims: {...newAccessToken.decoded}
      }
    },
    async session({ session, token }) {
      if (!token) {
        return {} as any
      }
      // it is token, but it is not. It's a user info with jwt token
      return {
        ...session,
        user: {
          ...token
        },
        token: {
          ...token
        }
      }
    } 
  }
}

export default NuxtAuthHandler(authOptions, runtimeConfig)

After click Submit, the Auth Module calls /api/auth/callback/credentials and received:

Request URL:
https://admin.hexasync.com/api/auth/callback/credentials?
Request Method:
POST
Status Code:
403 Forbidden
Payload: 
- redirect: false
- username: team@beehexa.com
- password: abc123456
- csrfToken: undefined
- callbackUrl: https://admin.hexasync.com/login

Response: {
    "url": "/api/auth/callback/credentials?",
    "statusCode": 403,
    "statusMessage": "CSRF Token Mismatch",
    "message": "CSRF Token Mismatch",
    "stack": ""
}

Additional context

No response

Logs

No response

beeGiaLe commented 4 months ago

Screenshot_20240208_175444_Edge.jpg

Screenshot_20240208_175517_Edge.jpg

Panca6 commented 4 months ago

same problem is anyone able to figure this out somehow?

espensgr commented 4 months ago

Auth.js has a skipCSRFcheck parameter in the config, but i cannot get it to work. It does not accept a Boolean it seems, it wants an Symbol 🤷‍♂️

Wazbat commented 1 month ago

I think I'm getting the same problem here? A CSRF protected error

ghyath5 commented 3 weeks ago

Is this package dead ??

I'm facing the same error in production. We can't event set skipCSRFcheck to true.

Wazbat commented 3 weeks ago

@ghyath5 I was able to resolve some of my problems by pinning @auth/core to a specific version in my package.json

{
    //...
    "@auth/core": "0.17.0",
    "@hebilicious/authjs-nuxt": "^0.3.5",
    //...
}