Dans cette vidéo, je vous montre pas à pas comment mettre en place un système complet de gestion des utilisateurs et d’authentification dans votre application SvelteKit grâce à Lucia Auth. Vous apprendrez notamment comment intégrer simplement la connexion avec Google, gérer efficacement les sessions grâce aux jetons et cookies, tout en utilisant une base de données SQLite. Je vous partage également des astuces pratiques sur l’utilisation du gestionnaire de paquets pnpm et la base à connaître sur Git et GitHub pour bien débuter votre projet web. Ce tutoriel est parfaitement adapté aux développeurs débutants qui veulent apprendre à sécuriser leurs applications web et gérer facilement leurs utilisateurs.
📦 Le code de départ de la vidéo précédente : https://github.com/tim-tiret/courses/tree/3cdda6a4e48203fcce8d8243fa12a464333ad8f0
📦 Le code complet est disponible ici : https://github.com/tim-tiret/courses/tree/34d3ff0dc81a52f8eb04e6eebf5cc71698aac803
Prérequis
- Éditeur de code : https://code.visualstudio.com/
- Node JS : https://nodejs.org/fr
- Connaissances de base en SQL : https://timtiret.com/apprendre-sql-bases-de-donnees-relationnelles-en-2025/
- Bases de SvelteKit : https://timtiret.com/creer-une-application-web-de-a-a-z/
Théorie sur l'authentification
Le client ne comprend que le HTML, le CSS et le JavaScript pour gérer l'interface et l'interactivité tandis que le serveur se chargé d'héberger l'application, de traiter les donnés et la sécurité. C'est lui qui décide d'accorder l'accès à une ressource à un utilisateur car l'on ne fait jamais confiance au client.

Pour créer un système d'authentification, 2 tables sont essentielles, celle des utilisateurs et celle des sessions. Une session est associée à un utilisateur, possède une date d'expiration et a également un Jeton. Ce jeton peut-être vu comme une carte d'identité qui permet à l'utilisateur de prouver que c'est lui. C'est une chaîne de caractères aléatoire (texte avec lettres aléatoires).

Quand on fait une requête sur une route nécessitant l'authentification (route protégée) mais que l'on a pas de jeton, le serveur nous refuse l'accès, même avec un s'il te plaît !

Mais lorsque l'on utilise un jeton valide, ça fonctionne quand même mieux ! Le système vérifie alors que la session existe et qu'elle n'est pas expirée et que l'utilisateur est autorisé à accéder à la ressource qu'il demande, puis si c'est le cas, il récupère l'info et lui renvoie.

Avant de pouvoir faire utiliser notre jeton, il est nécessaire de s'authentifier. Pour cela l'utilisateur a besoin de prouver qu'il est bien qui il prétend être. Soit via un couple identifiant mot de passe, soit avec par exemple une signature générée lors d'une connexion avec Google ou avec un autre service. Une fois la preuve vérifié, le serveur génère le jeton et crée la session en base de données. On enregistre alors un cookie pour se souvenir que l'utilisateur est bien connecté.

