
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
code
foropenid/unionid
to 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.authorized
to check theauth.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.