Skip to content

Commit 7ad5ef0

Browse files
committed
Initial commit.
0 parents  commit 7ad5ef0

11 files changed

+1229
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
.env

auth/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const {router} = require('./router');
2+
require('./strategies');
3+
4+
module.exports = {router};
5+

auth/router.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const express = require('express');
2+
const passport = require('passport');
3+
const jwt = require('jsonwebtoken');
4+
5+
const config = require('../config');
6+
7+
const createAuthToken = user => {
8+
return jwt.sign({user}, config.JWT_SECRET, {
9+
subject: user.username,
10+
expiresIn: config.JWT_EXPIRY,
11+
algorithm: 'HS256'
12+
});
13+
};
14+
15+
const router = express.Router();
16+
17+
router.post('/login',
18+
// The user provides a username and password to login
19+
passport.authenticate('basic', {session: false}),
20+
(req, res) => {
21+
const authToken = createAuthToken(req.user.apiRepr());
22+
res.json({authToken});
23+
}
24+
);
25+
26+
router.post('/refresh',
27+
// The user exchanges an existing valid JWT for a new one with a later
28+
// expiration
29+
passport.authenticate('jwt', {session: false}),
30+
(req, res) => {
31+
const authToken = createAuthToken(req.user);
32+
res.json({authToken});
33+
}
34+
);
35+
36+
module.exports = {router};

auth/strategies.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const passport = require('passport');
2+
const {BasicStrategy} = require('passport-http');
3+
const {Strategy: JwtStrategy, ExtractJwt} = require('passport-jwt');
4+
5+
const {User} = require('../users/models');
6+
7+
const basicStrategy = new BasicStrategy((username, password, callback) => {
8+
let user;
9+
User
10+
.findOne({username: username})
11+
.then(_user => {
12+
user = _user;
13+
if (!user) {
14+
return Promise.reject({
15+
reason: 'LoginError',
16+
message: 'Incorrect username',
17+
location: 'username'
18+
});
19+
}
20+
return user.validatePassword(password);
21+
})
22+
.then(isValid => {
23+
if (!isValid) {
24+
return Promise.reject({
25+
reason: 'LoginError',
26+
message: 'Incorrect password',
27+
location: 'password'
28+
});
29+
}
30+
return callback(null, user)
31+
})
32+
.catch(err => {
33+
if (err.reason === 'LoginError') {
34+
return callback(null, false, err);
35+
}
36+
return callback(err, false);
37+
});
38+
});
39+
40+
passport.use(basicStrategy);
41+
42+
const jwtStrategy = new JwtStrategy({
43+
secretOrKey: process.env.JWT_SECRET,
44+
// Look for the JWT as a Bearer auth header
45+
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('Bearer'),
46+
// Only allow HS256 tokens - the same as the ones we issue
47+
algorithms: ['HS256']
48+
},
49+
(payload, done) => {
50+
done(null, payload.user)
51+
}
52+
);
53+
54+
passport.use(jwtStrategy);
55+

config.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
exports.DATABASE_URL = process.env.DATABASE_URL ||
2+
global.DATABASE_URL ||
3+
'mongodb://localhost/basic-auth-demo';
4+
exports.PORT = process.env.PORT || 8080;
5+
exports.JWT_SECRET = process.env.JWT_SECRET;
6+
exports.JWT_EXPIRY = process.env.JWT_EXPIRY || '7d';

package.json

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "node-basic-auth",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"start": "node server.js"
8+
},
9+
"keywords": [],
10+
"author": "",
11+
"license": "ISC",
12+
"dependencies": {
13+
"bcryptjs": "^2.4.0",
14+
"body-parser": "^1.15.2",
15+
"dotenv": "^4.0.0",
16+
"express": "^4.14.0",
17+
"jsonwebtoken": "^7.4.1",
18+
"mongoose": "^4.7.4",
19+
"morgan": "^1.7.0",
20+
"passport": "^0.3.2",
21+
"passport-http": "^0.3.0",
22+
"passport-jwt": "^2.2.1"
23+
},
24+
"devDependencies": {
25+
"chai": "^3.5.0",
26+
"chai-http": "^3.0.0",
27+
"faker": "^3.1.0",
28+
"mocha": "^3.2.0"
29+
}
30+
}

server.js

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
require('dotenv').config();
2+
const bodyParser = require('body-parser');
3+
const express = require('express');
4+
const mongoose = require('mongoose');
5+
const morgan = require('morgan');
6+
const passport = require('passport');
7+
8+
const {router: usersRouter} = require('./users');
9+
const {router: authRouter} = require('./auth');
10+
11+
mongoose.Promise = global.Promise;
12+
13+
const {PORT, DATABASE_URL} = require('./config');
14+
15+
const app = express();
16+
17+
// Logging
18+
app.use(morgan('common'));
19+
20+
// CORS
21+
app.use(function (req, res, next) {
22+
res.header('Access-Control-Allow-Origin', '*');
23+
res.header('Access-Control-Allow-Headers', 'Content-Type');
24+
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE');
25+
next();
26+
});
27+
28+
app.use(passport.initialize());
29+
30+
app.use('/api/users/', usersRouter);
31+
app.use('/api/auth/', authRouter);
32+
33+
// A secret endpoint which needs a valid JWT to access it
34+
app.get('/api/secret',
35+
passport.authenticate('jwt', {session: false}),
36+
(req, res) => {
37+
return res.json({
38+
secret: 'rosebud'
39+
});
40+
});
41+
42+
app.use('*', (req, res) => {
43+
return res.status(404).json({message: 'Not Found'});
44+
});
45+
46+
// Referenced by both runServer and closeServer. closeServer
47+
// assumes runServer has run and set `server` to a server object
48+
let server;
49+
50+
function runServer() {
51+
return new Promise((resolve, reject) => {
52+
mongoose.connect(DATABASE_URL, err => {
53+
if (err) {
54+
return reject(err);
55+
}
56+
server = app.listen(PORT, () => {
57+
console.log(`Your app is listening on port ${PORT}`);
58+
resolve();
59+
})
60+
.on('error', err => {
61+
mongoose.disconnect();
62+
reject(err);
63+
});
64+
});
65+
});
66+
}
67+
68+
function closeServer() {
69+
return mongoose.disconnect().then(() => {
70+
return new Promise((resolve, reject) => {
71+
console.log('Closing server');
72+
server.close(err => {
73+
if (err) {
74+
return reject(err);
75+
}
76+
resolve();
77+
});
78+
});
79+
});
80+
}
81+
82+
if (require.main === module) {
83+
runServer().catch(err => console.error(err));
84+
};
85+
86+
module.exports = {app, runServer, closeServer};
87+

