nestjs / docs.nestjs.com

The official documentation https://docs.nestjs.com 📕
MIT License
1.18k stars 1.69k forks source link

Add OAuth2 example for passport #75

Open weeco opened 6 years ago

weeco commented 6 years ago

I was trying to use the new Passport module in order to implement the whole OAuth2 Process (Google Login). However I was not sure how I am supposed to handle the callback where one would usually call passport.authenticate().

Can you add documentation which shows an OAuth2 login via Google (or some other provider)?

weeco commented 6 years ago

@kamilmysliwiec https://github.com/bojidaryovchev/nest-angular/tree/master/src/server/modules/auth <- Is this the proper way to add the Facebook auth? If so I could probably create a pullrequest for this issue.

kamilmysliwiec commented 6 years ago

Looks promising :)

Offlein commented 6 years ago

I'm confused about this as well... But @weeco's code doesn't really look much like the current Nest documentation (export class AuthModule implements NestModule isn't something that you see ever in the docs, plus a lot of stuff that appears way more advanced than what you get from reading through the entire docs.nestjs.com sidebar).

...And the comment "Looks promising" is really mysterious -- are you saying that, yes, that is a proper implementation, presumably without going incredibly deeply into it? Or are you saying that it is promising but in no way "there" yet?

kamilmysliwiec commented 6 years ago

@Offlein a part with applying middleware inside AuthModule is probably redundant since we have @nestjs/passport module already. It wasn't always the case so this implementation doesn't differ too much with the previously recommended approach. All stuff inside services looks quite good though.

Offlein commented 6 years ago

@kamilmysliwiec Thanks very much. Sorry, I hadn't intended to push you into giving a more thorough review, but I appreciate it very much. I'll try to replicate @weeco's functionality for my own Google OAuth02 implementation. I'm using passport-google-oauth20 as opposed to the Google+ implemention he uses, and I'm having a lot of issues otherwise.

... That said (and not to muddy the comment thread up too much) I think NestJS is really well thought-out and, with the exception of some documentation issues, pretty usable in general! So thank you so much for your hard work.

kamilmysliwiec commented 6 years ago

No worries @Offlein 🙂 Thank you!

joe307bad commented 5 years ago

Has anyone successfully implemented an OAuth2 strategy? I am attempting to implement a strategy with passport-google-oauth2 and will report back once I have it working.

joe307bad commented 5 years ago

Hey everyone, so I have a basic example of what I consider the most succinct way of implementing Google OAuth2 with NestJS located here: https://github.com/joe307bad/sc-webservice. I am not sure if this is the correct way to do this, but it works and makes sense to me.

As you will see below, I used a method of combining the usage of passport-google-oauth2 to request the initial "code" from Google and using the googleapis npm package to request an Bearer token.

After obtaining this "code" using passport-google-oauth2, we then redirect the user to an oauth/callback path, submit the code to another Google service using the googleapis npm package, which then returns the access token we need to authenticate the user.

After receiving the access token, we can verify the authenticity of the user using a basic Bearer token/HttpStrategy. Within the HttpStrategy we will also want to create the user in our database if they do not exist and store any user information in a UserModel to pass around in our application.

Again, this probably isn't the proper way to do this, but for me, it works well enough until we get some documentation on how to only use one strategy (utilizing passport-google-oauth2 I assume).

I would appreciate any feedback! Hope this helps someone!

\ app.controller.ts

import { Get, Controller, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { OAuth2Service } from './core/oauth';
import { Credentials } from 'google-auth-library';

@Controller()
export class AppController {
  constructor(private readonly oauth2: OAuth2Service) { }

  @Get()
  root(): string {
    return "Hello World!";
  }

  @Get('oauth/callback')
  async callback(@Req() request): Promise<Credentials> {
    return await this.oauth2.getToken(request.query.code).then(_ => _.tokens);
  }

  @Get('protected')
  @UseGuards(AuthGuard())
  protected(): string {
    return "This is protected";
  }

  @Get('token')
  token() {
    return "token endpoint"
  }
}

\ auth.module.ts

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { GoogleStrategy } from './strategies';
import { PassportModule } from '@nestjs/passport';
import { authenticate } from 'passport';
import { HttpStrategy } from './strategies/http.strategy';
import { OAuth2Service } from '../core/oauth';

@Module({
    imports: [
        PassportModule.register({ defaultStrategy: 'bearer' })
    ],
    providers: [OAuth2Service, GoogleStrategy, HttpStrategy],
})
export class AuthModule implements NestModule {
    public configure(consumer: MiddlewareConsumer) {
        consumer
            .apply(authenticate('google', {
                session: true,
                scope: ['profile']
            }))
            .forRoutes('token');
    }
}

\ google.strategy.ts

import { Strategy } from 'passport-google-oauth20';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { OAuth2Service } from '../../core/oauth';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly oauth2: OAuth2Service) {
    super(oauth2.getConfig());
  }
}

