Advertisement
ViaBenevolentia

Untitled

Apr 16th, 2025
45
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 11.77 KB | Source Code | 0 0
  1. Hey! Maybe you've already resolved it and this might not be 100% relevant since I use Firebase to manage auth, but I'll share it there in case someone might need it :)
  2.  
  3. So I have a login route (routes/login/+page.svelte). I won't paste full UI code, but the script tag looks like:
  4. <script>
  5.   import { firebaseAuth } from "$lib/client/firebase.app.js";
  6.   import {
  7.     signInWithEmailAndPassword,
  8.     setPersistence,
  9.     inMemoryPersistence,
  10.   } from "firebase/auth";
  11.   import { goto } from "$app/navigation";
  12.   import { onMount } from "svelte";
  13.  
  14.   let email = $state("");
  15.   let password = $state("");
  16.   let loginError = $state("");
  17.   let isLoading = $state(false);
  18.  
  19.   onMount(() => {
  20.     setPersistence(firebaseAuth, inMemoryPersistence);
  21.   });
  22.  
  23.   async function login() {
  24.     isLoading = true;
  25.     loginError = "";
  26.     if (!email || !password) {
  27.       loginError = "Email and password are required";
  28.       isLoading = false;
  29.       return;
  30.     }
  31.  
  32.     try {
  33.       const result = await signInWithEmailAndPassword(
  34.         firebaseAuth,
  35.         email,
  36.         password
  37.       );
  38.       if (result?.user) {
  39.         const idToken = await result.user.getIdToken();
  40.  
  41.         const response = await fetch("/api/login", {
  42.           method: "POST",
  43.           headers: {
  44.             "Content-Type": "application/json",
  45.           },
  46.           body: JSON.stringify({ idToken }),
  47.         });
  48.  
  49.         if (!response.ok) {
  50.           const errorData = await response.json();
  51.           throw new Error(errorData.message || "Session login failed");
  52.         }
  53.  
  54.         goto("/");
  55.       } else {
  56.         loginError = "Login successful, but user data not found";
  57.       }
  58.     } catch (error) {
  59.       console.error("Login error:", error);
  60.       if (error instanceof Error) loginError = error.message;
  61.       else loginError = "An unknown error occurred during login.";
  62.     } finally {
  63.       isLoading = false;
  64.     }
  65.   }
  66. </script>
  67. So I use Firebase's client sdk to 'sign-in', i.e. check the email and password here on the  front end. Then, if successfull, I make a post request to a routes/api/login/+server.js with the firebase idToken in the body. The code for actual login looks like:
  68. import { getFirebaseAdminAuth } from '$lib/server/firebase.admin.js';
  69. import { error, json } from '@sveltejs/kit';
  70. import { dev } from '$app/environment';
  71.  
  72. // Constants for cookie settings
  73. const SESSION_COOKIE_NAME = '__session';
  74. const EXPIRES_IN_MILLISECONDS = 7 * 24 * 60 * 60 * 1000;
  75.  
  76. /** @typedef {import('cookie').CookieSerializeOptions} CookieSerializeOptions */
  77.  
  78. /**
  79.  * Handles POST requests to create a session cookie.
  80.  * @type {import('@sveltejs/kit').RequestHandler}
  81.  */
  82. export async function POST({ request, cookies }) {
  83.     const adminAuth = getFirebaseAdminAuth();
  84.  
  85.     try {
  86.         const body = await request.json();
  87.         const idToken = body.idToken;
  88.  
  89.         if (!idToken || typeof idToken !== 'string') {
  90.             throw error(400, { message: 'ID token must be provided as a string.' });
  91.         }
  92.         const expiresIn = EXPIRES_IN_MILLISECONDS;
  93.  
  94.         // Create the session cookie. This verifies the ID token.
  95.         const sessionCookie = await adminAuth.createSessionCookie(idToken, { expiresIn });
  96.  
  97.         /** @type {CookieSerializeOptions & { path: string }} */
  98.         const options = {
  99.             maxAge: expiresIn / 1000, // maxAge is in seconds
  100.             httpOnly: true,
  101.             secure: !dev, // Use secure cookies in production
  102.             path: '/',
  103.             sameSite: 'lax'
  104.         };
  105.  
  106.         cookies.set(SESSION_COOKIE_NAME, sessionCookie, options);
  107.  
  108.         return json({ status: 'success', message: 'Session cookie created successfully.' });
  109.  
  110.     } catch (err) {
  111.         console.error('Session login error:', err);
  112.         // Check if err is an object with a string 'code' property
  113.         if (
  114.             typeof err === 'object' &&
  115.             err !== null &&
  116.             'code' in err &&
  117.             typeof err.code === 'string' // Explicitly check if code is a string
  118.         ) {
  119.             // Now we know err.code is a string
  120.             if (err.code === 'auth/argument-error' || err.code === 'auth/invalid-id-token') {
  121.                 throw error(401, { message: 'Unauthorized: Invalid ID token.' });
  122.             }
  123.         }
  124.         // Default error for other issues or if err doesn't fit the expected structure
  125.         throw error(500, { message: 'Internal Server Error: Could not create session cookie.' });
  126.     }
  127. }
  128.  
  129. So, as you see, I simply set the cookie which will be saved in the browser. Now, the relevant part for this post. How do you now use this to protect routes and manage user's auth?
  130. import { getFirebaseAdminAuth } from '$lib/server/firebase.admin';
  131.  
  132. // Constants
  133. const SESSION_COOKIE_NAME = '__session'; // Same name as used in login endpoint
  134.  
  135. /**
  136.  * SvelteKit server hook to handle request processing, including session verification.
  137.  * @type {import('@sveltejs/kit').Handle}
  138.  */
  139. export async function handle({ event, resolve }) {
  140.     const adminAuth = getFirebaseAdminAuth();
  141.     const sessionCookie = event.cookies.get(SESSION_COOKIE_NAME);
  142.  
  143.     // Initialize locals.user
  144.     event.locals.user = null;
  145.  
  146.     if (sessionCookie) {
  147.         try {
  148.             // Verify the session cookie. checkRevoked is true to check if the session was revoked.
  149.             const decodedClaims = await adminAuth.verifySessionCookie(sessionCookie, true);
  150.             // Add user info to event.locals for use in pages and endpoints
  151.             event.locals.user = {
  152.                 uid: decodedClaims.uid,
  153.                 email: decodedClaims.email,
  154.                 email_verified: decodedClaims.email_verified,
  155.                 // Add other claims as needed (e.g., custom claims)
  156.                 // admin: decodedClaims.admin === true, // Example custom claim
  157.                 // You can add the full decodedClaims if needed, but often specific fields are sufficient
  158.                 // decodedClaims: decodedClaims
  159.             };
  160.             console.log('Session cookie verified successfully for user:', event.locals.user.uid);
  161.         } catch (error) {
  162.             // Session cookie is invalid or revoked. Clear it.
  163.             if (error instanceof Error && 'code' in error) {
  164.                 console.warn('Session cookie verification failed:', error?.code, error?.message);
  165.             } else {
  166.                 console.warn('Session cookie verification failed:', error);
  167.             }
  168.             event.cookies.delete(SESSION_COOKIE_NAME, { path: '/' });
  169.             event.locals.user = null;
  170.         }
  171.     } else {
  172.         // No session cookie found
  173.         console.log('No session cookie found.');
  174.     }
  175.  
  176.     // Continue processing the request
  177.     const response = await resolve(event);
  178.  
  179.     // Optionally add headers here if needed after response is generated
  180.     // response.headers.set('x-custom-header', 'value');
  181.  
  182.     return response;
  183. }
  184. So this hook intercepts any requests in the application and checks the cookie for any route. If successful, it stores the user details in the locals.
  185.  
  186. Now you can use this in +layout.server.js:
  187. import { redirect } from '@sveltejs/kit';
  188.  
  189. // List of routes that are publicly accessible without login
  190. const PUBLIC_ROUTES = ['/login']; // Add other public routes like '/register', '/forgot-password' if needed
  191.  
  192. /**
  193.  * Root layout server load function.
  194.  * Checks user authentication status for all routes.
  195.  * Passes user data down to all layouts and pages.
  196.  * @param {{ locals: App.Locals, url: URL }} event
  197.  * @returns {{ user: App.Locals['user'] }}
  198.  */
  199. export function load({ locals, url }) {
  200.     const { user } = locals; // User object from hooks.server.js (null if not logged in)
  201.     const currentPath = url.pathname;
  202.  
  203.     // If the user is not logged in AND the current path is not a public route
  204.     if (!user && !PUBLIC_ROUTES.includes(currentPath)) {
  205.         console.log(`Unauthenticated access attempt to ${currentPath}, redirecting to login.`);
  206.         // Preserve the intended destination for redirection after login (optional)
  207.         // const redirectTo = currentPath !== '/' ? `?redirect=${encodeURIComponent(currentPath)}` : '';
  208.         // throw redirect(307, `/login${redirectTo}`);
  209.         throw redirect(307, '/login'); // Redirect to login
  210.     }
  211.  
  212.     // If the user IS logged in but tries to access login page, redirect to home
  213.     if (user && currentPath === '/login') {
  214.         console.log(`Authenticated user accessing ${currentPath}, redirecting to home.`);
  215.         throw redirect(307, '/'); // Redirect logged-in users away from login
  216.     }
  217.  
  218.     // Make user data available to all layouts and pages
  219.     // Child `load` functions can access this via `parent()`
  220.     // Page/layout components access it via the `data = $props();`
  221.     return {
  222.         user: user // Pass the user object (or null) to the layout/page data
  223.     };
  224. }
  225. This is located in src/routes/, so it will be run first, redirecting you to login if you are not logged in, session cookie was deleted (i.e. you've logged out).
  226.  
  227. And for logout, you simply delete the cookie:
  228. routes/api/logout/+server.js
  229. import { error, json } from '@sveltejs/kit';
  230.  
  231. const SESSION_COOKIE_NAME = '__session'; // Must match the name used elsewhere
  232.  
  233. /**
  234.  * Handles POST requests to log the user out by clearing the session cookie.
  235.  * @type {import('@sveltejs/kit').RequestHandler}
  236.  */
  237. export async function POST({ cookies, locals }) {
  238.     const sessionCookie = cookies.get(SESSION_COOKIE_NAME);
  239.  
  240.     // Check if the user is actually logged in according to our hook
  241.     // This check is optional but good practice. The main goal is to clear the cookie.
  242.     if (!locals.user || !sessionCookie) {
  243.         // If no user in locals or no cookie, arguably they are already logged out client-side perception wise.
  244.         // We can still attempt to clear any potentially lingering cookie.
  245.         if (sessionCookie) {
  246.             cookies.delete(SESSION_COOKIE_NAME, { path: '/' });
  247.         }
  248.         return json({ status: 'success', message: 'User already logged out or no session found.' });
  249.     }
  250.  
  251.     try {
  252.         // No need to verify the cookie again here if we trust locals.user set by the hook.
  253.         // We just need to clear the cookie for this session.
  254.  
  255.         // Delete the session cookie
  256.         cookies.delete(SESSION_COOKIE_NAME, { path: '/' });
  257.  
  258.         // Log the logout action using the user ID from locals
  259.         console.log('User logged out successfully:', locals.user.uid);
  260.         return json({ status: 'success', message: 'Logout successful.' });
  261.  
  262.     } catch (err) {
  263.         console.error('Session logout error:', err);
  264.         // Even if deleting the cookie somehow fails, log the error.
  265.         // The primary action (cookie deletion) might still have partially succeeded client-side.
  266.         throw error(500, { message: 'Internal Server Error: Could not process logout.' });
  267.     }
  268. }
  269. And you can make a simple button in the UI that, when clicked, does:
  270.     async function logout() {
  271.         await fetch("/api/logout", { method: "POST" });
  272.         // Refresh page or use goto to navigate after logout
  273.         goto("/login");
  274.     }
  275.  
  276. Hope this helps :)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement