1. 程式人生 > >Auth in Nest.js and Angular

Auth in Nest.js and Angular

This series will we split up into two parts:

  • Part 1: back-end with Nest.js, Google OAuth and JWTs (this post)
  • Part 2: front-end with Angular, Material and NgRx

Back-end: Nest, Google OAuth and JWTs

We will start by building our back-end. Lets install the Nest.js CLI and start a new project:

$ npm i -g @nestjs/cli$ mkdir tutorial && cd tutorial$ nest new back-end && cd back-end

Press enter a couple of times and wait for the CLI to scaffold your app. You should now have a directory called back-end in which you can find your generated Nest.js app. The directory back-end/src contains the current code of our application. The file main.ts bootstraps the application and should look something like this:

app.module.ts

We can run our application by running the following command, which will automatically re-build our application when it detects changes:

$ npm run start:dev

Check whether your application works by executing the following command in another terminal:

Okay, we are up and running! If you are not yet familiar with the basic concepts of Nest.js (Modules, Services, Controllers, DI, etc.) you can checkout the docs here:

https://docs.nestjs.com. The "overview" section provides a good introduction to the concepts used.

We will start by creating an auth module which will contain our services, controllers and interfaces for handling auth in our application. Let's use the Nest.js CLI to generate an auth module, controller and service:

$ nest generate module auth$ nest generate controller auth$ nest generate service auth/auth

We should now have an auth directory with a module, controller and service. Our app will handle authorization by using Google as a single sign on provider. Our authorization and authentication flow will be as follows:

Auth flow
  • Front-end contacts back-end, which redirects theuser to Google login page
  • Users logs in and Google calls our back-end with user information
  • Back-end processes user information (registration / login + JWT generation)
  • Back-end redirects user to front-end with JWT as a parameter
  • Front-end will register JWT and attach it to each request made to our back-end

Google OAuth2 Strategy using @nestjs/passport

Let's get started on our Google login flow. We will need to install some dependencies for Nest.js and Passport.js:

$ npm i --save @nestjs/passport passport passport-google-oauth20

Create a google.strategy.ts file in the auth folder with the following content:

auth/google.strategy.ts

We created a class GoogleStrategy which extends the PassportStrategy from @nestjs/passport using the passport-google-oauth20 Strategy. The second argument to the PassportStrategyis the name of the strategy, in this case google. The validate function will handle a successful login on the Google login page. We simply log the profile of the user that Google sends us. Later on we will come back and add some logic for registering users and generating a real JWT, instead of our placeholder.

Import the GoogleStrategy as a provider in the AuthModule :

auth/auth.module.ts

We need to configure our OAuth2 strategy with credentials provided by Google. These credentials include the clientID and clientSecret . Let's go to https://console.developers.google.com and register our application. Create a new project, select it and enable the Google+ API by clicking on 'enable APIs and services' and searching for the Google+ API. Then go to 'credentials' and select the 'OAuth client ID' option when trying to create credentials.

Choose 'Web application' as application type and continue. Add http://localhost:3000 under the Authorized Javascript origins and add under Authorized redirect URIs. Google will redirect the user information to our callback URI when a user successfully logs in. Save and replace the clientID and clientSecretin your google.strategy.ts file by the tokens generated by Google. Your config should look like the one below.

OAuth client ID configuration

Important: do not leave your plain text google credentials in your application. Rather, use environment variables. A good way to do this in Nest.js is by creating a configService as described in the docs.

We now have a valid Passport strategy in Nest.js. However, we cannot use it yet, because we do not have any endpoints handling the initiation of the Google login flow and the callback containing the user information. Let's go to our auth.controller.ts and make the appropriate changes:

auth/auth.controller.ts

Pfew, lots of decorators. So what did we do here? We created two REST endpoints:

  • @Get('google') : when we go http://localhost:3000/auth/google in our browser the login flow will start. We protected this route using the @UseGuards decorator in combination with the AuthGuard from @nestjs/passport . The argument to AuthGuard is the same name as we used in our GoogleStrategy class. The AuthGuard will take care of the request and make sure our Passport strategy, which we created in theGoogleStrategy class, gets activated.
  • @Get('google/callback') : after the user has logged in, google will send the user information to this endpoint (we provided this URI to google when we registered our application!). We also protect this route using the same decorators as with the @Get('google') route, so our Passport strategy gets activated. When this endpoint gets called the validate function in our GoogleStrategy class gets activated and the flow is completed.

