Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

NestJS profile imageJay McDoniel
Jay McDoniel forNestJS

Posted on • Edited on

     

Setting Up Sessions with NestJS, Passport, and Redis

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 framework.

If you're here, you're either one of the avid readers of my, just stumbling about dev.to looking for something interesting to read, or you're searching for how to implement sessions withPassport andNestJS.Nest's own docs do a pretty good job of showing how to set up the use ofJWTs with Passport, but are lacking when it comes to how to use sessions. Maybe you want to use a session store because of supporting some legacy software. Maybe it's because JWTs bring too much complexity with scope. Maybe it's because you're looking for an easier way to set up refresh tokens. Whatever the case, this article is going to be for you.

Pre-requisites

I'm going to be using NestJS (it's in the title, so I hope that's obvious) and I'm going to be making use ofGuards so if you don't know what those are, I highly suggest reading up on them first. Don't worry, I'll wait.

I'm also going to be not using an HTTP client likePostman orInsomnia, but usingcURL instead. I lke living in the terminal as much as I can, as it gives me immediate feedback between my terminals. Feel free to use whichever you prefer, but the code snippets will be curls.

And speaking of immediate feedback, I'm also going to be usingtmux, which is a terminal multiplexer, to allow me to run multiple terminals at a time within the same window and logical grouping. This allows me to keep a single terminal window up and view my server logs, docker-compose instance and/or logs, and make curls without having to alt-tab to change views. Very handy, and very customizable.

Lastly, I'll be usingdocker and adocker-compose file to run aRedis instance for the session storage and to allow for running a redis-cli to be able to query the redis instance ran by Docker.

All of the code will be availableto follow along with and run here. Just note that to run it after you clone and run the install for the repo, you'll need tocd blog-posts/nestjs-passport-sessions and then runnest start --watch yourself. Just a side effect of how the repo is set up for my dev.to blogs.

Following along from scratch

If you're following along with the code that's pre-built, feel free to skip over this.

To set up a similar project from scratch, you'll need to first set up a Nest project, which is easiest through the Nest CLI

nest new session-authentication
Enter fullscreen modeExit fullscreen mode

Choose your package manager of choice, and then install the follow dependencies

pnpm i @nestjs/passport passport passport-local express-session redis connect-redis bcrypt
Enter fullscreen modeExit fullscreen mode

And the following peer dependencies

pnpm i-D @types/passport-local @types/express-session @types/connect-redis @types/bcrypt @types/redis
Enter fullscreen modeExit fullscreen mode

npm and yarn work fine as well, I just like pnpm as a package manager

Now you should be okay to follow along with the rest of the code, building as we go.

NestJS and Passport

The AuthGuard()

Like most@nestjs/ packages, the@nestjs/passport package ismostly a thin wrapper around passport, but Nest does do some cool things with the passport package that I think are worth mentioning. First, theAuthGuard mixin. At first glance, this mixin may look a little intimidating, but let's take it chunk by chunk.

exportconstAuthGuard:(type?:string|string[])=>Type<IAuthGuard>=memoize(createAuthGuard);
Enter fullscreen modeExit fullscreen mode

Ignoring thememoize call, thiscreateAuthGuard is where the magic of class creation happens. We end up passing thetype, if applicable, to thecreateAuthGuard method and will eventually pass that back to the@UseGuards(). Everything from here on, unless mentioned otherwise, will be a part of thecreateAuthGuard method.

