I am trying to create a Discord OAuth2 application from scratch. Everything has gone well except trying to implement state to prevent CSRF attacks. I don't know how to pass the state generated from the frontend to the backend API route to have it validated on the server side.
I tried to set up a cookie to store the value of the state value generated, but it's clearly not being passed to the backend.
Loginbutton.tsx
import React from 'react'
import { FaDiscord } from 'react-icons/fa6'
import styles from './button.module.css'
export default async function Loginbutton() {
const state = crypto.randomUUID();
// Store state in a cookie for verification later
document.cookie = `discord_oauth_state=${state}; path=/; Secure; HttpOnly; SameSite=Lax`;
// Take their email to log it into the database
const url = `;response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fdiscord%2Fcallback&scope=identify+email+guilds.members.read&state=${state}`;
return (
<div>
<a className={`${styles['button']} flex items-center rounded-[6px] py-2 w-fit space-x-3`} href={url} rel='noopener noreferrer' title='Sign In With Discord Button'>
<FaDiscord className='mt-[2px]' size={22} />
<span>Sign In With Discord</span>
</a>
</div>
)
}
Server-side API route
'use strict'
import axios from 'axios';
import { eq } from 'drizzle-orm';
import { NextRequest, NextResponse } from "next/server";
import { db } from '@/db/config';
import { discordUsers } from '@/db/schema';
import { createSession } from '@/app/lib/sessions/discordSession';
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const code = searchParams.get('code');
// const receivedState = searchParams.get('state');
if (!code) {
return NextResponse.json({ error: 'Code not found!' }, { status: 500 });
}
// Get stored state from cookie
const storedState = (await cookies()).get('discord_oauth_state')?.value;
if (!storedState || storedState !== receivedState) {
return NextResponse.json({ error: 'Invalid state!' }, { status: 403 });
}
const discordURL = "/";
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept-Encoding': 'application/x-www-form-urlencoded'
};
const params = new URLSearchParams({
client_id: process.env.CLIENT_ID!,
client_secret: process.env.CLIENT_SECRET!,
code: code as string,
grant_type: 'authorization_code',
redirect_uri: process.env.REDIRECT_URL!
})
// Token exchange block
try {
const response = await axios.post(`${discordURL}/oauth2/token`, params, { headers });
const { access_token } = response.data;
if (!access_token) {
return NextResponse.json({ error: 'Access token not found!' }, { status: 500 });
}
// User Data block
const userResponse = await axios.get(`${discordURL}/users/@me`, {
headers: {
Authorization: `Bearer ${access_token}`,
}
});
const { id: discordID, username } = userResponse.data;
// Check if the user exists
const existingUser = await db
.select()
.from(discordUsers)
.where(eq(discordUsers.discordID, discordID))
.limit(1);
if (existingUser.length > 0) {
const user = existingUser[0];
// Check for updates
const isChanged =
user.username !== username
if (isChanged) {
// Update user data if they exist
await db
.update(discordUsers)
.set({
username,
})
.where(eq(discordUsers.discordID, discordID));
}
} else {
// Insert new user, if record does not exist
await db.insert(discordUsers).values({
discordID,
username
});
console.log("User inserted");
}
// return NextResponse.json({ message: "Complete", status: 200 });
await createSession(discordID, username);
return NextResponse.redirect(process.env.CLIENT_REDIRECT_URL!);
} catch (error) {
if (axios.isAxiosError(error)) {
console.error(error.response?.data || error.message);
return NextResponse.json({ error: error.response?.data, status: error.response?.status });
}
}
}