DEV Community

Setting Up Sessions with NestJS, Passport, and Redis

Jay McDoniel on August 24, 2021

Jay is a member of the NestJS core team, primarily helping out the community on Discord and Github and contributing to various parts of the framewo...
Collapse
 
hinogi profile image
Stefan Schneider

import { session as passportSession, initialize as passportInitialize } from 'passport';
This doesn't seem to work.
import * as passport from 'passport';
and then passport.session(), passport.initialize() seems to work. Maybe passport has no esm export?!

Collapse
 
jmcdo29 profile image
Jay McDoniel

This could be dependent on of your trying to use esm, or your tsconfig. By default, Typescript in a node project still uses the CommonJS syntax and the above methods are named exports. This runs fine for me and I'm fact there shouldn't be a difference between import * as passport...passport.session() and import { session }...session(), as the second one should just be destructing the first

Collapse
 
hinogi profile image
Stefan Schneider • Edited

This is what it turns out with trying to use named exports.
error

Thread Thread
 
jmcdo29 profile image
Jay McDoniel • Edited

That's definitely strange. All the code is available in the mentioned git repo along with the steps to run it. This is everything I used to run the project locally while driving into the code, so I'm confident that it works. There's probably a difference in a tsconfig file somewhere

Thread Thread
 
hinogi profile image
Stefan Schneider

nest cli will generate a different tsconfig as in the repo. If you follow from scratch.

Thread Thread
 
jmcdo29 profile image
Jay McDoniel

Sure enough. That's super interesting. I couldn't modify the tsconfig to match the one in my sample repository either, though I know this was working there. Very strange issue indeed. It seemed to have to do with how the import was being generated

passport_1.session()
Enter fullscreen mode Exit fullscreen mode

vs

(0, passport_1.session)()
Enter fullscreen mode Exit fullscreen mode

I'll get the tutorial updated with your working fix.

Thread Thread
 
jmcdo29 profile image
Jay McDoniel

@hinogi are you, by chance, on typescript 4.4.2? I just noticed that difference in my test repo (using nest new) and my blog repo. The blog repo is on 4.3.5 and works fine, but the same config on 4.4.2 failed

Thread Thread
 
hinogi profile image
Stefan Schneider

Yes I am on 4.4.2

Thread Thread
 
jmcdo29 profile image
Jay McDoniel

Looks like this was a breaking change of 4.4.0 which explains why it works with 4.3.5 but not 4.4.2. Thanks for pointing it out and helping me see what else has changed.

Collapse
 
kallezz profile image
Kalle210496 • Edited

Just want to mention that this guide does not work with redis package version 4.
After troubleshooting I couldn't find a solution other than downgrading to ^3.1.2. Redis throws an error (below).

Some notes I discovered with redis v4+:
The type RedisClient is RedisClientType
host / port options for createClient have to be replaced by "url" option

Redis fails with:
return Promise.reject(new errors_1.ClientClosedError());
Error: The client is closed

Collapse
 
wolfhoundjesse profile image
Jesse M. Holmes

Your error, The client is closed, is due to this breaking change.

You’ll need to make a change to the code above (in addition to the changes you already mentioned, like the url property) to call connect() on your new client, or you will get an error from connect-redis asking you to supply the client directly. I’m writing this from my phone at 1:30am, so if it doesn’t come out right, I’ll fix it in the morning. 😂

import { Module } from '@nestjs/common';
import * as Redis from 'redis';

import { REDIS } from './redis.constants';

@Module({
  providers: [
    {
      provide: REDIS,
      useFactory: async () => {
        const client = Redis.createClient({
          url: 'rediss://username:password@your.redis.url', 
          legacyMode: true 
        })
        await client.connect()
        return client
      },
    },
  ],
  exports: [REDIS],
})
export class RedisModule {}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jmcdo29 profile image
Jay McDoniel

Thank you for pointing this out!

Collapse
 
danbachar profile image
Dan Bachar

hey @jmcdo29 ,

