docs.nestjs.com icon indicating copy to clipboard operation
docs.nestjs.com copied to clipboard

Passport docs (AuthModule)

Open kamilmysliwiec opened this issue 6 years ago • 34 comments

I'm submitting a...


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

Current behavior

Expected behavior

As a user, I would like to see more passport examples. https://github.com/nestjs/nest/issues/708 https://github.com/nestjs/nest/issues/833

Minimal reproduction of the problem with instructions

What is the motivation / use case for changing the behavior?

Environment


For Tooling issues:
- Node version: XX  
- Platform:  

Others:

kamilmysliwiec avatar Jul 04 '18 12:07 kamilmysliwiec

I coded up canonical Nest.js examples for Local, Google and Github (other OAuths are nearly identical ofcourse) strategies. I would be very happy to share these and possibly use them to improve the docs. How can I help?

nielsmeima avatar Aug 18 '18 20:08 nielsmeima

Wow, that is a great news @nielsmeima! Feel free to create a PR if you have some spare time 🙂

kamilmysliwiec avatar Aug 20 '18 06:08 kamilmysliwiec

@nielsmeima Hello, I just wanted to throw in my support for at least seeing your examples, even if they're ugly!

Offlein avatar Sep 11 '18 14:09 Offlein

@nielsmeima Any update on this?

weeco avatar Sep 30 '18 12:09 weeco

Sorry for the super late response! Had a busy start of the semester...

I have a working template for social providers via passport (Google, Github, etc.) which I use as a SSO option on an Angular application, in which further authentication is handled by using JWTs as described in the current Nest.js docs. I will try to at least show some material here!

I am also debating on writing a Medium post about Angular + OAuth2 SSO + Passport + JWTs + Nest.js. People interested in that?

nielsmeima avatar Oct 05 '18 16:10 nielsmeima

Sure I am interested! Please make sure to publish anything at all. I often read such comments and in the end I never hear or see any piece of code because the author lost interest or time to publish it.

weeco avatar Oct 05 '18 16:10 weeco

I know, have seen a lot of the same thing. I actually have some time tonight, so I will get right to it.

nielsmeima avatar Oct 05 '18 16:10 nielsmeima

Spend the whole evening on writing the back-end part of the Medium post. Code is embedded using JSFiddles and I will also put it on Github.

I will do the front-end (Angular) part tomorrow and will probably also post it then.

nielsmeima avatar Oct 05 '18 22:10 nielsmeima

The back-end part is live: https://medium.com/@nielsmeima/auth-in-nest-js-and-angular-463525b6e071

nielsmeima avatar Oct 06 '18 10:10 nielsmeima

Thanks for the article, going to read it tonight! Maybe @kamilmysliwiec or @BrunnerLivio wanna take a look at it. You could share it at twitter and mention https://twitter.com/nestframework?lang=en .

weeco avatar Oct 06 '18 12:10 weeco

Yes, will share it on Twitter today. What did you think about the article?

nielsmeima avatar Oct 08 '18 07:10 nielsmeima

The back-end part is live: https://medium.com/@nielsmeima/auth-in-nest-js-and-angular-463525b6e071

The article is really great and answers some unanswered questions. Thanks a lot for it. Would be great if it could be linked in the nestjs documentation?

mfechner avatar Oct 15 '18 16:10 mfechner

I attempted to follow the Inheritance section of https://docs.nestjs.com/techniques/authentication to implement a local + session AuthGuard, but had to make some adjustments:

import { ExecutionContext, Injectable } 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();

		// End any current session
		await request.logout();

		// Super canActivate() must establish an authenticated user before logIn() will work
		const can:boolean = await super.canActivate(context) as boolean;

		if (can) {
			// Establish a logIn session (https://github.com/nestjs/passport/issues/7)
			await super.logIn(request);
		}

		return can;
	}

	handleRequest(err, user, info) {
		return super.handleRequest(err, user, info);
	}
}

It doesn't feel like that's exactly what was intended, but does work in my case.

synapdk avatar Oct 15 '18 16:10 synapdk

@synapdk Thanks for sharing -- I've been trying to get Sessions working with Google OAuth2 for a long time. I have been busy with another project but am getting back into this.