\ http.strategy.ts

import { Strategy } from 'passport-http-bearer';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { OAuth2Service } from '../../core/oauth';

@Injectable()
export class HttpStrategy extends PassportStrategy(Strategy) {

  constructor(private readonly oauth2: OAuth2Service) {
    super();
  }

  async validate(token: string) {
    const user = await this.oauth2.verify(token)
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

\ oauth2.service.ts

import { google } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';
import { GetTokenResponse } from 'google-auth-library/build/src/auth/oauth2client';
import { googleClientId, googleClientSecret, redirectUrl } from '../../app.settings';

export interface IOAuthConfig {
    clientID: string;
    clientSecret: string;
    callbackURL: string;
    passReqToCallback: boolean;
}

export class OAuth2Service {

    private readonly _clientId: string = googleClientId;
    private readonly _clientSecret: string = googleClientSecret;
    private readonly _redirectUrl: string = redirectUrl

    private _oauth2Client: OAuth2Client;

    constructor() {

        this._oauth2Client = new google.auth.OAuth2(
            this._clientId,
            this._clientSecret,
            this._redirectUrl,
        );

    }

    getConfig(): IOAuthConfig {
        return {
            clientID: this._clientId,
            clientSecret: this._clientSecret,
            callbackURL: this._redirectUrl,
            passReqToCallback: true,
        }
    }

    async verify(token: string) {
        return await this._oauth2Client.verifyIdToken({
            idToken: token,
            audience: this._clientId
        })
    }

    async getToken(code: string): Promise<GetTokenResponse>{
        return await this._oauth2Client.getToken(code);
    }
}
zhenwenc commented 5 years ago

@joe307bad I think you don't need to use the library's underlaying method. Here is an alternative solution, let me know if I did anything wrong (only have 2 days NestJS experience hehe). NestJS is so awesome!

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import {
  Profile,
  Strategy,
  StrategyOptionWithRequest,
  VerifyFunctionWithRequest,
} from 'passport-google-oauth20';

import { AuthService } from './AuthService';

type AuthProvider = 'google' | 'facebook';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) {
  // Facebook strategy should be pretty much the same
  constructor(auth: AuthService) {
    super(
      <StrategyOptionWithRequest>{
        clientID: "foo",
        clientSecret: "foo",
        callbackURL: '<domain>/auth/google/callback',
        passReqToCallback: true,
      },
      <VerifyFunctionWithRequest>(async (
        req,     // express request object
        access,  // access token from Google
        refresh, // refresh token from Google
        profile, // user profile, parsed by passport
        done
      ) => {
        // transform the profile to your expected shape
        const myProfile: AuthProfile

        return auth
          .handlePassportAuth(myProfile)
          .then(result => done(null, result))
          .catch(error => done(error));
      })
    );
  }
}

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

  @Get(':provider(google|facebook)')
  async handleOauthRequest(
    @Req() req: Request,
    @Res() res: Response,
    @Next() next: NextFunction,
    @Param('provider') provider: AuthProvider
  ) {
    const params = {
      session: false,
      scope: ['<specify scope base on provider>'],
      callbackURL: `<domain>/auth/${provider}/callback`,
    };
    authenticate(provider, params)(req, res, next);
  }

  @Get(':provider(google|facebook)/callback')
  async handleOauthCallback(
    @Req() req: Request,
    @Res() res: Response,
    @Next() next: NextFunction,
    @Param('provider') provider: AuthProvider
  ) {
    const params = {
      session: false,
      state: req.query.state,
      callbackURL: `<domain>/auth/${provider}/callback`,
    };

    // We use callback here, but you can let passport do the redirect
    // http://www.passportjs.org/docs/downloads/html/#custom-callback
    authenticate(provider, params, (err, user) => {
      if (err) return next(err);
      if (!user) return next(new UnauthorizedException());

      // I generate the JWT token myself and redirect the user,
      // but you can make it more smart.
      this.generateTokenAndRedirect(req, res, user);
    })(req, res, next);
  }
}

