Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 79 additions & 42 deletions apps/backend/src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,47 @@ interface OAuthCallbackQuery {
export async function authRoutes(app: FastifyInstance) {
// ─── GitHub OAuth ───

app.get('/github', async (request: FastifyRequest, reply: FastifyReply) => {
const redirectUri = `${process.env.BACKEND_URL}/auth/github/callback`;
const clientState = (request.query as any).state || '';
const state = clientState ? `${clientState}_${generateState()}` : generateState();

const params = new URLSearchParams({
client_id: (process.env.GITHUB_CLIENT_ID || '').trim(),
redirect_uri: redirectUri,
scope: 'read:user user:email',
state,
});
const authUrl = `${GITHUB_AUTH_URL}?${params}`;
console.log('--- GITHUB OAUTH REDIRECT ---');
console.log('URL:', authUrl);
return reply.redirect(authUrl);
app.get('/github', async (request: FastifyRequest, reply: FastifyReply) => {
const redirectUri = `${process.env.BACKEND_URL}/auth/github/callback`;
const clientState = (request.query as any).state || '';
const state = clientState ? `${clientState}_${generateState()}` : generateState();

// Store state in a short-lived signed cookie before redirecting
reply.setCookie('oauth_state', state, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 600, // 10 minutes — plenty for a login round-trip
});

app.get('/github/callback', async (request: FastifyRequest<{ Querystring: OAuthCallbackQuery }>, reply: FastifyReply) => {
const { code } = request.query;
if (!code) {
return reply.status(400).send({ error: 'Missing authorization code' });
}
const params = new URLSearchParams({
client_id: (process.env.GITHUB_CLIENT_ID || '').trim(),
redirect_uri: redirectUri,
scope: 'read:user user:email',
state,
});
const authUrl = `${GITHUB_AUTH_URL}?${params}`;
console.log('--- GITHUB OAUTH REDIRECT ---');
console.log('URL:', authUrl);
return reply.redirect(authUrl);
});

app.get('/github/callback', async (request: FastifyRequest<{ Querystring: OAuthCallbackQuery }>, reply: FastifyReply) => {
const { code, state } = request.query;

// ── CSRF check ──────────────────────────────────────────────────────────────
const storedState = (request.cookies as any)?.oauth_state;
if (!state || !storedState || state !== storedState) {
return reply.status(400).send({ error: 'Invalid or missing OAuth state — possible CSRF attack' });
}
// Clear the state cookie immediately — prevents replay
reply.clearCookie('oauth_state', { path: '/' });
// ────────────────────────────────────────────────────────────────────────────

if (!code) {
return reply.status(400).send({ error: 'Missing authorization code' });
}

try {
// Exchange code for token
Expand Down Expand Up @@ -141,30 +160,48 @@ export async function authRoutes(app: FastifyInstance) {

// ─── Google OAuth ───

app.get('/google', async (request: FastifyRequest, reply: FastifyReply) => {
const redirectUri = `${process.env.BACKEND_URL}/auth/google/callback`;
const clientState = (request.query as any).state || '';
const state = clientState ? `${clientState}_${generateState()}` : generateState();

const params = new URLSearchParams({
client_id: (process.env.GOOGLE_CLIENT_ID || '').trim(),
redirect_uri: redirectUri,
response_type: 'code',
scope: 'openid email profile',
state,
access_type: 'offline',
});
const authUrl = `${GOOGLE_AUTH_URL}?${params}`;
console.log('--- GOOGLE OAUTH REDIRECT ---');
console.log('URL:', authUrl);
return reply.redirect(authUrl);
app.get('/google', async (request: FastifyRequest, reply: FastifyReply) => {
const redirectUri = `${process.env.BACKEND_URL}/auth/google/callback`;
const clientState = (request.query as any).state || '';
const state = clientState ? `${clientState}_${generateState()}` : generateState();

// Store state in a short-lived signed cookie before redirecting
reply.setCookie('oauth_state', state, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 600,
});

app.get('/google/callback', async (request: FastifyRequest<{ Querystring: OAuthCallbackQuery }>, reply: FastifyReply) => {
const { code } = request.query;
if (!code) {
return reply.status(400).send({ error: 'Missing authorization code' });
}
const params = new URLSearchParams({
client_id: (process.env.GOOGLE_CLIENT_ID || '').trim(),
redirect_uri: redirectUri,
response_type: 'code',
scope: 'openid email profile',
state,
access_type: 'offline',
});
const authUrl = `${GOOGLE_AUTH_URL}?${params}`;
console.log('--- GOOGLE OAUTH REDIRECT ---');
console.log('URL:', authUrl);
return reply.redirect(authUrl);
});

app.get('/google/callback', async (request: FastifyRequest<{ Querystring: OAuthCallbackQuery }>, reply: FastifyReply) => {
const { code, state } = request.query;

// ── CSRF check ──────────────────────────────────────────────────────────────
const storedState = (request.cookies as any)?.oauth_state;
if (!state || !storedState || state !== storedState) {
return reply.status(400).send({ error: 'Invalid or missing OAuth state — possible CSRF attack' });
}
reply.clearCookie('oauth_state', { path: '/' });
// ────────────────────────────────────────────────────────────────────────────

if (!code) {
return reply.status(400).send({ error: 'Missing authorization code' });
}

try {
const tokenRes = await fetch(GOOGLE_TOKEN_URL, {
Expand Down