Are you saying that you have a local username+password login that doesn't re-prompt because of a cookie-based session effectively working right now with just the pasted code?

I've gone through so many of these tutorials that ignore NestJS best-practices, or offer conflicting information about what to do. It seems like if you're using Sessions, according to that authentication page you linked to, you just set { session: true} as the parameter when doing PassportModule.register(...), but other places say I need to turn on Passport sessions (or even Express sessions?) at the root of the app in main.ts.

I'm trying to do this with the passport-google-oauth-20 module, but no matter what, any time I hit a controller action guarded by the "googleAuthGuard" guard that I made (implementing that strategy) I get the Google prompt anyway.

I don't even see any cookies for my application in this case.

Thanks for any help!

Offlein avatar Oct 20 '18 16:10 Offlein

Hello,

I just published a sample app that uses JWT short lived access tokens, and long lived refresh tokens. My initial goal is to have a secure auth sample based on NestJS, that will be used by a mobile app.

The refresh token has a sliding expiration time, so if the user uses the mobile app frequently he never get disconnected.

Also, when the user is logged in, we save the client id, it can be the smartphone name or something meaningful for the user, this client id is associated to the refresh token, this gives the possibility to the user to disconnect only from one device and stay connected in the other devices, he can also disconnect from all the devices.

Here is the repo : https://github.com/abouroubi/nestjs-auth-jwt

Comments, PR's are welcome.

abouroubi avatar Dec 05 '18 17:12 abouroubi

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.

I'll keep this open for now as I'm not sure #1365, and #264 are addressed yet.

johnbiundo avatar Jul 22 '19 22:07 johnbiundo

Guys here is what you are looking for - boilerplate with local/google auth, sessions, and ACL https://www.gemunion.io/documentation/authorization https://github.com/GemunIon/nestjs-auth/

@johnbiundo thanks for you code, I used it as starting point

TrejGun avatar Sep 01 '19 11:09 TrejGun

I'm trying to get openid-client to work with nestjs/passport..... since, PassportStrategy needs in the constructor the full params (client_id, client_secret) but openid-client needs to call an "Issuer" promise first (discover)/method.... so anyone could please give some advices to continue.

  1. first aproach: returns "client" must be an instance of "openid-client"
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import * as OpenIDClient from "openid-client"

@Injectable()
export class OpenIDStrategy extends PassportStrategy(OpenIDClient.Strategy, 'oidc') {
  constructor() {
    super({
      client_id: process.env.APP_AUTH_CLIENT_ID,
      client_secret: process.env.AUTH_CLIENT_SECRET,
      callbackURL: process.env.APP_AUTH_REDIRECT_URI,
      passReqToCallback: true,
      scope: [
        'openid',
      ]
    })
  }

  validate(request: any, accessToken: string, refreshToken: string, profile, done: Function) {
    return done(null, { ...profile, accessToken, refreshToken });
  }
}
  1. Here's what I've got in express (currently working code)...
(...)
try {
    trustIssuer = await OpenIDIssuer.discover(process.env.AUTH);

    const client = new trustIssuer.Client({
      client_id: process.env.APPLICATION_UID,
      client_secret: process.env.SECRET
    });

    client.CLOCK_TOLERANCE = 10000000;

    const params = {
      redirect_uri: `${process.env.REDIRECT_URI}/auth/cb`,
      scope: process.env.SCOPES
    }

    passport.use('oidc', new OpenIDStrategy({ client, params, passReqToCallback: false, usePKCE: false }, (tokenset, userinfo, done) => {
      tokenset.created_at = moment().format("X");
      tokenset.expires_at = moment().add(tokenset.expires_in - 72, "seconds").format("X");
      return done(null, { ...userinfo, ...tokenset });
    }));

    passport.serializeUser((userData, done) => {
      if (userData) {
        done(null, userData);
      } else {
        done("error", false);
      }
    });

    passport.deserializeUser((userData, done) => {
      if (userData) {
        done(null, userData);
      } else {
        done("error", false);
      }
    });
(...)

MrXploder avatar Sep 21 '19 21:09 MrXploder

@MrXploder have you figured out the "client" must be an instance of "openid-client" issue?

ides15 avatar Oct 03 '19 15:10 ides15

@ides15 Unfortunately, no. I ended up doing all this "openid strategy registration" in the bootstrapping section. Then I made a Module(Auth)->Controller to register the 2 routes that I need (auth and auth/cb) and a custom middleware to wrap the passport middleware.

Here is my full working code (sorry for my very bad English): (all paths are related to "src/*" proyect)

/main.ts file to register redis and passport configuration

import * as dotenv from "dotenv";

import * as path from "path";
import * as moment from "moment";
import * as session from "express-session";
import * as redis from "redis";
import * as redisConnect from "connect-redis";
import * as passport from "passport";
import * as bluebirdPromise from "bluebird";

dotenv.config({ path: path.resolve(process.env.PWD, "..", ".env") });
global.Promise = bluebirdPromise;

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Issuer as OpenIDIssuer, Strategy as OpenIDStrategy } from "openid-client";

