Sveltekit + Lucia Auth (Google OAuth) + MongoDB

Nos estaremos basando en el tutorial de Lucia resumiendolo y traduciendolo al español. Puedes ver el codigo completo en mi github API basica de sesiones con MongoDB Las sesiones permiten mantener el estado en el servidor, especialmente útil para la autenticación y la identificación del usuario. A cada sesión se le asigna un ID único que se guarda en el servidor y se usa como token. El cliente lo envía en solicitudes posteriores para asociarlas con su sesión y usuario. El ID de sesión puede guardarse en cookies o en el almacenamiento local del navegador, pero se recomienda usar cookies porque son más seguras contra XSS y más fáciles de manejar. Conexion con MongoDB Podemos utilizar MongoDB en la nube con atlas Después de registrate, puedes obtener tu cadena de conexion dando click en "connect" Podemos seleccionar la opción VS Code La guardamos en un archivo .env como MONGODB_URI Y exportamos la conexión como 'client' desde src/lib/server/db.js import { MongoClient } from 'mongodb'; import { MONGODB_URI } from '$env/static/private'; const uri = MONGODB_URI; let client; client = new MongoClient(uri); export default client; Generando ID de sesion Los usuarios usarán un token de sesión vinculado a una sesión, en lugar del ID directamente. El ID de sesión será un hash SHA-256 del token. Como SHA-256 es una función irreversible, incluso si hay una filtración de la base de datos, un atacante no podrá obtener tokens válidos. Utilizaremos Oslo para diversas operaciones. npm i @oslojs/encoding @oslojs/crypto Creando la API El token de sesión será una cadena de mínimo 20 bytes aleatorios codificada en base32 // src/lib/server/session.js import { encodeBase32LowerCaseNoPadding } from "@oslojs/encoding"; // ... export function generateSessionToken(): string { const bytes = new Uint8Array(20); crypto.getRandomValues(bytes); const token = encodeBase32LowerCaseNoPadding(bytes); return token; } El ID de sesión será un hash SHA-256 del token y tendrá una expiración de 30 días. import client from '$lib/server/db.js'; import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; // ... export async function createSession(token, userId) { const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); const session = { id: sessionId, userId, expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30) }; try { const mongoClient = await client.connect(); const database = mongoClient.db('adminhood'); const sessions = database.collection('sessions'); await sessions.insertOne(session); return session; } catch (error) { console.log(error); } } Validación de sesiones (2 pasos): Existe la sesión en la base de datos? Aun no ha expirado? Si la sesión está cerca de expirar se extenderá su vigencia., lo que mantiene activas las sesiones en uso y elimina las inactivas. Devolveremos tanto la sesión como el usuario asociado a ese ID. import client from '$lib/server/db.js'; import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; export async function validateSessionToken(token) { const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); try { const mongoClient = await client.connect(); const db = mongoClient.db('adminhood'); const sessions = db.collection('sessions'); const session = await sessions.findOne({ id: sessionId }); if (session === null) { return { session: null, user: null }; } const user = await db.collection('users').findOne({ _id: session.userId }); if (Date.now() >= session.expiresAt.getTime()) { await sessions.deleteOne({ id: sessionId }); return { session: null, user: null }; } if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) { session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30); const filter = { id: session.id }; const updateDoc = { $set: { expiresAt: session.expiresAt } }; await sessions.updateOne(filter, updateDoc); } return { session, user }; } catch (error) { console.log(error); } } Finalmente, invalidamos la sesionse simplemente borrandola de MongoDB. import client from '$lib/server/db.js'; export async function invalidateSession(sessionId) { try { const mongoClient = await client.connect(); const db = mongoClient.db('adminhood'); const sessions = db.collection('sessions'); await sessions.deleteOne({ id: sessionId }); } catch (error) { console.log(error); } }

Apr 26, 2025 - 09:52
 0
Sveltekit + Lucia Auth (Google OAuth) + MongoDB

Nos estaremos basando en el tutorial de Lucia resumiendolo y traduciendolo al español.
Puedes ver el codigo completo en mi github

API basica de sesiones con MongoDB

Las sesiones permiten mantener el estado en el servidor, especialmente útil para la autenticación y la identificación del usuario. A cada sesión se le asigna un ID único que se guarda en el servidor y se usa como token. El cliente lo envía en solicitudes posteriores para asociarlas con su sesión y usuario.

El ID de sesión puede guardarse en cookies o en el almacenamiento local del navegador, pero se recomienda usar cookies porque son más seguras contra XSS y más fáciles de manejar.

Conexion con MongoDB

Podemos utilizar MongoDB en la nube con atlas
Después de registrate, puedes obtener tu cadena de conexion dando click en "connect"

mongodb atlas
Podemos seleccionar la opción VS Code
Opciones de cadenas de conexion de mongodb
La guardamos en un archivo .env como MONGODB_URI

Y exportamos la conexión como 'client' desde src/lib/server/db.js

import { MongoClient } from 'mongodb';
import { MONGODB_URI } from '$env/static/private';

const uri = MONGODB_URI;

let client;

