Recently I was working on the onboarding experience for my startup KueCloud Genius and came across an interesting authentication problem. For authentication, OAuth and NextAuth.js is being used to give users a convenient and secure login process. This allows them to sign into our site using their existing accounts from major services like Google, Facebook, or Twitter. This eliminate the need to create and remember new usernames and passwords. And that's just speaking about the user experience. The developer experience is great too. But.. I digress.
Back to the problem. Before user invitations were implemented into the platform, the onboarding experience required a user to have the Manage Server permission on the server where they intended to add Genius. This permission allows the user to add Genius bot and the application commands to interact with it. The scope we previously required was: email identify bot applications.commands
.
The scope worked great until I realized that not every Discord user had to have bot applications.commands
scope, only the initial user setting up the Genius account. Not only was this scope required for invited users but they most likely did not have the Manage Server permission. So the main issue here is that invited users were being asked for bot applications.commands
scope when they were not authorized. This was preventing invited users from authenticating properly.
The solution to this problem lies in [...nextauth].js
. Per the documentation:
In Next.js, you can define an API route that will catch all requests that begin with a certain path. Conveniently, this is called Catch all API routes. When you define a/pages/api/auth/[...nextauth]
JS/TS file, you instruct NextAuth.js that every API request beginning with/api/auth/*
should be handled by the code written in the[...nextauth]
file.
The key takeaway from the documentation above is that this file is simply just an API endpoint handler.
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
// We can do anything here. This is where our solution will be implemented.`
return await NextAuth(req, res, {
// Configure the options such as the OAuth providers
...
DiscordProvider({
clientId: process.env.DISCORD_ID,
clientSecret: process.env.DISCORD_SECRET,
authorization: {
params: {
scope: process.env.DISCORD_SCOPE,
permissions: process.env.DISCORD_PERMISSIONS
}
}
}),
...
}
I searched the documentation for days before this clicked. It's just an API endpoint. I have access to the request and response objects!
The solution was actually easier than I thought. The solution I implemented was to add some logic before returning from the NextAuth
method. I used a cookie to determine if the user logging in was an invited user. If they were, then the scope required is only email identify
.
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
// Check for the invite code cookie
const inviteCode = req.cookies.inviteCode as string;
// If the user has an invite code change the scope
const discordScope = inviteCode ?
process.env.DISCORD_SCOPE : // "email identify"
process.env.DISCORD_ADMIN_SCOPE; // "email identify bot applications.commands"
return await NextAuth(req, res, {
// Configure the options such as the OAuth providers
...
DiscordProvider({
clientId: process.env.DISCORD_ID,
clientSecret: process.env.DISCORD_SECRET,
authorization: {
params: {
scope: discordScope,
permissions: process.env.DISCORD_PERMISSIONS
}
}
}),
...
}
As you can see the main difference here is making the Discord provider's scope
property dynamic using inviteCode
and a basic ternary operator.
And there you have it! Easy huh? Obvious? Perhaps. I feel that I am pretty good at reading and understanding documentation. I was in a hurry to get this fixed and it's obvious to me now that I was working too fast. The lesson I took away from this is that it's ok to take your time and otherwise you'll miss the details. That applies to life too.
Have a good one and thanks for reading!