// OpenIDIssuer.defaultHttpOptions.timeout = 10000;
const redisStore = redisConnect(session);
const redisClient = redis.createClient({port: process.env.REDIS_PORT, host: process.env.REDIS_HOST});

function redisHandle() {
  return new Promise((resolve, reject) => {
    redisClient.on("error", err => {
      console.error("Redis connection error");
      reject(err);
    });

    redisClient.on("ready", (err, res) => {
      if (!err) {
        console.info("Redis connection successful");
        resolve(res);
      } else {
        reject(err);
      }
    })
  })
}

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

  await redisHandle();

  const TrustIssuer = await OpenIDIssuer.discover(process.env.SERVICE_URI_AUTH);

  const params = {
    redirect_uri: process.env.APP_AUTH_REDIRECT_URI,
    scope: process.env.APP_AUTH_SCOPES,
  }

  const client = new TrustIssuer.Client({
    client_id: process.env.APP_AUTH_CLIENT_ID,
    client_secret: process.env.APP_AUTH_CLIENT_SECRET,
  });

  client.CLOCK_TOLERANCE = Infinity;

  passport.use('oidc', new OpenIDStrategy({ client, params, passReqToCallback: false, usePKCE: false }, (tokenset, userinfo, done) => {
    tokenset.created_at = moment().format("X");
    tokenset.expires_at = moment().add(tokenset.expires_in - 72, "seconds").format("X");
    return done(null, { ...userinfo, ...tokenset });
  }));

  passport.serializeUser((userData, done) => {
    if (userData) {
      done(null, userData);
    } else {
      done("error", false);
    }
  });

  passport.deserializeUser((userData, done) => {
    if (userData) {
      done(null, userData);
    } else {
      done("error", false);
    }
  });

  app.use(passport.initialize());
  app.use(passport.session());

  app.use(session({
    name: "app.sid",
    secret: 'qwerty',
    resave: true,
    unset: "destroy",
    saveUninitialized: true,
    cookie: {
      secure: false,
    },
    store: new redisStore({
      host: process.env.REDIS_HOST,
      port: process.env.REDIS_PORT,
      client: redisClient
    })
  }));

  await app.listen(3000);
}

bootstrap();

/auth/auth.module.ts to assign my 2 custom middlewares to the 2 openid routes

import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { PassportAuthMiddleware, PassportCallbackMiddleware } from "../common/middleware/passport";

@Module({
  controllers: [
    AuthController
  ],
  providers: []
})
export class AuthModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(PassportAuthMiddleware)
      .forRoutes({ path: "/auth", method: RequestMethod.GET })

    consumer
      .apply(PassportCallbackMiddleware)
      .forRoutes({ path: "/auth/cb", method: RequestMethod.GET })
  }
}

/auth/auth.controller.ts to register the 2 routes

import { Controller, Get } from '@nestjs/common';

@Controller('auth')
export class AuthController {
  @Get("")
  oidcLogin() { }

  @Get("cb")
  oidcCallback() { }
}

and finally /common/middleware/passport.ts to wrap 2 passport middlwares

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';

import * as passport from "passport"

@Injectable()
export class PassportAuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    return passport.authenticate("oidc")(req, res, next);
  }
}

@Injectable()
export class PassportCallbackMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    return passport.authenticate("oidc", { successRedirect: "/", failureRedirect: "/auth" })(req, res, next);
  }
}