@Injectable()
export class AuthService {
  async handlePassportAuth(profile: AuthProfile) {
    // Return the existing user, or create the user entity
    // form profile returned by the OAuth provider
    const user: User;

    // Preform your business logic here

    // Return the user instance
    return user;
  }
}

@Module({
  controllers: [AuthController],
  providers: [AuthService, GoogleStrategy, FacebookStrategy],
  exports: [AuthService],
})
export class AuthModule {}
joe307bad commented 5 years ago

Thanks @zhenwenc! This looks really promising. Especially since it avoids using middleware, which I think @kamilmysliwiec suggested was the correct way to go about it. A couple questions:

  1. What do you mean you don't need to use the library's underlaying method. Which method are you referring to?
  2. I think I am a fan of the getToken method from the google-auth-library. Is there a reason you would rather generate your own token rather than use this method?
  3. Where did you find the proper usage of StrategyOptionWithRequest and VerifyFunctionWithRequest from the passport-google-oauth20 library? These are definitely interesting but I can't find any docs on them.

Thanks again for the response. I will definitely use some of this in my own implementation.

zhenwenc commented 5 years ago

@joe307bad

Thanks @zhenwenc! This looks really promising. Especially since it avoids using middleware, which I think @kamilmysliwiec suggested was the correct way to go about it. A couple questions:

  1. What do you mean you don't need to use the library's underlaying method. Which method are you referring to?

In your oauth2.service.ts:

import { OAuth2Client } from 'google-auth-library';
import { GetTokenResponse } from 'google-auth-library/build/src/auth/oauth2client';

I see you verify the code returned from Google, I suspect that this logic should be embedded in the passport plugin. But I might be wrong.

  1. I think I am a fan of the getToken method from the google-auth-library. Is there a reason you would rather generate your own token rather than use this method?

Passport already returned the access token for you, see the second argument in VerifyFunctionWithRequest function. In my case, after the user authenticated with Google or Facebook, I will create and return a JWT token (containing payload like { id, email, role }) to the user. The access tokens from oauth providers are only staying in the backend.

If you don't need this step, I think the solution should be more simpler, such as you don't need to specify the callback for authenticate function in handleAuthCallback, passport can do it for you.

  1. Where did you find the proper usage of StrategyOptionWithRequest and VerifyFunctionWithRequest from the passport-google-oauth20 library? These are definitely interesting but I can't find any docs on them.

Oh I see.. I know its funny, I found them from passport-facebook as there is no type definition available for passport-google-oauth20. The API of these two libraries should be identical anyway.

Thanks again for the response. I will definitely use some of this in my own implementation.

kmturley commented 5 years ago

Looks like this post by @nielsmeima has a good approach: https://medium.com/@nielsmeima/auth-in-nest-js-and-angular-463525b6e071

It uses passport-google-oauth20, passport-jwt and jsonwebtoken

It would also be good to have the recommended client-side workflow e.g.

Angular:

