Open weeco opened 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.
Looks promising :)
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?
@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.
@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.
No worries @Offlein 🙂 Thank you!
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.
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);
}
}
@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 {}
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:
you don't need to use the library's underlaying method
. Which method are you referring to?getToken
method from the google-auth-library
. Is there a reason you would rather generate your own token rather than use this method?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.
@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:
- 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.
- I think I am a fan of the
getToken
method from thegoogle-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.
- Where did you find the proper usage of
StrategyOptionWithRequest
andVerifyFunctionWithRequest
from thepassport-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.
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
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
@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.
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?
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!
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.
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 😄
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
@kmturley Yeah, agreed. Those are the use cases that need to be documented.
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
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);
}
}
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
andjsonwebtoken
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);
}
}
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
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.
@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
@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.
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.
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);
}
}
}
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]
})
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
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.
half of this conversation is ambigious because i think we are mixing up OAuth2 with OIDC
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)?