if you guys have a better approach, let me know! :D

MrXploder avatar Oct 03 '19 16:10 MrXploder

Here is my example which works great!

// src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { PhotonService } from '../services/photon.service';
import { User } from '@generated/photon';

@Injectable()
export class AuthService {
  constructor(private readonly photon: PhotonService) {}

  async validateUser(subject: string): Promise<User> {
    const user = await this.photon.users.findOne({ where: { id: subject } });
    if (user) return user;
    return null;
  }

  async createValidatedUser(subject, email, firstname, lastname): Promise<User> {
    const user = await this.photon.users.create({
      data: {
        id: subject,
        email,
        firstname,
        lastname,
        role: 'USER',
      }
    });
    if (user) return user;
    return null;
  }
}
// src/auth/onelogin.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, Client, UserinfoResponse, TokenSet, Issuer } from 'openid-client';
import { User } from '@generated/photon';
import { AuthService } from './auth.service';

export const buildOpenIdClient = async () => {
  const TrustIssuer = await Issuer.discover(`https://${process.env.AUTH_ONELOGIN_SUBDOMAIN}.onelogin.com/oidc/.well-known/openid-configuration`);
  const client = new TrustIssuer.Client({
    client_id: process.env.AUTH_ONELOGIN_CLIENT_ID,
    client_secret: process.env.AUTH_ONELOGIN_CLIENT_SECRET,
    token_endpoint_auth_method: 'client_secret_post',
  });
  return client;
};

@Injectable()
export class OneloginStrategy extends PassportStrategy(Strategy, 'onelogin') {
  client: Client;

  constructor(private readonly authService: AuthService, client: Client) {
    super({
      client: client,
      params: {
        redirect_uri: process.env.AUTH_ONELOGIN_REDIRECT_URI,
        scope: process.env.AUTH_ONELOGIN_SCOPE,
      },
      passReqToCallback: false,
      usePKCE: false,
    });

    this.client = client;
  }

  async validate(tokenset: TokenSet): Promise<User> {
    const userinfo: UserinfoResponse = await this.client.userinfo(tokenset);

    try {
      const user = await this.authService.validateUser(userinfo.sub);
      if (user) return user;
      return await this.authService.createValidatedUser(
        userinfo.sub, userinfo.email, userinfo.given_name, userinfo.family_name
      );
    } catch (err) {
      throw new UnauthorizedException();
    }
  }
}
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { PhotonService } from '../services/photon.service';
import { LoginGuard } from '../guards/login.guard';
import { SessionGuard } from '../guards/session.guard';
import { AuthenticatedGuard } from '../guards/authenticated.guard';
import { GqlAuthGuard } from '../guards/gql-auth.guard';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { OneloginStrategy, buildOpenIdClient } from './onelogin.strategy';
import { SessionSerializer } from './session.serializer';

const OneloginStrategyFactory = {
  provide: 'OneloginStrategy',
  useFactory: async (authService: AuthService) => {
    const client = await buildOpenIdClient(); // secret sauce! build the dynamic client before injecting it into the strategy for use in the constructor super call.
    const strategy = new OneloginStrategy(authService, client);
    return strategy;
  },
  inject: [AuthService]
};

@Module({
  imports: [
    PassportModule.register({ session: true, defaultStrategy: 'onelogin' }),
  ],
  controllers: [AuthController],
  providers: [
    PhotonService,
    LoginGuard,
    SessionGuard,
    AuthenticatedGuard,
    GqlAuthGuard,
    AuthService,
    OneloginStrategyFactory,
    SessionSerializer,
  ],
  exports: [GqlAuthGuard, SessionGuard, LoginGuard, AuthenticatedGuard, AuthService],
})
export class AuthModule {}
// src/auth/auth.controller.ts
import { Controller, Get, UseGuards, Request } from '@nestjs/common';
import { User } from '@generated/photon';
import { CurrentUser } from '../users/user.decorator';
import { LoginGuard } from '../guards/login.guard';
import { SessionGuard } from '../guards/session.guard';
import { AuthenticatedGuard } from '../guards/authenticated.guard';

@Controller('auth')
export class AuthController {
  @UseGuards(LoginGuard)
  @Get('onelogin/login')
  public async login() {
  }

