SvelteKit Session Management

tl;dr: Manage session cookies using hooks.server.ts, the handle function, event.locals and app.d.ts.

Overview

I've been working on a simple web app. It doesn't need self-onboarding. It doesn't need 3rd parties for user identity management. Instead, Django's model of session management fits well here:

  • Authenticate via basic auth.
  • On successful authentication, set a session cookie.
  • When serving URLs that require authentication, check for a valid session cookie.

How does one do this in SvelteKit?

As I write, SvelteKit is still evolving quickly. A search for "sveltekit session management" turns up outdated results. For one example, StackOverflow has answers that use request.session, but as far as I can tell that property no longer exists.

A ramble through the Svelte discord pointed me in the right direction.

hooks.server.ts

In your SvelteKit project, add a src/hooks.server.ts file (or, if you are using JavaScript, src/hooks.server.js). In it, define a handle function, which starts by looking for a session cookie on the provided event.

export const handle = async function ({ event, resolve }) {
  extractSessionInfo(event)
  // ...
} satisfies Handle

Get the Cookie

This example uses a session cookie whose value is a JSON Web Token. Instead of a session ID, the token contains the name of an authenticated user.

extractSessionInfo simply extracts the username from the session cookie and saves it to the event's local storage.

function extractSessionInfo(event: RequestEvent) {
  const cookie = event.cookies.get('sess_tok')
  const username = Tokens.sessionUsernameFromToken(cookie)

  if (username != null && AppDB.shared?.user.isKnownUser(username)) {
    event.locals.username = username
  }
}

Here, Tokens represents a module that manipulates JWTs. AppDB provides a shared user database. You'd need to write these for yourself.

event.locals

event.locals holds state that can be shared among all the functions processing an event. You define it in src/app.d.ts. Here, it just needs to hold an optional username:

// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
  // interface Error {}
  interface Locals {
    username: string | undefined
  }
  // interface PageData {}
  // interface Platform {}
}

Finish Processing

Back in the handle function, finish processing the request by invoking the provided resolve function.

export const handle = async function ({ event, resolve }) {
  extractSessionInfo(event)

  const response = await resolve(event)
  // ...
} satisfies Handle

Set the Cookie

Finally, if a valid username was found, update the session cookie, set it on the response, and return the response.

export const handle = async function ({ event, resolve }) {
  extractSessionInfo(event)

  const response = await resolve(event)

  if (event.locals.username !== undefined) {
    Tokens.addSessionTokenForUsername(response.headers, event.locals.username)
  }

  return response
} satisfies Handle

Reject Unauthenticated Requests

That's pretty much it, except for one thing: suppose handle is processing a request that requires authentication, but a valid session cookie isn't found. In that case, handle needs to stop processing and return an error.

export const handle = async function ({ event, resolve }) {
  extractSessionInfo(event)

  if (needsAuth(event.request)) {
    if (event.locals.username === undefined) {
      return Tokens.invalidTokenResponse()
    }
  }

  const response = await resolve(event)

  if (event.locals.username !== undefined) {
    Tokens.addSessionTokenForUsername(response.headers, event.locals.username)
  }

  return response
} satisfies Handle

Here, needsAuth is a function that determines whether a request needs authentication.

Further Reading

I didn't take very good notes while sorting this out. But I know I owe thanks to https://github.com/sveltejs/realworld for its many clues.

Thanks also to the authors of SvelteKit's documentation: