nextauthjs / next-auth

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

Yandex provider + Adaptor issue (invalid_grant(code has expired)) #11611

Open KhekhaevSalekh opened 2 months ago

KhekhaevSalekh commented 2 months ago

Provider type

Yandex

Environment

  System:
    OS: Windows 10 10.0.19045
    CPU: (8) x64 AMD Ryzen 5 1400 Quad-Core Processor
    Memory: 11.94 GB / 23.93 GB
  Binaries:
    Node: 20.16.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.21 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 10.8.1 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Chromium (127.0.2651.74)
    Internet Explorer: 11.0.19041.4355
  npmPackages:
    @auth/core: ^0.34.2 => 0.34.2
    @auth/hasura-adapter: ^1.4.2 => 1.4.2
    next: ^14.1.0 => 14.1.0
    next-auth: ^4.24.7 => 4.24.7
    react: ^18 => 18.3.1

Reproduction URL

https://github.com/KhekhaevSalekh/next-auth-Yandex-issue

Describe the issue

When I use Yandex provider with next-auth all goes well. But when I connect hasura adapter i get error.

The error I get is (invalid_grant(code has expired)). When a new user presses "Sign in" by Yandex he does not go to home page. He appears at sign-in page. Also there is not next-auth.session-token coockie. In the same time all necessary information is written in the database. When he presses "Sign in" by Yandex second time he is on the home page.

With Google and GitHub authorization everything is good. I have checked this behavior on vercel and locally.

How to reproduce

Just start project from Reproduction URL and try to sighin with yandex.

Expected behavior

Normal sign in flow without this error.

degibons commented 2 months ago

The same problem with Yandex provider.

Terminal output:

GET /api/auth/callback/yandex?code=9756967&cid=47kjwvgdk0fxmxmfvuzyd46v2r 200 in 2886ms
error { error: 'invalid_grant', error_description: 'Code has expired' }

