--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ### Authentication
2 |
3 | Authentication is an **essential** part of most applications. There are a lot of different approaches and strategies to handle authentication. The approach taken for any project depends on its particular application requirements. This chapter provides several approaches to authentication that can be adapted to a variety of different requirements.
4 |
5 | [Passport](https://github.com/jaredhanson/passport) is the most popular node.js authentication library, well-known by the community and successfully used in many production applications. It's straightforward to integrate this library with a **Nest** application using the built-in `@nestjs/passport` module.
6 |
7 | In this chapter, we'll consider two representative use cases, and implement a complete end-to-end authentication solution for each:
8 | * Traditional web application with server-side template-driven HTML pages
9 | * API server that accepts REST/HTTP requests and returns JSON responses
10 |
11 | Note that in this chapter, we build the API Server use case on top of the Web application use case. This is not required of course, but is useful for illustrating the core concepts of authentication. At the end of the API Server section, we describe how to strip out the unnecessary components for an API server-only implementation. Because of this, we recommend reading the entire chapter in the order presented, even if you're only interested in the API server use case.
12 |
13 | #### Server-side web application use case
14 |
15 | Let's flesh out our requirements. For this use case, users will authenticate with a username and password. Once authenticated, the server will utilize Express sessions so that the user remains "logged in" until they choose to log out. We'll set up a protected route that is accessible only to an authenticated user.
16 |
17 | We start by installing the required packages, and building our basic routes.
18 |
19 | > Warning **Notice** For **any** Passport strategy you choose (there are many available [here](http://www.passportjs.org/packages/)), you'll always need the `@nestjs/passport` and `passport` packages. Then, you'll need to install the strategy-specific package (e.g., `passport-jwt` or `passport-local`) that scaffolds the particular authentication strategy you are building.
20 |
21 | Passport provides a strategy called [passport-local](https://github.com/jaredhanson/passport-local) that implements a username/password authentication strategy, which suits our needs for this use case. Since we are rendering some basic HTML pages, we'll also install the versatile and popular [express-handlebars](https://github.com/ericf/express-handlebars) package to make that a little easier. To support sessions and to provide a convenient way to give user feedback during login, we'll also utilize the express-session and connect-flash packages. With these basic requirements in mind, we can now start by scaffolding a brand new Nest application, and installing the dependencies:
22 |
23 | ```bash
24 | $ nest new auth-sample
25 | $ cd auth-sample
26 | $ npm install --save @nestjs/passport passport passport-local express-handlebars express-session connect-flash @types/express
27 | ```
28 | #### Web interface
29 |
30 | Let's start by building the templates we'll use for the UI of our authentication subsystem. Following a standard MVC type project structure, create the following folder structure (i.e., the `public` folder and its sub-folders):
31 |
32 |
33 |
src
34 |
35 |
public
36 |
37 |
views
38 |
39 |
layouts
40 |
41 |
42 |
43 |
44 |
45 | Now create the following handlebars templates, and configure Nest to use express-handlebars as the view engine. Refer [here](https://handlebarsjs.com/) for more on the handlebars template language, and [here](https://docs.nestjs.com/techniques/mvc) for more background on Nest-specific techniques for Server side rendered (MVC style) web apps.
46 |
47 | ##### Main layout
48 |
49 | Create `main.hbs` in the layouts folder, and add the following code. This is the outermost container for our views. Note the `{{ '{' }}{{ '{' }}{{ '{' }} body {{ '}' }}{{ '}' }}{{ '}' }}` line, which is where each individual view is inserted. This structure allows us to set up global styles. In this case, we're taking advantage of Google's widely used [material design lite](https://github.com/google/material-design-lite) component library to style our minimal UI. All of those dependencies are taken care of in the `` section of our layout.
50 |
51 | ```html
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
73 |
74 |
75 | {{ '{' }}{{ '{' }}{{ '{' }} body {{ '}' }}{{ '}' }}{{ '}' }}
76 |
77 |
78 | ```
79 |
80 | ##### Home page
81 |
82 | Create `home.hbs` in the `views` folder, and add the following code. This is the page users land on after authenticating.
83 | ```html
84 |
85 |
99 | ```
100 | ##### Login page
101 |
102 | Create `login.hbs` in the `views` folder, and add the following code. This is the login form.
103 | ```html
104 |
105 |
106 |
107 |
108 |
109 |
Nest Cats
110 |
111 |
112 |
126 |
127 |
128 |
129 |
130 | ```
131 | ##### Profile page
132 |
133 | Create `profile.hbs` in the `views` folder and add the following code. This page displays details about the logged in user. It's rendered on our protected route.
134 | ```html
135 |
136 |
153 | ```
154 |
155 | ##### Set up view engine
156 |
157 | Now we instruct Nest to use express-handlebars as the view engine. Modify the `main.ts` file so that it looks like this:
158 | ```typescript
159 | // main.ts
160 | import { NestFactory } from '@nestjs/core';
161 | import { NestExpressApplication } from '@nestjs/platform-express';
162 | import { join } from 'path';
163 | import { AppModule } from './app.module';
164 | import * as exphbs from 'express-handlebars';
165 |
166 | async function bootstrap() {
167 | const app = await NestFactory.create(AppModule);
168 | const viewsPath = join(__dirname, '/public/views');
169 | app.engine('.hbs', exphbs({ extname: '.hbs', defaultLayout: 'main' }));
170 | app.set('views', viewsPath);
171 | app.set('view engine', '.hbs');
172 |
173 | await app.listen(3000);
174 | }
175 | bootstrap();
176 | ```
177 |
178 | ##### Authentication routes
179 | The final step in this section is setting up our routes. Modify `app.controller.ts` so that it looks like this:
180 | ```typescript
181 | // src/app.controller.ts
182 | import { Controller, Get, Post, Request, Res } from '@nestjs/common';
183 | import { Response } from 'express';
184 |
185 | @Controller()
186 | export class AppController {
187 | @Get('/')
188 | index(@Request() req, @Res() res: Response) {
189 | res.render('login');
190 | }
191 |
192 | @Post('/login')
193 | login(@Request() req, @Res() res: Response) {
194 | res.redirect('/home');
195 | }
196 |
197 | @Get('/home')
198 | getHome(@Request() req, @Res() res: Response) {
199 | res.render('home');
200 | }
201 |
202 | @Get('/profile')
203 | getProfile(@Request() req, @Res() res: Response) {
204 | res.render('profile');
205 | }
206 |
207 | @Get('/logout')
208 | logout(@Request() req, @Res() res: Response) {
209 | res.redirect('/');
210 | }
211 | }
212 | ```
213 |
214 | At this point, you should be able to run the app:
215 | ```bash
216 | $ npm run start
217 | ```
218 | Now, browse to http://localhost:3000 and click through the basic UI. At this point, of course, you can click through the pages without logging in.
219 |
220 | #### Implementing Passport strategies
221 |
222 | We're now ready to implement the authentication feature. Let's start with an overview of the process used for **any** Passport strategy. It's helpful to think of Passport as a mini framework in itself. The elegance of the framework is that it abstracts the authentication process into a few basic steps that you customize based on the strategy you're implementing. It's like a framework because you configure it by supplying custom code in the form of callback functions, which Passport calls at the appropriate time. The nest-passport module wraps this framework in a Nest style package. We'll use that below, but first let's consider vanilla Passport.
223 |
224 | In vanilla Passport, you configure a strategy by providing two things:
225 | 1. A set of options that are specific to that strategy. For example, in a JWT strategy, you might provide a secret to sign tokens.
226 | 2. A "verify callback", which is where you tell Passport how to interact with your user store (where you manage user accounts). Here, you verify whether a user exists (or possibly create a new user), and whether their credentials are valid.
227 |
228 | In Nest, you achieve these functions by extending the `PassportStrategy` class. You pass the strategy options (item 1 above) by calling the `super()` method in your subclass, optionally passing in an options object. You provide the verify callback (item 2 above) by implementing a `validate()` method in your subclass.
229 |
230 | As mentioned, we'll utilize the passport-local strategy for this use case. We'll get to that implementation in a moment. Start by generating an `AuthModule` and in it, an `AuthService`:
231 |
232 | ```bash
233 | $ nest g module auth
234 | $ nest g service auth
235 | ```
236 |
237 | As we implement the `AuthService`, we'll find it useful to have a `UsersService`, so let's generate that module and service now:
238 |
239 | ```bash
240 | $ nest g module users
241 | $ nest g service users
242 | ```
243 |
244 | Replace the default contents of these generated files as shown below. For our sample app, the `UsersService` simply maintains a hard-coded in-memory list of users, and a method to retrieve one by username. In a real app, this is where you'd build your user model and persistence layer, using your library of choice (e.g., TypeORM, Sequelize, Mongoose, etc.).
245 |
246 | ```typescript
247 | // src/users/users.service.ts
248 | import { Injectable } from '@nestjs/common';
249 |
250 | @Injectable()
251 | export class UsersService {
252 | private readonly users;
253 |
254 | constructor() {
255 | this.users = [
256 | {
257 | userId: 1,
258 | username: 'john',
259 | password: 'changeme',
260 | pet: { name: 'alfred', picId: 1 },
261 | },
262 | {
263 | userId: 2,
264 | username: 'chris',
265 | password: 'secret',
266 | pet: { name: 'gopher', picId: 2 },
267 | },
268 | {
269 | userId: 3,
270 | username: 'maria',
271 | password: 'guess',
272 | pet: { name: 'jenny', picId: 3 },
273 | },
274 | ];
275 | }
276 |
277 | async findOne(username): Promise {
278 | return this.users.filter(user => user.username === username)[0];
279 | }
280 | }
281 | ```
282 |
283 | In the `UsersModule`, the only change is to add the `UsersService` to the exports array of the `@Module` decorator so that it is visible outside this module (we'll soon use it in our `AuthService`).
284 | ```typescript
285 | // src/users/users.module.ts
286 | import { Module } from '@nestjs/common';
287 | import { UsersService } from './users.service';
288 |
289 | @Module({
290 | providers: [UsersService],
291 | exports: [UsersService],
292 | })
293 | export class UsersModule {}
294 | ```
295 |
296 | Our `AuthService` has the job of retrieving a user and verifying the password.
297 |
298 | ```typescript
299 | // src/auth/auth.service.ts
300 | import { Injectable } from '@nestjs/common';
301 | import { UsersService } from '../users/users.service';
302 |
303 | @Injectable()
304 | export class AuthService {
305 | constructor(private readonly usersService: UsersService) {}
306 |
307 | async validateUser(username, pass): Promise {
308 | const user = await this.usersService.findOne(username);
309 | if (user && user.password === pass) {
310 | const { password, ...result } = user;
311 | return result;
312 | }
313 | return null;
314 | }
315 | }
316 | ```
317 |
318 | > Warning **Warning** Of course in a real application, you wouldn't store a password in plain text. You'd instead use a library like [bcrypt](https://github.com/kelektiv/node.bcrypt.js#readme), with a salted one-way hash algorithm. With that approach, you'd only store hashed passwords, and then compare the stored password to a hashed version of the **incoming** password, thus never storing or exposing user passwords in plain text. To keep our sample app simple, we violate that absolute mandate and use plain text. **Don't do this in your real app!**
319 |
320 | We'll call into our `validateUser()` method from our Passport local strategy subclass in a moment. The Passport library expects us to return a full user if the validation succeeds, or a null if it fails (failure is defined as either the user is not found, or the password does not match). In our code, we use a convenient ES6 spread operator to strip the password property from the user object before returning it. Upon successful validation, Passport then takes care of a few details for us, which we'll explore later on in the Sessions section.
321 |
322 | And finally, we update our `AuthModule` to import the `UsersModule`.
323 |
324 | ```typescript
325 | // src/auth/auth.module.ts
326 | import { Module } from '@nestjs/common';
327 | import { AuthService } from './auth.service';
328 | import { UsersModule } from '../users/users.module';
329 |
330 | @Module({
331 | imports: [UsersModule],
332 | providers: [AuthService],
333 | })
334 | export class AuthModule {}
335 | ```
336 |
337 | Our app will function now, but remains incomplete until we finish a few more steps. You can navigate to http://localhost:3000 and still move around without logging in (after all, we haven't implemented our Passport local strategy yet. We'll get there momentarily). Notice that if you **do** login (refer to the `UsersService` for username/passwords you can test with), the profile page now provides some (but not all) information about a "logged in" user.
338 |
339 | #### Implementing Passport local
340 |
341 | Now we can implement our Passport local **authentication strategy**. Create a file called `local.strategy.ts` in the `auth` folder, and add the following code:
342 |
343 | ```typescript
344 | // src/auth/local.strategy.ts
345 | import { Strategy } from 'passport-local';
346 | import { PassportStrategy } from '@nestjs/passport';
347 | import { Injectable, UnauthorizedException } from '@nestjs/common';
348 | import { AuthService } from './auth.service';
349 |
350 | @Injectable()
351 | export class LocalStrategy extends PassportStrategy(Strategy) {
352 | constructor(private readonly authService: AuthService) {
353 | super();
354 | }
355 |
356 | async validate(username: string, password: string) {
357 | const user = await this.authService.validateUser(username, password);
358 | if (!user) {
359 | throw new UnauthorizedException();
360 | }
361 | return user;
362 | }
363 | }
364 | ```
365 |
366 | We've followed the recipe described earlier for all Passport strategies. In our use case with passport-local, there are no configuration options, so our constructor simply calls `super()`, without an options object.
367 |
368 | We've also implemented the `validate()` method. For the local-strategy, Passport expects a `validate()` method with a signature like
369 |
370 | ```validate(username: string, password:string): any```
371 |
372 | Most of the work is done in our `AuthService` (and in turn, in our `UserService`), so this method is quite straightforward. The `validate()` method for **any** Passport strategy will follow a similar pattern. If a user is found and valid, it's returned so request handling can continue, and Passport can do some further housekeeping. If it's not found, we throw an exception and let our exceptions layer handle it.
373 |
374 | It turns out that the only really significant difference for each strategy is **how** you determine if a user exists and is "valid". For example, in a JWT strategy, depending on requirements, we may evaluate whether the `userId` carried in the decoded token matches a record in our user database, or matches a list of revoked tokens. Hence, this pattern of sub-classing and implementing strategy-specific validation is consistent, elegant and extensible.
375 |
376 | With the strategy in place, we have a few more tasks to complete:
377 | 1. Create Guards we can use to decorate routes so that the configured Passport strategy is invoked
378 | 2. Add `@UseGuards()` decorators as needed
379 | 3. Implement sessions so that users can stay logged in across requests
380 | 4. Configure Nest to use Passport and session-related features
381 | 5. Add a little polish to the user experience
382 |
383 | Let's get started. For the following sections, we'll want to adhere to a best practice project structure, so start by creating a few more folders. Under `src`, create a `common` folder. Inside `common`, create `filters` and `guards` folders. Our structure now looks like this:
384 |
385 |
386 |
src
387 |
388 |
auth
389 |
common
390 |
391 |
filters
392 |
guards
393 |
394 |
public
395 |
users
396 |
397 |
398 |
399 | #### Implement guards
400 |
401 | The Guards chapter describes the primary function of Guards: to determine whether a request will be handled by the route handler or not. That remains true, and we'll use that feature soon. However, in the context of using the nest-passport module, we will also introduce a slight new wrinkle that may at first be confusing, so let's discuss that now. Consider that your app can exist in two states, from an authentication perspective:
402 | 1. the user is **not** logged in (is not authenticated)
403 | 2. the user **is** logged in (is authenticated)
404 |
405 | In the first case (user is not logged in), we need to perform two distinct functions. First, we want to restrict the routes the unauthenticated user can access (i.e., deny access to restricted routes). We'll use Guards in their familiar capacity to handle this function. We'll do this through a standard, user-defined `AuthenticatedGuard` which we'll build shortly.
406 |
407 | Next, we also need to handle the **authentication step** itself (i.e., when a previously unauthenticated user attempts to login) to kick things off: setting up our session, and transitioning from the unauthenticated state to the authenticated state. Looking at our UI, it's easy to see that we'll handle this step via a `POST` request on our `/login` route. This raises the question: how exactly do we invoke the "login phase" of the Passport local strategy in that route?
408 |
409 | The answer is: by using another Guard. Similar to the way we extended the `PassportStrategy` class in the last section, we'll start with a default `AuthGuard` provided in the `@nestjs/passport` package, and extend it as needed, naming our new Guard `LoginGuard`. We'll then decorate our `POST /login` route with this `LoginGuard` to invoke our Passport local strategy.
410 |
411 | The second case enumerated above (logged in user) simply relies on the same standard user-defined `AuthenticatedGuard` we already discussed to enable access to protected routes for logged in users.
412 |
413 | Let's cover the `LoginGuard` first. Create a file called `login.guard.ts` in the `guards` folder and replace its default contents as follows:
414 |
415 | ```typescript
416 | // src/common/guards/login.guard.ts
417 | import { ExecutionContext, Injectable } from '@nestjs/common';
418 | import { AuthGuard } from '@nestjs/passport';
419 |
420 | @Injectable()
421 | export class LoginGuard extends AuthGuard('local') {
422 | async canActivate(context: ExecutionContext) {
423 | const result = (await super.canActivate(context)) as boolean;
424 | const request = context.switchToHttp().getRequest();
425 | await super.logIn(request);
426 | return result;
427 | }
428 | }
429 | ```
430 |
431 | There's a lot going on in these few lines of code, so let's walk through it.
432 | * Our Passport local strategy has a default name of 'local'. We reference that name in the `extends` clause of the `LoginGuard` we are defining in order to tie our custom Guard to the code supplied by the `passport-local` package. This is needed to disambiguate which class we are extending in case we end up using multiple Passport strategies in our app (each of which may provide a strategy-specific `AuthGuard`).
433 | * As with all Guards, the primary method we define/override is `canActivate()`, which is what we do here.
434 | * The body of `canActivate()` is setting up an Express session. Here's what's happening:
435 | * we call `canActivate()` on the super class, as we normally would in extending a super class method. Our super class provides the framework for invoking our Passport local strategy. Recall from the [Guards](https://docs.nestjs.com/guards) chapter that `canActivate()` returns a boolean indicating whether or not the target route will be called. When we get here, Passport will have run the previously configured strategy (from the super class) and will return a boolean to indicate whether or not the user has successfully authenticated. Here, we stash the result so we can do a little more processing before finally returning.
436 | * the key step for starting a session is to now invoke the `logIn()` method on our super class, passing in the current request. This actually calls a special method that Passport automatically added to our Express `Request` object during the previous step. See [here](http://www.passportjs.org/docs/configure/) and [here](http://www.passportjs.org/docs/login/) for more on Passport sessions and these special methods.
437 | * the Express session has now been setup, and we can return our `canActivate()` result, allowing only authenticated users to continue.
438 |
439 | #### Sessions
440 |
441 | Now that we've introduced sessions, there's one additional detail we need to take care of. Sessions are a way of associating a unique user with some server-side state information about that user.
442 |
443 | > warning **Notice** TBD: Add details on Passport sessions (deferred for this draft). Seems important to provide context for how to implement the serialize/deserialize protocol. Sadly, Passport docs don't have much here.
444 |
445 | > warning **Notice** Also, I am not 100% comfortable explaining how **the following implementation** works. I'll post a separate question about this.
446 |
447 | Create the `session.serializer.ts` file in the `auth` folder, and add the following code:
448 | ```typescript
449 | // src/auth/session.serializer.ts
450 | import { PassportSerializer } from '@nestjs/passport';
451 | import { Injectable } from '@nestjs/common';
452 | @Injectable()
453 | export class SessionSerializer extends PassportSerializer {
454 | serializeUser(user: any, done: Function): any {
455 | done(null, user);
456 | }
457 | deserializeUser(payload: any, done: Function): any {
458 | done(null, payload);
459 | }
460 | }
461 | ```
462 |
463 | We need to configure our `AuthModule` to use the Passport features we just defined. Update `auth.module.ts` to look like this:
464 | ```typescript
465 | // src/auth/auth.module.ts
466 | import { Module } from '@nestjs/common';
467 | import { AuthService } from './auth.service';
468 | import { UsersModule } from '../users/users.module';
469 | import { PassportModule } from '@nestjs/passport';
470 | import { LocalStrategy } from './local.strategy';
471 | import { SessionSerializer } from './session.serializer';
472 |
473 | @Module({
474 | imports: [UsersModule, PassportModule],
475 | providers: [AuthService, LocalStrategy, SessionSerializer],
476 | })
477 | export class AuthModule {}
478 | ```
479 |
480 | Now let's create our `AuthenticatedGuard`. This is a traditional Guard, as covered in the Guards chapter. Its role is simply to protect certain routes. Create the file `authenticated.guard.ts` in the `guards` folder, and add the following code:
481 |
482 | ```typescript
483 | // src/common/guards/authenticated.guard.ts
484 | import { ExecutionContext, Injectable, CanActivate } from '@nestjs/common';
485 |
486 | @Injectable()
487 | export class AuthenticatedGuard implements CanActivate {
488 | async canActivate(context: ExecutionContext) {
489 | const request = context.switchToHttp().getRequest();
490 | return request.user;
491 | }
492 | }
493 | ```
494 |
495 | The only thing to point out here is that in order to determine whether a user is authenticated or not, we simply test for the presence of a `user` property on the `Request` object. The reason this works is that Passport, upon successful authentication, automatically attaches this property to the `Request` object for us.
496 |
497 | #### Configure Nest to bootstrap features
498 |
499 | We can now instruct Nest to use the Passport features we've configured. Update `main.ts` to look like this:
500 |
501 | ```typescript
502 | // main.ts
503 | import { NestFactory } from '@nestjs/core';
504 | import { NestExpressApplication } from '@nestjs/platform-express';
505 | import { join } from 'path';
506 | import { AppModule } from './app.module';
507 |
508 | import * as session from 'express-session';
509 | import * as flash from 'connect-flash';
510 | import * as exphbs from 'express-handlebars';
511 | import * as passport from 'passport';
512 |
513 | async function bootstrap() {
514 | const app = await NestFactory.create(AppModule);
515 |
516 | const viewsPath = join(__dirname, '/public/views');
517 | app.engine('.hbs', exphbs({ extname: '.hbs', defaultLayout: 'main' }));
518 | app.set('views', viewsPath);
519 | app.set('view engine', '.hbs');
520 |
521 | app.use(
522 | session({
523 | secret: 'nest cats',
524 | resave: false,
525 | saveUninitialized: false,
526 | }),
527 | );
528 |
529 | app.use(passport.initialize());
530 | app.use(passport.session());
531 | app.use(flash());
532 |
533 | await app.listen(3000);
534 | }
535 | bootstrap();
536 | ```
537 |
538 | Here, we've added the session and Passport support to our Nest app.
539 |
540 | > Warning **Warning** As always, be sure to keep secrets out of your source code (**don't put your session secret in the code, as we did here; use environment variables or a config module instead**).
541 |
542 | Note carefully that the order is important (register the session middleware first, then initialize Passport, then configure Passport to use sessions). We'll see the use of the `flash` feature in a few minutes.
543 |
544 | #### Add route guards
545 |
546 | Now we're ready to start applying these Guards to routes. Update `app.controller.ts` to look like this:
547 |
548 | ```typescript
549 | // src/app.controller.ts
550 | import { Controller, Get, Post, Request, Res, UseGuards } from '@nestjs/common';
551 | import { Response } from 'express';
552 |
553 | import { LoginGuard } from './common/guards/login.guard';
554 | import { AuthenticatedGuard } from './common/guards/authenticated.guard';
555 |
556 | @Controller()
557 | export class AppController {
558 | @Get('/')
559 | index(@Request() req, @Res() res: Response) {
560 | res.render('login');
561 | }
562 |
563 | @UseGuards(LoginGuard)
564 | @Post('/login')
565 | login(@Request() req, @Res() res: Response) {
566 | res.redirect('/home');
567 | }
568 |
569 | @UseGuards(AuthenticatedGuard)
570 | @Get('/home')
571 | getHome(@Request() req, @Res() res: Response) {
572 | res.render('home', { user: req.user });
573 | }
574 |
575 | @UseGuards(AuthenticatedGuard)
576 | @Get('/profile')
577 | getProfile(@Request() req, @Res() res: Response) {
578 | res.render('profile', { user: req.user });
579 | }
580 |
581 | @Get('/logout')
582 | logout(@Request() req, @Res() res: Response) {
583 | req.logout();
584 | res.redirect('/');
585 | }
586 | }
587 | ```
588 |
589 | Above, we've imported our two new Guards and applied them appropriately. We use the `LoginGuard` on our `POST /login` route to initiate the authentication sequence in the Passport local strategy. We use `AuthenticatedGuard` on our protected routes to ensure they aren't accessible to unauthenticated users.
590 |
591 | We're also taking advantage of the Passport feature that automatically stores our `User` object on the `Request` object as `req.user`. With this handy feature, we can pass a variable into our handlebars templates (e.g., `res.render('profile', {{ '{' }} user: req.user {{ '}' }})`) to customize their content.
592 |
593 | Finally, we have added the call to `req.logout()` in our `logout` route. This relies on the Passport logout function, which, similar to the `logIn()` method we discussed earlier in the Sessions section, has been added to the Express `Request` object by Passport automatically upon successful authentication. When we invoke `logout()`, Passport tears down our session for us.
594 |
595 | You should now be able to test the authentication logic by attempting to navigate to a protected route. Try pointing your browser at localhost:3000/profile. You should get a 403 Forbidden error. Return to the root page at localhost:3000, and log in. Refer to `src/users/users.service.ts` for the hard-coded usernames and passwords that are accepted.
596 |
597 | #### Adding polish
598 | Let's address that ugly 403 Forbidden error page. If you navigate around the app, trying things like submitting an empty login request, a bad password, and logging out, you'll see that it's not a very good UX. Let's take care of a couple of things:
599 | 1. Let's send the user back to the login page whenever they fail to authenticate, and when they log out of the app
600 | 2. Let's provide a little feedback when a user types in an incorrect password
601 |
602 | The best way to handle the first requirement is to implement a Filter. Create the file `auth-exceptions.filter.ts` in the `filters` folder, and add the following code:
603 |
604 | ```typescript
605 | // src/common/filters/auth-exceptions.filter.ts
606 | import {
607 | ExceptionFilter,
608 | Catch,
609 | ArgumentsHost,
610 | HttpException,
611 | UnauthorizedException,
612 | ForbiddenException,
613 | } from '@nestjs/common';
614 | import { Response } from 'express';
615 | import { Request } from 'connect-flash';
616 |
617 | @Catch(HttpException)
618 | export class AuthExceptionFilter implements ExceptionFilter {
619 | catch(exception: HttpException, host: ArgumentsHost) {
620 | const ctx = host.switchToHttp();
621 | const response = ctx.getResponse();
622 | const request = ctx.getRequest();
623 |
624 | if (
625 | exception instanceof UnauthorizedException ||
626 | exception instanceof ForbiddenException
627 | ) {
628 | request.flash('loginError', 'Please try again!');
629 | response.redirect('/');
630 | } else {
631 | response.redirect('/error');
632 | }
633 | }
634 | }
635 | ```
636 |
637 | The only new element here from what's covered in Filters is the use of connect-flash. If a route returns either an `UnauthorizedException` or a `ForbiddenException`, we redirect to the root route with `response.redirect('/')`. We also use connect-flash to store a message in Passport's session. This mechanism allows us to temporarily persist a message upon redirect. Passport and connect-flash automatically take care of the details of storing, retrieving, and cleaning up those messages.
638 |
639 | The final touch is to display the flash message in our handlebars template. Update `app.controller.ts` to look like this. In this update, we're adding the `AuthExceptionFilter` and adding the flash parameters to our index (`/`) route.
640 |
641 | ```typescript
642 | // src/app.controller.tas
643 | import { Controller, Get, Post, Request, Res, UseGuards, UseFilters } from '@nestjs/common';
644 | import { Response } from 'express';
645 | import { LoginGuard } from './common/guards/login.guard';
646 | import { AuthenticatedGuard } from './common/guards/authenticated.guard';
647 | import { AuthExceptionFilter } from './common/filters/auth-exceptions.filter';
648 |
649 | @Controller()
650 | @UseFilters(AuthExceptionFilter)
651 | export class AppController {
652 | @Get('/')
653 | index(@Request() req, @Res() res: Response) {
654 | res.render('login', { message: req.flash('loginError') });
655 | }
656 |
657 | @UseGuards(LoginGuard)
658 | @Post('/login')
659 | login(@Request() req, @Res() res: Response) {
660 | res.redirect('/home');
661 | }
662 |
663 | @UseGuards(AuthenticatedGuard)
664 | @Get('/home')
665 | getHome(@Request() req, @Res() res: Response) {
666 | res.render('home', { user: req.user });
667 | }
668 |
669 | @UseGuards(AuthenticatedGuard)
670 | @Get('/profile')
671 | getProfile(@Request() req, @Res() res: Response) {
672 | res.render('profile', { user: req.user });
673 | }
674 |
675 | @Get('/logout')
676 | logout(@Request() req, @Res() res: Response) {
677 | req.logout();
678 | res.redirect('/');
679 | }
680 | }
681 | ```
682 |
683 | We now have a fully functional authentication system for our server side Web application.
684 |
685 | #### API Server use case
686 |
687 | Let's start by defining requirements. We're going to piggyback on the code written in the previous section. This means our sample application will include both server-side rendered HTML pages and REST-based routes returning JSON responses. While this may not represent your particular requirements, you can customize the code as needed. For example, in an API Server-only application, you can remove the session handling code, handlebars templates, and view engine components. We take this approach for several reasons:
688 | * Some applications do in fact serve both types of interaction
689 | * Much of the code is common to both scenarios. This demonstrates the abstraction power of both Nest and Passport, and shows how you can keep critical authentication code DRY
690 |
691 | Specific to our API Server, we need to meet the following requirements:
692 | * Allow users to authenticate, returning a [JSON Web Token(JWT)](https://jwt.io/) for use in subsequent calls to protected API endpoints
693 | * Create API routes which are protected based on the presence of a valid JWT as a bearer token in an [Authorization header](https://tools.ietf.org/html/rfc6750)
694 |
695 | We start by installing the packages required. The only additions are the packages required to support a JWT authentication method:
696 |
697 | ```bash
698 | $ npm install @nestjs/jwt passport-jwt
699 | ```
700 |
701 | The `@nest/jwt` package (see more [here](https://github.com/nestjs/jwt)) is a utility package that helps with JWT manipulation. The `passport-jwt` package is the Passport package that implements JWT authentication.
702 |
703 | #### Define API module and routes
704 |
705 | Now let's define a Module with a Controller to handle our API routes. We'll call this our `ApiModule`, and we'll prefix all of its route paths with `/api`.
706 |
707 | ```bash
708 | $ nest g module api
709 | $ nest g controller api
710 | ```
711 |
712 | Replace the default contents of the `api.controller.ts` file with the following:
713 |
714 | ```typescript
715 | // src/api/api.controller.ts
716 | import { Controller, Get, Request, Res, Post } from '@nestjs/common';
717 | import { Response } from 'express';
718 |
719 | @Controller('api')
720 | export class ApiController {
721 | @Post('/login')
722 | async login(@Res() res: Response) {
723 | res.json({ access_token: 'sampletoken' });
724 | }
725 |
726 | @Get('/me')
727 | getProfile(@Res() res: Response) {
728 | res.json({
729 | userId: 1,
730 | username: 'john',
731 | pet: { name: 'alfred', picId: 1 },
732 | });
733 | }
734 | }
735 | ```
736 |
737 | We're going to stub both routes for now while we get our JWT infrastructure in place.
738 |
739 | We stub the profile route (`GET /me`) because we don't know the identity of the user we should be returning. Why not just pass in the `userId` to the route? We have a better solution. Once we get our JWT handling in place, we'll be pulling `userId` from the JWT itself. This provides a more secure solution, as we can trust the JWT has not been tampered with.
740 |
741 | Update the `ApiModule` to import the `AuthModule`:
742 | ```typescript
743 | import { Module } from '@nestjs/common';
744 | import { ApiController } from './api.controller';
745 | import { AuthModule } from '../auth/auth.module';
746 |
747 | @Module({
748 | controllers: [ApiController],
749 | imports: [AuthModule],
750 | })
751 | export class ApiModule {}
752 | ```
753 |
754 | Let's test these shell routes. Ensure the app is running:
755 | ```bash
756 | $ npm run start:dev
757 | ```
758 |
759 | Since these routes are called programmatically, we'll use the commonly available [cURL](https://curl.haxx.se/) library to test them:
760 |
761 | ```bash
762 | $ # POST to /api/login
763 | $ curl -X POST http://localhost:3000/api/login
764 | $ # result -> {"access_token":"sampletoken"}
765 | $ # GET /api/me
766 | $ curl http://localhost:3000/api/me
767 | $ # result -> {"userId":1,"username":"john","pet":{"name":"alfred","picId":1}}
768 | ```
769 |
770 | #### Implementing Passport JWT
771 |
772 | Passport provides the [passport-jwt](https://github.com/mikenicholson/passport-jwt) strategy for securing RESTful endpoints with JSON Web Tokens. Start by creating a file called `jwt.strategy.ts` in the `auth` folder, and add the following code:
773 |
774 | ```typescript
775 | // src/auth/jwt.stratgy.ts
776 | import { ExtractJwt, Strategy } from 'passport-jwt';
777 | import { AuthService } from './auth.service';
778 | import { PassportStrategy } from '@nestjs/passport';
779 | import { Injectable, UnauthorizedException } from '@nestjs/common';
780 | import { jwtConstants } from './constants';
781 |
782 | @Injectable()
783 | export class JwtStrategy extends PassportStrategy(Strategy) {
784 | constructor(private readonly authService: AuthService) {
785 | super({
786 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
787 | ignoreExpiration: false,
788 | secretOrKey: jwtConstants.secret,
789 | });
790 | }
791 |
792 | async validate(payload: any) {
793 | return { userId: payload.sub }
794 | }
795 | }
796 | ```
797 |
798 | Create the file `constants.ts` in the `auth` folder, and add the following code. We use this to share the secret between the **Sign** and **Validate** phases of JWT authentication. Note the security cautions below.
799 |
800 | ```typescript
801 | export const jwtConstants = {
802 | secret: 'secretKey',
803 | };
804 | ```
805 |
806 | We've followed the same recipe described earlier for all Passport strategies. In this use case with passport-jwt, the strategy requires some initialization, so we do that by passing in an options object in the `super()` call. You can read more about the available options [here](https://github.com/mikenicholson/passport-jwt#configure-strategy). In our case, these options are:
807 | * jwtFromRequest: supplies the method by which the JWT will be extracted from the `Request`. We will use the standard approach of supplying a bearer token in the Authorization header of our API requests. Other options are described [here](https://github.com/mikenicholson/passport-jwt#extracting-the-jwt-from-the-request).
808 | * ignoreExpiration: just to be explicit, we choose the default `false` setting, which delegates the responsibility of ensuring that a JWT has not expired to the Passport module. This means that if our route is supplied with an expired JWT, the request will be denied and a `401 Unauthorized` response sent. Passport conveniently handles this automatically for us.
809 | * secretOrKey: we are using the expedient option of supplying a symmetric secret for signing the token. Other options, such as a PEM-encoded public key, may be more appropriate for production apps (see [here](https://github.com/mikenicholson/passport-jwt#extracting-the-jwt-from-the-request) for more information). In any case, as cautioned earlier, **do not expose this secret publicly**. Instead, use an appropriate secrets vault, environment variable, or configuration service in your production app.
810 |
811 | The `validate()` method deserves some discussion. Recall that this method is called by Passport during request authorization. At that point in the process, Passport has verified the JWT's signature and decoded the token. Passport then invokes our `validate()` method passing that decoded JWT as its single parameter. Based on the way JWT signing works, we can assume that we're receiving a token that we have previously signed and issued to a valid user. While we haven't written that code just yet, we can safely make the assumption that this token represents a user we have already validated against our user database.
812 |
813 | As a result of all this, our response to the `validate()` callback is trivial: we simply return an object containing the `userId` (note we choose a property name of `sub` to hold our `userId` value to be consistent with JWT standards). Recall again that the purpose of this returned object, within the Passport framework, is so that Passport can build a `user` object and attach it as a property on the `Request` object.
814 |
815 | It's also worth pointing out that this framework leaves us room ('hooks' as it were) to inject other business logic into the process. For example, we **could** do a database lookup in our `validate()` method to extract more information about the user, resulting in a more enriched `user` object being available in our `Request`. This is also the place we may decide to do further token validation, such as looking up the `userId` in a list of revoked tokens, enabling us to perform token revocation. The model we've implemented here in our sample code is a fast, "stateless JWT" model, where each API call is immediately authorized based on the presence of a valid JWT, and a small bit of information about the requester (its `userId`) is available in our Request pipeline.
816 |
817 | #### Implement JWT strategy guards
818 |
819 | We're now at the stage where we can implement Guards to:
820 | 1. Trigger authentication upon user login (this time, via a RESTful API request)
821 | 2. Enforce security on protected REST API endpoints
822 |
823 | Let's deal with the login Guard first. We're going to piggyback on the local-storage strategy to allow clients to login with a username and password. In our first use case (server-side web app), we took steps to ensure that a session was established. To accomplish that, we extended the built-in `AuthGuard` class and overrode the `canActivate()` method (see [Implement guards](#implement-guards)). In this use case, we don't need sessions, so we can use the out-of-the-box `AuthGuard` provided by the passport-local strategy.
824 |
825 | Let's update the `POST /api/login` route handler to do so. Open the `api.controller.ts` file in the `api` folder, and update the `@Post('/login')` route as shown below, as well as the several new imports. While we're here, we're also adding a Guard to the `@Get('/me')` route, and making a change to the `getProfile()` method body. We'll discuss that next.
826 |
827 | ```typescript
828 | // src/api/api.controller.ts
829 | import { Controller, Get, Request, Res, Post, UseGuards } from '@nestjs/common';
830 | import { Response } from 'express';
831 | import { AuthGuard } from '@nestjs/passport';
832 | import { AuthService } from '../auth/auth.service';
833 |
834 | @Controller('api')
835 | export class ApiController {
836 | constructor(private readonly authService: AuthService) {}
837 |
838 | @UseGuards(AuthGuard('local'))
839 | @Post('/login')
840 | async login(@Request() req, @Res() res: Response) {
841 | const token = await this.authService.login(req.user);
842 | res.json(token);
843 | }
844 |
845 | @UseGuards(AuthGuard('jwt'))
846 | @Get('/me')
847 | getProfile(@Request() req, @Res() res: Response) {
848 | res.json(req.user);
849 | }
850 | }
851 | ```
852 |
853 | Let's take a closer look at how a `GET /api/login` request is handled. We're using the built-in `AuthGuard` provided by the passport-local strategy. This means that:
854 | 1. The route handler will only be invoked if the user has successfully authenticated
855 | 2. The `req` parameter will contain a `user` property (populated by Passport during authentication)
856 |
857 | With this in mind, we can now finally generate a real JWT, and return it in this route, and we're done! We'll generate the JWT in our `authService`. Open the `auth.service.ts` file in the `auth` folder, and add the `login()` method, and import the `JwtService` as shown:
858 |
859 | ```typescript
860 | // sr/auth/auth.service.ts
861 | import { Injectable } from '@nestjs/common';
862 | import { UsersService } from '../users/users.service';
863 | import { JwtService } from '@nestjs/jwt';
864 |
865 | @Injectable()
866 | export class AuthService {
867 | constructor(
868 | private readonly usersService: UsersService,
869 | private readonly jwtService: JwtService,
870 | ) {}
871 |
872 | async validateUser(username, pass): Promise {
873 | const user = await this.usersService.findOne(username);
874 | if (user && user.password === pass) {
875 | const { password, ...result } = user;
876 | return result;
877 | }
878 | return null;
879 | }
880 |
881 | async login(user: any) {
882 | const payload = { username: user.username, sub: user.userId };
883 | return {
884 | access_token: this.jwtService.sign(payload),
885 | };
886 | }
887 | }
888 | ```
889 |
890 | We're using the `@nestjs/jwt` library, which supplies a `sign()` function to generate our JWT, which we'll return as a simple object with a single `access_token` property. Don't forget to inject the JwtService provider into the `AuthService`.
891 |
892 | We now need to update the `AuthModule` to import the new dependencies and configure the `JwtModule`. Open up `auth.module.ts` in the `auth` folder and update it to look like this:
893 |
894 | ```typescript
895 | // src/auth/auth.module.ts
896 | import { Module } from '@nestjs/common';
897 | import { AuthService } from './auth.service';
898 | import { LocalStrategy } from './local.strategy';
899 | import { UsersModule } from '../users/users.module';
900 | import { PassportModule } from '@nestjs/passport';
901 | import { SessionSerializer } from './session.serializer';
902 | import { JwtModule } from '@nestjs/jwt';
903 | import { jwtConstants } from './constants';
904 | import { JwtStrategy } from './jwt.strategy';
905 |
906 | @Module({
907 | imports: [
908 | UsersModule,
909 | PassportModule,
910 | JwtModule.register({
911 | secretOrPrivateKey: jwtConstants.secret,
912 | signOptions: { expiresIn: '60s' },
913 | }),
914 | ],
915 | providers: [AuthService, LocalStrategy, SessionSerializer, JwtStrategy],
916 | exports: [AuthService, PassportModule],
917 | })
918 | export class AuthModule {}
919 | ```
920 |
921 | We configure the `JwtModule` using `register()`, passing in a configuration object. By passing in the same `secret` used when we signed the JWT, we ensure that the **Verify** phase performed by Passport, and the **Sign** phase performed in our `AuthService`, use the same value. See [here](https://github.com/nestjs/jwt/blob/master/README.md) for more on the Nest JwtModule and [here](https://github.com/auth0/node-jsonwebtoken#usage) for more details on the available configuration options.
922 |
923 | Ensure the app is running, and test the routes using `cURL`.
924 |
925 | ```bash
926 | $ # GET /api/me
927 | $ curl http://localhost:3000/api/me
928 | $ # result -> {"statusCode":401,"error":"Unauthorized"}
929 | $ # POST /api/login
930 | $ curl -X POST http://localhost:3000/api/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
931 | $ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm... }
932 | $ # GET /api/me using access_token returned from previous step as bearer code
933 | $ curl http://localhost:3000/api/me -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."
934 | $ # result -> {"userId":1}
935 | ```
936 |
937 | Note that in the `AuthModule`, we configured the JWT to have an expiration of `60 seconds`. This is probably too short an expiration, and dealing with the details of token expiration and refresh is beyond the scope of this article. However, we chose that to demonstrate an important quality of JWTs and the Passport JWT strategy. If you wait 60 seconds after authenticating before attempting a `GET /api/me` request, you'll receive a `401 Unauthorized` response. This is because Passport automatically checks the JWT for its expiration time, saving you the trouble of doing so in your application.
938 |
939 | Our `GET /api/me` method is simply returning `req.user`, which contains just our `userId` property, as expected. We should really be returning a more complete `user` object. Let's make that change. We'll need a new method on our `UsersService`. Update it to look like this:
940 |
941 | ```typescript
942 | // src/users/users.service.ts
943 | import { Injectable } from '@nestjs/common';
944 |
945 | @Injectable()
946 | export class UsersService {
947 | private readonly users;
948 |
949 | constructor() {
950 | this.users = [
951 | {
952 | userId: 1,
953 | username: 'john',
954 | password: 'changeme',
955 | pet: { name: 'alfred', picId: 1 },
956 | },
957 | {
958 | userId: 2,
959 | username: 'chris',
960 | password: 'secret',
961 | pet: { name: 'gopher', picId: 2 },
962 | },
963 | {
964 | userId: 3,
965 | username: 'maria',
966 | password: 'guess',
967 | pet: { name: 'jenny', picId: 3 },
968 | },
969 | ];
970 | }
971 |
972 | async findOne(username): Promise {
973 | return this.users.filter(user => user.username === username)[0];
974 | }
975 |
976 | async findOneById(id): Promise {
977 | const found = this.users.filter(user => user.userId === id)[0];
978 | if (found) {
979 | const { password, ...result } = found;
980 | return result;
981 | }
982 | }
983 | }
984 | ```
985 | In `findOneById()`, we make use of an ES6 spread operator to remove the password before returning the `user` object.
986 |
987 | Finally, we update our `ApiController` to use this new find method:
988 |
989 | ```typescript
990 | // src/api/api.controller.ts
991 | import { Controller, Get, Request, Post, UseGuards } from '@nestjs/common';
992 | import { AuthGuard } from '@nestjs/passport';
993 | import { AuthService } from '../auth/auth.service';
994 | import { UsersService } from '../users/users.service';
995 |
996 | @Controller('api')
997 | export class ApiController {
998 | constructor(
999 | private readonly authService: AuthService,
1000 | private readonly usersService: UsersService,
1001 | ) {}
1002 |
1003 | @UseGuards(AuthGuard('local'))
1004 | @Post('/login')
1005 | async login(@Request() req) {
1006 | return this.authService.login(req.user);
1007 | }
1008 |
1009 | @UseGuards(AuthGuard('jwt'))
1010 | @Get('/me')
1011 | getProfile(@Request() req) {
1012 | return this.usersService.findOneById(req.user.userId);
1013 | }
1014 | }
1015 | ```
1016 |
1017 | Now, when we run our `GET /api/me` request (don't forget to authenticate again, and pass in the newly minted access_token), we get these results:
1018 |
1019 | ```bash
1020 | $ # GET /api/me using access_token returned from previous step as bearer code
1021 | $ curl http://localhost:3000/api/me -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."
1022 | $ # result -> {"userId":1,"username":"john","pet":{"name":"alfred","picId":1}}
1023 | ```
1024 |
1025 | We've now completed our JWT authentication implementation. JavaScript clients (such as Angular/React/Vue), and other JavaScript apps, can now authenticate and communicate securely with our API Server.
1026 |
1027 | #### Additional considerations
1028 |
1029 | ##### Default strategy
1030 |
1031 | In our `ApiController`, we pass the name of the strategy in the `@AuthGuard()` decorator. We need to do this because we've introduced **two** Passport strategies (passport-local and passport-jwt), both of which supply implementations of various Passport components. Passing the name disambiguates which implementation we're linking to. When multiple strategies are included in an application, we can declare a default strategy so that we no longer have to pass the name in the `@AuthGuard` decorator if using that default strategy. Here's how to register a default strategy when importing the `PassportModule`. This code would go in the `AuthModule`:
1032 |
1033 | ```typescript
1034 | import { Module } from '@nestjs/common';
1035 | import { AuthService } from './auth.service';
1036 | import { HttpStrategy } from './http.strategy';
1037 | import { UsersModule } from '../users/users.module';
1038 | import { PassportModule } from '@nestjs/passport';
1039 |
1040 | @Module({
1041 | imports: [
1042 | PassportModule.register({ defaultStrategy: 'jwt' }),
1043 | UsersModule,
1044 | ],
1045 | providers: [AuthService, HttpStrategy],
1046 | exports: [PassportModule, AuthService]
1047 | })
1048 | export class AuthModule {}
1049 | ```
1050 |
1051 | ##### Customize Passport
1052 | Any standard Passport customization options can be passed the same way, using the `register()` method. The available options depend on the strategy being implemented. For example:
1053 |
1054 | ```typescript
1055 | PassportModule.register({ session: true });
1056 | ```
1057 |
1058 | ##### Named strategies
1059 | When implementing a strategy, you can provide a name for it by passing a second argument to the `PassportStrategy` function. If you don't do this, each strategy will have a default name (e.g., 'jwt' for jwt-strategy):
1060 |
1061 | ```typescript
1062 | export class JwtStrategy extends PassportStrategy(Strategy, 'myjwt')
1063 | ```
1064 |
1065 | Then, you refer to this via a decorator like `@AuthGuard('myjwt')`.
1066 |
1067 | #### GraphQL
1068 |
1069 | In order to use an AuthGuard with [GraphQL](https://docs.nestjs.com/graphql/quick-start), extend the built-in AuthGuard class and override the getRequest() method.
1070 |
1071 | ```typescript
1072 | @Injectable()
1073 | export class GqlAuthGuard extends AuthGuard('jwt') {
1074 | getRequest(context: ExecutionContext) {
1075 | const ctx = GqlExecutionContext.create(context);
1076 | return ctx.getContext().req;
1077 | }
1078 | }
1079 | ```
1080 |
1081 | To use the above construct, be sure to pass the request (`req`) object as part of the context value in the GraphQL Module settings:
1082 |
1083 | ```typescript
1084 | GraphQLModule.forRoot({
1085 | context: ({ req }) => ({ req }),
1086 | });
1087 | ```
--------------------------------------------------------------------------------
/src/app.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 |
5 | describe('AppController', () => {
6 | let appController: AppController;
7 |
8 | beforeEach(async () => {
9 | const app: TestingModule = await Test.createTestingModule({
10 | controllers: [AppController],
11 | providers: [AppService],
12 | }).compile();
13 |
14 | appController = app.get(AppController);
15 | });
16 |
17 | describe('root', () => {
18 | it('should return "Hello World!"', () => {
19 | expect(appController.getHello()).toBe('Hello World!');
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | // src/app.controller.ts
2 | import {
3 | Controller,
4 | Get,
5 | Post,
6 | Request,
7 | Res,
8 | Render,
9 | UseGuards,
10 | UseFilters,
11 | } from '@nestjs/common';
12 | import { Response } from 'express';
13 | import { LoginGuard } from './common/guards/login.guard';
14 | import { AuthenticatedGuard } from './common/guards/authenticated.guard';
15 | import { AuthExceptionFilter } from './common/filters/auth-exceptions.filter';
16 |
17 | @Controller()
18 | @UseFilters(AuthExceptionFilter)
19 | export class AppController {
20 |
21 | @Get('/')
22 | @Render('login')
23 | index(@Request() req, @Res() res: Response): {message: string} {
24 | return { message: req.flash('loginError') };
25 | }
26 |
27 | @UseGuards(LoginGuard)
28 | @Post('/login')
29 | login(@Request() req, @Res() res: Response): void {
30 | res.redirect('/home');
31 | }
32 |
33 | @UseGuards(AuthenticatedGuard)
34 | @Get('/home')
35 | @Render('home')
36 | getHome(@Request() req, @Res() res: Response): {user: any } {
37 | return { user: req.user };
38 | }
39 |
40 | @UseGuards(AuthenticatedGuard)
41 | @Get('/profile')
42 | @Render('profile')
43 | getProfile(@Request() req: any, @Res() res: Response): {user: any} {
44 | return { user: req.user };
45 | }
46 |
47 | @Get('/logout')
48 | logout(@Request() req, @Res() res: Response): void {
49 | req.logout();
50 | res.redirect('/');
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 | import { AuthModule } from './auth/auth.module';
5 | import { UsersModule } from './users/users.module';
6 |
7 | @Module({
8 | imports: [AuthModule, UsersModule],
9 | controllers: [AppController],
10 | providers: [AppService],
11 | })
12 | export class AppModule {}
13 |
--------------------------------------------------------------------------------
/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(): string {
6 | return 'Hello World!';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/auth/auth.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AuthService } from './auth.service';
3 | import { UsersModule } from '../users/users.module';
4 | import { PassportModule } from '@nestjs/passport';
5 | import { LocalStrategy } from './local.strategy';
6 | import { SessionSerializer } from './session.serializer';
7 |
8 | @Module({
9 | imports: [UsersModule, PassportModule],
10 | providers: [AuthService, LocalStrategy, SessionSerializer],
11 | })
12 | export class AuthModule {}
13 |
--------------------------------------------------------------------------------
/src/auth/auth.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AuthService } from './auth.service';
3 |
4 | describe('AuthService', () => {
5 | let service: AuthService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [AuthService],
10 | }).compile();
11 |
12 | service = module.get(AuthService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | // auth.service.ts
2 | import { Injectable } from '@nestjs/common';
3 | import { UsersService } from '../users/users.service';
4 |
5 | @Injectable()
6 | export class AuthService {
7 | constructor(private readonly usersService: UsersService) {}
8 |
9 | async validateUser(username, pass): Promise {
10 | const user = await this.usersService.findOne(username);
11 | if (user && user.password === pass) {
12 | const { password, ...result } = user;
13 | return result;
14 | }
15 | return null;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/auth/local.strategy.ts:
--------------------------------------------------------------------------------
1 | // src/auth/local.strategy.ts
2 | import { Strategy } from 'passport-local';
3 | import { PassportStrategy } from '@nestjs/passport';
4 | import { Injectable, UnauthorizedException } from '@nestjs/common';
5 | import { AuthService } from './auth.service';
6 |
7 | @Injectable()
8 | export class LocalStrategy extends PassportStrategy(Strategy) {
9 | constructor(private readonly authService: AuthService) {
10 | super();
11 | }
12 |
13 | async validate(username: string, password: string) {
14 | const user = await this.authService.validateUser(username, password);
15 | if (!user) {
16 | throw new UnauthorizedException();
17 | }
18 | return user;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/auth/session.serializer.ts:
--------------------------------------------------------------------------------
1 | import { PassportSerializer } from '@nestjs/passport';
2 | import { Injectable } from '@nestjs/common';
3 | @Injectable()
4 | export class SessionSerializer extends PassportSerializer {
5 | serializeUser(user: any, done: (err: Error, user: any) => void): any {
6 | done(null, user);
7 | }
8 | deserializeUser(payload: any, done: (err: Error, payload: string) => void): any {
9 | done(null, payload);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/common/filters/auth-exceptions.filter.ts:
--------------------------------------------------------------------------------
1 | // src/common/filters/auth-exceptions.filter.ts
2 | import {
3 | ExceptionFilter,
4 | Catch,
5 | ArgumentsHost,
6 | HttpException,
7 | UnauthorizedException,
8 | ForbiddenException,
9 | } from '@nestjs/common';
10 | import { Request, Response } from 'express';
11 |
12 | @Catch(HttpException)
13 | export class AuthExceptionFilter implements ExceptionFilter {
14 | catch(exception: HttpException, host: ArgumentsHost) {
15 | const ctx = host.switchToHttp();
16 | const response = ctx.getResponse();
17 | const request = ctx.getRequest();
18 |
19 | if (
20 | exception instanceof UnauthorizedException ||
21 | exception instanceof ForbiddenException
22 | ) {
23 | request.flash('loginError', 'Please try again!');
24 | response.redirect('/');
25 | } else {
26 | response.redirect('/error');
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/common/guards/authenticated.guard.ts:
--------------------------------------------------------------------------------
1 | import { ExecutionContext, Injectable, CanActivate } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AuthenticatedGuard implements CanActivate {
5 | async canActivate(context: ExecutionContext) {
6 | const request = context.switchToHttp().getRequest();
7 | return request.user;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/common/guards/login.guard.ts:
--------------------------------------------------------------------------------
1 | // src/common/guards/login.guard.ts
2 | import { ExecutionContext, Injectable } from '@nestjs/common';
3 | import { AuthGuard } from '@nestjs/passport';
4 |
5 | @Injectable()
6 | export class LoginGuard extends AuthGuard('local') {
7 | async canActivate(context: ExecutionContext) {
8 | const result = (await super.canActivate(context)) as boolean;
9 | const request = context.switchToHttp().getRequest();
10 | await super.logIn(request);
11 | return result;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { NestExpressApplication } from '@nestjs/platform-express';
3 | import { join } from 'path';
4 | import { AppModule } from './app.module';
5 |
6 | import * as session from 'express-session';
7 | import flash = require('connect-flash');
8 | import * as exphbs from 'express-handlebars';
9 | import * as passport from 'passport';
10 |
11 | async function bootstrap() {
12 | const app = await NestFactory.create(AppModule);
13 |
14 | const viewsPath = join(__dirname, '../public/views');
15 | app.engine('.hbs', exphbs({ extname: '.hbs', defaultLayout: 'main' }));
16 | app.set('views', viewsPath);
17 | app.set('view engine', '.hbs');
18 |
19 | app.use(
20 | session({
21 | secret: 'nest cats',
22 | resave: false,
23 | saveUninitialized: false,
24 | }),
25 | );
26 |
27 | app.use(passport.initialize());
28 | app.use(passport.session());
29 | app.use(flash());
30 |
31 | await app.listen(3000);
32 | }
33 | bootstrap();
34 |
--------------------------------------------------------------------------------
/src/users/users.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { UsersService } from './users.service';
3 |
4 | @Module({
5 | providers: [UsersService],
6 | exports: [UsersService],
7 | })
8 | export class UsersModule {}
9 |
--------------------------------------------------------------------------------
/src/users/users.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { UsersService } from './users.service';
3 |
4 | describe('UsersService', () => {
5 | let service: UsersService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [UsersService],
10 | }).compile();
11 |
12 | service = module.get(UsersService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/users/users.service.ts:
--------------------------------------------------------------------------------
1 | // src/users/users.service.ts
2 | import { Injectable } from '@nestjs/common';
3 |
4 | @Injectable()
5 | export class UsersService {
6 | private readonly users: any[];
7 |
8 | constructor() {
9 | this.users = [
10 | {
11 | username: 'john',
12 | password: 'changeme',
13 | pet: { name: 'alfred', picId: 1 },
14 | },
15 | {
16 | username: 'chris',
17 | password: 'secret',
18 | pet: { name: 'gopher', picId: 2 },
19 | },
20 | {
21 | username: 'maria',
22 | password: 'guess',
23 | pet: { name: 'jenny', picId: 3 },
24 | },
25 | ];
26 | }
27 |
28 | async findOne(username: string): Promise {
29 | return this.users.filter((user: any) => user.username === username)[0];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import * as request from 'supertest';
3 | import { AppModule } from './../src/app.module';
4 |
5 | describe('AppController (e2e)', () => {
6 | let app;
7 |
8 | beforeEach(async () => {
9 | const moduleFixture: TestingModule = await Test.createTestingModule({
10 | imports: [AppModule],
11 | }).compile();
12 |
13 | app = moduleFixture.createNestApplication();
14 | await app.init();
15 | });
16 |
17 | it('/ (GET)', () => {
18 | return request(app.getHttpServer())
19 | .get('/')
20 | .expect(200)
21 | .expect('Hello World!');
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "target": "es6",
9 | "sourceMap": true,
10 | "outDir": "./dist",
11 | "baseUrl": "./"
12 | },
13 | "exclude": ["node_modules"]
14 | }
15 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": ["tslint:recommended"],
4 | "jsRules": {
5 | "no-unused-expression": true
6 | },
7 | "rules": {
8 | "quotemark": [true, "single"],
9 | "member-access": [false],
10 | "ordered-imports": [false],
11 | "max-line-length": [true, 150],
12 | "member-ordering": [false],
13 | "interface-name": [false],
14 | "arrow-parens": false,
15 | "object-literal-sort-keys": false
16 | },
17 | "rulesDirectory": []
18 | }
19 |
--------------------------------------------------------------------------------