The body of our googleLoginCallback function might look a bit weird at first: why does the req object all of a sudden have a user property with a JWT? Well, in our GoogleStrategy class we have a function validate which gets called when Google hits our http://localhost:3000/auth/google/callback endpoint. In this function we have a callback function done which we call as done(null, user) . The first argument is an error (in this case null) and the second argument is the user object we created, containing our placeholder JWT (we will take care of this later). Passport attaches object given to the callback function as a user property to the req and that is why we can access it in our googleLoginCallback function!

Last but not least, we redirect the request to a front-end route http://localhost:4200/login/succes/<JWT> when there is JWT. We will handle this on the front-end later on. We there is no JWT we redirect to a failure route. This way, our front-end can complete the user login and register the JWT token, whilst our back-end safely handles all the sensitive user information it received from Google.

Pfew! That was a lot of work and reading. Let's see if we can test our Google login flow! Open your browser and navigate to: http://localhost:3000/auth/google . You should now be able to login with your Google account. When you login, you will see your profile printed in your terminal and you will get redirected to localhost:4200/login/success/placeholderJWT if the login is successful, awesome!

How about that placeholderJWT?

Right… let's do something about that! Instead of returning a placeholder JWT, we want to create a real JWT. Let's install some dependecies for dealing with JWTs:

$ npm i --save jsonwebtoken

We will implement the logic for this in our generated auth.service.ts file containing our AuthService. Open the file and make the necessary changes:

auth/auth.service.ts

We created a function called validateOAuthLogin , which will return a JWT token.

Sidenote: you should put some registration logic here, so you can store your users in a database. This is not in the scope of this post, so we will not handle it here. However, the comments might give you an idea on how to implement such functionality.

Our function accepts a thirdPartyId (i.e. Google userId) and a provider enum. By implementing the function this way, it stays reusable and can thus be used for other social login providers like Github (simply repeat the same process as for the GoogleStrategy but now use the Github specific passport strategy, Github credentials and Github endpoints)

