Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/fastify/fastify-passport in…
Browse files Browse the repository at this point in the history
…to next
  • Loading branch information
gurgunday committed Jun 11, 2024
2 parents 6651701 + f33ce38 commit 7af9da0
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 20 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ fastifyPassport.use('test', new SomePassportStrategy()) // you'd probably use so

## Session cleanup on logIn

For security reasons the session is cleaned after login. You can manage this configuration at your own risk by using
`clearSessionOnLogin (default: true)` and `clearSessionIgnoreFields (default: ['passport', 'session'])`
For security reasons the session is cleaned after login. You can manage this configuration at your own risk by:
1) Include `keepSessionInfo` true option when perform the passport `.authenticate` call;
2) Include `keepSessionInfo` true option when perform the request `.login` call;
3) Using `clearSessionOnLogin (default: true)` and `clearSessionIgnoreFields (default: ['passport', 'session'])`.

## Difference between `@fastify/secure-session` and `@fastify/session`
`@fastify/secure-session` and `@fastify/session` are both session plugins for Fastify which are capable of encrypting/decrypting the session. The main difference is that `@fastify/secure-session` uses the stateless approach and stores the whole session in an encrypted cookie whereas `@fastify/session` uses the stateful approach for sessions and stores them in a session store.
Expand Down Expand Up @@ -136,6 +138,7 @@ Options:
message for failures (overrides any from the strategy itself).
- `assignProperty` Assign the object provided by the verify callback to given property
- `state` Pass any provided state through to the strategy (e.g. for Google Oauth)
- `keepSessionInfo` True to save existing session properties after authentication

An optional `callback` can be supplied to allow the application to override the default manner in which authentication attempts are handled. The callback has the following signature:

Expand Down
20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fastify/passport",
"version": "2.4.0",
"version": "2.5.0",
"description": "Simple, unobtrusive authentication for Fastify.",
"main": "dist/index.js",
"type": "commonjs",
Expand Down Expand Up @@ -66,7 +66,7 @@
"rimraf": "^5.0.5",
"set-cookie-parser": "^2.6.0",
"ts-jest": "^29.1.2",
"tsd": "^0.30.7",
"tsd": "^0.31.0",
"typescript": "~5.4.3"
},
"files": [
Expand Down
1 change: 1 addition & 0 deletions src/AuthenticationRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface AuthenticateOptions {
authInfo?: boolean
session?: boolean
pauseStream?: boolean
keepSessionInfo?: boolean
}

export type SingleStrategyCallback = (
Expand Down
14 changes: 11 additions & 3 deletions src/decorators/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@ export type DoneCallback = (err?: Error) => void
* @api public
*/
export async function logIn<T = unknown>(this: FastifyRequest, user: T): Promise<void>
export async function logIn<T = unknown>(this: FastifyRequest, user: T, options: { session?: boolean }): Promise<void>
export async function logIn<T = unknown>(this: FastifyRequest, user: T, options: { session?: boolean } = {}) {
export async function logIn<T = unknown>(
this: FastifyRequest,
user: T,
options: { session?: boolean; keepSessionInfo?: boolean }
): Promise<void>
export async function logIn<T = unknown>(
this: FastifyRequest,
user: T,
options: { session?: boolean; keepSessionInfo?: boolean } = {}
) {
if (!this.passport) {
throw new Error('passport.initialize() plugin not in use')
}
Expand All @@ -34,7 +42,7 @@ export async function logIn<T = unknown>(this: FastifyRequest, user: T, options:
this[property] = user
if (session) {
try {
await this.passport.sessionManager.logIn(this, user)
await this.passport.sessionManager.logIn(this, user, options)
} catch (e) {
this[property] = null
throw e
Expand Down
11 changes: 8 additions & 3 deletions src/session-managers/SecureSessionManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="@fastify/secure-session" />
import { FastifyRequest } from 'fastify'
import { AuthenticateOptions } from '../AuthenticationRoute'
import { SerializeFunction } from '../Authenticator'

/** Class for storing passport data in the session using `@fastify/secure-session` or `@fastify/session` */
Expand Down Expand Up @@ -33,13 +34,17 @@ export class SecureSessionManager {
}
}

async logIn(request: FastifyRequest, user: any) {
async logIn(request: FastifyRequest, user: any, options?: AuthenticateOptions) {
const object = await this.serializeUser(user, request)

// Handle @fastify/session to prevent token/CSRF fixation
if (request.session.regenerate) {
if (this.clearSessionOnLogin && object) {
await request.session.regenerate(this.clearSessionIgnoreFields)
const keepSessionInfoKeys: string[] = [...this.clearSessionIgnoreFields]
if (options?.keepSessionInfo) {
keepSessionInfoKeys.push(...Object.keys(request.session))
}
await request.session.regenerate(keepSessionInfoKeys)
} else {
await request.session.regenerate()
}
Expand All @@ -50,7 +55,7 @@ export class SecureSessionManager {
else if (this.clearSessionOnLogin && object) {
const currentFields = request.session.data() || {}
for (const field of Object.keys(currentFields)) {
if (this.clearSessionIgnoreFields.includes(field)) {
if (options?.keepSessionInfo || this.clearSessionIgnoreFields.includes(field)) {
continue
}
request.session.set(field, undefined)
Expand Down
42 changes: 42 additions & 0 deletions test/secure-session-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,46 @@ describe('SecureSessionManager', () => {
await sessionManger.logIn(request, user)
expect(request.session.regenerate).toHaveBeenCalledTimes(1)
})

test('should call request.session.regenerate function with all properties from session if keepSessionInfo is true', async () => {
const sessionManger = new SecureSessionManager(
{ clearSessionOnLogin: true },
((id) => id) as unknown as SerializeFunction
)
const user = { id: 'test' }
const request = {
session: { regenerate: jest.fn(() => {}), set: () => {}, data: () => {}, sessionValue: 'exist' }
} as unknown as FastifyRequest
await sessionManger.logIn(request, user, { keepSessionInfo: true })
expect(request.session.regenerate).toHaveBeenCalledTimes(1)
expect(request.session.regenerate).toHaveBeenCalledWith(['session', 'regenerate', 'set', 'data', 'sessionValue'])
})

test('should call request.session.regenerate function with default properties from session if keepSessionInfo is false', async () => {
const sessionManger = new SecureSessionManager(
{ clearSessionOnLogin: true },
((id) => id) as unknown as SerializeFunction
)
const user = { id: 'test' }
const request = {
session: { regenerate: jest.fn(() => {}), set: () => {}, data: () => {}, sessionValue: 'exist' }
} as unknown as FastifyRequest
await sessionManger.logIn(request, user, { keepSessionInfo: false })
expect(request.session.regenerate).toHaveBeenCalledTimes(1)
expect(request.session.regenerate).toHaveBeenCalledWith(['session'])
})

test('should call session.set function if no regenerate function provided and keepSessionInfo is true', async () => {
const sessionManger = new SecureSessionManager(
{ clearSessionOnLogin: true },
((id) => id) as unknown as SerializeFunction
)
const user = { id: 'test' }
const request = {
session: { set: jest.fn(), data: () => {}, sessionValue: 'exist' }
} as unknown as FastifyRequest
await sessionManger.logIn(request, user, { keepSessionInfo: false })
expect(request.session.set).toHaveBeenCalledTimes(1)
expect(request.session.set).toHaveBeenCalledWith('passport', { id: 'test' })
})
})

0 comments on commit 7af9da0

Please sign in to comment.