Hi, the below setup pretty much based on the v3 docs, but for some reason lucia.valdiateSession in validateRequest function always return { user: null, session: null }.
// auth.ts
import { Lucia, type Session, type User } from 'lucia';
import { Google } from 'arctic';
import { MongodbAdapter } from '@lucia-auth/adapter-mongodb';
import { connectDB } from '@/shared/utils/server/db/mongodb';
import mongoose from 'mongoose';
import { cache } from 'react';
import { cookies } from 'next/headers';
await connectDB(); // mogoose connect
const adapter = new MongodbAdapter(mongoose.connection.collection('sessions'), mongoose.connection.collection('users'));
export const lucia = new Lucia(adapter, {
sessionCookie: {
// this sets cookies with super long expiration
// since Next.js doesn't allow Lucia to extend cookie expiration when rendering pages
expires: false,
attributes: {
// set to `true` when using HTTPS
secure: process.env.NODE_ENV === 'production',
},
},
getUserAttributes: (attributes) => {
return {
google_id: attributes.google_id,
google_email: attributes.google_email,
};
},
});
declare module 'lucia' {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: DatabaseUserAttributes;
DatabaseSessionAttributes: DatabaseSessionAttributes;
}
}
interface DatabaseUserAttributes {
google_id: string;
google_email: string;
}
interface DatabaseSessionAttributes {
provider_id: string;
}
export const validateRequest = cache(
async (): Promise<{ user: User; session: Session } | { user: null; session: null }> => {
const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null;
if (!sessionId) {
return {
user: null,
session: null,
};
}
const result = await lucia.validateSession(sessionId);
// next.js throws when you attempt to set cookie when rendering page
try {
if (result.session?.fresh) {
const sessionCookie = lucia.createSessionCookie(result.session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
if (!result.session) {
const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
} catch {}
return result;
},
);
export const googleAuth = new Google(process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, process.env.GOOGLE_REDIRECT_URI);
// app/login/google/route.ts
import { googleAuth } from '@/lib/auth';
import { generateCodeVerifier, generateState } from 'arctic';
import { cookies } from 'next/headers';
export async function GET(): Promise<Response> {
const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = await googleAuth.createAuthorizationURL(state, codeVerifier, {
scopes: ['profile', 'email'],
});
cookies().set('google_oauth_state', state, {
path: '/',
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 60 * 10,
sameSite: 'lax',
});
cookies().set('code_verifier', codeVerifier, {
secure: true,
path: '/',
httpOnly: true,
maxAge: 60 * 10,
});
return Response.redirect(url);
}
// login/google/callback/route.ts
import { googleAuth, lucia } from '@/lib/auth';
import { cookies } from 'next/headers';
import { OAuth2RequestError } from 'arctic';
import { generateId } from 'lucia';
import User from '@/models/mongoose/User';
import { type NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest): Promise<Response> {
const url = new URL(request.url);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const storedState = cookies().get('google_oauth_state')?.value ?? null;
const storedCodeVerifier = cookies().get('code_verifier')?.value ?? null;
if (!code || !storedState || !storedCodeVerifier || state !== storedState) {
return new NextResponse(null, { status: 400 });
}
try {
const { accessToken } = await googleAuth.validateAuthorizationCode(code, storedCodeVerifier);
const googleUserResponse = await fetch('https://www.googleapis.com/oauth2/v1/userinfo', {
headers: { Authorization: `Bearer ${accessToken}` },
});
const googleUser: GoogleUser = await googleUserResponse.json();
const existingUser = await User.findOne({ google_id: googleUser.id });
if (existingUser) {
const session = await lucia.createSession(existingUser.id, { provider_id: 'google' });
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return new NextResponse(null, {
status: 302,
headers: {
Location: '/',
},
});
}
const userId = generateId(15);
await User.create({
id: userId,
google_id: googleUser.id,
google_email: googleUser.email,
});
const session = await lucia.createSession(userId, { provider_id: 'google' });
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
cookies().delete('google_oauth_state');
cookies().delete('code_verifier');
return new NextResponse(null, {
status: 302,
headers: {
Location: '/',
},
});
} catch (e) {
console.log(e);
// the specific error message depends on the provider
if (e instanceof OAuth2RequestError) {
console.log(e.message);
// invalid code
return new NextResponse(null, {
status: 400,
});
}
return new NextResponse(null, {
status: 500,
});
}
}
interface GoogleUser {
id: string;
email: string;
}
// User schema
import { Schema, model, models } from 'mongoose';
const UserSchema = new Schema({
google_id: {
type: String,
required: true,
},
google_email: {
type: String,
required: true,
},
} as const);
const UserModel = () => model('User', UserSchema);
export default (models.User || UserModel()) as ReturnType<typeof UserModel>;
// Session schema
import { Schema, model, models } from 'mongoose';
const SessionSchema = new Schema(
{
_id: {
type: String,
required: true,
},
user_id: {
type: String,
required: true,
},
expires_at: {
type: Date,
required: true,
},
} as const,
{ _id: false },
);
const SessionModel = () => model('Session', SessionSchema);
export default (models.Session || SessionModel()) as ReturnType<typeof SessionModel>;
// page.tsx
import { lucia, validateRequest } from '@/lib/auth';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
export default async function Page() {
const { user } = await validateRequest();
if (!user) {
return redirect('/login');
}
return (
<>
<h1>Hello world</h1>
<form action={logout}>
<button type="submit">Sign out</button>
</form>
</>
);
}
async function logout(): Promise<ActionResult> {
'use server';
const { session } = await validateRequest();
if (!session) {
return {
error: 'Unauthorized',
};
}
await lucia.invalidateSession(session.id);
const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return redirect('/login');
}
interface ActionResult {
error: string | null;
}
Package
@lucia-auth/adapter-mongodb
Describe the bug
Hi, the below setup pretty much based on the v3 docs, but for some reason
lucia.valdiateSession
invalidateRequest
function always return{ user: null, session: null }
.