Implement JWT authentication

This commit is contained in:
John Furrow
2016-06-21 23:16:24 -07:00
parent af2e582226
commit f996abf028
18 changed files with 674 additions and 70 deletions
@@ -0,0 +1,129 @@
import axios from 'axios';
import ActionTypes from '../constants/ActionTypes';
import AppDispatcher from '../dispatcher/AppDispatcher';
const AuthActions = {
authenticate: (credentials) => {
return axios.post('/auth/authenticate', credentials)
.then((json = {}) => {
return json.data;
})
.then((data) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_LOGIN_SUCCESS,
data
});
})
.catch((error) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_LOGIN_ERROR,
error: error.data.message
});
});
},
createUser: (credentials) => {
return axios.put('/auth/users', credentials)
.then((json = {}) => {
return json.data;
})
.then((data) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_CREATE_USER_SUCCESS,
data
});
})
.catch((error) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_CREATE_USER_ERROR,
error
});
});
},
deleteUser: (username) => {
return axios.delete(`/auth/users/${username}`)
.then((json = {}) => {
return json.data;
})
.then((data) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_DELETE_USER_SUCCESS,
data: {
username,
...data
}
});
})
.catch((error) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_DELETE_USER_ERROR,
error: {
username,
...error
}
});
});
},
fetchUsers: () => {
return axios.get('/auth/users')
.then((json = {}) => {
return json.data;
})
.then((data) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_LIST_USERS_SUCCESS,
data
});
})
.catch((error) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_LIST_USERS_ERROR,
error
});
});
},
register: (credentials) => {
return axios.post('/auth/register', credentials)
.then((json = {}) => {
return json.data;
})
.then((data) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_REGISTER_SUCCESS,
data
});
})
.catch((error) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_REGISTER_ERROR,
error
});
});
},
verify: () => {
// We need to prevent caching this endpoint.
return axios.get(`/auth/verify?${Date.now()}`)
.then((json = {}) => {
return json.data;
})
.then((data) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_VERIFY_SUCCESS,
data
});
})
.catch((error) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_VERIFY_ERROR,
error
});
});
}
};
export default AuthActions;
@@ -0,0 +1,105 @@
import classnames from'classnames';
import React from'react';
import EventTypes from '../../constants/EventTypes';
import FloodActions from '../../actions/FloodActions';
import AuthStore from '../../stores/AuthStore';
const METHODS_TO_BIND = ['handleAuthError', 'handleSubmitClick'];
export default class AuthForm extends React.Component {
constructor() {
super();
this.state = {error: null}
METHODS_TO_BIND.forEach((method) => {
this[method] = this[method].bind(this);
});
}
componentDidMount() {
AuthStore.listen(EventTypes.AUTH_LOGIN_ERROR, this.handleAuthError);
AuthStore.listen(EventTypes.AUTH_REGISTER_ERROR, this.handleAuthError);
}
componentWillUnmount() {
AuthStore.unlisten(EventTypes.AUTH_LOGIN_ERROR, this.handleAuthError);
AuthStore.unlisten(EventTypes.AUTH_REGISTER_ERROR, this.handleAuthError);
}
getValue(fieldName) {
return this.state[fieldName];
}
handleSubmitClick() {
if (this.props.mode === 'login') {
AuthStore.authenticate({
username: this.refs.username.value,
password: this.refs.password.value
});
} else {
AuthStore.register({
username: this.refs.username.value,
password: this.refs.password.value
});
}
}
handleAuthError(error) {
this.setState({error});
}
render() {
let actionText = null;
let error = null;
let headerText = null;
if (this.props.mode === 'login') {
actionText = 'Log In';
headerText = 'Login';
} else {
actionText = 'Create Account';
headerText = 'Create an Account';
}
if (!!this.state.error) {
error = (
<div className="form__row form__row--error">
<div className="form__column">
{this.state.error}
</div>
</div>
);
}
return (
<div className="form form--authentication">
<div className="form__wrapper">
<div className="form__row form__header">
<h1>{headerText}</h1>
</div>
<div className="form__row">
<div className="form__column">
<input className="textbox textbox--open" placeholder="Username"
ref="username" type="text" />
</div>
</div>
<div className="form__row">
<div className="form__column">
<input className="textbox textbox--open" placeholder="Password"
ref="password" type="password" />
</div>
</div>
{error}
</div>
<div className="form__actions">
<button className="button button--primary"
onClick={this.handleSubmitClick}>
{actionText}
</button>
</div>
</div>
);
}
}
@@ -0,0 +1,10 @@
import classnames from'classnames';
import React from'react';
import AuthForm from './AuthForm';
export default class LoginForm extends React.Component {
render() {
return <AuthForm mode="login" />;
}
}
@@ -0,0 +1,10 @@
import classnames from'classnames';
import React from'react';
import AuthForm from './AuthForm';
export default class RegistrationForm extends React.Component {
render() {
return <AuthForm mode="register" />;
}
}
@@ -1,4 +1,16 @@
const ActionTypes = {
AUTH_CREATE_USER_ERROR: 'AUTH_CREATE_USER_ERROR',
AUTH_CREATE_USER_SUCCESS: 'AUTH_CREATE_USER_SUCCESS',
AUTH_DELETE_USER_ERROR: 'AUTH_DELETE_USER_ERROR',
AUTH_DELETE_USER_SUCCESS: 'AUTH_DELETE_USER_SUCCESS',
AUTH_LIST_USERS_ERROR: 'AUTH_LIST_USERS_ERROR',
AUTH_LIST_USERS_SUCCESS: 'AUTH_LIST_USERS_SUCCESS',
AUTH_LOGIN_ERROR: 'AUTH_LOGIN_ERROR',
AUTH_LOGIN_SUCCESS: 'AUTH_LOGIN_SUCCESS',
AUTH_REGISTER_ERROR: 'AUTH_REGISTER_ERROR',
AUTH_REGISTER_SUCCESS: 'AUTH_REGISTER_SUCCESS',
AUTH_VERIFY_ERROR: 'AUTH_VERIFY_ERROR',
AUTH_VERIFY_SUCCESS: 'AUTH_VERIFY_SUCCESS',
CLIENT_ADD_TORRENT_ERROR: 'CLIENT_ADD_TORRENT_ERROR',
CLIENT_ADD_TORRENT_SUCCESS: 'CLIENT_ADD_TORRENT_SUCCESS',
CLIENT_CHECK_HASH_ERROR: 'CLIENT_CHECK_HASH_ERROR',
@@ -1,4 +1,16 @@
const EventTypes = {
AUTH_CREATE_USER_ERROR: 'AUTH_CREATE_USER_ERROR',
AUTH_CREATE_USER_SUCCESS: 'AUTH_CREATE_USER_SUCCESS',
AUTH_DELETE_USER_ERROR: 'AUTH_DELETE_USER_ERROR',
AUTH_DELETE_USER_SUCCESS: 'AUTH_DELETE_USER_SUCCESS',
AUTH_LIST_USERS_ERROR: 'AUTH_LIST_USERS_ERROR',
AUTH_LIST_USERS_SUCCESS: 'AUTH_LIST_USERS_SUCCESS',
AUTH_LOGIN_ERROR: 'AUTH_LOGIN_ERROR',
AUTH_LOGIN_SUCCESS: 'AUTH_LOGIN_SUCCESS',
AUTH_REGISTER_ERROR: 'AUTH_REGISTER_ERROR',
AUTH_REGISTER_SUCCESS: 'AUTH_REGISTER_SUCCESS',
AUTH_VERIFY_ERROR: 'AUTH_VERIFY_ERROR',
AUTH_VERIFY_SUCCESS: 'AUTH_VERIFY_SUCCESS',
CLIENT_ADD_TORRENT_ERROR: 'CLIENT_ADD_TORRENT_ERROR',
CLIENT_ADD_TORRENT_SUCCESS: 'CLIENT_ADD_TORRENT_SUCCESS',
CLIENT_SET_THROTTLE_ERROR: 'CLIENT_SET_THROTTLE_ERROR',
+145
View File
@@ -0,0 +1,145 @@
import AuthActions from '../actions/AuthActions';
import ActionTypes from '../constants/ActionTypes';
import AppDispatcher from '../dispatcher/AppDispatcher';
import BaseStore from './BaseStore';
import EventTypes from '../constants/EventTypes';
class AuthStoreClass extends BaseStore {
constructor() {
super();
this.token = null;
this.users = [];
}
authenticate(credentials) {
AuthActions.authenticate({
username: credentials.username,
password: credentials.password
});
}
createUser(credentials) {
AuthActions.createUser(credentials);
}
deleteUser(username) {
AuthActions.deleteUser(username);
}
fetchUserList() {
AuthActions.fetchUsers();
}
getToken() {
return this.token;
}
getUsers() {
return this.users;
}
handleCreateUserError(error) {
this.emit(EventTypes.AUTH_CREATE_USER_ERROR, error);
}
handleCreateUserSuccess(data) {
this.emit(EventTypes.AUTH_CREATE_USER_SUCCESS);
}
handleDeleteUserError(error) {
this.emit(EventTypes.AUTH_DELETE_USER_ERROR, error.username);
}
handleDeleteUserSuccess(data) {
this.emit(EventTypes.AUTH_DELETE_USER_SUCCESS, data.username);
}
handleListUsersError(error) {
this.emit(EventTypes.AUTH_LIST_USERS_ERROR);
}
handleListUsersSuccess(data) {
this.users = data;
this.emit(EventTypes.AUTH_LIST_USERS_SUCCESS);
}
handleLoginSuccess(data) {
this.emit(EventTypes.AUTH_LOGIN_SUCCESS);
this.token = data.token;
}
handleLoginError(error) {
this.token = null;
this.emit(EventTypes.AUTH_LOGIN_ERROR, error);
}
handleRegisterSuccess(data) {
this.emit(EventTypes.AUTH_REGISTER_SUCCESS, data);
}
handleRegisterError(error) {
this.emit(EventTypes.AUTH_REGISTER_ERROR, error);
}
register(credentials) {
AuthActions.register({
username: credentials.username,
password: credentials.password
});
}
verify() {
AuthActions.verify();
}
}
let AuthStore = new AuthStoreClass();
AuthStore.dispatcherID = AppDispatcher.register((payload) => {
const {action, source} = payload;
switch (action.type) {
case ActionTypes.AUTH_LOGIN_SUCCESS:
AuthStore.handleLoginSuccess(action.data);
break;
case ActionTypes.AUTH_LOGIN_ERROR:
AuthStore.handleLoginError(action.error);
break;
case ActionTypes.AUTH_LIST_USERS_SUCCESS:
AuthStore.handleListUsersSuccess(action.data);
break;
case ActionTypes.AUTH_LIST_USERS_ERROR:
AuthStore.handleListUsersError(action.error);
break;
case ActionTypes.AUTH_CREATE_USER_SUCCESS:
AuthStore.handleCreateUserSuccess(action.data);
break;
case ActionTypes.AUTH_CREATE_USER_ERROR:
AuthStore.handleCreateUserError(action.error.data);
break;
case ActionTypes.AUTH_DELETE_USER_SUCCESS:
AuthStore.handleDeleteUserSuccess(action.data);
break;
case ActionTypes.AUTH_DELETE_USER_ERROR:
AuthStore.handleDeleteUserError(action.error);
break;
case ActionTypes.AUTH_REGISTER_SUCCESS:
AuthStore.handleRegisterSuccess(action.data);
AuthStore.emit(EventTypes.AUTH_REGISTER_SUCCESS,
action.data);
break;
case ActionTypes.AUTH_REGISTER_ERROR:
AuthStore.handleRegisterError(action.error.data);
break;
case ActionTypes.AUTH_VERIFY_SUCCESS:
AuthStore.emit(EventTypes.AUTH_VERIFY_SUCCESS,
action.data);
break;
case ActionTypes.AUTH_VERIFY_ERROR:
AuthStore.emit(EventTypes.AUTH_VERIFY_ERROR,
action.error);
break;
}
});
export default AuthStore;
+9 -2
View File
@@ -10,6 +10,13 @@ export default class BaseStore extends EventEmitter {
this.setMaxListeners(20);
}
emit(eventName) {
super.emit(...arguments);
if (eventName == null) {
console.warn('Event is undefined!');
}
}
beginRequest(id) {
this.requests[id] = true;
}
@@ -19,7 +26,7 @@ export default class BaseStore extends EventEmitter {
}
isRequestPending(id) {
if (this.requests[id] == null || this.requests[id] === false) {
if (this.requests[id] == null) {
return false;
}
@@ -31,7 +38,7 @@ export default class BaseStore extends EventEmitter {
}
resolveRequest(id) {
this.requests[id] = false;
delete this.requests[id];
}
unlisten(event, callback) {
+1 -1
View File
@@ -178,7 +178,7 @@ class TorrentStoreClass extends BaseStore {
}
handleFetchTorrentsError(error) {
console.trace(error);
this.resolveRequest('fetch-torrents');
}
handleFetchTorrentsSuccess(torrents) {
@@ -59,11 +59,11 @@ class TransferDataStoreClass extends BaseStore {
handleSetThrottleSuccess(data) {
this.fetchTransferData();
this.emit(EventTypes.CLIENT_SET_THROTTLE_SUCCESS);
// this.emit(EventTypes.CLIENT_SET_THROTTLE_SUCCESS);
}
handleSetThrottleError(error) {
this.emit(EventTypes.CLIENT_SET_THROTTLE_ERROR);
// this.emit(EventTypes.CLIENT_SET_THROTTLE_ERROR);
}
handleTransferDataSuccess(transferData) {
+1
View File
@@ -3,6 +3,7 @@ const config = {
dbPath: './server/db/',
maxHistoryStates: 30,
pollInterval: 1000 * 5,
secret: 'flood',
scgi: {
host: 'localhost',
port: 5000,
+5 -2
View File
@@ -9,7 +9,8 @@
"dependencies": {
"axios": "^0.7.0",
"babel-polyfill": "^6.9.1",
"body-parser": "~1.12.0",
"bcrypt-nodejs": "0.0.3",
"body-parser": "^1.12.4",
"classnames": "^2.1.5",
"compression": "^1.6.1",
"cookie-parser": "~1.3.4",
@@ -28,9 +29,10 @@
"inuit-page": "^0.2.1",
"inuit-reset": "^0.1.1",
"isomorphic-fetch": "^2.1.1",
"jsonwebtoken": "^7.0.1",
"keymirror": "^0.1.1",
"lodash": "^4.3.0",
"morgan": "~1.5.1",
"morgan": "^1.5.3",
"multer": "^1.1.0",
"mv": "^2.1.1",
"nedb": "^1.7.2",
@@ -38,6 +40,7 @@
"object-assign": "^2.0.0",
"passport": "^0.3.2",
"passport-http": "^0.3.0",
"passport-jwt": "^2.1.0",
"pug": "^2.0.0-beta3",
"q": "^1.2.0",
"react": "^15.0.2",
+33 -42
View File
@@ -1,54 +1,47 @@
'use strict';
require('events').EventEmitter.defaultMaxListeners = Infinity;
var bodyParser = require('body-parser');
var compression = require('compression');
var cookieParser = require('cookie-parser');
var express = require('express');
var favicon = require('serve-favicon');
var logger = require('morgan');
var path = require('path');
let bodyParser = require('body-parser');
let compression = require('compression');
let cookieParser = require('cookie-parser');
let express = require('express');
let favicon = require('serve-favicon');
let morgan = require('morgan');
let passport = require('passport');
let path = require('path');
var clientRoutes = require('./routes/client');
var mainRoutes = require('./routes/index');
var app = express();
let app = express();
let apiRoutes = require('./routes/api');
let authRoutes = require('./routes/auth');
let Users = require('./models/Users');
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
// uncomment after placing your favicon in /assets
//app.use(favicon(__dirname + '/assets/favicon.ico'));
app.use(logger('dev'));
// TODO: Add favicon...
// app.use(favicon(__dirname + '/assets/favicon.ico'));
app.use(morgan('dev'));
app.use(passport.initialize());
app.use(compression());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'assets')));
app.use((req, res, next) => {
req.socket.on("error", (err) => {
console.trace(err);
});
res.socket.on("error", (err) => {
console.trace(err);
});
next();
});
require('./config/passport')(passport);
app.use('/', mainRoutes);
app.use('/client', clientRoutes);
app.use('/auth', authRoutes);
app.use('/api', apiRoutes);
// catch 404 and forward to error handler
// Catch 404 and forward to error handler.
app.use((req, res, next) => {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handlers
// development error handler
// will print stacktrace
// Development error handler, will print stacktrace.
if (app.get('env') === 'development') {
app.use((err, req, res, next) => {
res.status(err.status || 500);
@@ -57,17 +50,15 @@ if (app.get('env') === 'development') {
error: err
});
});
} else {
// Production error handler, no stacktraces leaked to user.
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
}
// production error handler
// no stacktraces leaked to user
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
module.exports = app;
+29
View File
@@ -0,0 +1,29 @@
'use strict';
let extractJWT = require('passport-jwt').ExtractJwt;
let jwtStrategy = require('passport-jwt').Strategy;
let config = require('../../config');
let Users = require('../models/Users');
// Setup work and export for the JWT passport strategy
module.exports = (passport) => {
let options = {
jwtFromRequest: extractJWT.fromAuthHeader(),
secretOrKey: config.secret
};
passport.use(new jwtStrategy(options, (jwtPayload, callback) => {
Users.lookupUser({username: jwtPayload.username}, (err, user) => {
if (err) {
return callback(err, false);
}
if (user) {
return callback(null, user);
}
return callback(null, false);
});
}));
};
+99
View File
@@ -0,0 +1,99 @@
'use strict';
let Datastore = require('nedb');
let bcrypt = require('bcrypt-nodejs');
let config = require('../../config');
class Users {
constructor() {
this.ready = false;
this.db = this.loadDatabase();
}
comparePassword(credentials, callback) {
this.db.findOne({username: credentials.username}).exec((err, user) => {
if (err) {
return callback(err);
}
// Username not found.
if (user == null) {
return callback(null, user);
}
bcrypt.compare(credentials.password, user.password, (err, isMatch) => {
if (err) {
return callback(err);
}
return callback(null, isMatch);
});
});
}
createUser(credentials, callback) {
if (!this.ready) {
callback({message: 'Users database is not ready.'});
}
bcrypt.genSalt(10, (err, salt) => {
if (err) {
callback(err);
return;
}
let username = credentials.username;
bcrypt.hash(credentials.password, salt, null, (err, hash) => {
if (err) {
callback(err);
return;
}
this.db.insert({username: username, password: hash}, (err, user) => {
if (err) {
callback(err);
return;
}
callback(null, {username: credentials.username});
});
});
});
}
initialUserGate(handlers) {
this.db.find({}, (err, users) => {
if (users && users.length > 0) {
return handlers.handleSubsequentUser();
}
return handlers.handleInitialUser();
});
}
loadDatabase() {
let db = new Datastore({
autoload: true,
filename: `${config.dbPath}authUsers.db`
});
db.ensureIndex({fieldName: 'username', unique: true});
this.ready = true;
return db;
}
lookupUser(credentials, callback) {
this.db.findOne({username: credentials.username}, (err, user) => {
if (err) {
return callback(err);
}
return callback(null, user);
});
}
}
module.exports = new Users();
+10 -20
View File
@@ -1,34 +1,24 @@
'use strict';
var express = require('express');
var passport = require('passport');
var router = express.Router();
var Strategy = require('passport-http').BasicStrategy;
let express = require('express');
let ajaxUtil = require('../util/ajaxUtil');
let client = require('../models/client');
let clientRoutes = require('./client');
let history = require('../models/history');
let passport = require('passport');
let router = express.Router();
let settings = require('../models/settings');
var users = require('../db/users');
history.startPolling();
passport.use(new Strategy(
(username, password, callback) => {
users.findByUsername(username, (err, user) => {
if (err) { return callback(err); }
if (!user) { return callback(null, false); }
if (user.password != password) { return callback(null, false); }
return callback(null, user);
});
}
));
router.use(passport.authenticate('jwt', {session: false}));
router.get('/', passport.authenticate('basic', { session: false }),
(req, res) => {
res.render('index', { title: 'Flood' });
}
);
router.use('/client', clientRoutes);
router.get('/', (req, res) => {
res.render('index', {title: 'Flood'});
});
router.get('/history', function(req, res, next) {
history.get(req.query, ajaxUtil.getResponseFn(res));
+61
View File
@@ -0,0 +1,61 @@
'use strict';
let express = require('express');
let jwt = require('jsonwebtoken');
let multer = require('multer');
let passport = require('passport');
let config = require('../../config');
let router = express.Router();
let Users = require('../models/Users');
// Allow unauthenticated registration if no users are currently registered.
router.use('/register', (req, res, next) => {
Users.initialUserGate({
handleInitialUser: next.bind(this),
handleSubsequentUser: passport.authenticate('jwt', {session: false}).bind(this, req, res, next)
});
});
router.post('/register', (req, res) => {
if(!req.body.username || !req.body.password) {
return res.json({success: false, message: 'Please enter username and password.'});
} else {
// Attempt to save the user
Users.createUser({
username: req.body.username,
password: req.body.password
}, (err, user) => {
if (err) {
return res.json({success: false, message: 'That username already exists.'});
}
return res.json({success: true, message: `Successfully created new user, ${user.username}.`});
});
}
});
router.post('/authenticate', (req, res) => {
let credentials = {
password: req.body.password,
username: req.body.username
};
Users.comparePassword(credentials, function(err, isMatch) {
if (isMatch == null) {
return res.send({success: false, message: 'Username not found.'});
}
if (isMatch && !err) {
// Create token if the password matched and no error was thrown.
let token = jwt.sign(credentials, config.secret, {
expiresIn: 60 * 60 * 24 * 7 // one week
});
return res.json({success: true, token: 'JWT ' + token});
} else {
return res.send({success: false, message: 'Authentication failed. Passwords did not match.'});
}
});
});
module.exports = router;
+1 -1
View File
@@ -2,12 +2,12 @@
let express = require('express');
let multer = require('multer');
let router = express.Router();
let ajaxUtil = require('../util/ajaxUtil');
let client = require('../models/client');
let clientUtil = require('../util/clientUtil');
let history = require('../models/history');
let router = express.Router();
let settings = require('../models/settings');
let upload = multer({