En pratique voici ce qu'il faut faire
Nous partons ici de l'application créée dans cette vidéo, que je vous invite à voir si ce n'est pas encore fait : https://youtu.be/DoJqLnP2Dhs
Je précise que si vous préférez, vous pouvez également vous inspirer de la doc officielle de Lucia :
- SQLite : https://lucia-auth.com/sessions/basic-api/sqlite
- Cookies et Middleware SvelteKit : https://lucia-auth.com/sessions/cookies/sveltekit
- Google OAuth : https://lucia-auth.com/tutorials/google-oauth/sveltekit
Créer la base de données avec gestion des utilisateurs
import Database from 'better-sqlite3';
const connection = Database('courses.db');
connection.exec(`
CREATE TABLE user (
id INTEGER NOT NULL PRIMARY KEY,
google_id TEXT NOT NULL,
email TEXT NOT NULL,
username TEXT NULL
);
CREATE TABLE session (
id TEXT NOT NULL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES user(id),
expires_at INTEGER NOT NULL
);
CREATE TABLE list (
id INTEGER PRIMARY KEY AUTOINCREMENT,
article TEXT NOT NULL,
user_id INTEGER NOT NULL
);
`);
Installer les dépendances (Oslo et Arctic)
pnpm i @oslojs/encoding @oslojs/crypto arctic
Connexion à la base de données
Pour gérer la connexion à la base de données sans devoir la refaire à chaque fois je crée ce fichier : /src/lib/db.js
import Database from 'better-sqlite3';
export const db = Database('courses.db');
export
permet que db
soit accessible depuis n'importe quel fichier.Gestions des session
Je crée ensuite un fichier dans mon lib : /src/lib/sessions.js
C'est ici que sont créées toutes les fonctions qui serviront à gérer mes sessions.
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
import { db } from './db';
import { sha256 } from '@oslojs/crypto/sha2';
export function generateSessionToken() {
const bytes = new Uint8Array(20);
crypto.getRandomValues(bytes);
const token = encodeBase32LowerCaseNoPadding(bytes);
return token;
}
export 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)
};
db.prepare('INSERT INTO session (id, user_id, expires_at) VALUES (?, ?, ?)').run([
session.id,
session.userId,
Math.floor(session.expiresAt.getTime() / 1000)
]);
return session;
}
export function validateSessionToken(token) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const currentsession = db
.prepare(
'SELECT session.id, session.user_id, session.expires_at, user.id AS userid FROM session INNER JOIN user ON user.id = session.user_id WHERE session.id = ?'
)
.all([sessionId]);
if (currentsession.length === 0) {
return { session: null, user: null };
}
const session = {
id: currentsession[0].id,
userId: currentsession[0].userid,
expiresAt: new Date(currentsession[0].expires_at * 1000)
};
const user = {
id: currentsession[0].userid
};
if (Date.now() >= session.expiresAt.getTime()) {
db.execute('DELETE FROM session WHERE id = ?', session.id);
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);
db.execute(
'UPDATE session SET expires_at = ? WHERE id = ?',
Math.floor(session.expiresAt.getTime() / 1000),
session.id
);
}
return { session, user };
}
export function invalidateSession(sessionId) {
db.prepare('DELETE FROM session WHERE id = ?').run(sessionId);
}
export async function invalidateAllSessions(userId) {
await db.prepare('DELETE FROM user_session WHERE user_id = ?').run(userId);
}
Gestion des cookies de sessions
Pour que le système retienne la carte d'identité d'un utilisateur, on place un cookie, voici donc 2 fonctions à ajouter à notre fichier sessions.js
qui permettent de faire cela.
export function setSessionTokenCookie(event, token, expiresAt) {
event.cookies.set('session', token, {
httpOnly: true,
sameSite: 'lax',
expires: expiresAt,
path: '/'
});
}
export function deleteSessionTokenCookie(event) {
event.cookies.set('session', '', {
httpOnly: true,
sameSite: 'lax',
maxAge: 0,
path: '/'
});
}
Middleware (vérification du jeton)
Afin de vérifier que l'utilisateur est connecté à chaque requête, l'on va créer ce que l'on appelle un middleware. C'est un système qui vérifie le jeton, la session et la validité et nous permet donc de récupérer l'utilisateur un peu partout côté serveur et de savoir s'il est connecté.
Pour cela je crée donc un fichier : /src/routes/hooks.server.js
import {
deleteSessionTokenCookie,
setSessionTokenCookie,
validateSessionToken
} from '$lib/sessions';
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);
};
La fonction handle
est automatiquement exécutée par SvelteKit à chaque requête. Je regarde donc s'il y a un token et si c'est le cas, je vérifie que la sessions est valide puis j'exécute resolve
, c'est à dire que je passe à la suite (le code qui se trouve dans le route courante).
Crée un projet chez Google
Pour obtenir une preuve de la part de Google que l'utilisateur est bien la personne qu'il prétend être, il nous faut demande à Google un identifiant et un secret qui nous permettrons de récupérer les information de l'utilisateur et de garantir que la signature fournie par Google est bien valide.
Pour cela, il est nécessaire de créer un projet sur Google Cloud Platform : https://console.cloud.google.com/
Une fois le projet créé, on crée un client OAuth : https://console.cloud.google.com/auth/overview/create
Pour plus de détails, cela se passe à 38 minutes et 57 secondes dans la vidéo 😉...
Une fois récupérés, je vais stocker les identifiants dans un fichier de variables d'environnement à la racine de mon projet /.env
GOOGLE_CLIENT_ID="xxxxxxxxxx.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="GOXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXX"
Configurer le provider Google Auth
Dans mon fichier sessions.js
, je vais ajouter le provider Google qui permet de configurer l'authentification avec Google.
import { Google } from 'arctic';
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET } from '$env/static/private';
// ... code existant ...
export const google = new Google(
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
'http://localhost:5173/login/google/callback'
);
Générer le lien de connexion Google
Avec arctic
, je peux maintenant créer un lien de connexion google qui me permettra de redirige l'utilisateur vers google le temps qu'il se connecte.
Je crée donc le fichier /src/routes/login/google/+server.js
:
import { google } from '$lib/sessions';
import { generateState, generateCodeVerifier } from 'arctic';
export async function GET(event) {
const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = google.createAuthorizationURL(state, codeVerifier, ['openid', 'profile', 'email']);
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()
}
});
}
Maintenant quand je me rends ou que je créer un lien vers localhost:5173/login/google
, je suis redirigé sur Google afin de me connecter.
Je crée donc ce lien dans ce fichier : /src/routes/login/+page.svelte
<h1>Sign in</h1>
<a href="/login/google">Sign in with Google</a>
Callback Google
Une fois que l'utilisateur s'est connecté avec Google, je dois récupérer la signature, la vérifier, récupérer les informations de l'utilisateur et enfin le créer ou le connecter.
Pour cela, je crée le fichier /src/routes/login/google/callback/+server.js
:
import { db } from '$lib/db';
import { createSession, generateSessionToken, google, setSessionTokenCookie } from '$lib/sessions';
import { decodeIdToken } from 'arctic';
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
return new Response(null, {
status: 400
});
}
const claims = decodeIdToken(tokens.idToken());
const googleUserId = claims.sub;
const username = claims.name;
const email = claims.email;
const existingUser = db.prepare('SELECT * FROM user WHERE google_id = ?').all([googleUserId]);
if (existingUser.length !== 0) {
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, existingUser[0].id);
setSessionTokenCookie(event, sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: '/'
}
});
}
const user = db
.prepare('INSERT INTO user (google_id, email, username) VALUES (?,?,?)')
.run([googleUserId, email, username]);
console.log(user);
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, user.lastInsertRowid);
setSessionTokenCookie(event, sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: '/'
}
});
}
Ça y est ! Lorsque je me connecte, j'ai maintenant un jeton et je suis 100% authentifié !
Adaptation multi utilisateurs de l'app
J'ai déjà adapté la base de données en ajoutant un user_id
sur les produits, maintenant il faut que je modifie légèrement le backend pour prendre en compte cet user_id
.
Je modifie donc mon fichier serveur : /src/routes/+page.server.js
import { fail, redirect } from '@sveltejs/kit';
import Database from 'better-sqlite3';
const connection = Database('courses.db');
export function load({ locals }) {
if (!locals.user) {
redirect(302, '/login');
}
console.log(locals.user.id);
const listedecourse = connection
.prepare('SELECT * FROM list WHERE user_id = ?')
.all([locals.user.id]);
return { courses: listedecourse };
}
export const actions = {
createitem: async ({ request, locals }) => {
if (!locals.user) {
redirect(302, '/login');
}
console.log('create item');
const formulaire = await request.formData();
const nomarticle = formulaire.get('article');
console.log(nomarticle);
connection
.prepare(`INSERT INTO list (article, user_id) VALUES (?, ?)`)
.run([nomarticle, locals.user.id]);
},
deleteitem: async ({ request, locals }) => {
if (!locals.user) {
redirect(302, '/login');
}
console.log('delete item');
const formulaire = await request.formData();
const idarticle = formulaire.get('id');
console.log(idarticle);
connection
.prepare(`DELETE FROM list WHERE id = ? AND user_id = ?`)
.run([idarticle, locals.user.id]);
}
};
Déconnexion
La possibilité de se déconnecter n'existe pas encore, alors ajoutons la maintenant !
Pour cela, je vais d'abord ajouter un formulaire avec bouton de déconnexion à ma page principale : /src/routes/+page.svelte
<form action="?/logout" method="POST">
<button type="submit"> Se déconnecter </button>
</form>
Ensuite, côté serveur, j'ajoute l'action logout
qui me permet de supprimer la sessions et les cookies de l'utilisateur (dans /src/routes/+page.server.js
).
import { deleteSessionTokenCookie, invalidateSession } from '$lib/sessions';
// ... code existant ...
export const actions = {
// ... actions existantes ...
logout: async (event) => {
if (event.locals.session === null) {
return fail(401);
}
await invalidateSession(event.locals.session.id);
deleteSessionTokenCookie(event);
return redirect(302, '/login');
}
}
Conclusion
C'est terminé ! 🎉
Merci pour votre lecture, j'espère vous avoir aidé.