first of all, I wanted to thank you for this article. It's very well written and have done a wonderful done explaining to me how session storage, passport and nest play together.
I followed and can confirm the flow works when using cURL, and I have a question about using a proper front end. I am using fetch to authenticate my statically-served frontend (i.e. React), and am getting an HTTP 201 from the server when I enter the right credentials.
How do I "save" the session and make sure the client can communicate with the server? I added credentials: 'include'` to my authenticated requests, and I keep getting HTTP 401, which leads me to thinking I'm missing something here still.
I was wondering if you had any idea what might that be.

Thanks,
Dan

Collapse
 
jmcdo29 profile image
Jay McDoniel

If you're getting a 401, that sounds like passport is being used on other routes than just the login, and is causing the issue here. Wouldn't be able to really tell without seeing code though

Collapse
 
danbachar profile image
Dan Bachar

already solved per discord, thanks!

Collapse
 
hiteshpathak profile image
Hitesh Pathak • Edited

With the latest versions for redis and connect-redis. I am getting TS errors.

configure(consumer: MiddlewareConsumer) {
consumer.apply(
session({
store: new (RedisStore(session))({
client: this.redis,
}),
saveUninitialized: false,
resave: false,
cookie: {
httpOnly: true,
},
secret: this.configService.get('SESSION_SECRET'),
}),
);
}

TS Error: Type 'RedisClientType' is not assignable to type 'Client'

Redis no longer exports RedisClient, which is what the article uses. For now I'm simply ignoring it with client: this.redis as Client with import { Client } from 'connect-redis'

If someone knows how to type this correctly, I'd be glad to get some pointers towards resolving this issue..

Collapse
 
khaledalhamwie profile image
khaled al hamwie

hi nice blog thank you for the effort
I would like to point to that the code inside the app.module in the source need to be changed to

// src/app.module.ts

import { Inject, Logger, MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
// used to be import * as  RedisStore from 'connect-redis';
import  RedisStore from 'connect-redis';
import * as session from 'express-session';
import * as passport from 'passport';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth';
import { REDIS, RedisModule } from './redis';

@Module({
  imports: [AuthModule, RedisModule],
  providers: [AppService, Logger],
  controllers: [AppController],
})
export class AppModule implements NestModule {
  constructor(@Inject(REDIS) private readonly redis: RedisClient) {}
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(
        session({
// used to be new (RedisStore(session))({ client: this.redis, logErrors: true }),
          store: new RedisStore({ client: this.redis }),
          saveUninitialized: false,
          secret: 'sup3rs3cr3t',
          resave: false,
          cookie: {
            sameSite: true,
            httpOnly: false,
            maxAge: 60000,
          },
        }),
        passport.initialize(),
        passport.session(),
      )
      .forRoutes('*');
  }
}

Enter fullscreen mode Exit fullscreen mode

and you also need to remove the package @types/connect-redis and the legacy option in redis.module.ts in the Redius.createClient check the git hub link bellow
the source
stack over flow post
stackoverflow.com/questions/757875...
git hub page
github.com/tj/connect-redis/releas...
and thank for the blog post

have a nice day

Collapse
 
makaseloli profile image
まかせロリ

How would I go about using Nest's ConfigService in the configure method of the AppModule? I need to set up the session options (e.g. secret, sameSite, maxAge) depending on the environment, so hard-coding is a no-go. Should I use some other configuration method instead?

Collapse
 
jmcdo29 profile image
Jay McDoniel

You can inject the ConfigService just like we do Redis and use this.config.get('SESSION-SECRET') or whatever else you would need to

Collapse
 
dokuslab profile image
DokusLab

Great article!

Is it necessary to protect this against CSRF?
I added the following to the Session options :



cookie: {
        sameSite: 'none', // Because my API sits on a diffrent domain
        secure: true,
        httpOnly: true,
},


Enter fullscreen mode Exit fullscreen mode

Should I add more protection like XSRF-TOKEN?

Collapse
 
motivatedcoder profile image
Ayoub Elmendoub

hey guys, i did setup session based authentication w/ redis store & i want to prevent the cookie object from being stored in redis

  {
    "cookie": {
        "originalMaxAge": null,
        "expires": null,
        "secure": false,
        "httpOnly": true,
        "path": "/",
        "sameSite": "lax"
    },
    "passport": {
        "user": {
            "username": "maria3",
            "hashedPassword": "$2b$10$9hGSvpgFkk/8ENRQUok0duAYg8N2hkFPYkrJwlo5kVXHmtSE/AdAW",
            "salt": "$2b$10$9hGSvpgFkk/8ENRQUok0du",
            "role": "CUSTOMER",
            "id": 7,
            "createdAt": "2022-05-16T15:09:12.514Z",
            "updatedAt": "2022-05-16T15:09:12.514Z",
            "deleteAt": null
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
wguerram profile image
wguerram

Thanks for sharing this, why do you think I'm having the following issue:

When using the LoggedInGuard in my app.controller an exception is thrown because passport is not initialized but if I use the LoggedInGuard in my auth.controller it's working. The one thing I have different I'm doing a MicroORMModule.forRoot call just before the session.

Collapse
 
wguerram profile image
wguerram

My issue was related to not adding a path in my controller decorator.

Collapse
 
ruslangonzalez profile image
Ruslan Gonzalez

Outstanding article!

Collapse
 
rantiev profile image
rantiev

Hi, What for you need redis? If you do request user from DB on every request in passport validate? You have your token as a cookie or localstorage item, you use it to get user from DB on every request? What is the reason to have session then.

Ig you fetch user once after login, then you could reuse this info from session. Otherwise what for?

Collapse
 
grostmaster profile image
Oleh Butsyk

How can I correctly make logout from the session?

Collapse
 
jmcdo29 profile image
Jay McDoniel

I believe that just req.logout() should do it , as passport should help in maintaining the session information under the hood

Collapse
 
wguerram profile image
wguerram

Try
req.logout():
req.session.destroy();