Skip to content
This repository was archived by the owner on Mar 5, 2025. It is now read-only.

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

@beeGiaLe

Description

@beeGiaLe

Environment

  • Operating System: Linux
  • Node Version: v18.19.0
  • Nuxt Version: 3.10.0
  • CLI Version: 3.10.0
  • Nitro Version: 2.8.1
  • Package Manager: yarn@1.22.21
  • Builder: -
  • User Config: app, devtools, typescript, $production, $development, runtimeConfig, nitro, vite, plugins, image, build, modules, components, alias, css
  • Runtime Modules: @nuxt/image@1.3.0, (), @hebilicious/authjs-nuxt@0.3.5
  • Build Modules: -

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions