-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #42 from HackYourFuture/user-registration
User registration and storing password sections
- Loading branch information
Showing
5 changed files
with
250 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
<mxfile host="app.diagrams.net" modified="2024-02-06T22:33:58.432Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0" etag="dTdYQEdK6-30oRl_CY9x" version="23.0.2" type="device"> | ||
<diagram name="Page-1" id="1Q6fCelEe2z6ASVgYXHQ"> | ||
<mxGraphModel dx="903" dy="633" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0"> | ||
<root> | ||
<mxCell id="0" /> | ||
<mxCell id="1" parent="0" /> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="PDGte3Nl42UgdPUzoMXS-1" target="PDGte3Nl42UgdPUzoMXS-12"> | ||
<mxGeometry relative="1" as="geometry" /> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-1" value="Registration endpoint" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | ||
<mxGeometry x="340" y="260" width="100" height="60" as="geometry" /> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-2" value="User Database" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" vertex="1" parent="1"> | ||
<mxGeometry x="700" y="140" width="60" height="80" as="geometry" /> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="PDGte3Nl42UgdPUzoMXS-5" target="PDGte3Nl42UgdPUzoMXS-1"> | ||
<mxGeometry relative="1" as="geometry" /> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-10" value="POST request" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="PDGte3Nl42UgdPUzoMXS-8"> | ||
<mxGeometry x="-0.1" relative="1" as="geometry"> | ||
<mxPoint x="-2" y="-10" as="offset" /> | ||
</mxGeometry> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-5" value="Client" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" vertex="1" parent="1"> | ||
<mxGeometry x="170" y="260" width="30" height="60" as="geometry" /> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-12" value="<div>Is valid&nbsp;</div><div>request?</div>" style="rhombus;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | ||
<mxGeometry x="520" y="250" width="90" height="80" as="geometry" /> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-15" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="PDGte3Nl42UgdPUzoMXS-12" target="PDGte3Nl42UgdPUzoMXS-5"> | ||
<mxGeometry relative="1" as="geometry"> | ||
<Array as="points"> | ||
<mxPoint x="565" y="180" /> | ||
<mxPoint x="185" y="180" /> | ||
</Array> | ||
</mxGeometry> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-16" value="Return HTTP error" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="PDGte3Nl42UgdPUzoMXS-15"> | ||
<mxGeometry x="0.0057" y="-1" relative="1" as="geometry"> | ||
<mxPoint x="14" y="1" as="offset" /> | ||
</mxGeometry> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-18" value="No" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="PDGte3Nl42UgdPUzoMXS-15"> | ||
<mxGeometry x="-0.8976" y="-1" relative="1" as="geometry"> | ||
<mxPoint y="8" as="offset" /> | ||
</mxGeometry> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="PDGte3Nl42UgdPUzoMXS-12" target="PDGte3Nl42UgdPUzoMXS-25"> | ||
<mxGeometry relative="1" as="geometry"> | ||
<Array as="points"> | ||
<mxPoint x="660" y="290" /> | ||
</Array> | ||
</mxGeometry> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-21" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="PDGte3Nl42UgdPUzoMXS-20"> | ||
<mxGeometry x="-0.5848" y="-2" relative="1" as="geometry"> | ||
<mxPoint x="8" y="-2" as="offset" /> | ||
</mxGeometry> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-24" value="<font style="font-size: 9px;">{ user data }</font>" style="shape=document;whiteSpace=wrap;html=1;boundedLbl=1;" vertex="1" parent="1"> | ||
<mxGeometry x="230" y="300" width="60" height="30" as="geometry" /> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-27" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="PDGte3Nl42UgdPUzoMXS-25" target="PDGte3Nl42UgdPUzoMXS-2"> | ||
<mxGeometry relative="1" as="geometry" /> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-29" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="PDGte3Nl42UgdPUzoMXS-25"> | ||
<mxGeometry relative="1" as="geometry"> | ||
<mxPoint x="180" y="340" as="targetPoint" /> | ||
<Array as="points"> | ||
<mxPoint x="730" y="360" /> | ||
<mxPoint x="180" y="360" /> | ||
<mxPoint x="180" y="340" /> | ||
</Array> | ||
</mxGeometry> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-30" value="Return HTTP success" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="PDGte3Nl42UgdPUzoMXS-29"> | ||
<mxGeometry x="-0.4313" y="-1" relative="1" as="geometry"> | ||
<mxPoint x="-104" as="offset" /> | ||
</mxGeometry> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-25" value="Create new user" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | ||
<mxGeometry x="690" y="270" width="80" height="40" as="geometry" /> | ||
</mxCell> | ||
<mxCell id="PDGte3Nl42UgdPUzoMXS-28" value="<font style="font-size: 9px;">{ user data }</font>" style="shape=document;whiteSpace=wrap;html=1;boundedLbl=1;" vertex="1" parent="1"> | ||
<mxGeometry x="740" y="230" width="60" height="30" as="geometry" /> | ||
</mxCell> | ||
</root> | ||
</mxGraphModel> | ||
</diagram> | ||
</mxfile> |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
# How to store user passwords | ||
|
||
## Why is it important? | ||
As developers, it is our responsibility to protect our user private data. Passwords are very personal data and should be stored securely in the database. | ||
|
||
If we do not secure our passwords in the database and our database get stolen or leaked, all our passwords can be easily read! This is very bad for our application because with all passwords exposed, anyone can login with any user. | ||
|
||
Leaked passwords can lead to even more problems: Many users reuse the same passwords for their email, banking app, social media accounts and more. Hackers often check stolen passwords and try to login to many different websites to check if the user reused the password. This practice one of the most popular methods hacking into social media and email accounts. | ||
|
||
To protect yourself, do not reuse passwords. You can enter your email in [haveibeenpwned.com](https://haveibeenpwned.com/) to check if your email was part of a security breach in the past. Spoiler alert: It is most likely that the answer is yes. But don't feel bad or panic, those data breaches are very common, happened to the largest websites like LinkedIn and Adobe. It is likely that you used one of those websites in the past. | ||
|
||
## How to NOT store user passwords | ||
|
||
In this great video, Tom Scott explaining how to NOT store user passwords. | ||
{% hyf-youtube src="https://www.youtube.com/watch?v=8ZtInClXe1Q" %} | ||
|
||
A few key takeouts from the video: | ||
* At first, encryption sounds like a good idea. However, in practice, it only leads to more issues when handling passwords. | ||
* Hashing is different from encryption and is a _one way operation_. Once the password is hashed, it is mathematically impossible to reverse it back to the original form. | ||
|
||
## Using bcrypt | ||
[Bcrypt](https://en.wikipedia.org/wiki/Bcrypt) is an industry standard password hashing method. So far it was proven to be secure enough to be used in real world applications. | ||
|
||
In NodeJS, we can use the popular [bcrypt](https://www.npmjs.com/package/bcrypt) npm package. This is one of many implementations of the bcrypt standard. | ||
|
||
```javascript | ||
import { hash, compare } from 'bcrypt'; | ||
``` | ||
|
||
The library provides two very important functions: | ||
* **hash** converts the password to a bcrypt format hash. We use this function before storing the password in the database. | ||
* **compare** checks if a hashed string equals to another string. We use this function when we want to check if a password is correct or not. | ||
|
||
### Hashing password | ||
|
||
To securely hash a password, we use bcrypt's `hash` function: | ||
|
||
```javascript | ||
const secretPassword = "12345678"; | ||
const saltRounds = 12; | ||
const hashedPassword = await hash(secretPassword, saltRounds); | ||
|
||
console.log(hashedPassword); | ||
``` | ||
|
||
The following code will output the hash. It looks something like this: | ||
> $2b$12$vfQ6eiT2aU2d.Im0LVBm6.dG3r1IYJg2FxmsnNBsuUHFPHTYYZ7rO | ||
As you can see, the hash looks completely random and is not remotely similar to the original password. It is now safe to store this hash in our database. | ||
|
||
The format of this hash is not very important. Bcrypt uses this format when we compare two passwords. | ||
|
||
#### salt rounds | ||
You may noticed that `hash` has a second parameter called `saltRounds`. The bigger the `saltRounds`, the more secure is the hash but also the slower it takes to calculate it. It is a tradeoff between speed and security. There is no magic number that works the best for all applications. In our example, we use the number `12` which is *arguably* secure enough for most applications. | ||
|
||
### Comparing hashed password | ||
As we learned before, it is impossible to reverse a password hash to it's original form. In order to check if a hash is the same as a given string, we use the `compare` function: | ||
|
||
```javascript | ||
const hashedPassword = '$2b$12$vfQ6eiT2aU2d.Im0LVBm6.dG3r1IYJg2FxmsnNBsuUHFPHTYYZ7rO'; | ||
|
||
await compare("11223344", hashedPassword); // false | ||
await compare("12345678", hashedPassword); // true | ||
``` | ||
|
||
#### How does it work? | ||
We cannot reverse the hash, how does bcrypt know that "11223344" is the wrong password? | ||
|
||
The trick is to hash the new password "11223344" through the same process like we did with the original password. If the result hash is the same, then the new password equal to the original password. This smart trick is how the `compare` method works. | ||
|
||
## Further Study | ||
|
||
1. Read: https://codahale.com/how-to-safely-store-a-password/ | ||
1. Watch: [Hashing Algorithms and Security - Computerphile](https://www.youtube.com/watch?v=b4b8ktEV4Bg) | ||
1. Watch: [Password Storage Tier List: encryption, hashing, salting, bcrypt, and beyond](https://www.youtube.com/watch?v=qgpsIBLvrGY) | ||
1. Watch: [Password Cracking - Computerphile](https://www.youtube.com/watch?v=7U-RbOKanYs) | ||
1. Watch: [ How to Choose a Password - Computerphile](https://www.youtube.com/watch?v=3NjQ9b3pgIg) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,87 @@ | ||
# User Registration | ||
|
||
If we would like to build a multi-user application, it is essential to store and track a list of all the allowed users to the application. In addition, we should also implement authentication and authorization. | ||
* Authentication system will prevent users to impersonate and make actions as other users. | ||
* Authorization system will prevent users from accessing resources that they are not allowed to be accessed. | ||
In order to build a multi-user application, it is essential to store and track a list of all the allowed users to the application. In addition, we should also implement authentication and authorization. | ||
* An authentication system will prevent users to impersonate and make actions as other users. | ||
* An authorization system will prevent users from accessing resources they are not allowed to. | ||
|
||
## Registration implementation | ||
|
||
In order to track the list of users, we often use a `user database`. This database will securely keep all the information about every user that is required for our application. This often includes the authentication credentials such as password. | ||
|
||
Explain the topic in detail including images, snippets, videos, ... whatever helps make things clear. | ||
Here is a diagram of the general flow for user registration: | ||
|
||
![Registration Diagram](assets/registration-diagram.png) | ||
|
||
1. The client sends a HTTP post request to the backend. The body will contain all the required information to register a new user like email and password. | ||
|
||
1. The backend reads the request and validates the request. Every application will have its own rules like password length, unique emails. | ||
|
||
1. If the request is invalid, the backend will send back an error so the client can try to register again with a valid request. | ||
|
||
1. When the request is correct, the backend will access the user database, store the new user and return a success message. | ||
|
||
1. Registration is now complete and we have a new user in our application! | ||
|
||
|
||
## Example implementation | ||
In this section we will implement a very simple registration endpoint. | ||
|
||
First, we need a user database. The simplest solution can be defining an array inside our server: | ||
|
||
```javascript | ||
const usersDatabase = []; | ||
``` | ||
In real world applications, we use a real database server like MySQL or MongoDB to store all the users. | ||
|
||
Next, lets create the endpoint: | ||
|
||
```javascript | ||
app.post('/register', (req, res) => { | ||
// Extract the information from the request body | ||
const newUser = { | ||
username: req.body.username, | ||
password: req.body.password | ||
}; | ||
// Check if user is valid | ||
if(!isValidUser(newUser)) { | ||
res.status(400).send("Invalid user").end(); | ||
return; | ||
} | ||
// Add the new user to the database | ||
usersDatabase.push(newUser); | ||
|
||
// Send HTTP 201 with the newly created user. We do not send the password | ||
res.status(201).send({ username: newUser.username }).end(); | ||
}); | ||
``` | ||
|
||
Let's break down the code into parts. The first section is extracting all the information about the user from the POST request and save it into an object. | ||
|
||
The send step is to check if the user is valid. Invalid user object can cause problems should not be stored in our database. The implementation of the `isValidUser` function can be as simple as this: | ||
|
||
```javascript | ||
const isValidUser = (user) => { | ||
if(!user.username || !user.password || !user.email) { | ||
return false; | ||
} | ||
return true; | ||
}; | ||
``` | ||
|
||
Or you can choose to make more complicated checks like minimum password length, email validation and more. This depends on the application requirements. | ||
|
||
If the user details are all correct, we then add the new user to the database and return success. Note the 201 HTTP status code, indicating that a new resource has been created. | ||
|
||
And that's it! We have now a working basic registration endpoint for our users to sign up. Take the time to implement this code yourself, test it and improve it. | ||
|
||
### User passwords | ||
In this example, we store the user passwords in the user database as-is. This is known to be a very bad and insecure practice. We should handle user passwords with great care and do everything we can to prevent them being leaked to others. Read more in the [How to store user passwords](/node-js/storing-passwords.md) section. | ||
|
||
## Further improvements | ||
Here are few suggestion on how we can improve the registration endpoint: | ||
|
||
1. The User database is stored in memory. This means that if we kill the NodeJS application, the user database will be deleted. In order to fix this, save the user database into a file in a JSON format using the [File system](https://nodejs.org/api/fs.html) module. | ||
1. Add additional checks to the `isValidUser` function. Check for the validity of the email, minimum password length, check the user database if the username has been previously registered. | ||
1. Create HTML client with a registration form and use `fetch` to send the POST request to the server. | ||
1. Handle user passwords with care after reading [How to store user passwords](/node-js/storing-passwords.md) | ||
|