client = new MongoClient(uri);

export default client;

Generando ID de sesion

Los usuarios usarán un token de sesión vinculado a una sesión, en lugar del ID directamente. El ID de sesión será un hash SHA-256 del token. Como SHA-256 es una función irreversible, incluso si hay una filtración de la base de datos, un atacante no podrá obtener tokens válidos.

Utilizaremos Oslo para diversas operaciones.
npm i @oslojs/encoding @oslojs/crypto

Creando la API

El token de sesión será una cadena de mínimo 20 bytes aleatorios codificada en base32

// src/lib/server/session.js
import { encodeBase32LowerCaseNoPadding } from "@oslojs/encoding";

// ...

export function generateSessionToken(): string {
    const bytes = new Uint8Array(20);
    crypto.getRandomValues(bytes);
    const token = encodeBase32LowerCaseNoPadding(bytes);
    return token;
}

El ID de sesión será un hash SHA-256 del token y tendrá una expiración de 30 días.

import client from '$lib/server/db.js';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";

// ...

export async function createSession(token, userId) {
    const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
    const session = {
        id: sessionId,
        userId,
        expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30)
    };
    try {
        const mongoClient = await client.connect();
        const database = mongoClient.db('adminhood');
        const sessions = database.collection('sessions');
        await sessions.insertOne(session);
        return session;
    } catch (error) {
        console.log(error);
    }
}

Validación de sesiones (2 pasos):

  1. Existe la sesión en la base de datos?
  2. Aun no ha expirado?

Si la sesión está cerca de expirar se extenderá su vigencia., lo que mantiene activas las sesiones en uso y elimina las inactivas.

Devolveremos tanto la sesión como el usuario asociado a ese ID.

import client from '$lib/server/db.js';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";

export async function validateSessionToken(token) {
    const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));

    try {
        const mongoClient = await client.connect();
        const db = mongoClient.db('adminhood');
        const sessions = db.collection('sessions');
        const session = await sessions.findOne({ id: sessionId });

        if (session === null) {
            return { session: null, user: null };
        }

        const user = await db.collection('users').findOne({ _id: session.userId });

        if (Date.now() >= session.expiresAt.getTime()) {
            await sessions.deleteOne({ id: sessionId });
            return { session: null, user: null };
        }
        if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) {
            session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30);
            const filter = { id: session.id };
            const updateDoc = {
                $set: {
                    expiresAt: session.expiresAt
                }
            };
            await sessions.updateOne(filter, updateDoc);
        }
        return { session, user };
    } catch (error) {
        console.log(error);
    }
}

Finalmente, invalidamos la sesionse simplemente borrandola de MongoDB.

import client from '$lib/server/db.js';

export async function invalidateSession(sessionId) {
    try {
        const mongoClient = await client.connect();
        const db = mongoClient.db('adminhood');
        const sessions = db.collection('sessions');
        await sessions.deleteOne({ id: sessionId });
    } catch (error) {
        console.log(error);
    }
}

Session cookies

Cookies

Protección contra falsificación de solicitud en sitios cruzados (CSRF) es obligatoria con cookies.
SvelteKit incluye protección CSRF básica por defecto usando la cabecera Origin.

Atributos recomendados para cookies de sesión:

  • HttpOnly: Solo accesibles desde el servidor
  • SameSite=Lax: Usar Strict para sitios críticos
  • Secure: Solo se envían por HTTPS (omitir en localhost)
  • Max-Age o Expires: Se debe Definir que persista la cookie
  • Path=/: Las cookies son accesibles desde todas las rutas

SvelteKit añade automáticamente "Secure" en producción.

// src/lib/server/session.js

// ...

export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date): void {
    event.cookies.set("session", token, {
        httpOnly: true,
        sameSite: "lax",
        expires: expiresAt,
        path: "/"
    });
}

export function deleteSessionTokenCookie(event: RequestEvent): void {
    event.cookies.set("session", "", {
        httpOnly: true,
        sameSite: "lax",
        maxAge: 0,
        path: "/"
    });
}

Validación de sesión

Los tokens de sesión pueden validarse con la función validateSessionToken(). Si la sesión es inválida, elimina la cookie de sesión.

Es importante establecer una nueva cookie de sesión tras la validación para extender su tiempo de vida.

Validaremos la sesión en el handle hook y pasaremos el contexto de autenticación actual a cada ruta.

import {
    validateSessionToken,
    setSessionTokenCookie,
    deleteSessionTokenCookie
} from '$lib/server/session';

export const handle = async ({ event, resolve }) => {
    const token = event.cookies.get('session') ?? null;
    if (token === null) {
        event.locals.user = null;
        event.locals.session = null;
        return resolve(event);
    }
    const { session, user } = await validateSessionToken(token);
    if (session !== null) {
        setSessionTokenCookie(event, token, session.expiresAt);
    } else {
        deleteSessionTokenCookie(event);
    }

    event.locals.session = session;
    event.locals.user = user;
    return resolve(event);
};

Tanto el usuario actual como la sesión estarán disponibles en las funciones load, actions y endpoints.

