lucia-auth / lucia

Authentication, simple and clean
https://lucia-auth.com
MIT License
9.19k stars 466 forks source link

[Bug]: lucia.validateSession returns `null` for both `session` and `user` using @lucia-auth/adapter-mongodb in Next.js App Router #1522

Closed iamalvisng closed 6 months ago

iamalvisng commented 6 months ago

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 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;
}
pilcrowOnPaper commented 6 months ago

The user table needs to have an _id field

https://lucia-auth.com/database/mongoose