
Wechat auth with authjs for Next.js app
Wechat provides web login based on the OAuth2.0 protocol.
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.
Workaround
The Auth.js provides the Credentials pattern for external authentication mechanisms.
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
codeforopenid/unionidto construct or find a user
The flow should be like below,
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.authorizedto check theauth.userto 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
signInpage 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/loginAPI 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.