Skip to content

Change from basic auth to form based login #562

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

Merged
merged 12 commits into from
Nov 10, 2016
Merged
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
77 changes: 69 additions & 8 deletions Parse-Dashboard/Authentication.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
"use strict";
var bcrypt = require('bcryptjs');
var csrf = require('csurf');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;

/**
* Constructor for Authentication class
Expand All @@ -8,8 +12,61 @@
* @param {boolean} useEncryptedPasswords
*/
function Authentication(validUsers, useEncryptedPasswords) {
this.validUsers = validUsers;
this.useEncryptedPasswords = useEncryptedPasswords || false;
this.validUsers = validUsers;
this.useEncryptedPasswords = useEncryptedPasswords || false;
}

function initialize(app) {
var self = this;
passport.use('local', new LocalStrategy(
function(username, password, cb) {
var match = self.authenticate({
name: username,
pass: password
});
if (!match.matchingUsername) {
return cb(null, false, { message: 'Invalid username or password' });
}
cb(null, match.matchingUsername);
})
);

passport.serializeUser(function(username, cb) {
cb(null, username);
});

passport.deserializeUser(function(username, cb) {
var user = self.authenticate({
name: username
}, true);
cb(null, user);
});

app.use(require('connect-flash')());
app.use(require('body-parser').urlencoded({ extended: true }));
app.use(require('cookie-session')({
key : 'parse_dash',
secret : 'magic',
cookie : {
maxAge: (2 * 7 * 24 * 60 * 60 * 1000) // 2 weeks
}
}));
app.use(passport.initialize());
app.use(passport.session());

app.post('/login',
csrf(),
passport.authenticate('local', {
successRedirect: '/apps',
failureRedirect: '/login',
failureFlash : true
})
);

app.get('/logout', function(req, res){
req.logout();
res.redirect('/login');
});
}

