@fastify/secure-session

repository·main·Indexed 42 days ago

https://github.com/fastify/fastify-secure-session

A secure, stateless cookie-based session implementation for Fastify version 8.3.0. It utilizes libsodium's Secret Key Box Encryption to ensure session data is encrypted and tamper-proof. Features include support for key rotation, multiple independent sessions, and integration with @fastify/cookie for security flags like __Host- prefixes.

Tokens
4.5K
Snippets
15
Records
17
Agent score
50%

What's inside fastify-secure-session

  1. How key rotation works

    main

    To support key rotation, pass an array of keys to the key option.

    1. Signing: The library always signs and encrypts new sessions using the first key in the array.
    2. Decoding: If decoding fails with the first key, the library attempts to decode using every subsequent key in the array.
    3. Automatic Rotation: When a session is successfully decoded using an older key (one not at index 0), the library automatically re-signs the cookie with the newest key (the first key in the array) upon the next request. This improves performance for subsequent calls.

    Best Practice: It is recommended to keep only 2 keys in the array at a time to minimize the performance cost of multiple decode attempts during a rotation period.

    const key1 = fs.readFileSync(path.join(__dirname, 'secret-key1'))
    const key2 = fs.readFileSync(path.join(__dirname, 'secret-key2'))
    
    fastify.register(require('@fastify/secure-session'), {
      // key1 is the new key used for signing
      // key2 is the old key used for fallback decoding
      key: [key1, key2],
      cookie: {
        path: '/'
      }
    })
  2. Add TypeScript types for SessionData

    main

    To get type safety for your session data, extend the SessionData interface using TypeScript declaration merging. This allows request.session.get() to return correctly typed values.

    declare module '@fastify/secure-session' {
      interface SessionData {
        foo: string;
      }
    }
    
    fastify.get('/', (request, reply) => {
      request.session.get('foo'); // typed `string | undefined`
      reply.send('hello world')
    })
  3. Register @fastify/secure-session

    main

    Register the plugin with your Fastify instance. You can provide a single configuration object or an array of objects to support multiple sessions.

    Key Configuration Options:

    • sessionName: The name of the attribute decorated on the request object (defaults to 'session').
    • cookieName: The name of the session cookie (defaults to the value of sessionName).
    • key: A Buffer containing the secret key. If using multiple sessions, sessionName is mandatory.
    • expiry: The amount of time (in seconds) the session is considered valid. This is independent of cookie expiration.
    • cookie: An object containing options for @fastify/cookie (e.g., path, httpOnly, secure, sameSite).
    const fastify = require('fastify')({ logger: false })
    const fs = require('node:fs')
    const path = require('node:path')
    
    fastify.register(require('@fastify/secure-session'), {
      sessionName: 'session',
      cookieName: 'my-session-cookie',
      key: fs.readFileSync(path.join(__dirname, 'secret-key')),
      expiry: 24 * 60 * 60, // 1 day
      cookie: {
        path: '/'
      }
    })
  4. Add TypeScript types for custom session names

    main

    If you are using a custom sessionName or multiple sessions, you should extend the FastifyRequest interface instead of SessionData to access the session via the custom property name.

    interface FooSessionData {
      foo: string;
    }
    
    declare module "fastify" {
      interface FastifyRequest {
        foo: Session<FooSessionData>;
      }
    }
    
    fastify.get('/', (request, reply) => {
      request.foo.get('foo'); // typed `string | undefined`
      reply.send('hello world')
    })
  5. Use a secret passphrase instead of a key file

    main

    If you prefer not to manage raw key files, you can provide a secret (a high-entropy passphrase) and a salt.

    Warning: This approach introduces a significant startup delay because the library must apply strong cryptography to derive the key from the passphrase at runtime.

    fastify.register(require('@fastify/secure-session'), {
      secret: 'averylogphrasebiggerthanthirtytwochars',
      salt: 'mq9hDxBVDbspDR6n',
      cookie: {
        path: '/',
        httpOnly: true
      }
    })
  6. Generate a secure session key

    main

    To use @fastify/secure-session with a pregenerated key, you must first generate a high-entropy key. You can do this using npx or by running the internal genkey.js script.

    Using npx (Linux/macOS):

    npx --yes @fastify/secure-session > secret-key

    Using npx (Windows PowerShell):

    npx --yes @fastify/secure-session | Out-File -Encoding default -NoNewline -FilePath secret-key

    Using the local node_modules script: If you have already installed the package, you can run:

    ./node_modules/@fastify/secure-session/genkey.js > secret-key
    npx --yes @fastify/secure-session > secret-key
  7. How session encryption and rotation work

    main

    The plugin uses sodium-native for high-security encryption.

    • Encryption: Sessions are encrypted using crypto_secretbox_easy. The resulting cookie is a combination of the base64-encoded ciphertext and the base64-encoded nonce, separated by a semicolon (;).
    • Key Rotation: If you provide an array of keys in the configuration, the plugin attempts to decrypt the incoming cookie using each key in sequence. If a key other than the first one successfully decrypts the data, the session is marked as changed = true. This allows the plugin to automatically re-encrypt the session using the primary (first) key, effectively rotating the key for that user on their next request.
    • Integrity: The session includes a timestamp (__ts) to handle expiry and ensure the session data is fresh.
  8. Configure cookie security options

    main

    The cookie configuration option is passed directly to @fastify/cookie's setCookie method. You can configure standard security flags like secure and sameSite.

    Setting secure: 'auto' will only set the secure flag when the request is made over HTTPS.

    If you use the __Host- cookie name prefix, browsers require that you also set path: '/', secure: true, and do not specify a domain.

    fastify.register(require('@fastify/secure-session'), {
      key: fs.readFileSync(path.join(__dirname, 'secret-key')),
      cookie: {
        path: '/',
        httpOnly: true,
        secure: true, // or 'auto' to only set secure when the request is over HTTPS
        sameSite: 'strict' // or 'lax' / 'none'
      }
    })
  9. Register @fastify/secure-session plugin

    main

    To enable secure sessions in your Fastify application, register the @fastify/secure-session plugin. You can provide a single configuration object or an array of configuration objects to support multiple independent sessions.

    Each configuration object can define:

    • sessionName: The name of the property attached to the request object (defaults to 'session').
    • cookieName: The name of the cookie sent to the client (defaults to the sessionName).
    • expiry: Session expiry in seconds (defaults to 86401, which is 24 hours).
    • cookieOptions or cookie: Standard cookie options. httpOnly defaults to true if not specified.
    • secret: A string or Buffer used to derive a key via sodium.crypto_pwhash. Must be at least 32 bytes. If using a secret, you can optionally provide a salt (must match sodium.crypto_pwhash_SALTBYTES).
    • key: A single key (Buffer or base64 string) or an array of keys for rotation. Keys must be exactly sodium.crypto_secretbox_KEYBYTES bytes long.
    const fastify = require('fastify')()
    const fastifySecureSession = require('@fastify/secure-session')
    
    fastify.register(fastifySecureSession, {
      secret: 'your-at-least-32-byte-long-secret-string',
      cookieName: 'my-session-cookie'
    })
  10. Use the __Host- cookie name prefix

    main

    To use the __Host- prefix for enhanced security, set the cookieName option and ensure the cookie configuration includes path: '/' and secure: true without a domain attribute.

    fastify.register(require('@fastify/secure-session'), {
      key: fs.readFileSync(path.join(__dirname, 'secret-key')),
      cookieName: '__Host-session',
      cookie: {
        path: '/',
        secure: true,
        sameSite: 'strict'
      }
    })
  11. Manually encode and decode sessions

    main

    For systems that do not use standard Fastify Request objects (such as @fastify/websocket), you can use the plugin's decorators to manually manage session lifecycle.

    • fastify.createSecureSession(data): Creates a new session object for manipulating data via .get() and .set().
    • fastify.encodeSecureSession(session, [sessionName]): Returns the signed and encrypted cookie string.
    • fastify.decodeSecureSession(cookieString, [cookieOptions], [sessionName]): Returns a Session object if decoding is successful, or null otherwise.

    Note: If you are using multiple sessions, you must provide the sessionName to both the encoding and decoding methods.

    fastify.createSecureSession({ foo: 'bar' })
    // => Session returns a session object for manipulating with .get and .set to then be encoded with encodeSecureSession
    
    fastify.encodeSecureSession(request.session)
    // => "abcdefg" returns the signed and encrypted cookie string, suitable for passing to a Set-Cookie header
    
    fastify.decodeSecureSession(request.cookies['session'])
    // => Session | null  returns a session object which you can use to .get values from if decoding is successful, and null otherwise
  12. Configure cookie options inside a route

    main

    You can override or update the cookie configuration for a specific request using the request.session.options() method. This method accepts any parameters compatible with @fastify/cookie's setCookie function. Note that maxAge is always passed in seconds.

    fastify.get('/ping', (request, reply) => {
      // Update cookie expiration
      request.session.options({ maxAge: 3600 })
    
      // Send the session cookie to the client even if data didn't change
      request.session.touch()
      reply.send('pong')
    })