In this post, we’ll explore how to create a NestJS back-end that handles OpenID Connect authentication for a React app that it serves up with an express-session. The session store will share the MongoDB instance that is also used for storing cats.
Disclaimer: I’m not a cat guy. I’m using cats for this tutorial because the NestJS documentation did. However, I did take the picture for this post when we discovered 5 wild baby cats in our backyard! They’ve all since found loving homes :)
Create: http -f POST localhost:3000/cats name=Gladiator age=4 breed=General
Read: http localhost:3000/cats
Update: http -f PUT localhost:3000/cats/5e3859ab9ffdd4d02913e0fc name=Maximus
Note: You’ll need to temporarily comment out @UseGuards(AuthenticatedGuard) on CatsController if you want to run the commands above since you’re not authenticated.
Create the back-end
If you run npm run start:dev you’ll see “Hello, World!” on http://localhost:3000! Cool, but we’ll want to go further than that ;) Let’s start working on Authentication
We won’t be using AuthService in this tutorial but we’re creating an empty one as this is where you’d probably check the Identity Provider’s user against what you care about in your own database.
Here’s what those dependencies are about:
@nestjs/passport - NestJS modules for working with passport
passport - Passport itself–authentication middleware for Node.js
openid-client - OIDC certified client library with a passport strategy
@nestjs/config - NestJS configuration support
express-session - For a session-based application
@nestjs/mongoose - NestJS modules for working with mongoose
mongoose - Mongoose itself–Node.js ODM for MongoDB
connect-mongo - Express session store that uses MongoDB
Let’s create an OpenID Connect passport strategy, based on the node-openid-client project.
Create LoginGuard to handle the OIDC dance
Replace AuthController with this
The two endpoints annotated with @UseGuards(LoginGuard) are those involved in the two kinds of authentication OAuth2 authorization code flow requires (i.e. user and client)
/login: redirects to authorization endpoint of Identity Provider for front-channel user authentication
/callback: receives the code grant from the Identity Provider and exchanges it, back-channel, for an id_token by means of client credential authentication
The /logout endpoint provides a way of using the end_session_endpoint if such an endpoint is discovered while the req.user object returned at the /user endpoint will be populated automatically by passport once the user is authenticated.
The way passport populates the req.user is by using a PassportSerializer. Passport serializes and deserializes user instances to and from the session using a PassportSerializer. Since we’re only using the user from the Identity Provider, we have a vanilla serializer that uses the user “as is”.
Let’s let our AuthModule know about our strategy, guard, and serializer.
And let’s be sure to let our AppModule know about our ConfigModule so that we can use our .env config file!
If you remember when we started our app originally, we just saw “Hello World!”. Let’s go to our AppController to add a link to login/logout and, if we have a user, display the user’s name instead!
Finally, we need to replace main.ts with the following to configure our session and to use passport with it.
If we drop the .env file in and start the app with npm run start:dev, we’ll see an error
node_modules/connect-mongo/src/types.d.ts:8:23 - error TS2688: Cannot find type definition file for 'express-session'.
We can fix that by adding type definitions for the express-session library
Now, when we go to http://localhost:3000/login, we’ll be directed to Google for user authentication!
Add some Cat CRUD
In order to prevent unauthorized calls from disturbing our cats, let’s create a generic AuthenticatedGuard that only allows calls by authenticated users on the annotated controller class or method.
Create the CatsModule and related controller and service
Let’s annotate the CatsController with @UseGuards(AuthenticatedGuard) to protect its endpoints and add an update method.
and let’s add an update method to CatsService
Finally, we’ll add the following to our imports array in our AppModule
Create the front-end
For the front-end we’ll be using create-react-app to build a react application with TypeScript.
Add import 'bootstrap/dist/css/bootstrap.min.css'; to index.tsx
Add Cat interface
Add CatsTable component
Replace App.tsx with…
Proxy requests to back-end in development
Finally, to get our client to proxy certain calls to our NestJS back-end on port 3000, we add http-proxy-middleware as per the CRA docs
And add the following setupProxy.js file to configure it
Start everything up
Note: Before we fire up the front-end on 3001, we need to change environment variables in the back-end’s .env file to point to 3001 instead of 3000. That’s because we want all redirects to come to the front-end in development. You’ll want to change the two environment variables back to 3000 when running this in production.
Start the back-end
In another terminal window, start the front-end
Watch the magic happen!
Serve static resources in production
To serve up static resources (like a React application) in production, add the dependency @nestjs/serve-static
and add the following to our imports array in AppModule to point to our front-end build.
Note: You’ll also want to remove AppController and AppService from AppModule so that the “Hello, World!” endpoint won’t conflict with the static resources we’re serving up.