Skip to content

WIP #72

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

WIP #72

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,58 @@ Your finished project must include all of the following requirements (further in
Be prepared to demonstrate your understanding of this week's concepts by answering questions on the following topics.

1. Differences between using _sessions_ or _JSON Web Tokens_ for authentication.

Sessions provide a way to persist data across requests. For example, we'll use them to save authentication information, so there is no need to re-enter credentials on every new request the client makes to the server. When using sessions, each client will have a unique session stored on the server. In ordee to transmit authentication information between the client and server. For that, we'll use cookies.

Every HTTP message has two main parts: the headers and the body. To send cookies, the server adds the Set-Cookie header to the response like so: "Set-Cookie": "session=12345". The browser will read the header and save a cookie called session with the value 12345 in this example
The browser will add the "Cookie": "session=12345" header on every subsequent request and the server. Cookies are not accessible from JavaScript or anywhere because they are cryptographically signed and very secure.


JSON Web Tokens (JWT) are a way to transmit information between parties in the form of a JSON object. The JSON information is most commonly used for authentication and information exchange.

When you design a backend application, if it is well designed, the application will be stateless. Especially when you have millions of users, you want to be able to horizontally scale your application. Horizontally scaling means being able to scale by adding servers. Vertically scaling means taking an existing server and adding capacity/computing power/space to it

Horizontal scaling generates some challenges related to your application design. If you are keeping session data in memory, then every request from a user would have to go back to the same server in order to validate the session id. You can generate sticky sessions, which allows that your requests always go to the same server until your session has expired. You can do that, but you have to plan for that.

When you have an application, there is always state (variables a and their values). The question is where you store that state. A stateless application means that the state is not going to be stored in the application (disk, database, memory). The application doesn’t keep track of it.

By keeping the state in the token, every time the client makes a request, the state that is in the token is sent to the application. The application needs a way to validate.


2. What does `bcryptjs` do to help us store passwords in a secure manner?

Authentication is the process by which our Web API verifies a client's identity that is trying to access a resource.
Some of the things we need to take into account when implementing authentication are:
• Password storage.
• Password strength.
• Brute-force safeguards.

Regarding password storage, there are the two main options: encryption and hashing.
• Encryption goes two ways. First, it utilizes plain text and private keys to generate encrypted passwords and then reverses the process to match an original password.
• Cryptographic hashes only go one way: parameters + input = hash. It is pure; given the same parameters and input, it generates the same hash.
Suppose the database of users and keys is compromised. In that case, it is possible to decrypt the passwords to their original values, which is bad because users often share passwords across different sites. This is one reason why cryptographic hashing is the preferred method for storing user passwords.

A common way that attackers circumvent hashing algorithms is by pre-calculating hashes for all possible character combinations up to a particular length using common hashing techniques.

In order to slow down hackers' ability to get at a user's password. To do so, we will add time to our security algorithm to produce what is known as a key derivation function.
[Hash] + [Time] = [Key Derivation Function].

Instead of writing our key derivation function, we use a well-known and popular module called bcryptjs. Bcryptjs features include:
• password hashing function
• implements salting both manually and automatically.
• accumulative hashing rounds

Having an algorithm that hashes the information multiple times (rounds) means an attacker needs to have the hash, know the algorithm used, and how many rounds were used to generate the hash in the first place.


3. How are unit tests different from integration and end-to-end testing?

Unit tests are where we isolate smaller units of software (often functions or methods). There are usually many unit tests in a codebase, and because these tests are meant to be run often, they need to run fast.

End to end testing (E2E testing) refers to a software testing method that involves testing an application's workflow from beginning to end. This method basically aims to replicate real user scenarios so that the system can be validated for integration and data integrity.

4. How does _Test Driven Development_ change the way we write applications and tests?

TDD allows writing smaller code having single responsibility rather than monolithic procedures with multiple responsibilities. This makes the code simpler to understand. TDD also forces to write only production code to pass tests based on user requirements.

As a result, unit tests are the preferred tool for test driven development (TDD). Developers regularly use them to test correctness in units of code (usually functions).
47 changes: 47 additions & 0 deletions api/auth/auth-middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const User = require('./auth-model.js')

const checkUserExists = async(req, res, next ) => {
try{
const { username } = req.body
const newUser = await User.findBy({username})
if(newUser[0]){
next({
status: 422,
message:'The username is already taken'})
} else {
next()
}
} catch(err){
next(err)
}
}



const validateBody = async(req, res, next ) => {
try{

const { username , password } = req.body
if(!username || !password ||
typeof password !== 'string' ||
!password.trim() ||
!username.trim() ) {
next({
status: 400,
message: 'username and password required'
})

} else {
next()
}

} catch(err){
next(err)
}
}


module.exports = {
checkUserExists,
validateBody
}
25 changes: 25 additions & 0 deletions api/auth/auth-model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const db = require('../../data/dbConfig');

function findBy(filter) {
return db('users')
.select('id', 'username', 'password')
.where(filter)
}

function findById(id) {
return db('users')
.select('id','username','password')
.where("id", id)
.first()
}

async function add({ username, password }) {
const [id] = await db('users').insert({ username, password })
return findById(id)
}

module.exports = {
findBy,
findById,
add,
};
72 changes: 67 additions & 5 deletions api/auth/auth-router.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
const router = require('express').Router();
const jwt = require("jsonwebtoken") // creates token
const secrets = require('../../config/secrets')
const User = require('./auth-model.js')

router.post('/register', (req, res) => {
res.end('implement register, please!');
const bcrypt = require('bcryptjs') // creates hash
const { BCRYPT_ROUNDS } = require('../../config')

const { checkUserExists, validateBody } = require('./auth-middleware')



router.post('/register',checkUserExists,validateBody, async (req, res, next) => {

try {
const { username , password} = req.body

// bcrypting the password before saving
const hash = bcrypt.hashSync(password, BCRYPT_ROUNDS)
// never save the plain text password in the db
const user = { 'username': username , 'password':hash}

const NewUser = await User.add(user)
res.status(201).json({'id': NewUser.id,
'username': NewUser.username,
'password': NewUser.password,
})
} catch(err) {
next(err)
}
});
//res.end('implement register, please!');
/*
IMPLEMENT
You are welcome to build additional middlewares to help with the endpoint's functionality.
Expand All @@ -27,10 +55,28 @@ router.post('/register', (req, res) => {
4- On FAILED registration due to the `username` being taken,
the response body should include a string exactly as follows: "username taken".
*/


router.post('/login', validateBody,async (req, res, next) => {
const { username , password } = req.body
const newUser = await User.findBy({username})
const user = newUser[0]
if(user && bcrypt.compareSync(password, user.password) ){
// generate a token and include it in the response
const token = generateToken(user)
res.status(200).json({
message :`welcome, ${user.username}`,
token: token
})
} else {
next({ status: 401, message: 'invalid credentials' })
}

});

router.post('/login', (req, res) => {
res.end('implement login, please!');


//res.end('implement login, please!');
/*
IMPLEMENT
You are welcome to build additional middlewares to help with the endpoint's functionality.
Expand All @@ -54,6 +100,22 @@ router.post('/login', (req, res) => {
4- On FAILED login due to `username` not existing in the db, or `password` being incorrect,
the response body should include a string exactly as follows: "invalid credentials".
*/
});


function generateToken (user) {
const payload = {
subject: user.id,
username: user.username
}

const options = {
expiresIn: '1d',
}
const secret = secrets.jwtSecret

return jwt.sign(payload,secret,options)

}


module.exports = router;
24 changes: 22 additions & 2 deletions api/middleware/restricted.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
module.exports = (req, res, next) => {
next();
const jwt = require("jsonwebtoken")
const secrets = require('../../config/secrets')

module.exports = async (req, res, next) => {
const token = req.headers.authorization
console.log('token',token )

if(token){ // Validate token
jwt.verify(token,secrets.jwtSecret ,(err,decodedToken )=>{
if(err) {
next({ status: 401, message:"token invalid"})
} else {
req.decodedJwt = decodedToken // return decoded token
next()
}
})
} else {
res.status(401).json({message: "token required"})
}


/*
IMPLEMENT

Expand All @@ -12,3 +31,4 @@ module.exports = (req, res, next) => {
the response body should include a string exactly as follows: "token invalid".
*/
};

15 changes: 14 additions & 1 deletion api/server.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');

const path = require('path')
const restrict = require('./middleware/restricted.js');

const authRouter = require('./auth/auth-router.js');
Expand All @@ -13,7 +13,20 @@ server.use(helmet());
server.use(cors());
server.use(express.json());

server.use(express.static(path.join(__dirname, '../client')))
server.use('/api/auth', authRouter);
server.use('/api/jokes', restrict, jokesRouter); // only logged-in users should have access!

server.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../client', 'index.html'))
})

server.use((err, req, res, next) => { // eslint-disable-line
res.status(err.status || 500).json({
message: err.message,
stack: err.stack,
})
})


module.exports = server;
55 changes: 53 additions & 2 deletions api/server.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,55 @@
// Write your tests here
const server = require('./server');
const request = require('supertest');
const db = require('../data/dbConfig');

test('sanity', () => {
expect(true).toBe(false)
expect(true).toBe(true)
});

beforeAll(async () => {
await db.migrate.rollback()
await db.migrate.latest()
});

afterAll(async () => {
await db.destroy()
})


beforeEach(async () => {
await request(server).post('/api/auth/register')
.send({
username: "Brody",
password: "youngCrazedPeeling"
})
})

describe('[POST] /register', () => {
test('causes a user to be added to the database', async () => {
const users = await db('users')
expect(users).toHaveLength(1)
})
test('responds with a newly created user', async () => {
const users = await db('users')
expect(users[0].username).toEqual("Brody")
})
})

describe('[POST] /login', () => {
let login
beforeEach(async () => {
login = await request(server).post('/api/auth/login')
.send({
username: "Brody",
password: "youngCrazedPeeling"
})
})

test('allows a user to login', async () => {
expect(login.text).toMatch('token')
})

test('responds with a greeting to logged in user', async () => {
expect(login.text).toMatch('welcome, Brody')
})
})
Empty file added config/.Rhistory
Empty file.
3 changes: 3 additions & 0 deletions config/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
BCRYPT_ROUNDS: process.env.BCRYPT_ROUNDS || 8
}
3 changes: 3 additions & 0 deletions config/secrets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
jwtSecret: process.env.JWT_SECRET || "shh"
}
Loading