Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
307 changes: 9 additions & 298 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@
"@fastify/csrf-protection": "^7.0.0",
"@fastify/secure-session": "^8.0.0",
"@fastify/session": "^11.0.0",
"@tsconfig/node20": "^20.1.6",
"@types/node": "^24.0.8",
"@types/passport": "^1.0.5",
"@types/set-cookie-parser": "^2.4.0",
"@types/passport": "^1.0.17",
"@types/set-cookie-parser": "^2.4.10",
"borp": "^0.20.0",
"eslint": "^9.17.0",
"fastify": "^5.0.0",
Expand All @@ -59,7 +60,7 @@
"passport-github2": "^0.1.12",
"passport-google-oauth": "^2.0.0",
"rimraf": "^6.0.1",
"set-cookie-parser": "^2.4.6",
"set-cookie-parser": "^2.7.1",
"tsd": "^0.32.0",
"typescript": "~5.9.2"
},
Expand Down
23 changes: 14 additions & 9 deletions src/AuthenticationRoute.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as http from 'node:http'
import AuthenticationError from './errors'
import Authenticator from './Authenticator'
import { AnyStrategy, Strategy } from './strategies'
import { FastifyReply, FastifyRequest } from 'fastify'
import { STATUS_CODES } from 'node:http'
import type { Authenticator } from './Authenticator'
import type { AnyStrategy } from './strategies'
import type { Strategy } from './strategies/base'
import { AuthenticationError } from './errors'
import type { FastifyReply, FastifyRequest } from 'fastify'
import { types } from 'node:util'

type FlashObject = { type?: string; message?: string }
Expand Down Expand Up @@ -69,6 +70,8 @@ export class AuthenticationRoute<StrategyOrStrategies extends string | Strategy
readonly options: AuthenticateOptions
readonly strategies: (string | Strategy)[]
readonly isMultiStrategy: boolean
readonly authenticator: Authenticator
readonly callback: AuthenticateCallback<StrategyOrStrategies> | undefined

/**
* Create a new route handler that runs authentication strategies.
Expand All @@ -79,12 +82,14 @@ export class AuthenticationRoute<StrategyOrStrategies extends string | Strategy
* @param callback optional custom callback to process the result of the strategy invocations
*/
constructor (
readonly authenticator: Authenticator,
authenticator: Authenticator,
strategyOrStrategies: StrategyOrStrategies,
options?: AuthenticateOptions,
readonly callback?: AuthenticateCallback<StrategyOrStrategies>
callback?: AuthenticateCallback<StrategyOrStrategies>
) {
this.options = options || {}
this.authenticator = authenticator
this.callback = callback

// Cast `name` to an array, allowing authentication to pass through a chain of strategies. The first strategy to succeed, redirect, or error will halt the chain. Authentication failures will proceed through each strategy in series, ultimately failing if all strategies fail.
// This is typically used on API endpoints to allow clients to authenticate using their preferred choice of Basic, Digest, token-based schemes, etc. It is not feasible to construct a chain of multiple strategies that involve redirection (for example both Facebook and Twitter), since the first one to redirect will halt the chain.
Expand Down Expand Up @@ -309,10 +314,10 @@ export class AuthenticationRoute<StrategyOrStrategies extends string | Strategy
}

if (this.options.failWithError) {
throw new AuthenticationError(http.STATUS_CODES[reply.statusCode]!, rstatus)
throw new AuthenticationError(STATUS_CODES[reply.statusCode]!, rstatus)
}

reply.send(http.STATUS_CODES[reply.statusCode])
reply.send(STATUS_CODES[reply.statusCode])
}

