next-auth: 'checks.state argument is missing' when using the custom JWT encode/decode methods

Environment

  System:
    OS: macOS 12.0.1
    CPU: (8) arm64 Apple M1 Pro
    Memory: 298.23 MB / 16.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 16.13.2 - ~/.nvm/versions/node/v16.13.2/bin/node
    Yarn: 1.22.17 - ~/.nvm/versions/node/v16.13.2/bin/yarn
    npm: 8.5.2 - ~/.nvm/versions/node/v16.13.2/bin/npm
  Browsers:
    Chrome: 99.0.4844.51
    Firefox: 97.0.2
    Safari: 15.1
  npmPackages:
    next: 12.1.0 => 12.1.0 
    next-auth: 4.3.0 => 4.3.0 
    react: 17.0.2 => 17.0.2 

Reproduction URL

https://github.com/boxyhq/jackson-hasura-nextjs/blob/main/pages/api/auth/[...nextauth].ts

Describe the issue

I’m having an issue with the next-auth. I’m customizing the JWT for Hasura by overriding the encode and decode methods.

In the [...nextauth].ts

export default NextAuth({
  providers: [
    BoxyHQSAMLProvider({
      issuer: `${process.env.BOXYHQ_SAML_URL}`,
      clientId: "dummy",
      clientSecret: "dummy",
    }),
  ],
  jwt: {
    encode: async ({ secret, token, maxAge }) => {
      console.log({ token });

      const jwtClaims = {
        sub: token?.sub,
        name: token?.name,
        email: token?.email,
        iat: Date.now() / 1000,
        exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60,
        expires: maxAge,
        "https://hasura.io/jwt/claims": {
          "x-hasura-allowed-roles": ["user"],
          "x-hasura-default-role": "user",
          "x-hasura-role": "user",
          "x-hasura-user-id": token?.sub,
        },
      };

      return jwt.sign(jwtClaims, secret, { algorithm: "HS256" });
    },
    decode: async ({ token, secret }) => {
      return jwt.verify(token as string, secret, {
        algorithms: ["HS256"],
      }) as any;
    },
  },
  debug: true,
});

I’m getting the following errors

[next-auth][error][OAUTH_CALLBACK_ERROR] 
https://next-auth.js.org/errors#oauth_callback_error checks.state argument is missing {
  error: {
    message: 'checks.state argument is missing',
    stack: 'TypeError: checks.state argument is missing\n' +
      '    at Client.oauthCallback (/node_modules/openid-client/lib/client.js:530:13)\n' +
      '    at oAuthCallback (/node_modules/next-auth/core/lib/oauth/callback.js:114:29)\n' +
      '    at async Object.callback (/node_modules/next-auth/core/routes/callback.js:50:11)\n' +
      '    at async NextAuthHandler (/node_modules/next-auth/core/index.js:139:28)\n' +
      '    at async NextAuthNextHandler (/node_modules/next-auth/next/index.js:21:19)\n' +
      '    at async /node_modules/next-auth/next/index.js:57:32\n' +
      '    at async Object.apiResolver (/node_modules/next/dist/server/api-utils/node.js:182:9)\n' +
      '    at async DevServer.runApi (/node_modules/next/dist/server/next-server.js:386:9)\n' +
      '    at async Object.fn (/node_modules/next/dist/server/base-server.js:488:37)\n' +
      '    at async Router.execute (/node_modules/next/dist/server/router.js:228:32)',
    name: 'TypeError'
  },
  providerId: 'boxyhq-saml',
  message: 'checks.state argument is missing'
}

The code is working perfectly if I remove jwt:{} section.

How to reproduce

We don’t have a live demo now.

Please see the code here https://github.com/boxyhq/jackson-hasura-nextjs/blob/main/pages/api/auth/[...nextauth].ts

I can share more information if needed.

Expected behavior

Nex-auth should return custom JWT successfully.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 10
  • Comments: 18

Most upvoted comments

In a month or so I’ll make/maintain a Hasura adapter.

If you’re using a custom JWT encoder, make sure to include the token.state in your claim under the state prop.

export const encode: JWTOptions['encode'] = async ({ secret, token }) => {
  if (!token) throw new Error('Missing token');
  const jwtClaims = {
    state: token.state, // <---- This is required for OAuth
    sub: token.sub,
    name: token.name,
    iat: Date.now() / 1000,
    exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60,
    'https://hasura.io/jwt/claims': {
      'x-hasura-allowed-roles': ['user'],
      'x-hasura-default-role': 'user',
      'x-hasura-role': 'user',
      'x-hasura-user-id': token.sub,
    },
  };
  return jwt.sign(jwtClaims, secret, { algorithm: 'RS512' });
};

Thanks, @nasatome. I got it working finally.

