September 10, 2020
Backend with TypeScript, PostgreSQL & Prisma: Authentication & Authz
In this third part of the series, we'll look at how to secure the REST API with passwordless authentication using Prisma for token storage and implement authorization.
Introduction
The goal of the series is to explore and demonstrate different patterns, problems, and architectures for a modern backend by solving a concrete problem: a grading system for online courses. This is a good example because it features diverse relations types and is complex enough to represent a real-world use case.
The recording of the live stream is available above and covers the same ground as this article.
What the series will cover
The series will focus on the role of the database in every aspect of backend development covering:
Topic | Part |
---|---|
Data Modeling | Part 1 |
CRUD | Part 1 |
Aggregations | Part 1 |
REST API layer | Part 2 |
Validation | Part 2 |
Testing | Part 2 |
Passwordless Authentication | Part 3 (current) |
Authorization | Part 3 (current) |
Integration with external APIs | Part 3 (current) |
Deployment | Coming up |
What you will learn today
In the first article, you designed a data model for the problem domain and wrote a seed script that uses Prisma Client to save data to the database.
In the second article of the series, you built a REST API on top of the data model and Prisma schema from the first article. You used Hapi to build the REST API, which allowed performing CRUD operations on resources via HTTP requests.
In this third article of the series, you will learn about the concepts behind authentication and authorization, how the two differ, and how to implement email-based passwordless authentication and authorization using JSON Web Tokens (JWT) with Hapi to secure the REST API.
Concretely, you will develop the following aspects:
- Passwordless Authentication: Add the ability to log in and sign up by sending an email with a unique token. Users complete the authentication process by sending the token received by email to the API and getting back a long-lived JWT token, giving access to API endpoints that require authentication.
- Authorization: Add authorization logic to restrict which resources users can access and manipulate.
By the end of this article, the REST API will be secured with authentication to access the REST endpoints. Additionally, you will add authorization rules to a subset of the endpoints using Hapi's pre
route option, thereby granting access depending on the specific user's permissions. The API with the authorization rules for all the endpoints will be available in this GitHub repository.
Note: Throughout the guide, you'll find various checkpoints that enable you to validate whether you performed the steps correctly.
Prerequisites
Assumed knowledge
This series assumes basic knowledge of TypeScript, Node.js, and relational databases. If you're experienced with JavaScript but haven't had the chance to try TypeScript, you can still follow along. The series will use PostgreSQL. However, most of the concepts apply to other relational databases such as MySQL. Familiarity with REST concepts is useful. Beyond that, no prior knowledge of Prisma is required, as the series covers that.
Development environment
You should have the following installed:
If you're using Visual Studio Code, the Prisma extension is recommended for syntax highlighting, formatting, and other helpers.
Note: If you don't want to use Docker, you can set up a local PostgreSQL database or a hosted PostgreSQL database on Heroku.
External services
A SendGrid account is required so that you can send the passwordless authentication emails from the backend. SendGrid offers a free tier that allows sending up to 100 emails a day.
Once you sign up, go to API Keys in the SendGrid console, generate an API key, and keep it somewhere safe.
Clone the repository
The source code for the series can be found on GitHub.
To get started, clone the repository and install the dependencies:
Note: By checking out the
part-3
branch you'll be able to follow the article from the same starting point.
Start PostgreSQL
To start PostgreSQL, run the following command from the real-world-grading-app
folder:
Note: Docker will use the
docker-compose.yml
file to start the PostgreSQL container.
Authentication and authorization concepts
Before diving into the implementation, we'll go through some concepts relating to authentication and authorization.
While the two terms are often used interchangeably, authentication and authorization serve different purposes. Generally speaking, they are both used to secure applications in complementary ways.
Put simply, authentication is the process of verifying who a user is, while authorization is the process of verifying what they have access to.
One example of authentication in the real world is a valid passport. The fact that you look like the person in an official document (that is hard to forge) authenticates that you are who you claim to be. For example, when you go to the airport, you present your passport, and you're allowed to go through security.
In the same example, authorization is the process by which you are allowed to board the flight: you present your boarding pass (that is typically scanned and verified against the database of flight passengers), and the ground attendant authorizes you to board the flight.
Authentication in web applications
Web applications typically use a username and password to authenticate users. If a valid username and password are passed, the application can verify that you're the user you claim to be because the password is supposed to be only known to you and the application.
Note: Web applications that use username/password authentication rarely store the password in clear text in the database. Instead, they use a technique called hashing to store a hash of the password. This allows the backend to verify the password without knowing it.
A hash function is a mathematical function that takes arbitrary input and always generates the same fixed-length string/number given the same input. The power of hash function lies in that you can go from a password to a hash but not from a hash to a password.
This allows verifying the password submitted by the user without storing the actual password. Storing password hashes protects users in the case of breached access to the database because it's not possible to log in with the hashed password.
In recent years, web security has become a growing concern, given the number of significant websites that have been breached. This trend has influenced how security is approached by introducing more secure authentication approaches such as multi-factor authentication.
Multi-factor authentication is an authentication method where the user is authenticated after successfully presenting two or more pieces of evidence (also known as factors). For example, when withdrawing money from an ATM, two authentication factors are required: possession of a bank card and a PIN code.
Since possession of a card is hard to verify for a web application, multi-factor authentication is often implemented by supplementing username/password with a one-time token generated by an authenticator app (an app installed on a smartphone or a special device which generates these passwords).
In this article, you will implement email-based passwordless authentication – a two-step approach that improves user experience and security. It works by sending a secret token to the user's email account when attempting to log in. Once the user opens the email and passes the token to the application, the application can authenticate the user and be certain that the user is the email account owner.
This approach relies on the user's email service, which can be assumed to have already authenticated the user. User experience is improved as the user doesn't need to set a password and remember it. Security is enhanced as the application is relieved from password management responsibilities, which can be an attack surface.
Outsourcing authentication to a user's email account means that the application will inherit the benefits and weaknesses of the user's email account security. But these days, most email services provide the option of second-factor authentication and other security measures.
Still, this approach avoids users choosing weak passwords, and likely reusing them on multiple websites. Removing passwords altogether means these users are more secure. There is no longer a password that can be guessed or brute-forced or cracked at all.
Authentication and signup/login flow
Email-based passwordless authentication is a two-step process that involves two token types.
The authentication flow will look as follows:
- The user calls the
/login
endpoint in the API with the email in the payload to begin the authentication process. - If the email is new, the user is created in the User table.
- An email token is generated by the backend and saved in the Token table
- The email token is sent to the user's email
- The user sends the email token (received via email), and the email address to the
/authenticate
endpoint - The backend validates the email token sent by the user. If valid and the token hasn't expired, a JWT token is generated and saved in the Token table.
- The JWT token is sent back to the user via the
Authorization
header.
There are two token types:
- Email token: A numerical eight-digit token that is valid for a short period, e.g. 10 minutes, and is sent to the user's email. The token's only purpose is to validate the user is associated with the email, which means it doesn't grant access to any of the grading-app related endpoints.
- Authentication token: A JWT token with
tokenId
in its payload. This token can be used to access protected endpoints by passing it in theAuthorization
header when making a request to the API. The token is long-lived in the sense that it's valid for 12 hours.
With this authentication strategy, a single endpoint handles both logging in and registration. It's is possible because the only difference between logging in and signing up is whether you're creating a row in the "User" table or not (if the user already exists).
JSON Web Tokens
JSON Web Tokens (JWT) are an open and standard method for representing claims securely between two parties. The standard defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.
A JWT token contains three parts which are encoded with Base64: header, payload, and signature and looks as follows (the parts are separated with a .
):
Note: Base64 is another way to represent data. It doesn't involve any encryption
If you use Base64 to decode the header and payload from above you will get the following:
- Header:
{"alg":"HS256","typ":"JWT"}
- Payload:
{"tokenId":9}
The token's signature part is created by passing the header, payload, and secret through a signing algorithm (in this case, HS256). The secret is only known to the backend and is used to verify the authenticity of the token.
In this article, JWT will be used for the long-lived authentication token. The token's payload will contain the tokenId
, which will be stored in the database and reference to the user for which the token was created. This allows the backend to find the associated user.
Note: This approach is known as stateful JWT, where the token references a session that is stored in the database. While that means that authenticating a request requires a round-trip to the database, which increases the time needed to serve a request, this approach is more secure because tokens can be revoked from by the backend.
Adding a token model to the Prisma schema
You need to store the tokens in the database so they can be verified when requests are made. In this step, you will add a new Token
model to the Prisma schema and update the User
model to make some fields optional.
Open the Prisma schema located in prisma/schema.prisma
and update as follows:
Let's go over the changes introduced:
- Enable the
connectOrCreate
andtransactionApi
preview features. These will be used in the next steps. - Remove the
aggregateApi
preview feature, which is stable as of Prisma 2.5.0. - In the
User
model thefirstName
andlastName
are now optional. This allows users to log in/register with just an email. - A new
Token
model was added. Each user can have many tokens making the relation a 1-n. TheToken
model contains the relevant fields accomodating for expiration, the two token types (with theTokenType
enum), and storage of the email token.
To migrate the database schema, create and run the migration as follows:
Checkpoint: You should see something like the following in the output:
Note: Running the
prisma migrate dev
command will also generate Prisma Client by default.
Add email sending functionality
Since the backend will send emails upon user login, you will create a plugin that will expose email sending functionality to the rest of the application. The Hapi plugin will follow a similar convention to the Prisma plugin.
The article will use SendGrid and the @sendgrid/mail
npm package for easy integration with the SendGrid API.
Adding the dependency
Creating the email plugin
Create a new file named email.ts
in the src/plugins/
folder:
And add the following to the file:
The plugin will expose the sendEmailToken
function on the server.app
object, which is accessible throughout your route handlers. It will use the SENDGRID_API_KEY
environment variable, which you will set in production using the key from the SendGrid console. During development, you can leave it unset, and the token will be logged instead of being sent via email.
Lastly, register the plugin in server.ts
:
Adding authentication with Hapi
To implement authentication you will begin by defining the /login
and /register
routes, which will handle the creation of a user and token in the database, sending the email token, verifying the email, and generating a JWT authentication token. It's worth noting that the two endpoints will handle the authentication process, but they will not secure the API.
To secure the API, once the two routes are defined, you will define an authentication strategy that uses the jwt
scheme provided by the hapi-auth-jwt2
library.
Note: Authentication in Hapi is based on the concept of schemes and strategies. Schemes are a way of handling authentication, whereas a strategy is a pre-configured instance of a schema. In this article, you will only need to define the strategy based on the
jwt
authentication scheme.
You will encapsulate all of this logic in an auth
plugin.
Adding the dependencies
Begin by adding the following dependencies to your project:
Creating the auth plugin
Next, you will create an auth plugin to encapsulate the authentication logic.
Create a new file named auth.ts
in the src/plugins/
folder:
And add the following to the file:
Note: The auth plugin defines dependencies on the
prisma
,hapi-auth-jwt2
, andapp/email
plugins. The prisma plugin was defined in part 2 of the series and will be used to access Prisma Client. Thehapi-auth-jwt2
plugin defines thejwt
authentication scheme, which you will use to define the authentication the strategy. Lastly, theapp/email
will ensure you can access thesendEmailToken
function.
Defining the login endpoint
In the register
function of authPlugin
, define a new login route as follows:
Note:
options.auth
is set to false so that the endpoint will remain open once you set the default authentication strategy which will by default require authentication for all routes that don't disable it explicitly.
Outside the register function of the plugin, add the following:
loginHandler
does the following:
- The email is taken from the request payload
- A token is generated and then saved to the database
- With
connectOrCreate
, if a user with the email address in the payload doesn't exist, it's created. Otherwise, a relation is created to the existing user. - The token is sent to the email address in the payload (or logged to the console if
SENDGRID_API_KEY
isn't set)
Finally, register the plugin in server.ts
:
Checkpoint:
- Start the server with
npm run dev
- Make a POST call to the
/login
endpoint with curl:curl --header "Content-Type: application/json" --request POST --data '{"email":"test@test.io"}' localhost:3000/login
. You should see a token logged from the backend:email token for test@test.io: 27948216
Defining the authentication endpoint
At this point, the backend can create users, generate email tokens, and send them via email. However, the tokens generated are still not functional. You will now implement the second step of authentication by creating the /authenticate
endpoint, verifying the email token against the database, and returning the user a long-lived JWT authentication token in the authorization
header.
Begin by adding the following route declaration to the authPlugin
:
The route requires both the email
and the emailToken
. Since only the legitimate user attempting to login will know both, guessing both the email
and emailToken
becomes more difficult, thereby reducing the risk of brute force attacks which guess the eight-digit number.
Next, add the following to auth.ts
:
Note: The environment variable
JWT_SECRET
can be generated by running the following command:node -e "console.log(require('crypto').randomBytes(256).toString('base64'));"
. This should always be set in production environments.
The handler fetches the email token from the database, ensures it's valid, creates a new API token in the database, generates a JWT token (with a reference to the token in the database), invalidates the email token, and returns the token in the Authorization
header.
Checkpoint:
- Start the server with
npm run dev
- Make a POST call to the
/login
endpoint with curl:curl --header "Content-Type: application/json" --request POST --data '{"email":"test@test.io"}' localhost:3000/login
you should see a token logged from the backend:email token for test@test.io: 13080740
. - Take that token and call the
/authenticate
endpoint with curl:curl -v --header "Content-Type: application/json" --request POST --data '{"email":"hello@prisma.io", "emailToken": "13080740"}' localhost:3000/authenticate
. - The response should have the
200
status and include anAuthorization
header which looks similar to this:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg
Defining the authentication strategy
The authentication strategy will define how the Hapi will verify requests to endpoints that require authentication. In this step, you will define the logic for verifying requests with a JWT token, by fetching the information about the user from the database using the tokenId
in the JWT token.
To define the authentication strategy, add the following to auth.ts
:
Inside the authPlugin.register
function add the following:
Lastly, add the validateAPIToken
function:
The validateAPIToken
function will get called before every route that uses the API_AUTH_STATEGY
(which you've set as the default in the previous step).
The purpose of the validateAPIToken
function is to determine whether to allow the request to proceed. This is done with the return object, which contains isValid
and credentials
:
isValid
: determines whether the token was successfully verified.credentials
can be used to pass information about the user to the request object. The object passed tocredentials
is accessible within the route handler viarequest.auth.credentials
.
In this case, we determine that if the token exists in the database, it is valid and hasn't expired.
If so, we fetch the courses the user is the teacher of (which will be used to implement authorization) and pass that along with the tokenId
, userId
, and isAdmin
to the credentials object.
Most endpoints require authentication (because of the default auth strategy), but there are still no authorization rules. That means that to access the GET /courses
endpoint, you now need to have a valid JWT token in the Authorization
header.
Checkpoint:
- Start the server with
npm run dev
- Make a GET call to the
/courses
endpoint with curl:curl -v localhost:3000/courses
. You should get a 401 status code with the following response:{"statusCode":401,"error":"Unauthorized","message":"Missing authentication"}
. - Make another call with the
Authorization
header with the token from the last checkpoint as follows:curl -H "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg" localhost:3000/courses
and the request should succeed
Congratulations, you have successfully implemented email-based passwordless authentication and secured the endpoints. Next, you will define authorization rules.
Adding authorization
The authorization model of the backend will define what a user is allowed to do. In other words, which entities are they allowed to perform operations on.
The main properties that will grant users permissions are:
- Is the user an admin (as denoted by the
isAdmin
fields in the user model)? If so, they will be allowed to perform every operation. - Is the user a teacher of a course? If so, the user will be allowed to perform CRUD operations on all course-specific resources such as tests, test results, and enrollment.
If a user is not an admin or a teacher of a course, they should still be able to create new courses, enroll as a student in existing courses, get their test results, and fetch and update their user profile.
Note: This approach mixes two authorization approaches, namely role-based and resource-based authorization. Deriving permissions from the course enrollment is a form of resource-based authorization. That means that actions are authorized based on a specific resource, i.e. enrollment in a course as a teacher allows the user to create related tests and submit test results. On the other hand, authorizing actions to admin users (with
isAdmin
set to true) is a form of role-based authorization where the user has the "admin" role.
Authorization rules for the endpoints
To implement the proposed authorization rules, we will first revisit the list of endpoints with the proposed authorization rules:
HTTP Method | Route | Description | Authorization rule |
---|---|---|---|
POST | /login | Start login/signup and send email token | Open |
POST | /authenticate | Authenticate user and create JWT token | Open (requires email token) |
GET | /profile | Get the authenticated user profile | Any authenticated user |
POST | /users | Create a user | Only Admin |
GET | /users/{userId} | Get a user | Only Admin or authenticated user |
PUT | /users/{userId} | Update a user | Only Admin or authenticated user |
DELETE | /users/{userId} | Delete a user | Only Admin or authenticated user |
GET | /users | Get users | Only Admin |
GET | /users/{userId}/courses | Get a user's enrollement incourses | Only Admin or authenticated user |
POST | /users/{userId}/courses | Enroll a user to a course (as student or teacher) | Only Admin or authenticated user |
DELETE | /users/{userId}/courses/{courseId} | Delete a user's enrollment to a course | Only Admin or authenticated user |
POST | /courses | Create a course | Any authenticated user |
GET | /courses | Get courses | Any authenticated user |
GET | /courses/{courseId} | Get a course | Any authenticated user |
PUT | /courses/{courseId} | Update a course | Only admin or teacher of course |
DELETE | /courses/{courseId} | Delete a course | Only admin or teacher of course |
POST | /courses/{courseId}/tests | Create a test for a course | Only admin or teacher of course |
GET | /courses/tests/{testId} | Get a test | Any authenticated user |
PUT | /courses/tests/{testId} | Update a test | Only admin or teacher of course |
DELETE | /courses/tests/{testId} | Delete a test | Only admin or teacher of course |
GET | /users/{userId}/test-results | Get a user's test results | Only Admin or authenticated user |
POST | /courses/tests/{testId}/test-results | Create test result for a test associated with a user | Only admin or teacher of course |
GET | /courses/tests/{testId}/test-results | Get multiple test results for a test | Only admin or teacher of course |
PUT | /courses/tests/test-results/{testResultId} | Update a test result (associated with a user and a test) | Only admin or grader of test |
DELETE | /courses/tests/test-results/{testResultId} | Delete a test result | Only admin or grader of test |
Note: The paths containing a parameter enclosed in
{}
, e.g.{userId}
represent a variable that is interpolated in the URL, e.g. inwww.myapi.com/users/13
theuserId
is13
.
authorization with Hapi
Hapi routes have the notion of pre
functions that allow breaking the handler logic into smaller and reusable functions.
pre
functions get called before the handler and allow taking over the response and returning an unauthorized error.
This is useful in the context of authorization because many of the authorization rules proposed in the table above will be the same for multiple routes/endpoints.
For example, checking whether the user is an admin will be the same for both the POST /users
and the GET /users
routes.
That allows you to reuse a single isAdmin
pre-function and assign to the two endpoints.
Adding authorization to the users endpoints
In this part, you will define pre
functions to implement the different authorization rules.
You will start with the three /users/{userId}
endpoints (GET
, POST
, and DELETE
) which should be authorized if the user making the request is an admin or if the user is requesting his own userId
.
Note: Hapi also provides a way to implement role-based authentication declaratively with scopes. However, the proposed resource-based authorization approach –where the user's permissions depend on the specific resource requested– requires more granular control that cannot be done with scopes, so
pre
functions are used.
To add a pre-function to verify the authorization rule in the GET /users/{userId}
route, declare the following function in src/plugins/user.ts
:
Then add the pre option to the route definition in src/plugins/user.ts
as follows:
The pre-function will now be called before getUserHandler
and only authorize access to admins or to users requesting their own userId.
Note: In the previous part, you've defined the default authentication strategy, so defining
options.auth
is not strictly required. But it's good practice to define the authentication requirements for every route explicitly.
Checkpoint: To verify the authorization logic has been correctly implemented, you will create a test user and test admin and call the /users/{userId}
endpoint:
- Start the server with
npm run dev
- Run the
seed-users
script to create a test user and test admin:npm run seed-users
. You should get a result similar to this:
- Login as
test@prisma.io
by making a call to thePOST /login
endpoint as follows:
- Take the logged token and call the
/authenticate
endpoint with curl:
- The response should have the
200
status and include anAuthorization
header which looks similar to this:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg
- Make a GET call to
/users/1
(where the number is the test user created in the first step of the checkpoint) with theAuthorization
header containing the token from the last checkpoint as follows:curl -H "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg" localhost:3000/users/1
and the request should succeed and you should see the user profile. - Make another GET call to
/users/2
with the same authorization header:curl -H "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg" localhost:3000/users/2
. This one should fail with a 403 forbidden error.
If all steps succeeded, the isRequestedUserOrAdmin
pre-function correctly authorizes users to access their own user profile. To test the admin functionality, repeat from the third step but log in as the test admin with the email test-admin@prisma.io
. The admin should be able to get both user profiles.
Moving the authorization pre-function to a separate module
So far you've defined the isRequestedUserOrAdmin
authorization pre-function and added it to the GET /users/{userId}
route. To make use of this in different routes, move the function from src/plugins/users.ts
to a separate module: src/auth-helpers.ts
. This module will allow you to keep the authorization logic organized in a single place and reuse it for routes defined in different plugins, e.g. the GET /users/{userId}/courses
route in user-enrollment.ts
.
Once you've moved the isRequestedUserOrAdmin
function into auth-helpers.ts
, add it as a pre-function to the following routes, which have the same authorization logic:
Module | Route |
---|---|
src/plugins/users.ts | DELETE /users/{userId} |
src/plugins/users.ts | PUT /users/{userId} |
src/plugins/users-enrollment.ts | GET /users/{userId}/courses |
src/plugins/users-enrollment.ts | POST /users/{userId}/courses |
src/plugins/users-enrollment.ts | DELETE /users/{userId}/courses |
src/plugins/test-results.ts | GET /users/{userId}/test-results |
Adding authorization to course specific endpoints
Teachers should be able to update courses and create tests for courses they are teachers of and admins. In this step, you will create another pre-function to verify that.
Define the following pre-function in auth-helpers.ts
:
The pre-function uses the teacherOf
array that is fetched in validateAPIToken
to check if the user is a teacher of the requested course.
Add the isTeacherOfCourseOrAdmin
as a pre-function to the following routes:
Module | Route |
---|---|
src/plugins/courses.ts | PUT /courses/{courseId} |
src/plugins/courses.ts | DELETE /courses/{courseId} |
src/plugins/tests.ts | POST /courses/{courseId}/tests |
Update the routes from the table by adding the following options.pre
:
You have now implemented two different authorization rules and added then as a pre-function to ten different routes in the backend.
Updating the tests
After implementing authentication and authorization in the REST API, the tests will fail because the routes now require the user to be authenticated. In this step, you will adapt the tests to consider authentication.
For example, the GET /users/{userId}
endpoint has the following test:
If you run this test now with npm run test -- -t="get user returns user"
the test will fail. This is because the test when the request reaches the endpoint it does not meet its authentication requirements. With Hapi's server.inject
–that simulates an HTTP request to the server–, you can add an auth
object with information about the authenticated user. The auth
object sets the credentials object as they would in the validateAPIToken
function in src/plugins/auth.ts
, for example:
The credentials
object passed matches the AuthCredentials
interface defined in src/plugins/auth.ts
:
Note: An interface in TypeScript is very similar to a type with some subtle differences. To learn more, checkout the TypeScript Handbook.
For the test to pass, you will create a user directly with Prisma in the test and construct the AuthCredentials
object as follows:
Checkpoint: Run npm run test -- -t="get user returns user"
to verify that the test passes.
At this point, you've fixed one test, but what about the others? Since creating the credentials object will be required in most of the tests, you can abstract it into a separate test-helpers.ts
module:
As a next step, write a test that that verifies the authorization rule allowing admins to fetch different user accounts with the GET /users/{userId}
endpoint.
Summary and next steps
Congratulations on making it this far. The article covered many concepts, starting with authentication and authorization concepts to implementing email-based passwordless authentication with Prisma, Hapi, and JWT. Lastly, you implemented authorization rules with Hapi's pre-functions. You also created an email plugin to provide the backend with the ability to send emails with SendGrid's API.
The auth plugin encapsulated the two routes for the authentication flow and used the jwt
authentication scheme to define the authentication strategy. In the authentication strategy's validate function, you checked tokens against the database and populated the credentials object with information relevant for the authorization rules.
You also carried out a database migration and introduced a new Token
table with an n-1 relation to the User
table with Prisma Migrate.
TypeScript helped auto-completing and verifying the correct use of types (ensuring they are in sync with the database schema).
You used Prisma Client extensively to fetch and persist data in the database.
The article covered authorization for a subset of all the endpoints. As next steps, you could do the following:
- Add authorization to the rest of the routes following the same principles.
- Add the credential object to all the tests.
- Generate and set the
JWT_SECRET
environment variable. - Set the
SENDGRID_API_KEY
environment variable and test the email functionality.
You can find the full source code on GitHub with authorization rules for all the endpoints implemented, and the tests adapted.
While Prisma aims to make working with relational databases easy, it's useful to understand the underlying database and authentication principles.
If you have questions, feel free to reach out on Twitter.
Don’t miss the next post!
Sign up for the Prisma Newsletter