the cover image of blog post Wechat auth with authjs for Next.js app

Wechat auth with authjs for Next.js app

2025-01-23
8 min read

Wechat provides web login based on the OAuth2.0 protocol. image Meanwhile Auth.js also supports OAuth flow for Next.js application; besides there is an official provider which was supposed to simplify the setup with minimun configuration as follows,

import { Auth } from "@auth/core" import WeChat from "@auth/core/providers/wechat" const request = new Request(origin) const response = await Auth(request, { providers: [WeChat({ clientId: AUTH_WECHAT_APP_ID, clientSecret: AUTH_WECHAT_APP_SECRET, platformType: "OfficialAccount", })], })

Everything looks like to be a good match and implementation a wechat authentication for a Next.js app doesn't seem to be a challenge.

Here comes the first problem

The wechat provider doesn't work, it throws AuthError. After some debugging and investigation it turns out that the Wechat API /sns/oauth2/access_token always returns the content-type as text/plain when receiving a request with application/w-www-form-urlencoded. This is not expected by the library oauth4webapi used by the built-in wechat provider. Probably Wechat API changed the api behaviour(bug?) afterwards. image

Workaround

The Auth.js provides the Credentials pattern for external authentication mechanisms.
image So basically instead of receiving the traditional credentails like email and password in the left part(example code) of illustration above , it will be the code for Wechat auth's case, and all we need is to verify the code and return a user, that's exactly the capability of the aforementioned Wechat API /sns/oauth2/access_token.

Tasks breakdown

Essentially to complete the Wechat auth, there are the main tasks,

  • session check and redirect to wechat entrypoint for non-logged-in access.
  • get the code from wechat redirection
  • exchange the code for openid/unionid to construct or find a user

The flow should be like below, image

Redirect to Wechat Entrypoint

  • Firstly, to recognize the non-logged-in access, the middleware is a perfect choice,
// middleware.ts // use `auth` for routes protection export { auth as middleware } from '@/auth'; export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], };
  • Then, use callbacks.authorized to check the auth.user to see if it is a logged-in access.
// auth.ts // the `auth` middleware underneath calls `callbacks.authroized` to check the session. export const authOptions: NextAuthConfig = { providers: [ CredentialsProvider({ credentials: { code: { type: 'text' }, }, async authorize(credentials) { ... }, }), ], callbacks: { authorized: async ({ auth, request: { nextUrl } }) => { const isLoggedIn = !!auth?.user; return isLoggedIn; }, }, };
  • Add the signIn page so the non-logged-in users can be redirected to /api/auth/login.
export const authOptions: NextAuthConfig = { pages: { signIn: '/api/auth/login', }, providers: [ CredentialsProvider({ credentials: { code: { type: 'text' }, }, ... }) ], ...
  • Prepare the /api/auth/login API with api route as follows.
// app/api/auth/[...nextauth]/route.ts export async function GET(request: NextRequest) { const { nextUrl } = request; // login entrypoint, redirec to wechat uri if (nextUrl.pathname === '/api/auth/login') { return redirectToWechat(); } return handlers.GET(request); } // app/actions/auth/login.ts 'use server' import { redirect } from 'next/navigation'; export async function redirectToWechat() { const appId = CONFIG.WECHAT.APPID; // The URI that Wechat redirect back with a code // e.g. https://{YOUR_WEB_APP}/api/auth/callback const redirectUri = CONFIG.WECHAT.REDIRECT_URI; const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect`; redirect(url); } }

Get the code from wechat redirect

This is the step 2 and step 3 in the flow diagram, if the first redirect to Wechat contains right parameters, Wechat will redirect back to the rediretUri with a code. It can be done with an API as follows,

// app/api/auth/[...nextauth]/route.ts import { signIn } from '@/auth'; export async function GET(request: NextRequest) { const { nextUrl } = request; // receiving callback from wechat, verifying the code and redirect if (nextUrl.pathname == '/api/auth/callback') { const code = nextUrl.searchParams.get('code'); if (!code) { return new Response('code missing', { status: 400 }); } await signIn('credentials', { code, redirect: true, }); } // login entrypoint, redirec to wechat uri if (nextUrl.pathname === '/api/auth/login') { return redirectToWechat(); } return handlers.GET(request); }

Exchange the code for the Wechat openId

In the Crendential pattern, the Authjs signIn will call the Credentials.authorized function to verify the credentials, it's the place where the authenticate logic should be put in; if the credentials are good a User should be returned for session construction.

import type { User } from 'next-auth'; export const authOptions: NextAuthConfig = { ... providers: [ CredentialsProvider({ credentials: { code: { type: 'text' }, }, async authorize(credentials) { try { if (!credentials?.code) { throw new Error('No code provided'); } // getAccessToken calls wechat api: `https://api.weixin.qq.com/sns/oauth2/access_token`` const wxToken = await getAccessToken(credentials.code); if (!wxToken || !wxToken.openid) { throw new Error('Invalid wechat code'); } const openId = tokenData.openid; const user = await findOrCreateUser(openId); return { id: user.id, openId: user.openId, } as User; } catch (error) { logger.error({ error }, 'Authentication error'); return null; } }, }), ],

As long as a User is returned, Authjs will take care of session and cookie setup.

That is pretty much it about how to implement wechat auth with authjs.

© 2025 Xavier Zhou