export default NextAuth({
  adapter: HasuraAdapter({
    endpoint: env.hasura.endpoint,
    adminSecret: env.hasura.adminSecret,
  }),
  providers: [
    BoxyHQSAMLProvider({
      issuer: env.jackson.endpoint,
      clientId: 'dummy',
      clientSecret: 'dummy',
    }),
  ],
  session: {
    strategy: 'jwt',
  },
  callbacks: {
    // Add the required Hasura claims
    async jwt({ token, profile }) {
      const allowedRoles = ['admin', 'developer'];
      const defaultRole = 'developer';

      // Fetch the group from profile and add it to the claims
      // If no group found in raw, assume user
      if (profile) {
        const raw = profile.raw as any;

        token['role'] = 'group' in raw ? raw.group : defaultRole;
      }

      return {
        ...token,
        'https://hasura.io/jwt/claims': {
          'x-hasura-allowed-roles': allowedRoles,
          'x-hasura-default-role': token.role,
          'x-hasura-role': token.role,
          'x-hasura-user-id': token?.sub,
        },
      };
    },

    // Add id and token (jwt) to the session object
    async session({ token, session }) {
      const encodedToken = jwt.sign(token!, env.nextauth.secret, {
        algorithm: 'HS256',
      });

      session.id = token.sub;
      session.token = encodedToken;
      session.role = token.role;

      return session;
    },
  },
  jwt: {
    async encode({ token, secret }) {
      return jwt.sign(token!, secret, { algorithm: 'HS256' });
    },
    async decode({ token, secret }) {
      return jwt.verify(token as string, secret, {
        algorithms: ['HS256'],
      }) as JWT;
    },
  },
});

@corysimmons , you’re absolutely right , the latest version doesn’t support checks: 'both'. the reason why I mentioned ‘both’ is because I was experimenting with GitHub OAuth provider so when I encountered the issue I thought of adding check for "state" but that doesn’t seems to work with me, the check for "both" worked just fine.

try adding checks: "both" on your provider

 providers: [
    BoxyHQSAMLProvider({
      issuer: `${process.env.BOXYHQ_SAML_URL}`,
      clientId: "dummy",
      clientSecret: "dummy",
      checks:"both",
    }),
  ]

I ended up switching off Hasura, but I did stumble upon this https://github.com/AmruthPillai/next-auth-hasura-adapter

After a lot of debugging, I discovered that Hasura documentation is outdated, it is made for version 3.x and we are using version 4.x of NextAuth.

here is a working example

import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import EmailProvider from 'next-auth/providers/email';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import { PrismaClient } from '@prisma/client';


let jwt = require('jsonwebtoken');

const prisma = new PrismaClient();

export default async function auth (req, res) {
  const providers = [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      authorization: {
        params: {
          prompt: 'consent',
          access_type: 'offline',
          response_type: 'code'
        }
      }
    }),
    EmailProvider({
      server: {
        host: process.env.EMAIL_SERVER_HOST,
        port: process.env.EMAIL_SERVER_PORT,
        auth: {
          user: process.env.EMAIL_SERVER_USER,
          pass: process.env.EMAIL_SERVER_PASSWORD
        }
      },
      from: process.env.EMAIL_FROM
    })

  ];

  return await NextAuth(req, res, {
      adapter: PrismaAdapter(prisma),
      providers,
      debug: false,
      session: {
        strategy: 'jwt',
        maxAge: 60 * 60 * 24,
        updateAge: 60 * 60 * 4
      },
        //openssl genrsa -out private.pem 2048
        //openssl rsa -in private.pem -pubout -out public.pem
        //https://www.google.com/search?q=install+pbcopy&oq=install+pbcopy
        //awk -v ORS='\\n' '1' public.pem | pbcopy
        //in hasura env set:
        //HASURA_GRAPHQL_JWT_SECRET={ "type": "RS256", "key": "<insert-your-public-key-here>"}
        //in nextjs env set JWT_SECRET the result:
        //awk -v ORS='\\n' '1' private.pem | pbcopy
        //JWT_SECRET:-----BEGIN PRIVATE KEY-----\xxxxxxxx...xxxxxxxx=\n-----END PRIVATE KEY-----\n --> notes that it ends in \n
      secret: process.env.JWT_SECRET.replace(/\\n/gm, '\n'), 
      jwt: {
        // The maximum age of the NextAuth.js issued JWT in seconds.
        // Defaults to `session.maxAge`.
        maxAge: 60 * 60 * 24,
        // You can define your own encode/decode functions for signing and encryption
        async encode ({ secret, token, maxAge }) {
          return await jwt.sign(token, secret, { algorithm: 'RS256'});
        },
        async decode ({ secret, token, maxAge }) {
          return await jwt.verify(token, secret, { algorithms: ['RS256']});
        }
      },
      callbacks: {
        async jwt ({
          token,
          user
        }) {
          console.log('JWT function Invoked');
          console.log('USER in JWT:', user);
          console.log('token JWT:', token);
          if (user && user.id) {
            //databases queries to allowed roles from id user
            token['https://hasura.io/jwt/claims'] = {
              'x-hasura-allowed-roles': ['commercial', 'admin'],
              'x-hasura-default-role': 'admin',
              'x-hasura-user-id': user.id.toString()
            };
          }
          return token;
        },
        async session ({
          session,
          token,
          user
        }) {
          return session;
        },
        async signIn ({
          user,
          account,
          profile
        }) {
          console.log('==============================');
          console.log('user: ', user);
          console.log('account: ', account);
          console.log('profile: ', profile);
          console.log('==============================');
          // if (user.email.endsWith('@your-allowed-domain.com')) {
          //
          //   const currentUser = your query
          //
          //   if (currentUser?.disabled) {
          //     return false;
          //   }
          //
          //   return true;
          // }

          return false;
        }
      }
    }
  );
}

edit: hours later --> I have just discovered that I can comment on the secret line. // secret: process.env.NEXTAUTH_SECRET.replace(/\\n/gm, '\n'),

use the variable: NEXTAUTH_SECRET and place it in quotes in the .env file, so that it is not necessary to use the .replace(/\n/gm, '\n'),

image