  @UseGuards(LoginGuard)
  @Get('onelogin/callback')
  public async callback(@Request() req) {
    return req.user;
  }

  @UseGuards(AuthenticatedGuard)
  @Get('secret')
  getTheSecret(@Request() req) {
    return 'a secret has been returned';
  }
}
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app/app.module';
import { ValidationPipe } from '@nestjs/common';
import * as session from 'express-session';
import * as passport from 'passport';
require('dotenv').config();

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

  // Validation
  app.useGlobalPipes(new ValidationPipe());

  // Authentication & Session
  app.use(session({
    secret: process.env.AUTH_SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true
    }
  }));
  app.use(passport.initialize());
  app.use(passport.session());

  // Cors
  app.enableCors();

  await app.listen(3000);
}

bootstrap();

hegelstad avatar Nov 24 '19 11:11 hegelstad

Here's a working example of LDAP authentication in NestJS that I've created for those looking for one: https://github.com/sbrannstrom/nestjs-passport-ldap-example

sbrannstrom avatar Jan 13 '20 14:01 sbrannstrom

I'm new to Node.js, let alone NestJS, but, thanks to others who have added their samples here, I've published a blog post and GitHub repo to demonstrate OpenID Connect (OIDC) authentication using the OpenID Certifiedâ„¢ passport strategy "openid-client". I'm using Google as my Identity Provider but it works equally well with Keycloak, Okta, etc. I serve up a create-react-app-typescript front-end leveraging the session from express-session and storing the session in MongoDB.

sdoxsee avatar Feb 06 '20 03:02 sdoxsee

I made a sample repository here in case it might help some people :) I use Nest.js, nestjs/passport with a local strategy and url redirection. Detailed article here.

Roms1383 avatar Mar 21 '20 08:03 Roms1383

thanks @Roms1383, your solution works perfect for me. This should be in the official docs

MrXploder avatar Mar 22 '20 20:03 MrXploder

I'm using @hegelstad 's example which is working great but I'm having one issue. My OpenID Connect provider can provide several different authentication methods, the only thing I need to do is to alter one of the params in the constructors super-call. For example, params.acr_values: 'urn:signicat:oidc:method:sbid' to params.acr_values: 'urn:signicat:oidc:method:siths' in order to switch from Swedish BankID to Swedish doctors SITHS-card login.

My question is, is it possible to alter this after the application has been started?

I'm currently using dotenv to have the variables read from a file. I've tried updating the variable from an endpoint but it does not change acr_value due to it being loaded at startup and it doesn't seem to work to update it after startup of the backend.

I'd appreciate any and all help :)

sbrannstrom avatar Jun 07 '20 11:06 sbrannstrom

@sbrannstrom I think you should create a ParamService and inject that into OneloginStrategyFactory similarly to how AuthService is injected to the Factory. Then it will be available in the factory and you can pass it into OneloginStrategy and have it available in the super call.

I am not aware if it supports to be changed when running, but if not you should consider just creating two separate instances.

@sdoxsee please credit me somehow in the blog post, doesn't have to be a big thing, and I would really appreciate it :D

hegelstad avatar Jun 07 '20 12:06 hegelstad

@hegelstad thank you kindly, I'll give that a try :)

sbrannstrom avatar Jun 07 '20 12:06 sbrannstrom

Hey @hegelstad. I had a link to your comment in my blog references but added your name (and GitHub profile) in this commit. Please create a PR if you'd prefer something else. Thanks again for your helpful comment!

sdoxsee avatar Jun 07 '20 13:06 sdoxsee

@sbrannstrom I think you should create a ParamService and inject that into OneloginStrategyFactory similarly to how AuthService is injected to the Factory. Then it will be available in the factory and you can pass it into OneloginStrategy and have it available in the super call.

I am not aware if it supports to be changed when running, but if not you should consider just creating two separate instances.

I tried updating the acr_values with dotenv (process.env.ACR_VALUES = 'foobar') but it does not seem to update after the backend has been started.... It seems like a lot of extra code to create separate instances for each auth method since we will use multiple (5+ and even more in the future).... So if anyone has any more ideas, I'd greatly appreciate it :) Thanks.

sbrannstrom avatar Jun 17 '20 08:06 sbrannstrom