question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

NestJS Authenticated sessions documentation has major gaps and is seemingly wrong

See original GitHub issue

I’m submitting a…


[ ] Regression 
[ ] Bug report
[ ] Feature request
[x] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior

“Session” instructions in “Authentication” section of documentation is erroneous and this functionality is otherwise undocumented. (And non-obvious.)

Expected behavior

It should be clear how to implement AuthGuard with a cookie-based sessions.

Please see my comment from Sept. 14 here.

At this point I DO have my browser at least storing a cookie after adding stuff directly to my main.ts, which I think is bad form:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as session from 'express-session';
import * as passport from 'passport';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
    app.use(session({
        secret: 'a secret',
        name: 'abcd',
        resave: true,
        saveUninitialized: true
    }));

    app.use(passport.initialize());
    app.use(passport.session());
  await app.listen(3000);
}
bootstrap();

This at least gets me a cookie with key “abcd” in my Chrome Developer Tools “Application” > “Cookies” section, with a long value (s%3Ac1xPblaYplJQL2DLmfIUVoLdLIbkjsYQ.e8Q69kfkdn1Mm96Clk6BMg4Ea0k647RnIkT2bP60Lwc).

In my custom Guard, I can actually inspect the request and see that it has a session property with a Session object in it. The Session object has an id RUHHx_IvcDIPFWt-9MHeZHruw1bSO7-O which is different from what’s stored in my cookie – but that may be normal. The request object also has a sessionID property, with the same ID… And a sessionStore property with a MemoryStore object that has as property of sessions in it, that’s just an empty object.

Going back to the documentation’s recommended code: just calling super.logIn(theRequestObject) sends me directly to my Google Auth strategy’s serializeUser function, which is supposed to callback with the user that gets passed to it and/or an error.

Unfortunately, there is never any user that gets passed to it. I’m not sure how the User is intended to be retrieved from the session memory store, or where it’s intended to be set to the memory store…

So there’s a ton of stuff missing here. 😦

Environment

Nest version: 5.4.0

For Tooling issues:

  • Node version: 8.11.3
  • Platform: Fedora 25; Windows 10 x64

Other: @kamilmysliwiec It seems at least 3 other people are confused about this. It’s very frustrating. I think NestJS is so cool, but I took a break from it largely because I was spinning my wheels with this very issue. It would be one thing if I felt like I was just confused, but as it stands the docs are clearly wrong (or at least misleading), so I don’t feel like I can move forward.

Issue Analytics

  • State:open
  • Created 5 years ago
  • Reactions:34
  • Comments:43 (9 by maintainers)

github_iconTop GitHub Comments

54reactions
ejhayescommented, Apr 25, 2019

I was able to get this working – here’s the solution I came up with:

configure sessions and passport to use sessions

In main.ts (or wherever you’re settting up nest):

app.use(session({
    secret: 'CHANGEME',
    resave: false,
    saveUninitialized: false,
  }));
  app.use(passport.initialize());
  app.use(passport.session());

session from the express-session package.

guards

I’m using passport-local so my LocalAuthGuard class looks like this:

import { Injectable, ExecutionContext, UnauthorizedException, Logger } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
    async canActivate(context: ExecutionContext): Promise<boolean> {
        const request = context.switchToHttp().getRequest();
        const result = (await super.canActivate(context) as boolean); // THIS MUST BE CALLED FIRST
        await super.logIn(request);
        return result;
    }

    handleRequest(err, user, info) {
        if (err || !user) {
            throw err || new UnauthorizedException();
        }

        return user;
    }
}

configure the serializer

If you look at the implementation of PassportSerializer (from @nestjs/passport) you’ll notice that both serializeUser and deserializeUser are abstract…so they need to be implemented by you. Take a look: https://github.com/nestjs/passport/blob/master/lib/passport/passport.serializer.ts

My barebones serializer looks like this:

import { PassportSerializer } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

@Injectable()
export class CookieSerializer extends PassportSerializer {
    serializeUser(user: any, done: (err: any, id?: any) => void): void {
        done(null, user);
    }

    deserializeUser(payload: any, done: (err: any, id?: any) => void): void {
        done(null, payload);
    }
}

update auth module

Then in the module (for example I’m using AuthModule), this needs to be included. So my AuthModule class looks like this:

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { LocalStrategy } from './strategies/local.strategy';
import { PassportModule } from '@nestjs/passport';
import { CookieSerializer } from './cookie.serializer';

@Module({
    imports: [
        PassportModule.register({session: true}),
    ],
    providers: [AuthService, LocalStrategy, CookieSerializer],
    controllers: [AuthController],
    exports: [PassportModule],
})
export class AuthModule { }

session guard

Create a guard to check to see if the user is in the session.

import { CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';

export class SessionGuard implements CanActivate {
    canActivate(context: ExecutionContext): boolean | Promise<boolean> {
        const httpContext = context.switchToHttp();
        const request = httpContext.getRequest();

        try {
            if (request.session.passport.user) {
                return true;
            }
        } catch (e) {
            throw new UnauthorizedException();
        }

    }
}

putting it all together

Here is how this can be used in a controller:

import { Controller, Get, Post, Param, Body, UseGuards, HttpCode, HttpStatus, Req } from '@nestjs/common';
import { CreateUserDto } from './dto/createUser';
import { AuthGuard } from '@nestjs/passport';
import { LoginCredentialsDto } from './dto/login.dto';
import { LocalAuthGuard } from './guards/local.guard';
import { SessionGuard } from './session.guard';
import { SessionUser } from './sessionuser.decorator';

@Controller('auth')
export class AuthController {
    @Get('me')
    @UseGuards(SessionGuard)
    getMetadataArgsStorage(@SessionUser() user: any) {
        return user;
    }

    @Post('login')
    @HttpCode(HttpStatus.OK)
    @UseGuards(LocalAuthGuard)
    login(@Body() login: LoginCredentialsDto, @Req() req): Promise<any> {
        return;
    }
}

You can test this out with cURL/Postman:

Logging in:

$ curl -X POST http://localhost:3000/auth/login -d "username=jdoe&password=1" -v
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> POST /auth/login HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Length: 23
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 23 out of 23 bytes
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Set-Cookie: connect.sid=s%3ADuKdgMobTzL7pHXa9oszUqYRy2J85Eiv.11yD1Gq3NwgdSASUfBYU%2B7sxBIwLH9bqwv1pGtq2T%2BU; Path=/; HttpOnly
< Date: Thu, 25 Apr 2019 21:44:25 GMT
< Connection: keep-alive
< Content-Length: 0
<
* Connection #0 to host localhost left intact

Then to test it out:

$ curl -X GET http://localhost:3000/auth/me -H 'Cookie: connect.sid=s%3AfBdKLAFybLq6xh13NeET3LDEQjfxv7cD.XyZanjMKgAq9SlBUVcDsCdZxJn0auKCgWZCnczaLczQ' -v
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /auth/me HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
> Cookie: connect.sid=s%3ADuKdgMobTzL7pHXa9oszUqYRy2J85Eiv.11yD1Gq3NwgdSASUfBYU%2B7sxBIwLH9bqwv1pGtq2T%2BU
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: text/html; charset=utf-8
< Content-Length: 43
< ETag: W/"2b-p23HHgfkAEp1vHx70BYS/ngFWwQ"
< Date: Thu, 25 Apr 2019 21:51:04 GMT
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
{"firstName":"John","lastName":"Doe"}

Thanks @kamilmysliwiec for all your work on NestJS – seems very slick!

11reactions
dngconsultingcommented, Nov 7, 2019

Hi all,

After digging into whole Nestjs framework, I find it really cool. But, the security aspects are very painful. Implementing a simple local-strategy session should not be as difficult as the code @TrejGun has provided (thanx for that btw). We have to provide :

  • local strategy class
  • canActivate handler
  • serializer/deserializer
  • findUser service and so forth
  • Custom annotations

Look at this snippet from Spring Security for example :

@RestController
@RequestMapping("/api")

public class TestService {
    @Secured("ROLE_USER")
    @RequestMapping("/currentUser")    
    public Principal information(Principal principal) {    
       return principal;
    }

  @PreAuthorize("hasRole('ROLE_USER') and hasRole('ROLE_TRAINER')")    
  @RequestMapping("/courses")    
   public List<Course> courses() {        
      return courseDAO.getCourses(); 
   }}

This code can be used for LDAP, local strategy, Google/Facebook and whatever you want without changing any single line of code in your controllers.

With UserGuard/Auth annotations, we tie the controllers code with custom annotations. There’s no notion of Principal, only req.session.passport user which is very low level.

We should be able to say in app.module that we want a Passport local strategy (session/basic or other) and all the boilerplate code that we generally see should be generated or implemented in NestJS for us.

My 2 cents

Sami

Read more comments on GitHub >

github_iconTop Results From Across the Web

Authentication | NestJS - A progressive Node.js framework
Authenticate a user by verifying their "credentials" (such as username/password, JSON Web Token (JWT), or identity token from an Identity Provider); Manage ...
Read more >
Authentication and Sessions for MVC Apps with NestJS
The short answer is: if you really want to take advantage of Nest, it helps to try to learn to do things "the...
Read more >
NestJS - Redirect user from guard when check fails
(node:11526) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Can't set headers after they are sent. ( ...
Read more >
NestJS Authentication without Passport - Trilon Consulting
Sure, NestJS has @nestjs/passport to integrate with it, but what is passport really doing for you under the hood?
Read more >
API with NestJS #35. Using server-side sessions instead of ...
We look into using server-side sessions for authentication and ... The more users we have logged in, the more significant strain it puts...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found