login() {
    this.loginWindow = window.open('http://localhost:8080/auth/google', '', 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no');
    window.addEventListener('message', (event) => {
      this.onLogin(event.data);
    });
  }

  onLogin(token) {
    console.log('onLogin', token);
    localStorage.setItem('token', token);
    this.loginWindow.close();
    window.removeEventListener('message', this.onLogin);
  }

NestJS:

    @Get('google/callback')
    @UseGuards(AuthGuard('google'))
    googleLoginCallback(@Req() req) {
      const jwt: string = req.user.jwt;
      if (jwt) {
        return `<html><body><script>window.opener.postMessage('${jwt}', 'http://localhost:4200')</script></body></html>`;
      } else {
        return 'There was a problem signing in...';
      }
    }

I created a backend + frontend example to that approach here: https://github.com/kmturley/appengine-datastore-nest-angular

tristan957 commented 5 years ago

This is the most poorly documented part of NestJS. Every single person is doing this differently. Is there some standard way to do authentication with a third party provider? I have gotten the following:

// auth.controller.ts

import { Controller, Logger, UseGuards, Post, Get, Req, Query } from "@nestjs/common";
import AuthService, { AuthProvider } from "./auth.service";
import { AuthGuard } from "@nestjs/passport";
import { IncomingMessage } from "http";
import { GoogleStrategy } from "./strategies";
import { Request } from "express";

@Controller("auth")
export default class AuthController {
    private static readonly logger = new Logger(AuthController.name);
    private readonly authService: AuthService;
    private readonly googleStrategy: GoogleStrategy;

    public constructor(authService: AuthService, googleStrategy: GoogleStrategy) {
        this.authService = authService;
        this.googleStrategy = googleStrategy;
    }

    @Get("google/login")
    @UseGuards(AuthGuard(AuthProvider.GOOGLE))
    // eslint-disable-next-line class-methods-use-this, no-empty-function
    public googleLogin(): void {}

    @Get("google/callback")
    // eslint-disable-next-line class-methods-use-this
    public googleLoginCallback(@Req() request: IncomingMessage, @Query("code") code: string): string {
        return code;
    }
}
// auth.service.ts
import { Injectable } from "@nestjs/common";
import { UserRepository, User } from "../user";
import { InjectRepository } from "@nestjs/typeorm";

export enum AuthProvider {
    GOOGLE = "google"
}

@Injectable()
export default class AuthService {
    private readonly userRepository: UserRepository;

    public constructor(@InjectRepository(User) userRepository: UserRepository) {
        this.userRepository = userRepository;
    }

    public async validateUser(token: string): Promise<User | undefined> {
        const user = await this.userRepository.findOneByToken(token);
        if (user === undefined) {
            console.log("create account????");
        }

        return user;
    }
}
// google.strategy.ts
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import AuthService, { AuthProvider } from "../auth.service";
import {
    OAuth2Strategy,
    IOAuth2StrategyOptionWithRequest,
    Profile,
    VerifyFunction
} from "passport-google-oauth";
import { Request } from "express";
import { ConfigService } from "../../config";

@Injectable()
export default class GoogleStrategy extends PassportStrategy(OAuth2Strategy, AuthProvider.GOOGLE) {
    public constructor(authService: AuthService, configService: ConfigService) {
        const options: IOAuth2StrategyOptionWithRequest & { scope: string | string[] } = {
            clientID: configService.googleOAuthClientId,
            clientSecret: configService.googleOAuthClientSecret,
            callbackURL: "http://localhost:8080/api/v1/auth/google/callback",
            passReqToCallback: true,
            scope: ["profile", "email"]
        };

        super(
            options,
            async (
                req: Request,
                accessToken: string,
                refreshToken: string,
                profile: Profile,
                done: VerifyFunction
            ): Promise<void> => {
                console.log("MY NAME IS TRISTAN PARTIN");
                console.log(accessToken);
                console.log(refreshToken);
                console.log(profile);

                const user = await authService.validateUser(accessToken);
                if (user === undefined) {
                    done(new UnauthorizedException());
                }

                done(undefined, user);
            }
        );
    }
}
// google.guard.ts
import { Injectable, ExecutionContext, UnauthorizedException } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { Observable } from "rxjs";
import { User } from "../../user";

@Injectable()
export default class GoogleAuthGuard extends AuthGuard("google") {
    public canActivate(ctx: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
        super.logIn(ctx.switchToHttp().getRequest());
        return super.canActivate(ctx);
    }

    // eslint-disable-next-line class-methods-use-this
    public handleRequest<U extends User>(err: Error, user: U, info?: string): U {
        if (err !== undefined) {
            throw err;
        }

        if (user === undefined) {
            throw new UnauthorizedException();
        }

        return user;
    }
}
// auth.module.ts

import { Module } from "@nestjs/common";
import { UserModule } from "../user";
import AuthService from "./auth.service";
import { GoogleStrategy } from "./strategies";
import { PassportModule } from "@nestjs/passport";
import { ConfigModule } from "../config";
import AuthController from "./auth.controller";

@Module({
    imports: [
        ConfigModule,
        UserModule,
        PassportModule.register({ defaultStrategy: "google", property: "profile", session: true })
    ],
    controllers: [AuthController],
    providers: [AuthService, GoogleStrategy]
})
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export default class AuthModule {}

I feel like I am doing this fairly idiomatically, but I don't know how to call the anonymous async function in the GoogleStrategy parent constructor. Any suggestions? My approach seems to be much simpler than some of the things in this thread. All that being said, this web framework has been extremely enjoyable and the documentation has been exemplary up to this point. Thank you NestJS contributors.

My problem was that I was not also guarding the callback endpoint. :+1: fixed

johnbiundo commented 5 years ago

@tristan957 Ah, I see you answered your question since I saw the notification. Cool 👍.

Just for the sake of exchanging some learnings on this, if I understood your question, I think it's worth mentioning what I think happens. So, during the funky OAuth2 "dance", the Passport.authenticate() method gets called twice. Once from your first route (in your case /google/login), and then again during the callback (/google/callback). It's sort of oddly "overloaded" in a sense. (To me, the Passport API would be more clear if they separated these calls into two separate functions, but whatever).

In the first call to authenticate(), since there is no ?code= querystring parameter in the route, Passport initiates the OAuth2 login flow (requesting an authorization code, and in the course of that process, Google, if necessary, asks for consent if it hasn't yet been given and/or promps for Google login if needed). So to be clear (at least as far as I can surmise), lack of a querystring param here prompts this part of the flow.

Then, in the callback portion of the flow, what Passport is doing is orchestrating the exchange of the authorization code for an access token, and then calling your verify callback. It does this part because it finds a ?code=... querystring param. So, it's a kind of strange dance that you need to choreograph just right. In a pure Express app, you include a call to passport.authenticate() in both routes; in Nest, you use the AuthGuard to perform the same thing. But in the end, you don't directly call that async function, Passport does.

I'm sure you knew all this, but it's a little confusing how it all works, so I thought I'd write down (at least my best hypothesis) for what's actually happening. Would love to hear corrections to the theory if I'm off base. I'm sorely tempted to look more at the Passport code to confirm all this, but maybe sometime later. It's kind of amazing that this isn't clearly documented in any Passport documentation or tutorial I've seen.

tristan957 commented 5 years ago

Actually this was my first time using an OAuth workflow with PassportJS and NestJS. I semi understood it at a high level, but not totally. Your comment definitely helps. I would be interested in submitting a pull request to this repository covering this using the code I have written out, your comment, and my general learnings. Are there any pull requests in progress which cover this already?

johnbiundo commented 5 years ago

To my knowledge there are not yet any.

I have been doing some editing of the Overview portion of the docs through a series of PRs over the last month or so. I have one open on Custom decorators, but after that am going to start working on some other portions. I had started thinking about Authentication - both because it's an area that I think has a lot of confusion, and because I'm focusing on that now for my application. This is what's led me down this path, and the learnings I mentioned above.

So all that said, I have some early thoughts on how to help improve this part of the doc, but nothing concrete yet. Authentication is a rabbit hole. Understanding it takes you through Passport, then to Oauth, and nuances of each Social Provider, and into things like JWTs, sessions, etc., all of which is loaded with layers and ever-changing opinions, and 4000 semi-conflicting tutorials on Medium. There's also a lot of great documentation out there (https://openid.net/developers/libraries/, everything Robert Broeckelmann writes, etc.), but marrying it to Express and Nest is sort of cutting edge.

The biggest open question I have right now about documenting social login is that - from what I am starting to understand now - there is a very different approach depending on whether you are doing a SPA or a traditional web application. So from my perspective, as I learn about this, I'm trying to think about the best way to organize documentation to support different use cases without adding too much complexity or redundancy to the docs.

So, sorry for the long answer, but as far as I'm concerned, yes of course you are encourage to submit a PR. I just wanted to let you know I'm thinking about the same topic area, but don't yet have well-formulated plans. Perhaps we can collaborate some. Either way, no harm submitting a PR with some helpful commentary in the mean time. Good to have more people helping in this area!

tristan957 commented 5 years ago

I would definitely be interested in sharing the code I have written out with you as it pertains to Google authentication. I am sure you are much more knowledgeable than I am on this subject. For reference, I am writing all of this for a store front for selling math textbooks and accompanying exercises. General react front-end type stuff with a NestJS + TypeORM back-end.

johnbiundo commented 5 years ago

So a SPA basically? I noticed in your code sample you're returning a user in the verify callback, and then maybe pulling off the authorization code param in the route? I wasn't sure what you were doing here, so thought maybe it was a traditional web app and you were just handing the authorization code around for some other reason. I'm sure this is just a subset of the code for now.

Anyway, the documentation challenge I see is that this part of the code, in isolation, doesn't solve the whole problem (and could even be confusing). I think for a traditional web app (serving HTML pages via something like handlebars) the solution is pretty easy, and your code is close; just have to add session handling (though I haven't yet tried to do that with Nest, so not sure if that adds much complexity).

But for a SPA, I would think that a generally useful document would also want to address JWT handling for how you do authentication for YOUR API. E.g., after authenticating with Google, you (either add a user, or) pull the user profile, generate a JWT, and serve that to the SPA.

Every time I think about this, it starts down a rabbit hole both for how to best architect it, and how to best document it.

In any case, useful discussion. One thing, short of a pull request, might be once you have your code stable, to extract this stuff out into a separate github repo. If nothing else, it's easy to exchange ideas that way, and it could be useful as a standalone example for others that end up down this path. Might be a good place to continue this discussion which has meandered a bit 😄

kmturley commented 5 years ago

Ideal situation for documentation and examples is to identify and show the top few use-cases. I think this page does a good job of explaining the different approaches: https://developers.google.com/identity/protocols/OAuth2

It starts with the three main scenarios, then provides an example for each

Scenarios:

I found the Realworld example app was quite a good starting point for reference: https://github.com/gothinkster/realworld https://github.com/lujakob/nestjs-realworld-example-app

johnbiundo commented 5 years ago

@kmturley Yeah, agreed. Those are the use cases that need to be documented.

futbotism commented 5 years ago

Apologies if this isn't the correct place to post this but i would wonder how we would go about using something like this library https://github.com/abouroubi/passport-google-verify-token

Essentially, instead of the browser hitting the GET on the server route, i would like to send a post of the google token id to the backend and then have the backend respond with the authenticated user

In my controller

  @Post('google')
  public googleLogin(@Body() googleUser: AuthGoogle) {
      passport.authenticate('google-verifiy-token', (req, res) => {
        // do something with req.user
      });
  }

Then in a provider

@Injectable()
export class GoogleStrategyService {
  public constructor(private readonly authService: AuthService) {
    passport.use(new Strategy(
      {
        clientID: 'mytokensfjdhsfkjhfsddkj',
        clientSecret: 'mySecretldksjfsldkf'
      },
      (accessToken, refreshToken, profile, done) => {
        console.log('ehlloo');
        return done(null, profile);
      }
    ));
  }
}

The app seems to set up the strategy but i don't get any call backs triggering

futbotism commented 5 years ago

In relation to my comment above about using a post to verify the google user as opposed to a get and redirect flow, i settled on this approach if anyone else is interested. I like it because i can post to one endpoint and have verification & user upsert occur in that request handler and then return the authorised user. Don't need to integrate the sign up flow with middlewares or passport at all.

Would really appreciate any feedback about this approach...

import { OAuth2Client } from 'google-auth-library';

  @Post('google')
  public async googleLogin(@Body() googleUser: AuthGoogle) {
    const client = new OAuth2Client('jhsdfgjhdfsgjsdhblah.apps.googleusercontent.com');
    const ticket = await client.verifyIdToken({
      idToken: googleUser.token,
      audience: 'jhsdfgjhdfsgjsdhblah.apps.googleusercontent.com',
    });
    const payload = ticket.getPayload();

    if (payload) {
      const email = payload.email as string;
      let user = await this.userService.findOneByEmail(email);

      if (user === undefined) {
        user = await this.userService.create({ email });
      }

      return this.authService.authenticate(user);
    }
  }
wangdicoder commented 5 years ago

Looks like this post by @nielsmeima has a good approach: https://medium.com/@nielsmeima/auth-in-nest-js-and-angular-463525b6e071

It uses passport-google-oauth20, passport-jwt and jsonwebtoken

It would also be good to have the recommended client-side workflow e.g.

Angular:

login() {
  this.loginWindow = window.open('http://localhost:8080/auth/google', '', 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no');
  window.addEventListener('message', (event) => {
    this.onLogin(event.data);
  });
}

onLogin(token) {
  console.log('onLogin', token);
  localStorage.setItem('token', token);
  this.loginWindow.close();
  window.removeEventListener('message', this.onLogin);
}

NestJS:

    @Get('google/callback')
    @UseGuards(AuthGuard('google'))
    googleLoginCallback(@Req() req) {
      const jwt: string = req.user.jwt;
      if (jwt) {
        return `<html><body><script>window.opener.postMessage('${jwt}', 'http://localhost:4200')</script></body></html>`;
      } else {
        return 'There was a problem signing in...';
      }
    }

I created a backend + frontend example to that approach here: https://github.com/kmturley/appengine-datastore-nest-angular

I think this is a much more nest.js style to implement the strategy. The strategy constructor is only passed options as the parameter, and then you need to provide a validate method for the callback.

I just copy docs from the website:

We've also implemented the validate() method. For each strategy, Passport will call the verify function (implemented with the validate() method in @nestjs/passport) using an appropriate strategy-specific set of parameters. For the local-strategy, Passport expects a validate() method with the following signature: validate(username: string, password:string): any

So, if you create a strategy file called google.strategy.ts, then will be implemented like this,

import { BadRequestException, Injectable, InternalServerErrorException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, StrategyOptionsWithRequest, Profile, VerifyCallback } from 'passport-google-oauth20';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      clientID: '',
      clientSecret: '',
      callbackURL: '',
      passReqToCallback: true,
      scope: ['profile', 'email'],
    } as StrategyOptionsWithRequest);
  }

  async validate(request: any, accessToken: string, refreshToken: string, profile: Profile, done: VerifyCallback) {
    if (!profile) {
      done(new BadRequestException(), null);
    }
    // Get google account information
    const name = profile.displayName;
    const email = profile.emails[0].value;
    // Add your verify logic here...

    // if error, provide an error info
    done(new InternalServerErrorException(), null);
    // if verified, pass user info
    done(undefined, user);
  }
}
cj-wang commented 5 years ago

