I'm trying to use NextAuth to authenticate with a custom oauth2 provider (Whoop), but after the login completes on the whoop servers and I'm redirected back to my application, NextAuth throws the following error:
[next-auth][error][OAUTH_CALLBACK_ERROR]
https://next-auth.js.org/errors#oauth_callback_error invalid_client (Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)) {
error: OPError: invalid_client (Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method))
at processResponse (webpack-internal:///(sc_server)/./node_modules/openid-client/lib/helpers/process_response.js:35:19)
at Client.grant (webpack-internal:///(sc_server)/./node_modules/openid-client/lib/client.js:1191:28)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async Client.oauthCallback (webpack-internal:///(sc_server)/./node_modules/openid-client/lib/client.js:520:30)
at async oAuthCallback (webpack-internal:///(sc_server)/./node_modules/next-auth/core/lib/oauth/callback.js:120:22)
at async Object.callback (webpack-internal:///(sc_server)/./node_modules/next-auth/core/routes/callback.js:18:83)
at async AuthHandler (webpack-internal:///(sc_server)/./node_modules/next-auth/core/index.js:202:38)
at async NextAuthRouteHandler (webpack-internal:///(sc_server)/./node_modules/next-auth/next/index.js:49:30)
at async NextAuth._args$ (webpack-internal:///(sc_server)/./node_modules/next-auth/next/index.js:83:24)
at async eval (webpack-internal:///(sc_server)/./node_modules/next/dist/server/future/route-modules/app-route/module.js:242:37) {
name: 'OAuthCallbackError',
code: undefined
},
providerId: 'whoop',
message: 'invalid_client (Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method))'
}
My config is as follows:
// /api/auth/[...nextauth]/route.ts
import NextAuth, { AuthOptions } from "next-auth";
export const authOptions: AuthOptions = {
// debug: true,
providers: [
{
id: "whoop",
name: "Whoop",
type: "oauth",
token: "https://api.prod.whoop.com/oauth/oauth2/token",
authorization: {
url: "https://api.prod.whoop.com/oauth/oauth2/auth",
params: {
scope: "read:profile read:workout read:recovery",
},
},
clientId: process.env.WHOOP_CLIENT_ID,
clientSecret: process.env.WHOOP_CLIENT_SECRET,
userinfo: "https://api.prod.whoop.com/developer/v1/user/profile/basic",
profile(profile) {
return {
id: profile.user_id,
first_name: profile.first_name,
last_name: profile.last_name,
email: profile.email,
};
},
}
]
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
I'm fairly certain that this isn't an issue with any of the variables present in the config. The clientID, secret, callback url, and scopes all work great with whoop. Additionally, the profile
function doesn't seem to be causing issues - I've tried setting the values manually without accessing the profile
argument and the error persists.
Any help on debugging this would be appreciated!
I've been dealing with this issue for a week and finally found the root of the problem:
[next-auth][error][OAUTH_CALLBACK_ERROR] https://next-auth.js.org/errors#oauth_callback_error invalid_client (Client authentication failed (e.g., unknown client, no client authentication included, or unsup error: OPError: invalid_client (Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method))
at processResponse (webpack-internal:///(rsc)/./node_modules/.pnpm/openid-client@5.6.4/node_modules/openid-client/lib/helpers/process_response.js:37:19)
at Client.grant (webpack-internal:///(rsc)/./node_modules/.pnpm/openid-client@5.6.4/node_modules/openid-client/lib/client.js:1206:28)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
I went into the process_response.js file of openid-client, did some digging and console logging and found a hint to the error:
Lines 31-35 of process_response.js:
function processResponse(response, { statusCode = 200, body = true, bearer = false } = {}) {
if (response.statusCode !== statusCode) {
console.log("processResponse body raw - ", response.body)
if (bearer) {
...
console.log() output:
processResponse body raw - {
error: 'invalid_client',
error_description: 'Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)',
error_hint: `The OAuth 2.0 Client supports client authentication method "client_secret_post", but method "client_secret_basic" was requested. You must configure thod" value to accept "client_secret_basic".`,
status_code: 401
}
I then searched for the meaning of this and found the client option on the docs of next-auth Client Option.
From this I added the client option to the config with the flag like the hint says:
client: {
token_endpoint_auth_method: "client_secret_post",
},
And then it worked first try. Here is my config file and invocation in NextAuthOptions object:
whoop.ts
import { OAuthConfig } from "next-auth/providers";
import axios from "axios";
import { OAuthProviderOptions } from "@/types";
const callbackUrl = `${process.env.NEXTAUTH_URL}/api/auth/callback/whoop`;
const authorizationURL = `${process.env.WHOOP_API_HOSTNAME}/oauth/oauth2/auth`;
const tokenURL = `${process.env.WHOOP_API_HOSTNAME}/oauth/oauth2/token`;
const whoopProfileURL = `${process.env.WHOOP_API_HOSTNAME}/developer/v1/user/profile/basic`;
const WhoopProvider = (options: OAuthProviderOptions): OAuthConfig<any> => ({
...{
id: "whoop",
name: "Whoop",
type: "oauth",
version: "2.0",
client: {
token_endpoint_auth_method: "client_secret_post",
},
authorization: {
url: authorizationURL,
params: {
scope:
"offline read:profile read:recovery read:cycles read:sleep read:workout read:body_measurement",
redirect_uri: callbackUrl,
response_type: "code",
},
},
token: {
url: tokenURL,
params: {
grant_type: "authorization_code",
redirect_uri: callbackUrl,
},
},
checks: ["pkce", "state"],
userinfo: {
async request(context) {
try {
const { access_token, expires_at, refresh_token } = context.tokens;
const options = {
method: "GET",
url: whoopProfileURL,
headers: {
Authorization: "Bearer " + access_token,
"Accept-Language": "en_US",
"Content-Type": "application/json",
},
};
const { data: user } = await axios(options);
return user;
} catch (error) {
console.log(error);
}
},
},
profile: (profile) => {
return {
id: profile.user_id,
name: `${profile.first_name} ${profile.last_name}`,
email: profile.email,
};
},
},
...options,
});
export default WhoopProvider;
NextAuthOptions:
import { prisma } from "@/lib/prisma";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import type { NextAuthOptions } from "next-auth";
import WhoopProvider from "@/lib/providers/whoop";
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
pages: {
signIn: "/login",
},
session: {
strategy: "jwt",
},
providers: [
WhoopProvider({
clientId: process.env.WHOOP_CLIENT_ID as string,
clientSecret: process.env.WHOOP_CLIENT_SECRET as string,
}),
],
callbacks: {
session({ session, token, user }) {
return session;
},
},
secret: process.env.NEXTAUTH_SECRET,
};