Google OAuth

Crea una App OAuth

Crea un cliente OAuth en Google Cloud Console. Configura la URI de redirección como como http://localhost:5173/api/oauth/google/callback. Guarda el Client ID y Secret en el archivo .env.

# .env
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""

instala Arctic

npm install arctic
Inicializa el proveedor de Google con el Client ID, Client Secret y la URI de redirección.

// src/lib/server/google-oauth.js
import { Google } from 'arctic';
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET } from '$env/static/private';

export const google = new Google(
    GOOGLE_CLIENT_ID,
    GOOGLE_CLIENT_SECRET,
    'http://localhost:5173/api/oauth/google/callback'
);

Pagina Sign in

Crea routes/login/+page.svelte y añade un botón que enlace a /login/google.

Sign in

href="/login/google">Sign in with Google

Crear URL de autorización

Crea una ruta API en routes/api/oauth/google/+server.js. Genera un estado y codigo de verificacíon, y crea una URL de autorización con los scopes openid y profile. Guarda el state y code verifier y redirije a la página de inicio de sesión de Google.

import { generateState, generateCodeVerifier } from 'arctic';
import { google } from '$lib/server/google-oauth';

export async function GET(event) {
    const state = generateState();
    const codeVerifier = generateCodeVerifier();
    const url = google.createAuthorizationURL(state, codeVerifier, ['openid', 'profile']);

    event.cookies.set('google_oauth_state', state, {
        path: '/',
        httpOnly: true,
        maxAge: 60 * 10, // 10 minutes
        sameSite: 'lax'
    });
    event.cookies.set('google_code_verifier', codeVerifier, {
        path: '/',
        httpOnly: true,
        maxAge: 60 * 10, // 10 minutes
        sameSite: 'lax'
    });

    return new Response(null, {
        status: 302,
        headers: {
            Location: url.toString()
        }
    });
}

Valida el callback

Crea una ruta API en routes/api/oauth/google/callback/+server.js para manejar el callback. Verifica que el state coincida con el almacenado, valida el código de autorización y guarda el codeVerifier. Si incluiste los scopes openid y profile, Google devolverá un ID token con el perfil del usuario. Revisa si el usuario ya está registrado; si no, créalo. Finalmente, genera una sesión y configura la cookie para completar la autenticación

import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/session';
import { google } from '$lib/server/google-oauth';
import { decodeIdToken } from 'arctic';
import { getUserFromGoogleId, createUser } from '$lib/server/user.js';

export async function GET(event) {
    const code = event.url.searchParams.get('code');
    const state = event.url.searchParams.get('state');
    const storedState = event.cookies.get('google_oauth_state') ?? null;
    const codeVerifier = event.cookies.get('google_code_verifier') ?? null;
    if (code === null || state === null || storedState === null || codeVerifier === null) {
        return new Response(null, {
            status: 400
        });
    }
    if (state !== storedState) {
        return new Response(null, {
            status: 400
        });
    }

    let tokens;
    try {
        tokens = await google.validateAuthorizationCode(code, codeVerifier);
    } catch (e) {
        // Invalid code or client credentials
        console.log(e);
        return new Response(null, {
            status: 400
        });
    }
    const claims = decodeIdToken(tokens.idToken());
    const googleUserId = claims.sub;
    const name = claims.name;

    const existingUser = await getUserFromGoogleId(googleUserId);

    if (existingUser !== null) {
        const sessionToken = generateSessionToken();
        const session = await createSession(sessionToken, existingUser._id);
        setSessionTokenCookie(event, sessionToken, session.expiresAt);
        return new Response(null, {
            status: 302,
            headers: {
                Location: '/profile'
            }
        });
    }

    const user = await createUser(googleUserId, name);

    const sessionToken = generateSessionToken();
    const session = await createSession(sessionToken, user._id);
    setSessionTokenCookie(event, sessionToken, session.expiresAt);
    return new Response(null, {
        status: 302,
        headers: {
            Location: '/profile'
        }
    });
}

Consigue el usuario actual

Puedes conseguir la sesión y el usuario actuales desde locals

// routes/profile/+page.server.js
import { redirect } from '@sveltejs/kit';

export const load = async ({ locals }) => {
    if (!locals.user) {
        return redirect(302, '/login');
    }
    const username = locals.user.name;
    return { username };
};

Sign out

Implementa el sing out invalidando la sesión actual y remueve la cookie asociada.

import { fail, redirect } from '@sveltejs/kit';
import { invalidateSession, deleteSessionTokenCookie } from '$lib/server/session';

export const actions = {
    signOut: async (event) => {
        if (event.locals.session === null) {
            return fail(401);
        }
        await invalidateSession(event.locals.session.id);
        deleteSessionTokenCookie(event);
        return redirect(302, '/login');
    }
};

Resultado final

Finalmente podremos ver el nombre del usuario en la pagina de perfil.

// routes/profile/+page.svelte
<script>
    let { data } = $props();
</script>

<p>hello {data.username}</p>

<form method="post" use:enhance action="/?/signOut">
    <button>Logout</button>
</form>