Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to redirect within authorization callback #12532

Open
ralphsmith80 opened this issue Jan 22, 2025 · 0 comments
Open

Unable to redirect within authorization callback #12532

ralphsmith80 opened this issue Jan 22, 2025 · 0 comments
Labels
bug Something isn't working triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime.

Comments

@ralphsmith80
Copy link

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: '[email protected]',
          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: '[email protected]',
          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: '[email protected]',
          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 [email protected], 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

@ralphsmith80 ralphsmith80 added bug Something isn't working triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime. labels Jan 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime.
Projects
None yet
Development

No branches or pull requests

1 participant