[auth][debug]: callback route error details {
  "method": "GET",
  "query": {
    "code": "9756967",
    "cid": "47kjwvgdk0fxmxmfvuzyd46v2r"
  }
}
[auth][error] CallbackRouteError: Read more at https://errors.authjs.dev#callbackrouteerror
[auth][cause]: Error: TODO: Handle OAuth 2.0 response body error
    at handleOAuth (webpack-internal:///(rsc)/./node_modules/@auth/core/lib/actions/callback/oauth/callback.js:112:19)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
...

Also, sometimes there is an error with PKCE verification, but authorization occurs. Quite unstable provider.

[auth][error] InvalidCheck: PKCE code_verifier cookie was missing. Read more at https://errors.authjs.dev#invalidcheck
    at Object.use (webpack-internal:///(rsc)/./node_modules/@auth/core/lib/actions/callback/oauth/checks.js:57:19)

Environment

OS: Windows 10
Node: 20.11
Chrome 127.0.6533.120
next-auth: 5.0.0-beta.20
@auth/prisma-adapter: 2.4.2
next: 14.2.5
KhekhaevSalekh commented 2 months ago

The same problem with Yandex provider.

Terminal output:

GET /api/auth/callback/yandex?code=9756967&cid=47kjwvgdk0fxmxmfvuzyd46v2r 200 in 2886ms
error { error: 'invalid_grant', error_description: 'Code has expired' }

[auth][debug]: callback route error details {
  "method": "GET",
  "query": {
    "code": "9756967",
    "cid": "47kjwvgdk0fxmxmfvuzyd46v2r"
  }
}
[auth][error] CallbackRouteError: Read more at https://errors.authjs.dev#callbackrouteerror
[auth][cause]: Error: TODO: Handle OAuth 2.0 response body error
    at handleOAuth (webpack-internal:///(rsc)/./node_modules/@auth/core/lib/actions/callback/oauth/callback.js:112:19)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
...

Also, sometimes there is an error with PKCE verification, but authorization occurs. Quite unstable provider.

[auth][error] InvalidCheck: PKCE code_verifier cookie was missing. Read more at https://errors.authjs.dev#invalidcheck
    at Object.use (webpack-internal:///(rsc)/./node_modules/@auth/core/lib/actions/callback/oauth/checks.js:57:19)

Environment

OS: Windows 10
Node: 20.11
Chrome 127.0.6533.120
next-auth: 5.0.0-beta.20
@auth/prisma-adapter: 2.4.2
next: 14.2.5

I spent about a week troubleshooting this error, and the only workable solution I’ve found so far involves opening a second tab. However, I'm not satisfied with this workaround.

It would be great if the next-auth developers could consider supporting an OAuth flow similar to the one used by the Yandex provider.

The issue originates from the line in the signIn function from next-auth/react/index.js:

window.location.href = url.

To investigate further, you can insert a process.exit() command right before this line and log the URL for different providers (I tested with Google, GitHub, and Yandex). Then, in your browser's console, manually click on the URI. The behavior for Yandex differs from the others. It likely needs something like

window.open(url).

Regarding your second issue related to PKCE, you shouldn’t encounter this error if you haven't modified the default settings for the Yandex provider. Yandex defaults to using checks='state', so perhaps this parameter was changed (by you). Although I haven't extensively used it, even with the checks=['state', 'pkce'] setting, I didn't experience the same error.

KhekhaevSalekh commented 2 months ago

I will continue to add different approachs I found in the repository which is attached to this issue (reproduction url). I hope that next-auth developers will answer. Maybe I missed something and the error can be solved straightforward.

KhekhaevSalekh commented 2 months ago

The first approach is not acceptable (second tab) because it looks strange relative to the Google or GitHub. I will write second approach. I would like to read your thoughts. Is it acceptable approach? For my local and prod this works fine for now.

1) In Yandex Oauth API console for REDIRECT_URI we use something like this http://localhost:3000/api/customYandex. 2) In next-auth options configuration file for Yandex provider we use:

Yandex({
      clientId: process.env.YANDEX_CLIENT_ID!,
      clientSecret: process.env.YANDEX_CLIENT_SECRET!, 
      authorization:{ 
        params:{ 
          redirect_uri:`${process.env.NEXTAUTH_URL}/api/customYandex`
        },  
      },
      allowDangerousEmailAccountLinking: true,  
      httpOptions: {
        timeout: 10000,
      },
    }),

In openIDClient library there is by default written 10 sec. So maybe there is no need to write timeout: 10000. But when I saw this code for first time this parameter was written already. So I decide to not change this.

3) If in this api we write

import {NextRequest, NextResponse } from "next/server";
import { cookies } from 'next/headers';

export async function GET(req : NextRequest) {
  const url = new URL(req.url);
  console.log('URL', url)
  const code = url.searchParams.get('code');
  const cid = url.searchParams.get('cid');
  const state = url.searchParams.get('state');
  const redirectUrl = ${process.env.NEXTAUTH_URL}/api/auth/callback/yandex?code=${code}&cid=${cid}&state=${state};
  return NextResponse.redirect(redirectUrl);
}

we get two consols and the error `invalid_grant (code has expired).

3) So I change this code like this:

import {NextRequest, NextResponse } from "next/server";
import { cookies } from 'next/headers';

export async function GET(req : NextRequest) {
  const url = new URL(req.url);

  const code = url.searchParams.get('code');
  const cid = url.searchParams.get('cid');
  const state = url.searchParams.get('state');

  const cookieStore = cookies();
  const firstRequestReceived = cookieStore.get('yandex_oauth_first_received');

  if (!firstRequestReceived) {
    cookieStore.set('yandex_oauth_first_received', 'true', {
      maxAge: 15, 
      path: '/api/customYandex', 
    });
    return new Response(null, { status: 204 }); 
  }
  cookieStore.set('yandex_oauth_first_received', 'false', {
    maxAge: -1, 
    path: '/api/customYandex',
  });
  const redirectUrl = ${process.env.NEXTAUTH_URL}/api/auth/callback/yandex?code=${code}&cid=${cid}&state=${state};
  return NextResponse.redirect(redirectUrl);
}

What do you think about it? What problem can arise from this approach?

KhekhaevSalekh commented 2 months ago

The approach above works. But 1 of 20 attempts failed. I noted that it is enough to set timer like this:

import { NextResponse } from "next/server";

export async function GET(req) {
  const url = new URL(req.url);
  const code = url.searchParams.get('code');
  const cid = url.searchParams.get('cid');

  await new Promise(resolve => setTimeout(resolve, 5000));

  const redirectUrl = `http://localhost:3000/api/auth/callback/yandex?code=${code}&cid=${cid}`;
  return NextResponse.redirect(redirectUrl);
}
degibons commented 2 months ago

The approach above works. But 1 of 20 attempts failed. I noted that it is enough to set timer like this:

import { NextResponse } from "next/server";

export async function GET(req) {
  const url = new URL(req.url);
  const code = url.searchParams.get('code');
  const cid = url.searchParams.get('cid');

  await new Promise(resolve => setTimeout(resolve, 5000));

  const redirectUrl = `http://localhost:3000/api/auth/callback/yandex?code=${code}&cid=${cid}`;
  return NextResponse.redirect(redirectUrl);
}

Thanks for this workaround, it's worked for me. But I noticed that the described problem only occurs when the app starts "cold" (e.g. right after I run next dev and there is no cache for components). Experimentally, I found out that a 3 seconds timeout is enough for me.