Hi everyone, my name is Niels Meima. I am a freelance full-stack developer and a MSc. Computing Science student from the University of Groningen. I develop web applications using Angular and Node.js as a freelancer. I really like that Angular is such a structured framework and when I found Nest.js, which shares a lot of concepts and the language used by Angular, it immediately peeked my interest.
Almost every application has some kind of auth going on. In this small series we will be building a Nest.js back-end implementing a REST API which will be communicating with an Angular front-end. The back-end should be able to authorize users using social providers (i.e. Google) and authenticate them using JSON Web Tokens (JWTs). Our Angular front-end will offer a simple login page and a dashboard only accessible by authorized and authenticated users. We will be building everything from scratch and I will try to be as elaborate as possible. Let's get going!
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:
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:
$ curl http://localhost:3000
$ Hello World!
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:
- 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
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
google.strategy.ts file in the
auth folder with the following content:
We created a class
GoogleStrategy which extends the
@nestjs/passport using the passport-google-oauth20
Strategy. The second argument to the
PassportStrategyis the name of the strategy, in this case
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.
GoogleStrategy as a provider in the
We need to configure our OAuth2 strategy with credentials provided by Google. These credentials include the
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/auth/google/callback under Authorized redirect URIs. Google will redirect the user information to our callback URI when a user successfully logs in. Save and replace the
google.strategy.ts file by the tokens generated by Google. Your config should look like the one below.
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:
Pfew, lots of decorators. So what did we do here? We created two REST endpoints:
@Get('google'): when we go
http://localhost:3000/auth/googlein our browser the login flow will start. We protected this route using the
@UseGuardsdecorator in combination with the
@nestjs/passport. The argument to
AuthGuardis the same name as we used in our
AuthGuardwill take care of the request and make sure our Passport strategy, which we created in the
GoogleStrategyclass, 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
validatefunction in our
GoogleStrategyclass 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
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
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:
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'));"
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 the
GoogleStrategy class and calling it in the
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 the
auth folder with the following content:
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 (
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
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:
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:
$ curl http://localhost:3000/auth/protected
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 OK
Content-Type: text/html; charset=utf-8
Date: Fri, 05 Oct 2018 21:53:38 GMT
Connection: keep-aliveJWT is working!
Yes, we can now access our protected endpoint and we get the expected response:
JWT is working! .
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!
You can also find the complete and working back-end on Github: https://github.com/nielsmeima/nestjs-angular-auth/tree/master/back-end
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.