I've been following @nielsmeima's approach as well, to let passport be engaged by AuthGuard. While I used a slightly different way to pass the jwt token to client, by redirecting to the client callback component with the token as a query parameter. Not sure if it's a good practice though. What I've done in AuthController:

  @UseGuards(AuthGuard('google'))
  @Get('google')
  async googleLogin() {
  }

  @UseGuards(AuthGuard('google'))
  @Get('oauth2/callback')
  async googleCallback(@Request() req, @Response() res) {
    const loginResult = await this.authService.login(req.user);
    res.redirect(`/auth/oauth2/callback?accessToken=${loginResult.accessToken}`);
  }

then in the angular app, receive and save the token with the client auth module, which is Nebular in my case.

  ngOnInit(): void {
    const accessToken = this.activatedRoute.snapshot.queryParams.accessToken;
    const token = this.authStrategy.createToken(accessToken, true);
    this.tokenService.set(token);
    const redirect = this.authStrategy.getOption('token.redirectUri');
    if (redirect) {
      this.router.navigateByUrl(redirect);
    }
  }

server code: https://github.com/cj-wang/mean-start-2/tree/master/server/src/api/auth client code: https://github.com/cj-wang/mean-start-2/blob/master/client/src/app/auth/oauth2/oauth2-callback.component.ts

