Add support for fastify-passport package
Is there an existing issue that is already proposing this?
- [X] I have searched the existing issues
Is your feature request related to a problem? Please describe it
Hi! Based on this thread https://github.com/fastify/help/issues/382 it seems that fastify-passport package is stable and can help resolve the problem related to this issue https://github.com/nestjs/nest/issues/5702 . While the workaround mentioned in issue works i believe this package could also help resolve the problem.
Describe the solution you'd like
Add support for fastify-passport package.
Teachability, documentation, adoption, migration strategy
none
What is the motivation / use case for changing the behavior?
none
Any update on this please?
app.getHttpAdapter().getInstance().decorateReply('setHeader', function (key, value) { this.raw.setHeader(key, value); });
app.getHttpAdapter().getInstance().decorateReply('end', function () { this.raw.end(); });
app.getHttpAdapter().getInstance().decorateReply('setHeader', function (key, value) { this.raw.setHeader(key, value); }); app.getHttpAdapter().getInstance().decorateReply('end', function () { this.raw.end(); });
The key issue is native support for passport's guards & strategies.
I'd like to help work on this.
But I found that:
-
fastify-passportis a direct port frompassport -
passportis a peer dependency of@nest/passportand the api was used in the code
How do you think we should bring fastify-passport in without interupting express's passport? Maybe we should make them optional and let user to pass in the passport instance?
When do we know when this has been done?
Once `fastify-passport` becomes stable, we'll add support for it in the `@nestjs/passport` package.
Originally posted by @kamilmysliwiec in #5702 (comment)
stable by what metric? They are currently at 3.x.
I was able to get the tests to pass on initial release of nestjs 11 via this hacked up patch. The createPassportContext bit is particularly gnarly and could likely be greatly simplified and the e2e tests probably shouldn't be done this way, but rather be split out. I just don't know yet how to do that.
EDIT: I also realized i'm calling the request decorated method for authenticate but the global one could probably be used instead.
diff --git a/lib/auth.guard.ts b/lib/auth.guard.ts
index 4a7dcc1..054e7d9 100644
--- a/lib/auth.guard.ts
+++ b/lib/auth.guard.ts
@@ -9,7 +9,7 @@ import {
Optional,
UnauthorizedException
} from '@nestjs/common';
-import * as passport from 'passport';
+import passport from '@fastify/passport';
import { Type } from './interfaces';
import {
AuthModuleOptions,
@@ -87,11 +87,7 @@ function createAuthGuard(type?: string | string[]): Type<IAuthGuard> {
request: TRequest
): Promise<void> {
const user = request[this.options.property || defaultOptions.property];
- await new Promise<void>((resolve, reject) =>
- request.logIn(user, this.options, (err) =>
- err ? reject(err) : resolve()
- )
- );
+ await request.logIn(user, this.options);
}
handleRequest(err, user, info, context, status): TUser {
@@ -113,14 +109,22 @@ function createAuthGuard(type?: string | string[]): Type<IAuthGuard> {
const createPassportContext =
(request: any, response: any) =>
- (type: string | string[], options: any, callback: Function) =>
- new Promise<void>((resolve, reject) =>
- passport.authenticate(type, options, (err, user, info, status) => {
- try {
- request.authInfo = info;
- return resolve(callback(err, user, info, status));
- } catch (err) {
- reject(err);
- }
- })(request, response, (err: any) => (err ? reject(err) : resolve()))
- );
+ async (type: string | string[], options: any, callback: Function) =>
+ new Promise((resolve, reject) => {
+ try {
+ return request.passport.authenticate(
+ type,
+ options,
+ (request, _response, err, user, info, status) => {
+ try {
+ request.authInfo = info;
+ return resolve(callback(err, user, info, status));
+ } catch (err) {
+ reject(err);
+ }
+ }
+ )(request, response);
+ } catch (error) {
+ reject(error);
+ }
+ });
diff --git a/lib/index.ts b/lib/index.ts
index 66b75aa..e56a630 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -3,4 +3,5 @@ export * from './auth.guard';
export * from './interfaces';
export * from './passport.module';
export * from './passport/passport.serializer';
+export * from './passport/fastify-passport.serializer';
export * from './passport/passport.strategy';
diff --git a/lib/passport/fastify-passport.serializer.ts b/lib/passport/fastify-passport.serializer.ts
new file mode 100644
index 0000000..443567f
--- /dev/null
+++ b/lib/passport/fastify-passport.serializer.ts
@@ -0,0 +1,30 @@
+import passport from '@fastify/passport';
+import type { FastifyRequest } from 'fastify';
+
+export abstract class FastifyPassportSerializer {
+ abstract userSerializer(user: unknown, request: FastifyRequest): Promise<any>;
+ abstract userDeserializer(
+ payload: unknown,
+ request: FastifyRequest
+ ): Promise<any>;
+
+ constructor() {
+ const passportInstance = this.getPassportInstance();
+
+ passportInstance.registerUserSerializer(
+ async (user: Record<string, unknown>, request: FastifyRequest) => {
+ return await this.userSerializer(user, request);
+ }
+ );
+ passportInstance.registerUserDeserializer(
+ async (payload: string, request: FastifyRequest) => {
+ const result = this.userDeserializer(payload, request);
+ return result;
+ }
+ );
+ }
+
+ getPassportInstance() {
+ return passport;
+ }
+}
diff --git a/lib/passport/passport.strategy.ts b/lib/passport/passport.strategy.ts
index 8541e51..8064761 100644
--- a/lib/passport/passport.strategy.ts
+++ b/lib/passport/passport.strategy.ts
@@ -1,4 +1,4 @@
-import * as passport from 'passport';
+import passport from '@fastify/passport';
import { Type, WithoutCallback } from '../interfaces';
export type AllConstructorParameters<T> = T extends {
diff --git a/package.json b/package.json
index 5311361..170aa9f 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
],
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
+ "@fastify/passport": "^3.0.2",
"passport": "^0.5.0 || ^0.6.0 || ^0.7.0"
},
"devDependencies": {
@@ -30,10 +31,13 @@
"@commitlint/config-angular": "19.7.0",
"@eslint/eslintrc": "3.2.0",
"@eslint/js": "9.18.0",
+ "@fastify/cookie": "^11.0.1",
+ "@fastify/session": "^11.0.0",
"@nestjs/common": "11.0.3",
"@nestjs/core": "11.0.3",
"@nestjs/jwt": "11.0.0",
"@nestjs/platform-express": "11.0.3",
+ "@nestjs/platform-fastify": "11.0.3",
"@nestjs/testing": "11.0.3",
"@types/jest": "29.5.14",
"@types/node": "22.10.7",
@@ -43,6 +47,7 @@
"eslint": "9.18.0",
"eslint-config-prettier": "10.0.1",
"eslint-plugin-prettier": "5.2.3",
+ "fastify": "^5.0.0",
"globals": "15.14.0",
"husky": "9.1.7",
"jest": "29.7.0",
@@ -68,4 +73,4 @@
"type": "git",
"url": "https://github.com/nestjs/passport"
}
-}
+}
\ No newline at end of file
diff --git a/test/common/app.e2e-spec.ts b/test/common/app.e2e-spec.ts
index 7dd2e5f..0f29b4f 100644
--- a/test/common/app.e2e-spec.ts
+++ b/test/common/app.e2e-spec.ts
@@ -1,4 +1,11 @@
+import fastifyCookie from '@fastify/cookie';
+import fastifySession from '@fastify/session';
+import fastifyPassport from '@fastify/passport';
import { INestApplication } from '@nestjs/common';
+import {
+ FastifyAdapter,
+ NestFastifyApplication
+} from '@nestjs/platform-fastify';
import { Test } from '@nestjs/testing';
import { spec, request } from 'pactum';
import { AppModule as WithRegisterModule } from '../with-register/app.module';
@@ -9,13 +16,23 @@ describe.each`
${WithRegisterModule} | ${'with'}
${WithoutRegisterModule} | ${'without'}
`('Passport Module $RegisterUse register()', ({ AppModule }) => {
- let app: INestApplication;
+ let app: NestFastifyApplication;
beforeAll(async () => {
const modRef = await Test.createTestingModule({
imports: [AppModule]
}).compile();
- app = modRef.createNestApplication();
+ app = modRef.createNestApplication<NestFastifyApplication>(
+ new FastifyAdapter()
+ );
+ const fastifyInstance = app.getHttpAdapter().getInstance();
+ await fastifyInstance.register(fastifyCookie);
+ await fastifyInstance.register(fastifySession, {
+ secret: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxs3cr3t'
+ });
+ await fastifyInstance.register(fastifyPassport.initialize());
+ await fastifyInstance.register(fastifyPassport.secureSession());
+
await app.listen(0);
const url = (await app.getUrl()).replace('[::1]', 'localhost');
request.setBaseUrl(url);
I also would like to use Fastify, Nest.js and Passport together in my application, so really looking forward to this!
i’ve used passport with fastify in nestjs before with saml2 and oauth2 strategies. @d6nn9 is correct in that a temporary workaround is possible by decorating mostly) the reply object with express methods.
app.getHttpAdapter().getInstance().decorateReply('setHeader', function (key, value) { this.raw.setHeader(key, value); }); app.getHttpAdapter().getInstance().decorateReply('end', function () { this.raw.end(); });
The original workaround doesn't work with fastify-secure-session if you need to persist session data to the callback URL as the onSend hook isn't triggered. Replacing this.raw.end() with this.send() seems to fix that.
Any updates?