nextauthjs / nextauthjs/next-auth

Unable to redirect within authorization callback

Open
#12,532 0 comments 4 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug triage
Dominant language
TypeScript
Stars
28.4k
Forks
4k
PR merge metrics
No merged PRs in 30d

Description

Environment
  System:
    OS: Linux 5.15 Ubuntu 22.04.5 LTS 22.04.5 LTS (Jammy Jellyfish)
    CPU: (20) x64 Intel(R) Core(TM) i9-10850K CPU @ 3.60GHz
    Memory: 8.32 GB / 15.56 GB
    Container: Yes
    Shell: 5.8.1 - /usr/bin/zsh
  Binaries:
    Node: 20.17.0 - ~/.volta/tools/image/node/20.17.0/bin/node
    npm: 10.8.2 - ~/.volta/tools/image/node/20.17.0/bin/npm
    pnpm: 9.9.0 - ~/.volta/bin/pnpm
  Browsers:
    Chrome: 131.0.6778.108
  npmPackages:
    next: 15.1.5 => 15.1.5 
    next-auth: 5.0.0-beta.25 => 5.0.0-beta.25 
    react: ^19.0.0 => 19.0.0 
Reproduction URL

https://github.com/ralphsmith80/authjs-authorized-callback/blob/main/src/auth.ts#L39

Describe the issue

Issue a redirect in the authorized callback does not work. I've noted that it will works by adjusting the code in one of two ways. Either by adjusting the middleware or by not overriding the authorized callback, but if you need to apply middleware and override the authorized callback I think you're stuck.

Also located in the repo here

Breaking test case

export { auth as middleware } from '@/auth'

// Optionally, don't invoke Middleware on some paths
export const config = {
  /*
   * Match all routes except for the following:
   * - api/* (API routes)
   * - _next/static/* (static files)
   * - _next/image* (image optimization files)
   * - favicon.ico
   * - robots.txt
   * - home page (root route)
   */
  matcher: [
    '/((?!api|_next/static|_next/image|images|fonts|favicon.ico|robots.txt|$).*)',
  ],
}
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import { type Provider } from 'next-auth/providers'

const providers: Provider[] = [
  Credentials({
    name: 'credentials',
    credentials: {
      email: { label: 'Email', type: 'email', placeholder: 'smith' },
      password: { label: 'Password', type: 'password' },
    },
    async authorize(credentials) {
      const users = [
        {
          id: '1',
          email: 'test@test.com',
          name: 'test',
          password: 'pass',
        },
      ]
      const user = users.find(
        (user) =>
          user.email === credentials?.email &&
          user.password === credentials?.password
      )
      return user ? { id: user.id, email: user.email, name: user.name } : null
    },
  }),
]

export const { auth, handlers, signIn, signOut } = NextAuth({
  session: {
    strategy: 'jwt',
    maxAge: 5, // 5 seconds for testing
  },
  providers,
  callbacks: {
    authorized: async ({ auth }) => {
      // Logged in users are authenticated, otherwise redirect to login page based on the middleware matcher
      return !!auth
    },
  },
})

Working with middleware matcher adjustment

This is less ideal because it require the page to not be protected by middleware, but I'm showing it for completeness. The change is to modify the matcher to include the previously protected page dashboard. This results in the server action being invoked which contains the necessary protection and redirect.

export { auth as middleware } from '@/auth'

// Optionally, don't invoke Middleware on some paths
export const config = {
  /*
   * Match all routes except for the following:
   * - api/* (API routes)
   * - _next/static/* (static files)
   * - _next/image* (image optimization files)
   * - favicon.ico
   * - robots.txt
   * - home page (root route)
   */
  matcher: [
    '/((?!api|_next/static|_next/image|images|fonts|favicon.ico|robots.txt|dashboard$).*)',
  ],
}

Working without the authorized callback override

This is more ideal since it allows the page to be protected by middleware. I've removed the authorized callback in the auth.ts file. The down side is that this will not work if you need to override the authorized callback.

import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import { type Provider } from 'next-auth/providers'

const providers: Provider[] = [
  Credentials({
    name: 'credentials',
    credentials: {
      email: { label: 'Email', type: 'email', placeholder: 'smith' },
      password: { label: 'Password', type: 'password' },
    },
    async authorize(credentials) {
      const users = [
        {
          id: '1',
          email: 'test@test.com',
          name: 'test',
          password: 'pass',
        },
      ]
      const user = users.find(
        (user) =>
          user.email === credentials?.email &&
          user.password === credentials?.password
      )
      return user ? { id: user.id, email: user.email, name: user.name } : null
    },
  }),
]

export const { auth, handlers, signIn, signOut } = NextAuth({
  session: {
    strategy: 'jwt',
    maxAge: 5, // 5 seconds for testing
  },
  providers,
  callbacks: {
  },
})

Working with middleware matcher and authorized callback

This is the solution if you want your matcher protect that page and you want to override the authorized callback.

This does not work. How can we make this work?

import { NextResponse } from 'next/server'
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import { type Provider } from 'next-auth/providers'

const providers: Provider[] = [
  Credentials({
    name: 'credentials',
    credentials: {
      email: { label: 'Email', type: 'email', placeholder: 'smith' },
      password: { label: 'Password', type: 'password' },
    },
    async authorize(credentials) {
      const users = [
        {
          id: '1',
          email: 'test@test.com',
          name: 'test',
          password: 'pass',
        },
      ]
      const user = users.find(
        (user) =>
          user.email === credentials?.email &&
          user.password === credentials?.password
      )
      return user ? { id: user.id, email: user.email, name: user.name } : null
    },
  }),
]

export const { auth, handlers, signIn, signOut } = NextAuth({
  session: {
    strategy: 'jwt',
    maxAge: 5, // 5 seconds for testing
  },
  providers,
  callbacks: {
    authorized: async ({ auth, request }) => {
      // Logged in users are authenticated, otherwise redirect to login page based on the middleware matcher
      if (!auth) {
        const url = `${new URL(request.nextUrl.origin)}api/auth/signin`
        console.log('####not authorized', url)
        // return NextResponse.json('Invalid auth token', { status: 401 })
        return NextResponse.redirect(url)
        // return Response.redirect(url)
        // return false
      }

      return true
    },
  },
})
How to reproduce
  1. clone the repo
  2. install dependencies
  3. run the dev server
  4. currently it is in a working state with a 5 second timeout on the session
  5. login with the mock credentials test@test.com, pass
  6. wait 5+ seconds
  7. click either of the buttons
  8. works
  9. uncomment the authorized callback
  10. repeat steps 5, 6, and 7
  11. fails
Expected behavior

The user should be redirected to the sign in page.

Actual behavior

A missing CSRF error is logged in the server console.

[auth][error] MissingCSRF: CSRF token was missing during an action signin. Read more at https://errors.authjs.dev#missingcsrf

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the linked reproduction, especially middleware.ts and src/auth.ts, and run the documented steps after the five-second session timeout. Compare behavior with and without the authorized callback and with the matcher adjustment. Done means the protected-page action redirects to the sign-in page without logging MissingCSRF.

Written by the indexing model from the issue text.

Assessment

Tech stack
next.js, node.js, react, typescript
Domain
api, authentication, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.