Feelthewind commented 5 years ago

It would be good if it's possible to pass options to AuthGuard. Now i have to use PassportModule.register and the options i give at this point are used for all the other social login as well. I had to give accessType to get refreshToken for google login and use this way.

johnbiundo commented 5 years ago

@Feelthewind I think this is discussed in @nestjs/passport#57. There's a comment there that might help: https://github.com/nestjs/passport/issues/57#issuecomment-510610374

hegelstad commented 4 years ago

@johnbiundo

But for a SPA, I would think that a generally useful document would also want to address JWT handling for how you do authentication for YOUR API. E.g., after authenticating with Google, you (either add a user, or) pull the user profile, generate a JWT, and serve that to the SPA.

This is what I am doing, but isn't session the way to go here too? My understanding is that session is the golden standard and JWT are not secure enough? I am using an Authorization Flow to OIDC (OneLogin) and I get the token back in the callback, but I am not sure if I need to think of anything else.

edit: https://github.com/nestjs/docs.nestjs.com/issues/99#issuecomment-557878531 this is my setup, but the question above still remains relevant.

rsegecin commented 4 years ago

Hi, I don't know if you guys have taken notice but the passport-facebook-token repo has been updated after its latest released version on npm and it implements OAuth2Strategy. I didn't manage to make this code compile on a nest project, for some reason it won't resolve some functions that there's on authenticate middleware from passport pkg. Although there's a package called "passport-facebook-token-nest" that was transpiled from passport-facebook-token to what it looks like es2015 version on which you are able to add into an AuthGuard strategy as shown:

