nestjs / nest

A progressive Node.js framework for building efficient, scalable, and enterprise-grade server-side applications with TypeScript/JavaScript 🚀
https://nestjs.com
MIT License
67.44k stars 7.6k forks source link

How to work with passport and sessions #1365

Closed dilame closed 5 years ago

dilame commented 5 years ago

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

I don't understand how to implement passport sessions with session storage in nest. I read all the documentation three times and searched all the Google - no info. But it's the most common task. Basically all i need is save current user in session using req.logIn() and then check it with req.isAuthenticated() in, maybe, guard.

Now i have simple google stategy

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor() {
    super({
      clientID: '',
      clientSecret: '',
      callbackURL: 'http://localhost:3000/auth/google/callback',
      passReqToCallback: true,
      scope: ['profile'],
    });
  }

  async validate(request: any, accessToken: string, refreshToken: string, profile) {
    return profile;
  }
}

By the way, shoud i add implements AbstractStrategy to this? Without it WebStorm says

Method can be static Unused method validate

But there is nothing in docs about AbstractStrategy.

And auth.controller.ts

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {
  }

  @Get('google')
  @UseGuards(AuthGuard('google'))
  googleLogin() {
    // initiates the Google OAuth2 login flow
  }

  @Get('google/callback')
  @UseGuards(AuthGuard('google'))
  async googleLoginCallback(@Req() req, @Res() res, @Session() session) {
    // here i have req.user with profile
  }
}

How am i supposed to save authentication in session with req.logIn()?

P.S. i will try to imrove docs if we will find the answers

dilame commented 5 years ago

One more thing. I created custom guard as described in https://docs.nestjs.com/techniques/authentication And even include it in providers section of module, no console logs.


@Injectable()
export class GoogleAuthGuard extends AuthGuard('google'){
  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    console.log('Custom google guard');
  }
  handleRequest(err, user, info) {
    console.log('Custom google guard');
  }
}
jpinkster commented 5 years ago

Just wanted to give a heads up, the default option for sessions is false, so this may be why you are not getting sessions to create for you.

https://github.com/nestjs/passport/blob/master/lib/options.ts

May need to add PassportModule.register({ session: true }) in the module import

Also, your GoogleAuthGuard needs to follow the canActivate signature

@Injectable()
export class GoogleAuthGuard extends AuthGuard('google') {
    canActivate(context: ExecutionContext) {
        const request = context.switchToHttp().getRequest();
        super.logIn(request);
        return super.canActivate(context);
    }
}
dilame commented 5 years ago

But my GoogleAuthGuard follows this signature, you can see it in my last message, the problem that it is not called. It seems like i did the things right, after 3 days of googling and reading nest sources. Now i can say that Nest docs needs a huge improvements in Authentication section 😄

kamilmysliwiec commented 5 years ago

Let's track this issue here https://github.com/nestjs/docs.nestjs.com/issues/99

ghost commented 5 years ago

但我GoogleAuthGuard遵循这个签名,你可以在我的上一条消息中看到它,它没有被调用的问题。 经过3天的谷歌搜索和阅读巢源,似乎我做的事情是正确的。现在我可以说Nest文档需要对Authentication部分进行大幅改进😄

解决了吗?我也遇到了这个问题。

madsleejensen commented 5 years ago

Here is how i got passport with a local strategy and sessions to work.

// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // add 'express-session' (npm install express-session)
  app.use(session({
    secret: 'hello-world',
  }));

  app.use(cookieParser());

  // init 'passport' (npm install passport)
  app.use(passport.initialize());
  app.use(passport.session());

  await app.listen(3000);
}
// app.module.ts

@Module({
  imports: [
    // configure default options for passport
    PassportModule.register({
      defaultStrategy: 'local',
      session: true,
    }),
  ],
  controllers: [
    UserController,
  ],
  providers: [
    AuthService,

    // add our providers
    LocalStrategy, // simply by importing them will register them to passport (under the hood it calls `passport.use(...)`)
    LocalSerializer, 
    LocalAuthGuard,
  ],
})
export class AppModule {}
import { Strategy } from 'passport-local';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(
    private readonly authService: AuthService,
  ) {
    super();
  }

  async validate(email: string, password: string, done: CallableFunction) {
    // add your custom login check here. and return the User object.

    return await this.authService.signIn(email, password)
      .then(user => {
        done(null, user);
      })
      .catch(error => done(error));
  }
}

Create logic on how our user object should be serialized / deserialized. Under the hood, this will setup the passport.serializeUser(...) and passport.deserializeUser(...)

@Injectable()
export class LocalSerializer extends PassportSerializer {
  constructor(
    private readonly authService: AuthService,
    private readonly userRepository: UserRepository,
  ) {
    super();
  }

  serializeUser(user: User, done: CallableFunction) {
    done(null, user.id);
  }

  async deserializeUser(userId: string, done: CallableFunction) {
    return await this.userRepository.findOneOrFail({ id: Number(userId) })
      .then(user => done(null, user))
      .catch(error => done(error));
  }
}

By default the AuthGuard does not call req.logIn(...) to fix this we extend the AuthGuard and simply call it ourself if successfully logged in. (this will trigger the session storage)

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const can = await super.canActivate(context);
    if (can) {
      const request = context.switchToHttp().getRequest();
      super.logIn(request);
    }

    return true;
  }
}
// user.controller.ts

export class UserController {
  @Get('/signin')
  @UseGuards(new LocalAuthGuard()) // notice we use our customized AuthGuard
  signIn(@Query() signInDTO: UserSignInDTO) {
    return {
      message: 'You are now signed in',
    };
  }
}
dilame commented 5 years ago

@madsleejensen nice example, thank you. I have one fix for your code - we shoud

await super.logIn(request);

Because if you do some async stuff in serializeUser - the session won't establish

johnbiundo commented 5 years ago

For those following/landing on this thread, there are a couple of recent documents that should help 1) The authentication chapter was completely re-written (as of July 2019) with an end-to-end example 2) A recent (July 2019) article on dev.to on sessions covers sessions and was reviewed by the Nest core team.

dilame commented 5 years ago

Still no examples with session in docs 😄 But second article looks pretty useful, thank you!

TrejGun commented 5 years ago

Guys here is what you are looking for - boilerplate with local/google auth, sessions, and ACL https://github.com/TrejGun/nest-authorization

lock[bot] commented 4 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.