passport icon indicating copy to clipboard operation
passport copied to clipboard

Add support for fastify-passport package

Open Niewdanka opened this issue 1 year ago • 10 comments

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

Niewdanka avatar May 15 '24 21:05 Niewdanka

Any update on this please?

olawalejuwonm avatar Jun 21 '24 14:06 olawalejuwonm

app.getHttpAdapter().getInstance().decorateReply('setHeader', function (key, value) { this.raw.setHeader(key, value); });
app.getHttpAdapter().getInstance().decorateReply('end', function () { this.raw.end(); });

d6nn9 avatar Sep 27 '24 19:09 d6nn9

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.

dfenerski avatar Sep 28 '24 04:09 dfenerski

I'd like to help work on this.

But I found that:

  1. fastify-passport is a direct port from passport
  2. passport is a peer dependency of @nest/passport and 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?

songkeys avatar Sep 29 '24 14:09 songkeys

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)

SzymonGonet avatar Oct 29 '24 20:10 SzymonGonet

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);

jadejr avatar Jan 24 '25 04:01 jadejr

I also would like to use Fastify, Nest.js and Passport together in my application, so really looking forward to this!

jhrncar avatar Feb 03 '25 15:02 jhrncar

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.

mainfraame avatar Feb 06 '25 11:02 mainfraame

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.

mrsimonemms avatar Feb 26 '25 17:02 mrsimonemms

Any updates?

Forceres avatar May 23 '25 23:05 Forceres