applyFlashOrMessage (event: 'success' | 'failure', request: FastifyRequest, result?: FlashObject) {
Expand Down
10 changes: 6 additions & 4 deletions src/Authenticator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { FastifyPluginAsync, FastifyRequest, RouteHandlerMethod } from 'fastify'
import fastifyPlugin from 'fastify-plugin'
import { AuthenticateCallback, AuthenticateOptions, AuthenticationRoute } from './AuthenticationRoute'
import type { FastifyPluginAsync, FastifyRequest, RouteHandlerMethod } from 'fastify'
import { fastifyPlugin } from 'fastify-plugin'
import { type AuthenticateCallback, type AuthenticateOptions, AuthenticationRoute } from './AuthenticationRoute'
import { CreateInitializePlugin } from './CreateInitializePlugin'
import { SecureSessionManager } from './session-managers/SecureSessionManager'
import { AnyStrategy, SessionStrategy, Strategy } from './strategies'
import type { AnyStrategy } from './strategies/index'
import type { Strategy } from './strategies/base'
import { SessionStrategy } from './strategies/SessionStrategy'

export type SerializeFunction<User = any, SerializedUser = any> = (
user: User,
Expand Down
13 changes: 8 additions & 5 deletions src/CreateInitializePlugin.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import fp from 'fastify-plugin'
import { logIn, logOut, isAuthenticated, isUnauthenticated } from './decorators'
import Authenticator from './Authenticator'
import flash = require('@fastify/flash')
import { fastifyPlugin } from 'fastify-plugin'
import flash from '@fastify/flash'
import type Authenticator from './Authenticator'
import { logIn } from './decorators/login'
import { logOut } from './decorators/logout'
import { isAuthenticated } from './decorators/is-authenticated'
import { isUnauthenticated } from './decorators/is-unauthenticated'

export function CreateInitializePlugin (passport: Authenticator) {
return fp(async (fastify) => {
return fastifyPlugin(async (fastify) => {
fastify.register(flash)
fastify.decorateRequest('passport', {
getter () {
Expand Down
2 changes: 1 addition & 1 deletion src/decorators/is-authenticated.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FastifyRequest } from 'fastify'
import type { FastifyRequest } from 'fastify'

export function isAuthenticated (this: FastifyRequest): boolean {
const property = this.passport.userProperty
Expand Down
2 changes: 1 addition & 1 deletion src/decorators/is-unauthenticated.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FastifyRequest } from 'fastify'
import type { FastifyRequest } from 'fastify'

export function isUnauthenticated (this: FastifyRequest): boolean {
return !this.isAuthenticated()
Expand Down
2 changes: 1 addition & 1 deletion src/decorators/login.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FastifyRequest } from 'fastify'
import type { FastifyRequest } from 'fastify'

export type DoneCallback = (err?: Error) => void
/**
Expand Down
2 changes: 1 addition & 1 deletion src/decorators/logout.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FastifyRequest } from 'fastify'
import type { FastifyRequest } from 'fastify'

/**
* Terminate an existing login session.
Expand Down
2 changes: 1 addition & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class AuthenticationError extends Error {
export class AuthenticationError extends Error {
status: number

constructor (message: string, status: number) {
Expand Down
51 changes: 48 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,54 @@
import { Authenticator } from './Authenticator'
import './type-extensions' // necessary to make sure that the fastify types are augmented
import type { flashFactory } from '@fastify/flash/lib/flash'
import type { logIn } from './decorators/login'
import type { logOut } from './decorators/logout'
import type { isAuthenticated } from './decorators/is-authenticated'
import type { isUnauthenticated } from './decorators/is-unauthenticated'
import Authenticator from './Authenticator'

const passport = new Authenticator()

// Workaround for importing fastify-passport in native ESM context
module.exports = exports = passport
export default passport
export { Strategy } from './strategies'
export { Strategy } from './strategies/base'
export { Authenticator } from './Authenticator'

declare module 'fastify' {
/**
* An empty interface representing the type of users that applications using `fastify-passport` might assign to the request
* Suitable for TypeScript users of the library to declaration merge with, like so:
* ```
* import { User } from "./my/types";
*
* declare module 'fastify' {
* interface PassportUser {
* [Key in keyof User]: User[Key]
* }
* }
* ```
*/
interface PassportUser {}

interface ExpressSessionData {
[key: string]: any
}

interface FastifyRequest {
flash: ReturnType<typeof flashFactory>['request']

login: typeof logIn
logIn: typeof logIn
logout: typeof logOut
logOut: typeof logOut
isAuthenticated: typeof isAuthenticated
isUnauthenticated: typeof isUnauthenticated
passport: Authenticator
user?: PassportUser
authInfo?: Record<string, any>
account?: PassportUser
}

interface FastifyReply {
flash: ReturnType<typeof flashFactory>['reply']
}
}
10 changes: 5 additions & 5 deletions src/session-managers/SecureSessionManager.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { FastifyRequest } from 'fastify'
import { AuthenticateOptions } from '../AuthenticationRoute'
import { SerializeFunction } from '../Authenticator'
import { FastifySessionObject } from '@fastify/session'
import { Session, SessionData } from '@fastify/secure-session'
import type { FastifyRequest } from 'fastify'
import type { AuthenticateOptions } from '../AuthenticationRoute'
import type { SerializeFunction } from '../Authenticator'
import type { FastifySessionObject } from '@fastify/session'
import type { Session, SessionData } from '@fastify/secure-session'

type Request = FastifyRequest & { session: FastifySessionObject | Session<SessionData> }

Expand Down
2 changes: 1 addition & 1 deletion src/strategies/SessionStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Strategy } from './base'
import { DeserializeFunction } from '../Authenticator'
import type { DeserializeFunction } from '../Authenticator'
import type { FastifyRequest } from 'fastify'

/**
Expand Down
2 changes: 1 addition & 1 deletion src/strategies/base.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FastifyRequest } from 'fastify'
import type { FastifyRequest } from 'fastify'

export class Strategy {
name: string
Expand Down
6 changes: 3 additions & 3 deletions src/strategies/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Strategy as ExpressStrategy } from 'passport'
import type { Strategy } from './base'
export * from './base'
export * from './SessionStrategy'
import { Strategy } from './base'
export { Strategy } from './base'
export { SessionStrategy } from './SessionStrategy'

export type AnyStrategy = Strategy | ExpressStrategy
43 changes: 0 additions & 43 deletions src/type-extensions.ts

This file was deleted.

17 changes: 11 additions & 6 deletions test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ export class TestStrategy extends Strategy {
}

export class TestDatabaseStrategy extends Strategy {
readonly database: Record<string, { id: string; login: string; password: string }>

constructor (
name: string,
readonly database: Record<string, { id: string; login: string; password: string }> = {}
database: Record<string, { id: string; login: string; password: string }> = {}
) {
super(name)
this.database = database
}

authenticate (request: any, _options?: { pauseStream?: boolean }) {
Expand All @@ -57,8 +60,10 @@ export class TestDatabaseStrategy extends Strategy {
/** Class representing a browser in tests */
export class TestBrowserSession {
cookies: Record<string, string>
server: FastifyInstance

constructor (readonly server: FastifyInstance) {
constructor (server: FastifyInstance) {
this.server = server
this.cookies = {}
}

Expand All @@ -83,15 +88,15 @@ type SessionOptions = FastifyRegisterOptions<FastifySessionOptions | SecureSessi
const loadSessionPlugins = (server: FastifyInstance, sessionOptions: SessionOptions = null) => {
if (process.env.SESSION_PLUGIN === '@fastify/session') {
server.register(fastifyCookie)
const options = <FastifyRegisterOptions<FastifySessionOptions>>(sessionOptions || {
const options = sessionOptions || {
secret: 'a secret with minimum length of 32 characters',
cookie: { secure: false }
})
server.register(fastifySession, options)
}
server.register(fastifySession, options as FastifyRegisterOptions<FastifySessionOptions>)
} else {
server.register(
fastifySecureSession,
<FastifyRegisterOptions<SecureSessionPluginOptions>>(sessionOptions || { key: SecretKey })
(sessionOptions || { key: SecretKey }) as FastifyRegisterOptions<SecureSessionPluginOptions>
)
}
}
Expand Down
5 changes: 4 additions & 1 deletion test/multi-strategy-callback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { Strategy } from '../src/strategies'

// Strategy that always fails with a specific status
class FailingStrategy extends Strategy {
constructor (name: string, private status: number = 401) {
readonly status: number

constructor (name: string, status: number = 401) {
super(name)
this.status = status
}

authenticate (_request: any, _options?: { pauseStream?: boolean }) {
Expand Down
4 changes: 2 additions & 2 deletions tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"sourceMap": true,
"moduleResolution": "nodenext",
"esModuleInterop": true,
"declaration": true,
"target": "es2017",
"module": "nodenext",
"pretty": true,
"noEmitOnError": true,
Expand All @@ -20,7 +20,7 @@
"isolatedModules": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"lib": ["ESNext"]
"erasableSyntaxOnly": true
},
"include": ["src", "test"]
}
Loading