Skip to content

Commit 744fb7e

Browse files
committed
Hashed and salted password storage using inbuilt Node.js crypto library pbkdf2
1 parent de5793e commit 744fb7e

File tree

6 files changed

+50
-13
lines changed

6 files changed

+50
-13
lines changed

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ This project is a clone of hacker news rewritten with universal JavaScript, usin
3636
- Docker - (Container Deployment)
3737

3838
### Benefits
39+
3940
**Front End**
4041
- Declarative UI - (react)
4142
- Flux State Management - (redux)
42-
- GraphQL Query Colocation - (react-apollo)
43+
- GraphQL Fragment Colocation - (react-apollo)
44+
- Prefetch Page Assets - (next)
4345

4446
**Server**
4547
- Universal JS - (node & express)
@@ -57,6 +59,7 @@ This project is a clone of hacker news rewritten with universal JavaScript, usin
5759
- Hot Module Reloading - (next)
5860
- Snapshot Testing - (jest)
5961
- Faster Package Install - (yarn)
62+
- JS Best Practices - (eslint)
6063

6164

6265
### Architecture Overview

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hackernews",
3-
"version": "0.3.0",
3+
"version": "0.4.0",
44
"description": "A hacker news clone built from the ground up to demonstrate React and GraphQL",
55
"engines": {
66
"node": ">=6.9.4"

src/config.js

+6
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,9 @@ export const HOST = (process.browser && window.location.host) || `${HOST_NAME}:$
1616
export const APP_URI = `http://${HOST}`;
1717
export const GRAPHQL_URL = `${APP_URI}${graphQLPath}`;
1818
export const GRAPHIQL_URL = `${APP_URI}${graphiQLPath}`;
19+
20+
/*
21+
Cryptography
22+
https://nodejs.org/api/crypto.html#crypto_crypto_pbkdf2_password_salt_iterations_keylen_digest_callback
23+
*/
24+
export const passwordIterations = 10000;

src/data/models/User.js

+18-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import * as DB from '../Database';
22
import * as HNDB from '../HNDataAPI';
33
import cache from '../Cache';
4+
import {
5+
createHash,
6+
createSalt,
7+
} from '../../helpers/hashPassword';
8+
import {
9+
passwordIterations,
10+
} from '../../config';
411

512
export default class User {
613
constructor(props) {
@@ -18,27 +25,34 @@ export default class User {
1825
this.likes = props.likes || [];
1926
this.posts = props.posts || [];
2027
this.password = props.password || undefined;
28+
this.passwordSalt = props.passwordSalt || undefined;
2129
}
2230

2331
static getUser = id => cache.getUser(id) || HNDB.fetchUser(id);
2432

2533
static getPostsForUser = id => DB.getNewsItems()
2634
.filter(newsItem => newsItem.submitterId === id);
2735

28-
static validPassword = (id, password) => {
36+
static validPassword = async (id, password) => {
2937
const user = cache.getUser(id);
30-
if (user) return user.password === password;
38+
if (user) return await createHash(password, user.passwordSalt, passwordIterations) === user.password;
3139
return false;
3240
}
3341

34-
static registerUser = ({ id, password }) => {
42+
static registerUser = async ({ id, password }) => {
3543
if (id.length < 3 || id.length > 32) throw new Error('User ID must be between 3 and 32 characters.');
3644
if (password.length < 8 || password.length > 100) throw new Error('User password must be longer than 8 characters.');
3745
if (cache.getUser(id)) throw new Error('Username is taken.');
46+
47+
const passwordSalt = createSalt();
48+
const hashedPassword = await createHash(password, passwordSalt, passwordIterations);
49+
3850
const user = new User({
3951
id,
40-
password,
52+
password: hashedPassword,
53+
passwordSalt,
4154
});
55+
4256
cache.setUser(user.id, user);
4357
return user;
4458
}

src/helpers/hashPassword.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import crypto from 'crypto';
2+
3+
export const createHash = (password, salt, iterations) => new Promise((resolve, reject) => {
4+
const saltBuffer = typeof salt === 'string' ? Buffer.from(salt, 'base64') : salt;
5+
6+
const callback = (err, derivedKey) => err ? reject(err) : resolve(derivedKey.toString('base64'));
7+
crypto.pbkdf2(password, saltBuffer, iterations, 512 / 8, 'sha512', callback);
8+
});
9+
10+
export const createSalt = () => crypto.randomBytes(128).toString('base64');

src/server.js

+11-7
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ app.prepare()
4747
passport.use(new LocalStrategy(
4848
(username, password, done) => {
4949
const user = User.getUser(username);
50-
// if (err) { return done(err); }
50+
// if (err) return done(err);
5151
if (!user) {
5252
return done(null, false, { message: 'Incorrect username.' });
5353
}
@@ -83,14 +83,18 @@ app.prepare()
8383
server.use(bodyParser.urlencoded({ extended: false }));
8484
server.use(passport.session());
8585

86-
server.post('/login', (req, res, next) => {
86+
server.post('/login', async (req, res, next) => {
8787
if (req.body.creating) {
8888
if (!req.user) {
89-
User.registerUser({
90-
id: req.body.username,
91-
password: req.body.password,
92-
});
93-
req.session.returnTo = `${req.body.goto}${req.body.username}`;
89+
try {
90+
await User.registerUser({
91+
id: req.body.username,
92+
password: req.body.password,
93+
});
94+
req.session.returnTo = `${req.body.goto}${req.body.username}`;
95+
} catch (err) {
96+
req.session.returnTo = '/login?how=user';
97+
}
9498
} else req.session.returnTo = '/login?how=user';
9599
} else req.session.returnTo = req.body.goto;
96100
next();

0 commit comments

Comments
 (0)