users/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const {User} = require('./models');
2+
const {router} = require('./router');
3+
4+
module.exports = {User, router};

users/models.js

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const bcrypt = require('bcryptjs');
2+
const mongoose = require('mongoose');
3+
4+
mongoose.Promise = global.Promise;
5+
6+
const UserSchema = mongoose.Schema({
7+
username: {
8+
type: String,
9+
required: true,
10+
unique: true
11+
},
12+
password: {
13+
type: String,
14+
required: true
15+
},
16+
firstName: {type: String, default: ""},
17+
lastName: {type: String, default: ""}
18+
});
19+
20+
UserSchema.methods.apiRepr = function() {
21+
return {
22+
username: this.username || '',
23+
firstName: this.firstName || '',
24+
lastName: this.lastName || ''
25+
};
26+
}
27+
28+
UserSchema.methods.validatePassword = function(password) {
29+
return bcrypt.compare(password, this.password);
30+
}
31+
32+
UserSchema.statics.hashPassword = function(password) {
33+
return bcrypt.hash(password, 10);
34+
}
35+
36+
const User = mongoose.model('User', UserSchema);
37+
38+
module.exports = {User};

users/router.js

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
const express = require('express');
2+
const bodyParser = require('body-parser');
3+
const passport = require('passport');
4+
5+
const {User} = require('./models');
6+
7+
const router = express.Router();
8+
9+
const jsonParser = bodyParser.json();
10+
11+
// Post to register a new user
12+
router.post('/', jsonParser, (req, res) => {
13+
const requiredFields = ['username', 'password'];
14+
const missingField = requiredFields.find(field => !(field in req.body));
15+
16+
if (missingField) {
17+
return res.status(422).json({
18+
code: 422,
19+
reason: 'ValidationError',
20+
message: 'Missing field',
21+
location: missingField
22+
});
23+
}
24+
25+
const stringFields = ['username', 'password', 'firstName', 'lastName'];
26+
const nonStringField = stringFields.find(field =>
27+
(field in req.body) && typeof req.body[field] !== 'string'
28+
);
29+
30+
if (nonStringField) {
31+
return res.status(422).json({
32+
code: 422,
33+
reason: 'ValidationError',
34+
message: 'Incorrect field type: expected string',
35+
location: nonStringField
36+
});
37+
}
38+
39+
const nonEmptyFields = ['username', 'password'];
40+
const emptyField = nonEmptyFields.find(field => req.body[field].trim() === '');
41+
42+
if (emptyField) {
43+
return res.status(422).json({
44+
code: 422,
45+
reason: 'ValidationError',
46+
message: 'Incorrect field length',
47+
location: emptyField
48+
});
49+
}
50+
51+
let {username, password, firstName, lastName} = req.body;
52+
53+
return User
54+
.find({username})
55+
.count()
56+
.then(count => {
57+
if (count > 0) {
58+
// There is an existing user with the same username
59+
return Promise.reject({
60+
code: 422,
61+
reason: 'ValidationError',
62+
message: 'Username already taken',
63+
location: 'username'
64+
});
65+
}
66+
// If there is no existing user, hash the password
67+
return User.hashPassword(password)
68+
})
69+
.then(hash => {
70+
return User
71+
.create({
72+
username,
73+
password: hash,
74+
firstName,
75+
lastName
76+
})
77+
})
78+
.then(user => {
79+
return res.status(201).json(user.apiRepr());
80+
})
81+
.catch(err => {
82+
// Forward validation errors on to the client, otherwise give a 500
83+
// error because something unexpected has happened
84+
if (err.reason === 'ValidationError') {
85+
return res.status(err.code).json(err);
86+
}
87+
res.status(500).json({code: 500, message: 'Internal server error'});
88+
});
89+
});
90+
91+
// Never expose all your users like below in a prod application
92+
// we're just doing this so we have a quick way to see
93+
// if we're creating users. keep in mind, you can also
94+
// verify this in the Mongo shell.
95+
router.get('/', (req, res) => {
96+
return User
97+
.find()
98+
.then(users => res.json(users.map(user => user.apiRepr())))
99+
.catch(err => res.status(500).json({message: 'Internal server error'}));
100+
});
101+
102+
module.exports = {router};

0 commit comments

Comments
 (0)