We use the sign function from jsonwebtoken to sign our object containing the thirdPartyId and the provider using our JWT_SECRET_KEY and an options object. The options object describes how long the JWT is valid. In our case it is valid for 3600 seconds or one hour. More configuration options can be found in the jsonwebtoken docs (https://github.com/auth0/node-jsonwebtoken).

Important: you should replace the value of JWT_SECRET_KEY with your own generated secret key. You can generate a JWT by using the following command:

$ node -e "console.log(require('crypto').randomBytes(256).toString('base64'));"$ vwts1k+tsUuMYh1ZIGQ5eeWu/DjlHy2xlNXsBC6dzyFXRhVrC/d2R4SbhLhsbiWlJHqTEHPUA9N7l+UfGziEYixc0xqif5PHY+d7DojbebbFws/mik07eJf6MkE+SAC1jbQm2EY6C6vhIdcXbIDLwnjL3ePzyW4Itu68N4nbugPRkQO/5T0N27TDCBNQG8vDkh0609iZFU3bw5609Egu7H2XwiR6sqPv7xj1j1Qw8TipLoQ/XSuzmArgsWABQu6u6X/KKh6dTSTDWroCQNwx1Y1870uwNBZKWiBYAFhCWFdqEX6uWZyp4XZZ0lgYWXK67/4qkfgSDal7wbHihJ4lNw==

Important: do not leave your plain text JWT_SECRET_KEY in your application. Rather, use environment variables. A good way to do this in Nest.js is by creating a configService as described in the docs.

Great! Let's now change our validate function in the google.strategy.ts file to make use of our validateOAuthLogin function by injecting our AuthService into the constructor of theGoogleStrategy class and calling it in the validate function.

auth/google.strategy.ts

Great, we now return a real JWT if a user signs in with their Google account. Try logging in again. When you are now redirected to http://localhost:4200/login/succes/<JWT> you should see an actual JWT instead of placeholderJWT . A JWT is simply a Base64 encoded string. You can copy the JWT and decode it on the following website: https://www.base64decode.org/. Paste your JWT and click on decode. You should see a JSON object with the following properties:

  • thirdPartyId : the id of your Google account
  • provider : the provider which you used to log in, in our case 'google'.
  • iat : the epoch time of when our JWT was issued by our back-end
  • exp : the epoch time of when our JWT expires

Important: JWTs are just simple Base64 encoded strings. Do not put any sensitive information about the user in the JWT. This information would be easily accessible to anyone managing to get hold of the JWT.

Another part done! On to the next.

Protecting our API using JWTs

Great, we can now login using Google and generate JWTs. We can use the JWT to authenticate ourselves with our back-end, when we want access to protected resources. We are now going to implement the Passport.js JWT strategy, so we can protect our API. Let's install the Passport strategy dependency for JWTs:

$ npm i --save passport-jwt

Now, create the JWT strategy file jwt.strategy.ts in theauth folder with the following content:

auth/jwt.strategy.ts

The JwtStrategy class is very similar to our GoogleStrategy class. However, we now extend the PassportStrategy using the Strategy imported from the passport-jwt package and we provide 'jwt' as a name for our strategy. In the constructor's super call we instruct Passport to extract the JWT as a Bearer token from the request made to our API and use the secret provided. The secret we provide to secretOrKey should be the same as the one we used in our AuthService . We call the done callback with the decoded JWT payload. We do not need to decode this ourselves, Passport handles this for us. This payload object is, just as it was the case in the GoogleStrategy , attached to the request under the user property (req.user ).

Wait, but what if the JWT provided to our back-end would be invalid (i.e. not signed by our secret key) or expired? Well, Passport will also handle this for us and will send a response with an HTTP status code of 401 , indicating the user is unauthorized and thus the JWT being invalid.

Sidenote: we could also implement some logic to validate the claims of our JWT token here. For example, the token could contain a property describing the roles of a user. It would be necessary to check whether the user still has the roles the JWT claims to have. However, this is outside the scope of this post. The comments provide an idea on how this could be implemented.

Let's now register our JwtStrategy in the AuthModule :

auth/auth.module.ts

Great, let's move on to creating an API endpoint and protecting it with our JWT strategy! Let's go back to our auth.controller.ts file and add an extra endpoint:

auth/auth.controller.ts

We added an endpoint /auth/protected and used the @UseGuards decorator with the AuthGuard using the 'jwt' strategy. Our route should now be protected. Let's test this with the following command:

We get a HTTP status code 401 , indicating we unauthorized. Now let's get a JWT from our back-end by logging in by going to http://localhost:3000/auth/google. Copy the JWT from the redirect URL in your browser. Now execute the following command, but replace <YOUR_JWT_HERE> with the JWT you copied:

$ curl -i http://localhost:3000/auth/protected -H "Authorization: Bearer <YOUR_JWT_HERE>"$ HTTP/1.1 200 OKX-Powered-By: ExpressContent-Type: text/html; charset=utf-8Content-Length: 18ETag: W/"12-PGbHJZOgiw3wT+Qbl2IshJem8RE"Date: Fri, 05 Oct 2018 21:53:38 GMTConnection: keep-alive
JWT is working!

Yes, we can now access our protected endpoint and we get the expected response: JWT is working! .

Summary

Wow, that was a lot of work. This is it for the back-end part though! We are now able to login using Google. Our back-end then receives the user information from Google and will redirect us to our front-end (which we are gonna setup next) with our own JWT. We protected our API using the JWT strategy and access it only if we attach a valid JWT. The authorization and authentication back-end flow is done. Good job!

Next part

In the next part we will take a look at building an Angular front-end with Angular Material for the UI and NgRx for state management. The front-end will make use of the authorization and authentication flow we just built. Also, instead of navigating to our back-end manually to start the login flow, we will provide a nice google button. I will be working on part two in the coming week.

Front-end teaser