/**
Expand All @@ -18,20 +75,22 @@ function Authentication(validUsers, useEncryptedPasswords) {
* @param {Object} userToTest
* @returns {Object} Object with `isAuthenticated` and `appsUserHasAccessTo` properties
*/
function authenticate(userToTest) {
let bcrypt = require('bcryptjs');

function authenticate(userToTest, usernameOnly) {
var appsUserHasAccessTo = null;
var matchingUsername = null;

//they provided auth
let isAuthenticated = userToTest &&
//there are configured users
this.validUsers &&
//the provided auth matches one of the users
this.validUsers.find(user => {
let isAuthenticated = userToTest.name == user.user &&
(this.useEncryptedPasswords ? bcrypt.compareSync(userToTest.pass, user.pass) : userToTest.pass == user.pass);
if (isAuthenticated) {
let isAuthenticated = false;
let usernameMatches = userToTest.name == user.user;
let passwordMatches = this.useEncryptedPasswords ? bcrypt.compareSync(userToTest.pass, user.pass) : userToTest.pass == user.pass;
if (usernameMatches && (usernameOnly || passwordMatches)) {
isAuthenticated = true;
matchingUsername = user.user;
// User restricted apps
appsUserHasAccessTo = user.apps || null;
}
Expand All @@ -41,10 +100,12 @@ function authenticate(userToTest) {

return {
isAuthenticated,
matchingUsername,
appsUserHasAccessTo
};
}

Authentication.prototype.initialize = initialize;
Authentication.prototype.authenticate = authenticate;

module.exports = Authentication;
73 changes: 55 additions & 18 deletions Parse-Dashboard/app.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'use strict';
const express = require('express');
const basicAuth = require('basic-auth');
const path = require('path');
const packageJson = require('package-json');
const csrf = require('csurf');
const Authentication = require('./Authentication.js');
var fs = require('fs');

const currentVersionFeatures = require('../package.json').parseDashboardFeatures;
Expand Down Expand Up @@ -58,22 +59,27 @@ module.exports = function(config, allowInsecureHTTP) {
app.enable('trust proxy');
}

const users = config.users;
const useEncryptedPasswords = config.useEncryptedPasswords ? true : false;
const authInstance = new Authentication(users, useEncryptedPasswords);
authInstance.initialize(app);

// CSRF error handler
app.use(function (err, req, res, next) {
if (err.code !== 'EBADCSRFTOKEN') return next(err)

// handle CSRF token errors here
res.status(403)
res.send('form tampered with')
});

// Serve the configuration.
app.get('/parse-dashboard-config.json', function(req, res) {
let response = {
apps: config.apps,
newFeaturesInLatestVersion: newFeaturesInLatestVersion,
};

const users = config.users;
const useEncryptedPasswords = config.useEncryptedPasswords ? true : false;

let auth = null;
//If they provide auth when their config has no users, ignore the auth
if (users) {
auth = basicAuth(req);
}

//Based on advice from Doug Wilson here:
//https://github.com/expressjs/express/issues/2518
const requestIsLocal =
Expand All @@ -90,12 +96,10 @@ module.exports = function(config, allowInsecureHTTP) {
return res.send({ success: false, error: 'Configure a user to access Parse Dashboard remotely' });
}

let Authentication = require('./Authentication');
const authInstance = new Authentication(users, useEncryptedPasswords);
const authentication = authInstance.authenticate(auth);

const successfulAuth = authentication.isAuthenticated;
const appsUserHasAccess = authentication.appsUserHasAccessTo;
const authentication = req.user;

const successfulAuth = authentication && authentication.isAuthenticated;
const appsUserHasAccess = authentication && authentication.appsUserHasAccessTo;

if (successfulAuth) {
if (appsUserHasAccess) {
Expand All @@ -111,9 +115,8 @@ module.exports = function(config, allowInsecureHTTP) {
return res.json(response);
}

if (users || auth) {
if (users) {
//They provided incorrect auth
res.set('WWW-Authenticate', 'Basic realm=Authorization Required');
return res.sendStatus(401);
}

Expand Down Expand Up @@ -146,8 +149,42 @@ module.exports = function(config, allowInsecureHTTP) {
}
}

app.get('/login', csrf(), function(req, res) {
if (!users || (req.user && req.user.isAuthenticated)) {
return res.redirect('/apps');
}
let mountPath = getMount(req);
let errors = req.flash('error');
if (errors && errors.length) {
errors = `<div id="login_errors" style="display: none;">
${errors.join(' ')}
</div>`
}
res.send(`<!DOCTYPE html>
<head>
<link rel="shortcut icon" type="image/x-icon" href="${mountPath}favicon.ico" />
<base href="${mountPath}"/>
<script>
PARSE_DASHBOARD_PATH = "${mountPath}";
</script>
</head>
<html>
<title>Parse Dashboard</title>
<body>
<div id="login_mount"></div>
${errors}
<script id="csrf" type="application/json">"${req.csrfToken()}"</script>
<script src="${mountPath}bundles/login.bundle.js"></script>
</body>
</html>
`);
});

// For every other request, go to index.html. Let client-side handle the rest.
app.get('/*', function(req, res) {
if (users && (!req.user || !req.user.isAuthenticated)) {
return res.redirect('/login');
}
let mountPath = getMount(req);
res.send(`<!DOCTYPE html>
<head>
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,17 @@
"LICENSE"
],
"dependencies": {
"basic-auth": "^1.0.3",
"bcryptjs": "^2.3.0",
"body-parser": "^1.15.2",
"commander": "^2.9.0",
"connect-flash": "^0.1.1",
"cookie-session": "^2.0.0-alpha.1",
"csurf": "^1.9.0",
"express": "^4.13.4",
"json-file-plus": "^3.2.0",
"package-json": "^2.3.1",
"bcryptjs": "^2.3.0"
"passport": "^0.3.2",
"passport-local": "^1.0.0"
},
"devDependencies": {
"babel-core": "~5.8.12",
Expand Down
2 changes: 1 addition & 1 deletion src/components/CSRFInput/CSRFInput.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import React from 'react';
// containing the CSRF token into a form
let CSRFInput = () => (
<div style={{ margin: 0, padding: 0, display: 'inline' }}>
<input name='authenticity_token' type='hidden' value={getToken()} />
<input name='_csrf' type='hidden' value={getToken()} />
</div>
);

Expand Down
65 changes: 65 additions & 0 deletions src/components/LoginForm/LoginForm.example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2016-present, Parse, LLC
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
import LoginForm from 'components/LoginForm/LoginForm.react';
import LoginRow from 'components/LoginRow/LoginRow.react';
import React from 'react';

export const component = LoginForm;

export const demos = [
{
render() {
return (
<div style={{ background: '#06283D', height: 500, position: 'relative' }}>
<LoginForm
header='Access your Dashboard'
footer={<a href='javascript:;'>Forgot something?</a>}
action='Log In'>
<LoginRow
label='Email'
input={<input type='email' />} />
<LoginRow
label='Password'
input={<input type='password' />} />
</LoginForm>
</div>
);
}
}, {
render() {
return (
<div style={{ background: '#06283D', height: 700, position: 'relative' }}>
<LoginForm
header='Sign up with Parse'
footer={
<div>
<span>Signing up signifies that you have read and agree to the </span>
<a href='https://parse.com/about/terms'>Terms of Service</a>
<span> and </span>
<a href='https://parse.com/about/privacy'>Privacy Policy</a>.
</div>
}
action='Sign Up'>
<LoginRow
label='Email'
input={<input type='email' placeholder='email@domain' autoComplete='off' />} />
<LoginRow
label='Password'
input={<input type='password' placeholder='The stronger, the better' autoComplete='off' />} />
<LoginRow
label='App Name'
input={<input type='text' placeholder='Name your first app' />} />
<LoginRow
label='Company'
input={<input type='text' placeholder='(Optional)' />} />
</LoginForm>
</div>
);
}
}
];
45 changes: 45 additions & 0 deletions src/components/LoginForm/LoginForm.react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2016-present, Parse, LLC
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
import CSRFInput from 'components/CSRFInput/CSRFInput.react';
import Icon from 'components/Icon/Icon.react';
import PropTypes from 'lib/PropTypes';
import React from 'react';
import styles from 'components/LoginForm/LoginForm.scss';
import { verticalCenter } from 'stylesheets/base.scss';

// Class-style component, because we need refs
export default class LoginForm extends React.Component {
render() {
return (
<div className={styles.login} style={{ marginTop: this.props.marginTop || '-220px' }}>
<Icon width={80} height={80} name='infinity' fill='#093A59' />
<form method='post' ref='form' action={this.props.endpoint} className={styles.form}>
<CSRFInput />
<div className={styles.header}>{this.props.header}</div>
{this.props.children}
<div className={styles.footer}>
<div className={verticalCenter} style={{ width: '100%' }}>
{this.props.footer}
</div>
</div>
<input
type='submit'
disabled={!!this.props.disableSubmit}
onClick={() => {
if (this.props.disableSubmit) {
return;
}
this.refs.form.submit()
}}
className={styles.submit}
value={this.props.action} />
</form>
</div>
);
}
}
Loading