diff --git a/src/app.d.ts b/src/app.d.ts
index cde121735f2267b226266f67efa776c580c41892..155c2a39ef69b5804bf5ddc4e5c99e7c7f4063ec 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -5,27 +5,8 @@ declare namespace App {
   type OpenIdConnectLocals =
     import("$lib/server/openid_connect_handler").OpenIdConnectLocals
   type UserLocals = import("$lib/server/user_handler").UserLocals
-  interface Locals
-    extends CookiesLocals,
-      SessionLocals,
-      UserLocals,
-      OpenIdConnectLocals {
-    /// The server session stored in database
-    session: {
-      nonce?: string
-      /// URL to redirect to after authentication (or logout)
-      redirectUrl?: string
-      /// Temporary token used to retrieve session, when its cookie was not given in HTTP
-      /// request headers
-      /// For example, a HTTP request may omit the session cookie header after a redirection
-      /// from an external site.
-      token?: string
-      // /// Unique ID used to retrieve user
-      // userId?: number
-      // /// Token used by OpenIdConnect to uniquely identify user
-      // userIdToken?: string
-    }
-
+  interface Locals extends UserLocals, OpenIdConnectLocals {
+    id_token?: string
     user?: import("$lib/users").User
   }
   // interface PageData {}
diff --git a/src/lib/auditors/queries.ts b/src/lib/auditors/queries.ts
index c527ac448b82023ce8fe0b829f2155c316ab5f49..13960fa67c9d061748787a63cb5c2157b491e6c4 100644
--- a/src/lib/auditors/queries.ts
+++ b/src/lib/auditors/queries.ts
@@ -1,4 +1,3 @@
-import type { Auditor } from "@auditors/core"
 import {
   auditArray,
   auditChain,
@@ -9,6 +8,7 @@ import {
   auditString,
   auditSwitch,
   auditTest,
+  type Auditor,
 } from "@auditors/core"
 
 export function auditQueryArray(...auditors: Auditor[]) {
diff --git a/src/lib/auditors/responses.ts b/src/lib/auditors/responses.ts
index 7e23a8ba16c4d605c0ee33cc3e7a591ce84b19e1..e7bb16d33851d779236c5c7bffd5b6592fa194ea 100644
--- a/src/lib/auditors/responses.ts
+++ b/src/lib/auditors/responses.ts
@@ -4,20 +4,13 @@ import { laxAudit } from "@auditors/core"
 export function auditJsonResponse(auditor?: Auditor | null) {
   return async function (
     audit: Audit,
-    responseUnknown: unknown,
+    response: Response,
   ): Promise<[unknown, unknown]> {
-    if (responseUnknown == null) {
-      return [responseUnknown, null]
+    if (response == null) {
+      return [response, null]
     }
-    if (typeof responseUnknown !== "object") {
-      return audit.unexpectedType(responseUnknown, "object")
-    }
-    const response = responseUnknown as {
-      ok: boolean
-      json: () => Promise<{ error?: { details?: unknown } }>
-      status: number
-      statusText: string
-      text: () => Promise<string>
+    if (typeof response !== "object") {
+      return audit.unexpectedType(response, "object")
     }
     if (!response.ok && (response.status < 400 || response.status >= 404)) {
       return [
@@ -51,7 +44,7 @@ export function auditJsonResponse(auditor?: Auditor | null) {
 }
 
 export function validateJsonResponse(auditor?: Auditor | null) {
-  return async function (response: unknown): Promise<[unknown, unknown]> {
+  return async function (response: Response): Promise<[unknown, unknown]> {
     return auditJsonResponse(auditor)(laxAudit, response)
   }
 }
diff --git a/src/lib/components/NavBar.svelte b/src/lib/components/NavBar.svelte
index 6da2785069e3016036258694e3660cf5067ba7af..b8e0a95f327a10c7197fdf3899fff2a24e531969 100644
--- a/src/lib/components/NavBar.svelte
+++ b/src/lib/components/NavBar.svelte
@@ -10,9 +10,19 @@
 
   $: authenticationEnabled = $page.data.authenticationEnabled
 
-  $: pathname = $page.url.pathname
+  $: ({ data } = $page)
 
-  $: user = $page.data.user
+  $: ({ pathname } = $page.url)
+
+  $: ({ baseUrl, user } = data)
+
+  $: loginUrl = `/auth/login?redirect=${encodeURIComponent(
+    new URL(pathname, baseUrl).toString(),
+  )}`
+
+  $: logoutUrl = `/auth/logout?redirect=${encodeURIComponent(
+    new URL(pathname, baseUrl).toString(),
+  )}`
 
   function help() {
     $showTutorial = true
@@ -162,7 +172,7 @@
             <div class="flex items-center justify-between gap-2">
               <a
                 class="flex rounded-sm p-1 text-sm uppercase text-white hover:bg-gray-400 hover:bg-opacity-20 hover:text-white focus:outline-none"
-                href="/authentication/signin/leximpact?redirect={pathname}"
+                href={loginUrl}
               >
                 <div class="flex items-center">
                   <!-- Material icon: VPN key-->
@@ -233,8 +243,8 @@
                 aria-labelledby="user-menu"
               >
                 <a
-                  href="/authentication/signout"
                   class="block rounded-sm border-b px-4 py-3 text-sm uppercase text-black hover:bg-gray-300 hover:bg-opacity-20 focus:outline-none"
+                  href={logoutUrl}
                   type="button"
                 >
                   <div class="flex items-center">
@@ -339,8 +349,8 @@
           {#if authenticationEnabled}
             {#if user === undefined}
               <a
-                href="/authentication/signin/leximpact?redirect={pathname}"
                 class="block border-b px-4 py-3 text-sm font-bold text-gray-700 hover:bg-gray-100"
+                href={loginUrl}
                 role="menuitem"
               >
                 <!-- Material icon: VPN key-->
@@ -357,8 +367,8 @@
               >
             {:else}
               <a
-                href="/authentication/signout"
                 class="block border-b-4 px-4 py-3 text-base text-gray-700 hover:bg-gray-100"
+                href={logoutUrl}
                 role="menuitem"
               >
                 <!-- Material icon: Power Settings New-->
diff --git a/src/lib/server/openid_connect_handler.ts b/src/lib/server/openid_connect_handler.ts
index ca17634f1ad0f1b40d5c3954a06aabff705b054a..8435c220716fa700b668920c8b8b522de85ecb11 100644
--- a/src/lib/server/openid_connect_handler.ts
+++ b/src/lib/server/openid_connect_handler.ts
@@ -1,8 +1,8 @@
 import type { Handle } from "@sveltejs/kit"
 import {
-  type Client as OpenIdConnectClient,
   custom as openIdConnectCustom,
   Issuer as OpenIdConnectIssuer,
+  type Client as OpenIdConnectClient,
 } from "openid-client"
 
 import config from "$lib/server/config"
diff --git a/src/lib/server/user_handler.ts b/src/lib/server/user_handler.ts
index aadefb085f41b8767acdbf8d1c5e9de484a1cd76..edb6ff76956a5a4b4031332f09f55d2bb044eb4f 100644
--- a/src/lib/server/user_handler.ts
+++ b/src/lib/server/user_handler.ts
@@ -10,12 +10,22 @@ export interface UserLocals {
 
 export const userHandler: Handle = async ({ event, resolve }) => {
   const { cookies, locals } = event
+
+  const idTokenJwt = cookies.get("id_token")
+  if (idTokenJwt !== undefined) {
+    try {
+      locals.id_token = jwt.verify(idTokenJwt, config.jwtSecret) as string
+    } catch (e) {
+      console.warn(`Invalid JSON Web Token for id_token: ${idTokenJwt}. ${e}`)
+    }
+  }
+
   const userJwt = cookies.get("user")
   if (userJwt !== undefined) {
     try {
       locals.user = jwt.verify(userJwt, config.jwtSecret) as User
     } catch (e) {
-      console.warn(`Invalid JSON Web Token: ${userJwt}. ${e}`)
+      console.warn(`Invalid JSON Web Token for user: ${userJwt}. ${e}`)
     }
   }
   return await resolve(event)
diff --git a/src/lib/users.ts b/src/lib/users.ts
index 19b723dc090dc704a879a7164dbce2c061d9638f..8b42c69a8c21d686170f279e1674622929bf29fd 100644
--- a/src/lib/users.ts
+++ b/src/lib/users.ts
@@ -1,3 +1,13 @@
+export interface SigninPayload {
+  nonce: string
+  redirectUrl: string
+  token: string
+}
+
+export interface SignoutPayload {
+  redirectUrl: string
+}
+
 export interface User {
   email: string // "john@doe.com"
   email_verified: boolean
@@ -7,6 +17,6 @@ export interface User {
   locale: string // "fr"
   name: string // "John Doe"
   preferred_username: string // "jdoe",
-  roles?: string[] // [ 'offline_access', 'default-roles-leximpact', 'uma_authorization' ],
+  roles?: string[] // [ "offline_access", "default-roles-leximpact", "uma_authorization" ],
   sub: string // "12345678-9abc-def0-1234-56789abcdef0"
 }
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts
index 3a7d4d6a4a513921c6a0c8bbd624b77ac12418c3..d44103283ae2f3d6c7c7e78ff90d0e15ead625aa 100644
--- a/src/routes/+layout.server.ts
+++ b/src/routes/+layout.server.ts
@@ -1,11 +1,10 @@
 import type { RepositoryConfig } from "$lib/repositories"
 import type { User } from "$lib/users"
 import config from "$lib/server/config"
-import { oauth2Authenticator } from "$lib/server/oauth2"
 
 import type { LayoutServerLoad } from "./$types"
 
-const { githubPersonalAccessToken, openfiscaRepository } = config
+const { githubPersonalAccessToken, openfiscaRepository, openIdConnect } = config
 
 export const load: LayoutServerLoad = async (
   event,
@@ -36,13 +35,13 @@ export const load: LayoutServerLoad = async (
   title: string
   user?: User
 }> => {
-  const oauth2Session = await oauth2Authenticator?.getSession(event)
-  const user = oauth2Session?.user
+  const { locals } = event
+  const { user } = locals
   return {
     advanced: config.advanced,
     apiBaseUrls: config.apiBaseUrls,
     apiWebSocketBaseUrls: config.apiWebSocketBaseUrls,
-    authenticationEnabled: oauth2Authenticator !== undefined,
+    authenticationEnabled: openIdConnect !== undefined,
     baseUrl: config.baseUrl,
     childrenKey: config.childrenKey,
     familyEntityKey: config.familyEntityKey,
diff --git a/src/routes/auth/login/+server.ts b/src/routes/auth/login/+server.ts
index 7e8017621cc688c0e102ac4dd8e66679f5bdd7ed..b4b1be9e741bc71d6dd0cc88a1ee971cea53acfb 100644
--- a/src/routes/auth/login/+server.ts
+++ b/src/routes/auth/login/+server.ts
@@ -1,11 +1,13 @@
 import { error } from "@sveltejs/kit"
+import jwt from "jsonwebtoken"
 import { generators } from "openid-client"
 
 import { validateLoginLogoutQuery } from "$lib/server/auditors/queries"
+import config from "$lib/server/config"
 
 import type { RequestHandler } from "./$types"
 
-export const GET: RequestHandler = async ({ locals, url }) => {
+export const GET: RequestHandler = async ({ cookies, locals, url }) => {
   const [query, queryError] = validateLoginLogoutQuery(url.searchParams) as [
     { redirect: string },
     unknown,
@@ -29,20 +31,25 @@ export const GET: RequestHandler = async ({ locals, url }) => {
   }
   const { redirect: redirectUrl } = query
 
-  const session = locals.session
-
   if (locals.openIdConnectClient !== undefined) {
     const nonce = generators.nonce()
-    session.nonce = nonce
-    session.redirectUrl = redirectUrl
     const token = crypto.randomUUID()
-    session.token = token
     const authorizationUrl = locals.openIdConnectClient.authorizationUrl({
       scope: "openid email profile",
       response_mode: "form_post",
       nonce,
       state: token,
     })
+    cookies.set(
+      "signin",
+      jwt.sign({ nonce, redirectUrl, token }, config.jwtSecret, {
+        expiresIn: "1h",
+      }),
+      {
+        path: "/",
+        sameSite: "none", // Same-Site must be set to none, otherwise cookie will not be available.
+      },
+    )
     return new Response(undefined, {
       status: 302,
       headers: { location: authorizationUrl },
diff --git a/src/routes/auth/login_callback/+server.ts b/src/routes/auth/login_callback/+server.ts
index 2c194f2d2342d18b5f5eb707f5969c8fb03fe291..29a482a592507213c34440d4d8459c907ccf8ced 100644
--- a/src/routes/auth/login_callback/+server.ts
+++ b/src/routes/auth/login_callback/+server.ts
@@ -1,103 +1,67 @@
-import type { ZammadUser } from "@progedo/lib"
-import type { RequestHandler } from "@sveltejs/kit"
-import dedent from "dedent-js"
+import { error, type RequestHandler } from "@sveltejs/kit"
+import jwt from "jsonwebtoken"
+import type { Client as OpenIdConnectClient } from "openid-client"
 
-import { db } from "$lib/server/database"
-import { hashJsonObject, type SessionRecord } from "$lib/server/session_handler"
-import { fetchZammadApi, fetchZammadApiList } from "$lib/server/zammad"
+import config from "$lib/server/config"
+import type { SigninPayload } from "$lib/users"
 
-export const POST: RequestHandler = async ({ locals, request, url }) => {
-  const client = locals.openIdConnectClient
+export const POST: RequestHandler = async ({
+  cookies,
+  locals,
+  request,
+  url,
+}) => {
+  const openIdConnectClient = locals.openIdConnectClient as OpenIdConnectClient
   // Since method callbackParams doesn't accept a SvelteKit request as a parameter,
   // create a dummy URL for a GET request and use it as a parameter of callbackParams.
   const queryString = await request.text()
   const fakeUrlString = new URL("?" + queryString, url).toString()
-  const params = client.callbackParams(fakeUrlString)
-  // Since session cookie is not sent after a redirection from authentication server,
-  // use params.state as session token and delete newly created empty session.
-  await db.none(
-    dedent`
-      DELETE FROM sessions
-      WHERE id = $<id>
-    `,
-    { id: locals.sessionId },
-  )
-  const token = params.state
-  const sessionRecord: SessionRecord | null = await db.oneOrNone(
-    dedent`
-      SELECT *
-      FROM sessions
-      WHERE token = $<token>
-    `,
-    { token },
-  )
-  if (sessionRecord === null) {
-    console.error(
-      "OpenIDConnect authentication failed: Unable to retrieve current session from state token.",
-    )
-    return new Response(`Authentication failed: Unable to retrieve current session.`, { status: 302, headers: { location: "/" } })
+  const params = openIdConnectClient.callbackParams(fakeUrlString)
+  const signinJwt = cookies.get("signin")
+  cookies.delete("signin", { path: "/" })
+  if (signinJwt === undefined) {
+    console.warn(`Authentication failed: Missing signin cookie`)
+    return new Response(`Authentication failed: Missing signin cookie.`, {
+      status: 302,
+      headers: { location: "/" },
+    })
+  }
+  let signinPayload: SigninPayload
+  try {
+    signinPayload = jwt.verify(signinJwt, config.jwtSecret) as SigninPayload
+  } catch (e) {
+    console.error(`Invalid JSON Web Token: ${signinJwt}. ${e}`)
+    throw error(401, `Invalid JSON Web Token`)
+  }
+  if (params.state !== signinPayload.token) {
+    console.warn("Authentication failed: Token mismatch")
+    return new Response(`Authentication failed: Token mismatch.`, {
+      status: 302,
+      headers: { location: "/" },
+    })
   }
-  const session = sessionRecord.data as App.Locals["session"]
-  session.token = sessionRecord.token
-  locals.session = session
-  locals.sessionHash = hashJsonObject(session)
-  locals.sessionId = sessionRecord.id
-
-  // Remove token from session after sessionHash has been computed,
-  // to ensure that session will be updated by session handler.
-  delete session.token
 
   // State must be removed from params before calling callback method.
   delete params.state
-  const tokenSet = await client.callback(url.toString(), params, {
-    nonce: session.nonce as string,
+  const tokenSet = await openIdConnectClient.callback(url.toString(), params, {
+    nonce: signinPayload.nonce,
   })
-  delete session.nonce
   // console.log("Received tokenSet:", tokenSet)
 
-  const authentication = tokenSet.claims()
-  // console.log("Received authentication claims:", authentication)
+  const user = tokenSet.claims()
+  // console.log("Received authentication claims:", user)
 
-  let user: (ZammadUser & { error: unknown }) | undefined = undefined
-  const users = (await fetchZammadApiList(
-    `users/search?expand=true&query=${encodeURIComponent(
-      authentication.email,
-    )}`,
-  )) as ZammadUser[] & { error: unknown }
-  if (users.error !== undefined) {
-    console.error(
-      `OpenIDConnect authentication failed: An error occurred when retrieving user by its email ${authentication.email}: ${users.error}`,
-    )
-    return new Response(`OpenIDConnect authentication failed: An error occurred when retrieving user by its email ${authentication.email}: ${users.error}`, { status: 302, headers: { location: "/" } })
+  if (tokenSet.id_token !== undefined) {
+    cookies.set("id_token", jwt.sign(tokenSet.id_token, config.jwtSecret), {
+      path: "/",
+    })
   }
-  user = users.find(({ email }) => email === authentication.email) as
-    | (ZammadUser & { error: unknown })
-    | undefined
-  if (user === undefined) {
-    user = (await fetchZammadApi("users?expand=true", {
-      body: JSON.stringify({
-        email: authentication.email,
-        firstname: authentication.given_name,
-        lastname: authentication.family_name,
-      }),
-      headers: {
-        "Content-Type": "application/json",
-      },
-      method: "POST",
-    })) as ZammadUser & { error: unknown }
-    if (user.error !== undefined) {
-      console.error(
-        `OpenIDConnect authentication failed: An error occurred while creating Zammad user ${authentication.email}: ${user.error}`,
-      )
-      return new Response(`OpenIDConnect authentication failed: An error occurred while creating Zammad user ${authentication.email}: ${user.error}`, { status: 302, headers: { location: "/" } })
-    }
-  }
-  locals.user = user
-  session.userId = user.id
-  // Keep id_token, to use it for single logout.
-  session.userIdToken = tokenSet.id_token
+  cookies.set("user", jwt.sign(user, config.jwtSecret), {
+    path: "/",
+  })
 
-  const redirectUrl = session.redirectUrl
-  delete session.redirectUrl
-  return new Response(undefined, { status: 302, headers: { location: redirectUrl ?? "/" } })
+  return new Response(undefined, {
+    status: 302,
+    headers: { location: signinPayload.redirectUrl },
+  })
 }
diff --git a/src/routes/auth/logout/+server.ts b/src/routes/auth/logout/+server.ts
index d93a3135dc39bc2e4fad06d804430385bde7b5cb..111e48230947cf6faa6999bff1286cc0479326f5 100644
--- a/src/routes/auth/logout/+server.ts
+++ b/src/routes/auth/logout/+server.ts
@@ -1,10 +1,12 @@
 import { error } from "@sveltejs/kit"
+import jwt from "jsonwebtoken"
 
 import { validateLoginLogoutQuery } from "$lib/server/auditors/queries"
+import config from "$lib/server/config"
 
 import type { RequestHandler } from "./$types"
 
-export const GET: RequestHandler = ({ locals, url }) => {
+export const GET: RequestHandler = ({ cookies, locals, url }) => {
   const [query, queryError] = validateLoginLogoutQuery(url.searchParams) as [
     { redirect: string },
     unknown,
@@ -28,24 +30,41 @@ export const GET: RequestHandler = ({ locals, url }) => {
   }
   const { redirect: redirectUrl } = query
 
-  const { session } = locals
-  if (session.userIdToken === undefined) {
-    delete locals.user
-    delete session.userId
+  cookies.delete("id_token", { path: "/" })
+  cookies.delete("user", { path: "/" })
+  const idToken = locals.id_token
+  delete locals.id_token
+  delete locals.user
+
+  if (idToken === undefined) {
     return new Response(undefined, {
       status: 302,
       headers: { location: redirectUrl },
     })
   }
-  session.redirectUrl = redirectUrl
-  const endSessionUrl = locals.openIdConnectClient.endSessionUrl({
-    id_token_hint: session.userIdToken,
-  })
-  delete locals.user
-  delete session.userId
-  delete session.userIdToken
-  return new Response(undefined, {
+  if (locals.openIdConnectClient !== undefined) {
+    cookies.set(
+      "signout",
+      jwt.sign({ redirectUrl }, config.jwtSecret, {
+        expiresIn: "1h",
+      }),
+      {
+        path: "/",
+        sameSite: "none", // Same-Site must be set to none, otherwise cookie will not be available.
+      },
+    )
+    const endSessionUrl = locals.openIdConnectClient.endSessionUrl({
+      id_token_hint: idToken,
+    })
+    return new Response(undefined, {
+      status: 302,
+      headers: { location: endSessionUrl },
+    })
+  }
+
+  console.error(`No authentication method defined`)
+  return new Response("No authentication method defined", {
     status: 302,
-    headers: { location: endSessionUrl },
+    headers: { location: "/" },
   })
 }
diff --git a/src/routes/auth/logout_callback/+server.ts b/src/routes/auth/logout_callback/+server.ts
index 5e8932dd4816bd1df01ed241bf43ea2efb740b1d..8956b0c6c2dcee979f6e0a0fec0e74aaad7c61e3 100644
--- a/src/routes/auth/logout_callback/+server.ts
+++ b/src/routes/auth/logout_callback/+server.ts
@@ -1,8 +1,34 @@
-import type { RequestHandler } from "@sveltejs/kit"
+import { error, type RequestHandler } from "@sveltejs/kit"
+import jwt from "jsonwebtoken"
+import type { Client as OpenIdConnectClient } from "openid-client"
 
-export const GET: RequestHandler = async ({ locals }) => {
-  const { session } = locals
-  const { redirectUrl } = session
-  delete session.redirectUrl
-  return new Response(undefined, { status: 302, headers: { location: redirectUrl ?? "/" } })
+import config from "$lib/server/config"
+import type { SignoutPayload } from "$lib/users"
+
+export const GET: RequestHandler = async ({ cookies, locals }) => {
+  const openIdConnectClient = locals.openIdConnectClient as OpenIdConnectClient
+  // Since method callbackParams doesn't accept a SvelteKit request as a parameter,
+  // create a dummy URL for a GET request and use it as a parameter of callbackParams.
+  const signoutJwt = cookies.get("signout")
+  cookies.delete("signout", { path: "/" })
+
+  if (signoutJwt === undefined) {
+    console.warn(`Logout failed: Missing signout cookie`)
+    return new Response(`Logout failed: Missing signout cookie.`, {
+      status: 302,
+      headers: { location: "/" },
+    })
+  }
+  let signoutPayload: SignoutPayload
+  try {
+    signoutPayload = jwt.verify(signoutJwt, config.jwtSecret) as SignoutPayload
+  } catch (e) {
+    console.error(`Invalid JSON Web Token: ${signoutJwt}. ${e}`)
+    throw error(401, `Invalid JSON Web Token`)
+  }
+
+  return new Response(undefined, {
+    status: 302,
+    headers: { location: signoutPayload.redirectUrl },
+  })
 }