import { Strategy } from 'passport-facebook-token-nest';
import { jwtConstants } from './constants';
import { PassportStrategy } from '@nestjs/passport';
import { StrategyOptionsWithRequest, Profile } from 'passport-facebook-token';
import { Injectable, BadRequestException } from '@nestjs/common';

@Injectable()
export class FacebookStrategy extends PassportStrategy(Strategy, 'facebook-token') {
    constructor() {
        super({
            clientID: jwtConstants.facebook.appId,
            clientSecret: jwtConstants.facebook.appSecret,
            callbackURL: '',
            passReqToCallback: true,
            scope: ['profile', 'email'],
        } as StrategyOptionsWithRequest);
    }       
    async validate(request: any, accessToken: string, refreshToken: string, profile: Profile, done: any) {
        if (!profile) {
            done(new BadRequestException(), null);
        }
        console.log(profile);
        return profile;
    }
} 

Maybe our solution would be understanding why OAuth2Strategy won't resolve some function or transpile every strategy.

PS: I've just noticed that previous versions of passport-facebook-token also implements OAuth2Strategy but it is somehow different compared to the code contained in the package from npm.

SaveYourTime commented 4 years ago

aybe our solution would be understanding why OAuth2Strategy won't resolve some function or transpile every strategy.

PS: I've just noticed that previous versions of passport-facebook-token also implements OAuth2Strategy but it is so