classMixinAuthGuard<TUser=any>implementsCanActivate{constructor(@Optional()protectedreadonlyoptions?:AuthModuleOptions){this.options=this.options||{};if(!type&&!this.options.defaultStrategy){newLogger('AuthGuard').error(NO_STRATEGY_ERROR);}}...
Enter fullscreen modeExit fullscreen mode

The constructor allows for an optional injection ofAuthModuleOptions. This is what is passed toPassportModule.register(). This just allows Nest to figure out if thedefaultStrategy is used or the named one passed toAuthGuard.

asynccanActivate(context:ExecutionContext):Promise<boolean>{constoptions={...defaultOptions,...this.options,...awaitthis.getAuthenticateOptions(context)};const[request,response]=[this.getRequest(context),this.getResponse(context)];constpassportFn=createPassportContext(request,response);constuser=awaitpassportFn(type||this.options.defaultStrategy,options,(err,user,info,status)=>this.handleRequest(err,user,info,context,status));request[options.property||defaultOptions.property]=user;returntrue;}
Enter fullscreen modeExit fullscreen mode

This here reads through decently well, we have custom methods for getting the authentication options (defaults to returningundefined), getting therequest andresponse objects (defaults tocontext.switchToHttp().getRequest()/getResponse()), and then thiscreatePassportContext method that is called and then it's return is immediately called with the strategy name and options. Then, we setreq.user to the return ofpassportFn and returntrue to let the request continue. The next code block is not a part of the mixin orMixinAuthGuard class.

constcreatePassportContext=(request,response)=>(type,options,callback:Function)=>newPromise<void>((resolve,reject)=>passport.authenticate(type,options,(err,user,info,status)=>{try{request.authInfo=info;returnresolve(callback(err,user,info,status));}catch(err){reject(err);}})(request,response,err=>(err?reject(err):resolve())),);
Enter fullscreen modeExit fullscreen mode

Here's where some magic may be seen to happen: Nest ends up callingpassport.authenticate for us, so that we don't have to call it ourselves. In doing so, it wraps passport in a promise, so that we can manage the callback properly, and provides it's own handler to theauthenticate function. This entire method is actually creating a different callback function so that we can end up callingthis.handleRequest with theerr,user,info, andstatus returned by passport. This can take a bit of time to understand, and isn't necessarily needed, but it's usually good to know whatsome of the code under the hood is doing.

handleRequest(err,user,info,context,status):TUser{if(err||!user){throwerr||newUnauthorizedException();}returnuser;}
Enter fullscreen modeExit fullscreen mode

This is pretty straightforward, but it's useful to know this method is here.As mentioned in Nest's docs if you need to do any debugging about why the request is failing, here is a good place to do it. Generally just adding the lineconsole.log({ err, user, info, context, status }) is enough, and will help you figure out pretty much anything going wrong within the passport part of the request.

There's two other classes I want to talk about before getting to the implementation, but I promise it'll be worth it!

The PassportStrategy()

So the next mixin we have to look at is thePassportStrategy mixin. This is how we end up actually registering our strategy class'svalidate method to passport'sverify callback. This mixin does a little bit more in terms of some advance JS techniques, so again, lets take this chunk by chunk.

exportfunctionPassportStrategy<TextendsType<any>=any>(Strategy:T,name?:string|undefined):{new(...args):InstanceType<T>;}{abstractclassMixinStrategyextendsStrategy{
Enter fullscreen modeExit fullscreen mode

This part is pretty straightforward, we're just passing the passport strategy class and an optional renaming of the strategy to the mixin.

constructor(...args:any[]){constcallback=async(...params:any[])=>{constdone=params[params.length-1];try{constvalidateResult=awaitthis.validate(...params);if(Array.isArray(validateResult)){done(null,...validateResult);}else{done(null,validateResult);}}catch(err){done(err,null);}};
Enter fullscreen modeExit fullscreen mode

This is the first half of the constructor. You'll probably notice right of the bat that we don;'t callsuper, at least not yet. This is because we're setting up the callback to be passed to passport later. So what's happening here is we're setting up a function that's going to be callingthis.validate and getting the result from it. If that result happens to be an array, we spread the array (passport will use the first value), otherwise we'll end up calling thedone callback with just the result. If there happens to be an error, in good ole callback style, it'll be passed as the first value to thedone method.

super(...args,callback);constpassportInstance=this.getPassportInstance();if(name){passportInstance.use(name,thisasany);}else{passportInstance.use(thisasany);}}
Enter fullscreen modeExit fullscreen mode

Now we end up callingsuper, and in doing so, we overwrite the originalverify with the new callback we just created. This sets up the entire passport Strategy class that we're going to use for the strategy's name. Now all that's left to do is tell passport about it, by callingpassportInstance.use(this) (or passing the custom name as the first argument).

If any of that went a little too deep, don't worry. It's something you can come back to if you really want, but isn't necessary for the rest of ths article.

PassportSerializer

Finally, an actual class! This is the most straightforward and the last bit of passport I'll talk about before getting into the implementation of sessions. This classusually won't be used in Nest applications _unless you are using sessions, and we're about to see why.

So passport has the notion of serializing and deserializing a user. Serializing a user is just taking the user's information and compressing it/ making it as minimal as possible. In many cases, this is just using theID of the user. Deserializing a user is the opposite, taking an ID and hydrating an entire user out of it. This usually means a call to a database, but it's not necessary if you don't want to worry about it. Now, Nest has aPassportSerializer class like so:

exportabstractclassPassportSerializer{abstractserializeUser(user:any,done:Function);abstractdeserializeUser(payload:any,done:Function);constructor(){constpassportInstance=this.getPassportInstance();passportInstance.serializeUser((user,done)=>this.serializeUser(user,done));passportInstance.deserializeUser((payload,done)=>this.deserializeUser(payload,done));}getPassportInstance(){returnpassport;}}
Enter fullscreen modeExit fullscreen mode

You should only ever have one class extending thePassportSerializer, and what it should do is set up the general serialization and deserialization of the user for the session storage. Theuser passed toserializeUser is usually the same value asreq.user, and thepayload passed todeserializeUser is the value passed as the second parameter to thedone ofserializeUser. This will make a bit more sens when it is seen in code.

Break Time

Okay, that was a lot of information about NestJS and Passport all at once, and some pretty complex code to go through. Take a break here if you need to. Get some coffee, stretch your legs, go play that mobile game you've been wanting to. Whatever you want to do, or continue on with the article if you want.

Running Redis Locally

You can either install and run redis locally on your machine, or you can use adocker-compose.yml file to run redis inside a container. The following compose fle is what I used while working on this article

# docker-compose.ymlversion:'3'services:redis:image:redis:latestports:-'6379:6379'rcli:image:redis:latestlinks:-rediscommand:redis-cli -h redis
Enter fullscreen modeExit fullscreen mode

And then to run redis, I just useddocker compose up redis -d. When I needed to run the redis CLI, I useddocker compose run rcli to connect to the redis instance via the docker network.

Setting Up the Middleware

Now on to the middleware we're going to be using: for setting up sessions and a way to store them, I'm going to be usingexpress-session, andconnect-redis for the session and session store, andredis as the redis client for connect-redis. I'm also going to be setting up our middleware via aNest middleware instead of usingapp.use in thebootstrap so that when we do e2e testing, the middleware is already set up (that's out of the scope of this article). I've also got redis set up as acustom provider using the following code

// src/redis/redis.module.tsimport{Module}from'@nestjs/common';import*asRedisfrom'redis';import{REDIS}from'./redis.constants';@Module({providers:[{provide:REDIS,useValue:Redis.createClient({port:6379,host:'localhost'}),},],exports:[REDIS],})exportclassRedisModule{}
Enter fullscreen modeExit fullscreen mode
// src/redis/redis.constants.tsexportconstREDIS=Symbol('AUTH:REDIS');
Enter fullscreen modeExit fullscreen mode

which allows for us to use@Inject(REDIS) to inject the redis client. Now we can configure our middleware like so:

// src/app.module.tsimport{Inject,Logger,MiddlewareConsumer,Module,NestModule}from'@nestjs/common';import*asRedisStorefrom'connect-redis';import*assessionfrom'express-session';import*aspassportfrom'passport';import{RedisClient}from'redis';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],})exportclassAppModuleimplementsNestModule{constructor(@Inject(REDIS)privatereadonlyredis:RedisClient){}configure(consumer:MiddlewareConsumer){consumer.apply(session({store:new(RedisStore(session))({client:this.redis,logErrors:true}),saveUninitialized:false,secret:'sup3rs3cr3t',resave:false,cookie:{sameSite:true,httpOnly:false,maxAge:60000,},}),passport.initialize(),passport.session(),).forRoutes('*');}}
Enter fullscreen modeExit fullscreen mode

and have passport ready to use sessions. There's two important things to note here:

  1. passport.initialize() must be called beforepassport.session().
  2. session() must be called beforepassport.initialize()

With this now out of the way, let's move on to our auth module.

The AuthModule

To start off, let's define ourUser as the following

// src/auth/models/user.interface.tsexportinterfaceUser{id:number;firstName:string;lastName:string;email:string;password:string;role:string;}
Enter fullscreen modeExit fullscreen mode

And then haveRegisterUserDto andLoginUserDto as

// src/auth/models/register-user.dto.tsexportclassRegisterUserDto{firstName:string;lastName:string;email:string;password:string;confirmationPassword:string;role='user';}
Enter fullscreen modeExit fullscreen mode

and

// src/auth/models/login-user.dto.tsexportclassLoginUserDto{email:string;password:string;}
Enter fullscreen modeExit fullscreen mode

Now we'll set up ourLocalStrategy as

// src/auth/local.strategy.tsimport{Injectable}from'@nestjs/common';import{PassportStrategy}from'@nestjs/passport';import{Strategy}from'passport-local';import{AuthService}from'./auth.service';@Injectable()exportclassLocalStrategyextendsPassportStrategy(Strategy){constructor(privatereadonlyauthService:AuthService){super({usernameField:'email',});}asyncvalidate(email:string,password:string){returnthis.authService.validateUser({email,password});}}
Enter fullscreen modeExit fullscreen mode

Notice here we're passingusernameField: 'email' tosuper. This is because in ourRegisterUserDto andLoginUserDto we're using theemail field and notusername which is passport's default. You can change thepasswordField too, but I had no reason to do that for this article. Now we'll make ourAuthService,

// src/auth/auth.service.tsimport{BadRequestException,Injectable,UnauthorizedException}from'@nestjs/common';import{compare,hash}from'bcrypt';import{LoginUserDto,RegisterUserDto}from'./models';import{User}from'./models/user.interface';@Injectable()exportclassAuthService{privateusers:User[]=[{id:1,firstName:'Joe',lastName:'Foo',email:'joefoo@test.com',// Passw0rd!password:'$2b$12$s50omJrK/N3yCM6ynZYmNeen9WERDIVTncywePc75.Ul8.9PUk0LK',role:'admin',},{id:2,firstName:'Jen',lastName:'Bar',email:'jenbar@test.com',// P4ssword!password:'$2b$12$FHUV7sHexgNoBbP8HsD4Su/CeiWbuX/JCo8l2nlY1yCo2LcR3SjmC',role:'user',},];asyncvalidateUser(user:LoginUserDto){constfoundUser=this.users.find(u=>u.email===user.email);if(!user||!(awaitcompare(user.password,foundUser.password))){thrownewUnauthorizedException('Incorrect username or password');}const{password:_password,...retUser}=foundUser;returnretUser;}asyncregisterUser(user:RegisterUserDto):Promise<Omit<User,'password'>>{constexistingUser=this.users.find(u=>u.email===user.email);if(existingUser){thrownewBadRequestException('User remail must be unique');}if(user.password!==user.confirmationPassword){thrownewBadRequestException('Password and Confirmation Password must match');}const{confirmationPassword:_,...newUser}=user;this.users.push({...newUser,password:awaithash(user.password,12),id:this.users.length+1,});return{id:this.users.length,firstName:user.firstName,lastName:user.lastName,email:user.email,role:user.role,};}findById(id:number):Omit<User,'password'>{const{password:_,...user}=this.users.find(u=>u.id===id);if(!user){thrownewBadRequestException(`No user found with id${id}`);}returnuser;}}
Enter fullscreen modeExit fullscreen mode

our controller

// src/auth/auth.controller.tsimport{Body,Controller,Post,Req,UseGuards}from'@nestjs/common';import{LocalGuard}from'../local.guard';import{AuthService}from'./auth.service';import{LoginUserDto,RegisterUserDto}from'./models';@Controller('auth')exportclassAuthController{constructor(privatereadonlyauthService:AuthService){}@Post('register')registerUser(@Body()user:RegisterUserDto){returnthis.authService.registerUser(user);}@UseGuards(LocalGuard)@Post('login')loginUser(@Req()req,@Body()user:LoginUserDto){returnreq.session;}}
Enter fullscreen modeExit fullscreen mode

and our serializer

// src/auth/serialization.provider.tsimport{Injectable}from'@nestjs/common';import{PassportSerializer}from'@nestjs/passport';import{AuthService}from'./auth.service';import{User}from'./models/user.interface';@Injectable()exportclassAuthSerializerextendsPassportSerializer{constructor(privatereadonlyauthService:AuthService){super();}serializeUser(user:User,done:(err:Error,user:{id:number;role:string})=>void){done(null,{id:user.id,role:user.role});}deserializeUser(payload:{id:number;role:string},done:(err:Error,user:Omit<User,'password'>)=>void){constuser=this.authService.findById(payload.id);done(null,user);}}
Enter fullscreen modeExit fullscreen mode

along with our module

// src/auth/auth.module.tsimport{Module}from'@nestjs/common';import{PassportModule}from'@nestjs/passport';import{AuthController}from'./auth.controller';import{AuthService}from'./auth.service';import{LocalStrategy}from'./local.strategy';import{AuthSerializer}from'./serialization.provider';@Module({imports:[PassportModule.register({session:true,}),],providers:[AuthService,LocalStrategy,AuthSerializer],controllers:[AuthController],})exportclassAuthModule{}
Enter fullscreen modeExit fullscreen mode

All we need to do for theAuthSerializer is to add it to theproviders array. Nest will instantiate it, which will end up callingpassport.serializeUser andpassport.deserializeUser for us (told you going over that would be useful).

The Guards

So now let's get to our guards, as you'll notice up in theAuthController we're not usingAuthGuard('local'), butLocalGuard. The reason for this is because we need to end up callingsuper.logIn(request), which theAuthGuardhas, but doesn't make use of by default. This just ends up callingrequest.login(user, (err) => done(err ? err : null, null)) for us, which is how the user serialization happens. This is what kicks off the session. I'll repeat that because it'ssuper important.super.logIn(request)is how the user gets a session. To make use of this method, we can set up theLocalGuard as below

// src/local.guard.tsimport{ExecutionContext,Injectable}from'@nestjs/common';import{AuthGuard}from'@nestjs/passport';@Injectable()exportclassLocalGuardextendsAuthGuard('local'){asynccanActivate(context:ExecutionContext):Promise<boolean>{constresult=(awaitsuper.canActivate(context))asboolean;awaitsuper.logIn(context.switchToHttp().getRequest());returnresult;}}
Enter fullscreen modeExit fullscreen mode

We have another guard as well, theLoggedInGuard. This guards ends up just callingrequest.isAuthenticated() which is a method that passport ends up adding to the request object when sessions are in use. We can use this instead of having to have the user pass us the username and password every request, because there will be a cookie with the user's session id on it.

// src/logged-in.guard.tsimport{CanActivate,ExecutionContext,Injectable}from'@nestjs/common';@Injectable()exportclassLoggedInGuardimplementsCanActivate{canActivate(context:ExecutionContext){returncontext.switchToHttp().getRequest().isAuthenticated();}}
Enter fullscreen modeExit fullscreen mode

And now we have one other guard for checking if a user is an admin or not.

// src/admin.guard.tsimport{ExecutionContext,Injectable}from'@nestjs/common';import{LoggedInGuard}from'./logged-in.guard';@Injectable()exportclassAdminGuardextendsLoggedInGuard{canActivate(context:ExecutionContext):boolean{constreq=context.switchToHttp().getRequest();returnsuper.canActivate(context)&&req.session.passport.user.role==='admin';}}
Enter fullscreen modeExit fullscreen mode

This guard extends our usualLoggedInGuard and checks for the user's role, which is saved in the redis session, via theAuthSerializer we created earlier.

A couple of extra classes

There's a few other classes that I'm making use of. It'll be easiest to view them in the GitHub repo, but I'll add them here if you just want to copy paste:

// src/app.controller.tsimport{Controller,Get,UseGuards}from'@nestjs/common';import{AdminGuard}from'./admin.guard';import{AppService}from'./app.service';import{LoggedInGuard}from'./logged-in.guard';@Controller()exportclassAppController{constructor(privatereadonlyappService:AppService){}@Get()publicRoute(){returnthis.appService.getPublicMessage();}@UseGuards(LoggedInGuard)@Get('protected')guardedRoute(){returnthis.appService.getPrivateMessage();}@UseGuards(AdminGuard)@Get('admin')getAdminMessage(){returnthis.appService.getAdminMessage();}}
Enter fullscreen modeExit fullscreen mode
// src/app.service.tsimport{Injectable}from'@nestjs/common';@Injectable()exportclassAppService{getPublicMessage():string{return'This message is public to all!';}getPrivateMessage():string{return'You can only see this if you are authenticated';}getAdminMessage():string{return'You can only see this if you are an admin';}}
Enter fullscreen modeExit fullscreen mode
// src/main.tsimport{Logger}from'@nestjs/common';import{NestFactory}from'@nestjs/core';import{AppModule}from'./app.module';constbootstrap=async()=>{constapp=awaitNestFactory.create(AppModule);constlogger=app.get(Logger);awaitapp.listen(3000);logger.log(`Application listening at${awaitapp.getUrl()}`);};bootstrap();
Enter fullscreen modeExit fullscreen mode

Testing out the flow

So now, we can run everything all together and test out the flow. First things first, make sure the Redis instance is running. Without that, the server won't start. Once it's running, runnest start --watch to start the server in dev mode which will recompile and restart on file change. Now it's time to send somecurls.

Testing Existing Users

So let's start off with some existing user test. We'll try to log in as Joe Foo.

curl http://localhost:3000/auth/login-d'email=joefoo@test.com&password=Passw0rd!'-c cookie.joe.txt
Enter fullscreen modeExit fullscreen mode

If you aren't familiar with curl, the-d make the request a POST, and sends the data asapplication/x-www-form-urlencoded which Nest accepts by default. The-c tells curl that it should start the cookie engine and save the cookies to a file. If all goes well, you should get a response like

{"cookie":{"originalMaxAge":60000,"expires":"2021-08-16T05:30:51.621Z","httpOnly":false,"path":"/","sameSite":true},"passport":{"user":1}}
Enter fullscreen modeExit fullscreen mode

Now we can send a request to/protected and get our protected response back

curl http://localhost:3000/protected-b cookie.joe.txt
Enter fullscreen modeExit fullscreen mode

With-b we are telling curl to use the cookies found in this file.

Now let's check the registration:

curl http://localhost:3000/auth/register-c cookie.new.txt-d'email=new.email@test.com&password=password&confirmationPassword=password&firstName=New&lastName=Test'
Enter fullscreen modeExit fullscreen mode

You'll notice that no session was created for the new user, which means they still need to log in. Now let's send that login request

curl http://localhost:3000/auth/login-c cookie.new.txt-d'email=new.email@test.com&password=password'
Enter fullscreen modeExit fullscreen mode

And check that we did indeed create a session

curl http://localhost:3000/protected-b cookie.new.txt`
Enter fullscreen modeExit fullscreen mode

And just like that, we've implemented a session login with NestJS, Redis, and Passport.

To view the session IDs in redis, you can connect the redis-cli to the running instance and runKEYS * to get all of the set keys. By defaultconnect-redis usessess: as a session key prefix.

Conclusion

Phew, okay, that was definitely a longer article than I had anticipated with a much deeper focus on Nest's integration with Passport, but hopefully it helps paint a picture of how everything ties together. With the above, it should be possible to integrate sessions with any kind of login, basic, local, OAuth2.0, so long as the user object remains the same.

One last thing to note, when using sessions, cookies are a must. The client must be able to work with cookies, otherwise the session will essentially be lost on each request.

If you have any questions, feel free to leave a comment or find me on theNestJS Discord Server

Top comments(32)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
hinogi profile image
Stefan Schneider
  • Joined

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

CollapseExpand
 
jmcdo29 profile image
Jay McDoniel
My passion is Full Stack development using NestJS and Angular. I provide a ton of support to NestJS on their Discord server.
  • Location
    Concrete Washington
  • Work
    Software Architect at Trilon
  • Joined

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 betweenimport * as passport...passport.session() andimport { session }...session(), as the second one should just be destructing the first

CollapseExpand
 
hinogi profile image
Stefan Schneider
  • Joined
• Edited on• Edited

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

Thread Thread
 
jmcdo29 profile image
Jay McDoniel
My passion is Full Stack development using NestJS and Angular. I provide a ton of support to NestJS on their Discord server.
  • Location
    Concrete Washington
  • Work
    Software Architect at Trilon
  • Joined
• Edited on• 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
  • Joined

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

Thread Thread
 
jmcdo29 profile image
Jay McDoniel
My passion is Full Stack development using NestJS and Angular. I provide a ton of support to NestJS on their Discord server.
  • Location
    Concrete Washington
  • Work
    Software Architect at Trilon
  • Joined

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 modeExit fullscreen mode

vs

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

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

Thread Thread
 
jmcdo29 profile image
Jay McDoniel
My passion is Full Stack development using NestJS and Angular. I provide a ton of support to NestJS on their Discord server.
  • Location
    Concrete Washington
  • Work
    Software Architect at Trilon
  • Joined

@hinogi are you, by chance, on typescript4.4.2? I just noticed that difference in my test repo (usingnest new) and my blog repo. The blog repo is on4.3.5 and works fine, but the same config on4.4.2 failed

Thread Thread
 
hinogi profile image
Stefan Schneider
  • Joined

Yes I am on 4.4.2

Thread Thread
 
jmcdo29 profile image
Jay McDoniel
My passion is Full Stack development using NestJS and Angular. I provide a ton of support to NestJS on their Discord server.
  • Location
    Concrete Washington
  • Work
    Software Architect at Trilon
  • Joined

Looks like this was abreaking 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.

CollapseExpand
 
kallezz profile image
Kalle210496
  • Joined
• Edited on• 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

CollapseExpand
 
wolfhoundjesse profile image
Jesse M. Holmes
I follow new coders. Brass-bander. ADHD Avenger. Lover of giant hounds. Believer in the impossible.
  • Location
    Annapolis, Maryland, United States
  • Education
    Bachelor of Music Performance
  • Joined

Your error,The client is closed, is due tothis breaking change.

You’ll need to make a change to the code above (in addition to the changes you already mentioned, like theurl property) to callconnect() on your new client, or you will get an error fromconnect-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*asRedisfrom'redis';import{REDIS}from'./redis.constants';@Module({providers:[{provide:REDIS,useFactory:async()=>{constclient=Redis.createClient({url:'rediss://username:password@your.redis.url',legacyMode:true})awaitclient.connect()returnclient},},],exports:[REDIS],})exportclassRedisModule{}
Enter fullscreen modeExit fullscreen mode
CollapseExpand
 
jmcdo29 profile image
Jay McDoniel
My passion is Full Stack development using NestJS and Angular. I provide a ton of support to NestJS on their Discord server.
  • Location
    Concrete Washington
  • Work
    Software Architect at Trilon
  • Joined

Thank you for pointing this out!

CollapseExpand
 
danbachar profile image
Dan Bachar
  • Joined

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 usingfetch 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

CollapseExpand
 
jmcdo29 profile image
Jay McDoniel
My passion is Full Stack development using NestJS and Angular. I provide a ton of support to NestJS on their Discord server.
  • Location
    Concrete Washington
  • Work
    Software Architect at Trilon
  • Joined

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

CollapseExpand
 
danbachar profile image
Dan Bachar
  • Joined

already solved per discord, thanks!

CollapseExpand
 
green-new profile image
green
  • Joined
• Edited on• Edited

As of march 6th, 2025 - seems to work well. A couple things I had to change to get it to work.
You have to callawait this.redis.connect for it to work properly, and set theconfigure function to asynchronous. Also, with some changes I guess, thethis.redis needs to be of typeRedisClientType, I dont know why but it works (authentication shouldn't be this complicated to be honest). I was able to get my backend to return the session in the login POST.
Cheers.

exportclassAppModuleimplementsNestModule{constructor(@Inject(REDIS)privatereadonlyredis:RedisClientType,privateconfigService:ConfigService){}asyncconfigure(consumer:MiddlewareConsumer){awaitthis.redis.connect()consumer.apply(session({store:newRedisStore({client:this.redis,ttl:6000,}),saveUninitialized:false,secret:String(this.configService.get('REDIS_SECRET')),resave:false,cookie:{sameSite:true,httpOnly:true,// 2 hoursmaxAge:2*60*60*1000}}),passport.initialize(),passport.session()).forRoutes('*')}}
Enter fullscreen modeExit fullscreen mode
CollapseExpand
 
ahmet_baverylmaz_9abcf1 profile image
Ahmet Baver Yılmaz
  • Joined

How did you get this working? I did the same, but the session is saved to the memory store instead of redis store. What am I doing wrong?

CollapseExpand
 
green-new profile image
green
  • Joined

Not sure without seeing any code.

CollapseExpand
 
makaseloli profile image
まかせロリ
  • Joined

How would I go about using Nest'sConfigService in theconfigure method of theAppModule? 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?

CollapseExpand
 
jmcdo29 profile image
Jay McDoniel
My passion is Full Stack development using NestJS and Angular. I provide a ton of support to NestJS on their Discord server.
  • Location
    Concrete Washington
  • Work
    Software Architect at Trilon
  • Joined

You can inject theConfigService just like we doRedis and usethis.config.get('SESSION-SECRET') or whatever else you would need to

CollapseExpand
 
hiteshpathak profile image
Hitesh Pathak
  • Joined
• Edited on• 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 withclient: this.redis as Client withimport { Client } from 'connect-redis'

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

CollapseExpand
 
motivatedcoder profile image
Ayoub Elmendoub
  • Joined

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 modeExit fullscreen mode
CollapseExpand
 
wguerram profile image
wguerram
  • Joined

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.

CollapseExpand
 
wguerram profile image
wguerram
  • Joined

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

CollapseExpand
 
dokuslab profile image
DokusLab
IT specialist application development
  • Joined

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 modeExit fullscreen mode

Should I add more protection like XSRF-TOKEN?

CollapseExpand
 
khaledalhamwie profile image
khaled al hamwie
  • Joined

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.tsimport { 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 modeExit fullscreen mode

and you also need to remove the package @types/connect-redis and the legacy option in redis.module.ts in the Redius.createClientcheck 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

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

nestjs node.js typescript javascript

More fromNestJS

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp