@fastify/passport

repository·main·Indexed 43 days ago

https://github.com/fastify/fastify-passport

A port of the Passport.js library for the Fastify ecosystem, providing simple and unobtrusive authentication. It allows developers to use various authentication strategies to protect Fastify routes, supporting both stateless (@fastify/secure-session) and stateful (@fastify/session) session management. Version 4.0.2.

Tokens
11K
Snippets
32
Records
40
Agent score
53%

What's inside fastify-passport

  1. Implement User Serialization and Deserialization

    main

    Since @fastify/passport can only store JSON objects in the session, you should not store rich class instances. Instead, store only a unique identifier (like a user ID) and use serializers/deserializers to fetch the full user object from your database on subsequent requests.

    • registerUserSerializer(async (user, request) => ...): Defines what data to store in the session (e.g., user.id).
    • registerUserDeserializer(async (id, request) => ...): Defines how to retrieve the user from the database using the stored ID.
    // Store only the ID in the session
    fastifyPassport.registerUserSerializer(async (user, request) => user.id);
    
    // Fetch the full user from the database using the ID
    fastifyPassport.registerUserDeserializer(async (id, request) => {
      return await User.findById(id);
    });
  2. Initialize @fastify/passport with initialize() and secureSession()

    main

    To use @fastify/passport, you must add two mandatory hooks to your Fastify instance:

    1. initialize(): Sets up the internal hooks for the @fastify/passport instance.
    2. secureSession(): Connects @fastify/passport to @fastify/secure-session to enable session-based authentication storage.

    Both must be added to your Fastify setup for the plugin to function correctly.

    // Note: This is a conceptual representation of the required setup steps
    await fastify.register(fastifyPassport);
    fastify.after(async () => {
      await fastify.register(fastifyPassport.initialize);
      await fastify.register(fastifyPassport.secureSession);
    });
  3. Protect routes and handle authentication

    main

    Authentication vs. Authorization

    • authenticate(): Use this as a preValidation hook for routes that perform a login (e.g., the login route and the OAuth callback route). It runs the strategy but does not check if a session already exists.
    • request.isAuthenticated(): Use this to guard protected routes. Once secureSession() is registered, logged-in requests will have request.user automatically populated.

    Avoiding Infinite Redirect Loops

    Do not use authenticate() as a guard on a protected route for redirect-based strategies (like Google OAuth). This causes an infinite loop because the strategy will redirect to the IdP, which redirects back to the callback, which redirects back to the protected route, triggering the strategy again.

    Correct Pattern:

    1. Use authenticate() on the login and callback routes.
    2. Use a custom guard checking request.isAuthenticated() on protected routes.
    // 1. The guard function
    const requireLogin = async (request, reply) => {
      if (!request.isAuthenticated()) {
        request.session.set('returnTo', request.url)
        return reply.redirect('/auth/login')
      }
    }
    
    // 2. The login routes (performs the login)
    server.get('/auth/login', { preValidation: fastifyPassport.authenticate('google') }, () => {})
    server.get(
      '/auth/callback',
      { preValidation: fastifyPassport.authenticate('google', { successRedirect: '/' }) },
      () => {}
    )
    
    // 3. The protected route (requires a login)
    server.get('/', { preValidation: requireLogin }, async (request) => request.user)
  4. Initialize @fastify/passport with @fastify/secure-session

    main

    To use @fastify/passport with @fastify/secure-session, you must register both the session plugin and the passport initialization plugins. Both fastifyPassport.initialize() and fastifyPassport.secureSession() are mandatory.

    Note: @fastify/secure-session is a stateless approach that stores the entire session in an encrypted cookie, whereas @fastify/session is stateful and uses a session store.

    import fastifyPassport from '@fastify/passport'
    import fastifySecureSession from '@fastify/secure-session'
    import fs from 'fs'
    import path from 'path'
    
    const server = fastify()
    
    // 1. Set up secure sessions (Mandatory)
    server.register(fastifySecureSession, { key: fs.readFileSync(path.join(__dirname, 'secret-key')) })
    
    // 2. Initialize @fastify/passport and connect it to the session storage (Mandatory)
    server.register(fastifyPassport.initialize())
    server.register(fastifyPassport.secureSession())
    
    // 3. Register a strategy
    fastifyPassport.use('test', new SomePassportStrategy())
  5. Strongly type the FastifyRequest.user property with TypeScript

    main

    Since @fastify/passport is written in TypeScript, you can provide strong typing for the request.user property using declaration merging. You must re-declare the PassportUser interface within the fastify module to include the properties expected from your authentication strategies.

    You can either define the properties directly or make PassportUser extend an existing type.

    // Option 1: Define properties directly
    declare module 'fastify' {
      interface PassportUser {
        id: string
      }
    }
    
    // Option 2: Extend an existing type
    import { User } from './my/types'
    
    declare module 'fastify' {
      interface PassportUser extends User {}
    }
  6. Initialize @fastify/passport with @fastify/session

    main

    If you prefer using @fastify/session (the stateful approach), you can instantiate an Authenticator and register it with your server. You still need to register fastifyPassport.initialize() and fastifyPassport.secureSession().

    import { Authenticator } from '@fastify/passport'
    import fastifyCookie from '@fastify/cookie'
    import fastifySession from '@fastify/session'
    
    const server = fastify()
    const fastifyPassport = new Authenticator()
    
    server.register(fastifyCookie)
    server.register(fastifySession, { secret: 'secret with minimum length of 32 characters' })
    
    // Initialize and connect to session storage
    server.register(fastifyPassport.initialize())
    server.register(fastifyPassport.secureSession())
    
    fastifyPassport.use('test', new SomePassportStrategy())
  7. Use multi-strategy authentication chains

    main

    You can pass an array of strategies to an authentication handler to create a chain. The handler will attempt each strategy in series.

    Behavioral Rules:

    1. Success/Redirect/Error: The first strategy that succeeds, redirects, or encounters an error will halt the chain.
    2. Failures: If a strategy calls fail(), the handler catches the failure and proceeds to the next strategy in the array.
    3. Total Failure: If all strategies in the chain fail, the handler executes the failure logic (e.g., redirecting to failureRedirect or returning a 401).

    Note: This pattern is ideal for API endpoints supporting multiple credential types (e.g., Basic, Digest, Token). It is not recommended for strategies involving redirects (like OAuth2 providers), as the first strategy that attempts a redirect will halt the chain before subsequent strategies can be tried.

  8. Configure session cleanup on logIn

    main

    By default, @fastify/passport cleans the session after a successful login for security. You can manage this behavior using the following options:

    1. Set keepSessionInfo: true in the .authenticate() call.
    2. Set keepSessionInfo: true in the request.login() call.
    3. Configure the global settings:
      • clearSessionOnLogin: defaults to true.
      • clearSessionIgnoreFields: defaults to ['passport', 'session'].
  9. How SessionStrategy restores user state

    main

    The SessionStrategy works by checking the current session for a storable user value.

    1. It retrieves the user data from the session via the sessionManager.
    2. If valid data is found, it calls the provided deserializeUser function.
    3. If deserializeUser returns a user, that user is assigned to request[request.passport.userProperty] (typically request.user).
    4. If deserializeUser returns nothing (null/undefined), the strategy automatically calls logOut(request) to clear the invalid session.

    This strategy ensures that once a user has logged in via an OAuth or Local strategy, they remain authenticated in subsequent requests as long as their session is valid.

  10. Understand the PassportSession type

    main

    The PassportSession type represents the structural shape of the session object provided by either @fastify/session or @fastify/secure-session. Since fastify-passport is compatible with both, it uses this local interface to ensure compatibility without requiring both session plugins to be installed.

    Key methods include:

    • get(key: string): unknown: Retrieves a value from the session.
    • set(key: string, value: unknown): void: Sets a value in the session.
    • data(): unknown: Returns the underlying session data.
    • regenerate?(ignoreFields?: string[]): void | Promise<void>: (Optional) Regenerates the session ID, optionally ignoring specific fields.
    export type PassportSession = {
      get(key: string): unknown
      set(key: string, value: unknown): void
      data(): unknown
      regenerate?(ignoreFields?: string[]): void | Promise<void>
    }
  11. Authenticate requests with authenticate()

    main

    The authenticate method returns a preValidation hook used to validate users and sign them in. It is typically used on login routes.

    Behavior:

    • It applies the specified strategy (or array of strategies) to the request.
    • On success: The user is populated at request.user and a session is established (if session: true).
    • On failure: An unauthorized response is sent (unless failWithError: true is used).

    Strategies:

    • You can pass a strategy name (string) or the Strategy instance itself.
    • You can pass an array of names/instances to try multiple strategies in order.
    • Warning: Do not use multiple OAuth2 strategies in a single authenticate call, as the first one that triggers a redirect will prevent the others from running. Instead, create separate routes for each OAuth provider.

    Options:

    • session: (Boolean, default true) Save login state in session.
    • successRedirect: (String) URL to redirect to after successful login.
    • failureRedirect: (String) URL to redirect to after failed login.
    • successReturnToOrRedirect: (Boolean) Redirect to the URL stored in session under returnTo if available, otherwise to the provided URL.
    • keepSessionInfo: (Boolean) If true, preserves existing session properties after authentication.
    • failWithError: (Boolean) Throws an AuthenticationError instead of sending a failure response, allowing your Fastify error handler to catch it.
    • authInfo: (Boolean, default true) Whether to store the strategy's info object at request.authInfo.
    • assignProperty: (String) Assign the verify callback result to a specific property.
    • state: (Object) Pass state through to the strategy (e.g., for Google OAuth).
    • successMessage / successFlash: (String | Boolean) Override success messages.
    • failureMessage / failureFlash: (String | Boolean) Override failure messages.

    Custom Callback: You can provide a callback to override default handling: async (request, reply, err, user, info, status) => { ... } If a callback is provided, you are responsible for logging in the user and managing the session.

    // Authenticate using a registered strategy named 'local'
    fastifyPassport.authenticate('local', { successRedirect: '/', failureRedirect: '/login' });
    
    // Authenticate using multiple strategies in order
    fastify.get(
      '/',
      { preValidation: fastifyPassport.authenticate(['bearer', 'basic', 'google'], { authInfo: false }) },
      async (request, reply, err, user, info, status) => {
        if (err !== null) {
          console.warn(err)
        } else if (user) {
          console.log(`Hello ${user.name}!`) 
        }
      }
    )
    
    // Use a strategy instance directly without registering it globally
    fastify.get(
      '/',
      {
        preValidation: fastifyPassport.authenticate([new BearerTokenStrategy(), new BasicAuthStrategy()], {
          authInfo: false,
        }),
      },
      async (request, reply, err, user, info, status) => { /* ... */ }
    )