Actually, you could just import Strategy from passport-facebook-token like this(see below), and pass it to PassportStrategy as an argument. import * as Strategy from 'passport-facebook-token';

The entire facebook.strategy.ts would be...

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Profile } from 'passport';
import * as Strategy from 'passport-facebook-token';
import { AuthRepository } from '../auth.repository';
import { UserRepository } from '../../users/user.repository';
import { ProviderType } from '../../providers/provider-type.enum';

@Injectable()
export class FacebookStrategy extends PassportStrategy(Strategy, 'facebook') {
  constructor(
    private authRepository: AuthRepository,
    private userRepository: UserRepository,
  ) {
    super({
      clientID: process.env.FACEBOOK_APP_ID,
      clientSecret: process.env.FACEBOOK_APP_SECRET,
      fbGraphVersion: 'v7.0',
    });
  }

  async validate(
    accessToken: string,
    refreshToken: string,
    profile: Profile,
    done: (error: any, user?: any, info?: any) => void,
  ): Promise<void> {
    const { id } = profile;

    let user = await this.userRepository.findUserByProvider(
      id,
      ProviderType.FACEBOOK,
    );
    if (user) {
      return done(null, user);
    }

    try {
      user = await this.authRepository.signUpWithThirdPartyProvider(profile);
      done(null, user);
    } catch (error) {
      done(error);
    }
  }
}
edwardchanjw commented 3 years ago

For simplicity we now have old way similar with Node.js structure. Actually I am prefer the flow would be so obvious, less than paragraph of information, so some project we don't have to worked on typescript. (Save time to avoid compiling, if the microservices is just so small).

export class GoogleStrategyService {
  public constructor(private readonly authService: AuthService) {
    passport.use(new Strategy(
      {
        clientID: 'mytokensfjdhsfkjhfsddkj',
        clientSecret: 'mySecretldksjfsldkf'
      },
      (accessToken, refreshToken, profile, done) => {
        console.log('ehlloo');
        return done(null, profile);
      }
    ));
  }
}

or as @zhenwenc , Kind of Free Spirit

@Module({
  controllers: [AuthController],
  providers: [AuthService, GoogleStrategy, FacebookStrategy],
  exports: [AuthService],
})

or as @tristan957 , Kind of NestJS Structure as Authentication but I don't like any structure reinvented, without mention where it is from, or innovate and why the boilerplate. (Unless it is already mention in the Authentication )

@Module({
    imports: [
        ConfigModule,
        UserModule,
        PassportModule.register({ defaultStrategy: "google", property: "profile", session: true })
    ],
    controllers: [AuthController],
    providers: [AuthService, GoogleStrategy]
})
thisismydesign commented 3 years ago

Here's a tutorial for a full-stack OAuth2 authentication flow with NestJS using @nestjs/passport and passport-google-auth: OAuth2 in NestJS for Social Login (Google, Facebook, Twitter, etc)

And here's one for Cognito via OAuth2: Cognito via OAuth2 in NestJS: Outsourcing Authentication Without Vendor Lock-in

msrumon commented 1 year ago

Can we all just put Google aside and implement an in-house oAuth2 server? oauth2orize looks interesting, I just wish someone would write a wrapper around NestJS.

chihabhajji commented 10 months ago

half of this conversation is ambigious because i think we are mixing up OAuth2 with OIDC