Skip to content

Commit be9f498

Browse files
JeremyPleaseflovilmart
authored andcommitted
Change from basic auth to form based login (#562)
* Restore login related files (with some tweaks) from 9afc03b * Add eslint and editorconfig for linting and consistency * Clean up the login page and unused scss * Update server to respond to use passport users and server /login * Implement form based login with passport, passport-local, and cookie-session * Add log out button to footer * Disable CSRFInput for now as it isn't properly implemented * Remove unused basic-auth module * Revert "Add eslint and editorconfig for linting and consistency" This reverts commit 008092b. * Add two more test cases for Authentication.authenticate * Code clean up * Add proper CSRF handling to login through express and existing React components
1 parent c49b057 commit be9f498

File tree

16 files changed

+444
-40
lines changed

16 files changed

+444
-40
lines changed

Parse-Dashboard/Authentication.js

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
"use strict";
2+
var bcrypt = require('bcryptjs');
3+
var csrf = require('csurf');
4+
var passport = require('passport');
5+
var LocalStrategy = require('passport-local').Strategy;
26

37
/**
48
* Constructor for Authentication class
@@ -8,8 +12,61 @@
812
* @param {boolean} useEncryptedPasswords
913
*/
1014
function Authentication(validUsers, useEncryptedPasswords) {
11-
this.validUsers = validUsers;
12-
this.useEncryptedPasswords = useEncryptedPasswords || false;
15+
this.validUsers = validUsers;
16+
this.useEncryptedPasswords = useEncryptedPasswords || false;
17+
}
18+
19+
function initialize(app) {
20+
var self = this;
21+
passport.use('local', new LocalStrategy(
22+
function(username, password, cb) {
23+
var match = self.authenticate({
24+
name: username,
25+
pass: password
26+
});
27+
if (!match.matchingUsername) {
28+
return cb(null, false, { message: 'Invalid username or password' });
29+
}
30+
cb(null, match.matchingUsername);
31+
})
32+
);
33+
34+
passport.serializeUser(function(username, cb) {
35+
cb(null, username);
36+
});
37+
38+
passport.deserializeUser(function(username, cb) {
39+
var user = self.authenticate({
40+
name: username
41+
}, true);
42+
cb(null, user);
43+
});
44+
45+
app.use(require('connect-flash')());
46+
app.use(require('body-parser').urlencoded({ extended: true }));
47+
app.use(require('cookie-session')({
48+
key : 'parse_dash',
49+
secret : 'magic',
50+
cookie : {
51+
maxAge: (2 * 7 * 24 * 60 * 60 * 1000) // 2 weeks
52+
}
53+
}));
54+
app.use(passport.initialize());
55+
app.use(passport.session());
56+
57+
app.post('/login',
58+
csrf(),
59+
passport.authenticate('local', {
60+
successRedirect: '/apps',
61+
failureRedirect: '/login',
62+
failureFlash : true
63+
})
64+
);
65+
66+
app.get('/logout', function(req, res){
67+
req.logout();
68+
res.redirect('/login');
69+
});
1370
}
1471

1572
/**
@@ -18,20 +75,22 @@ function Authentication(validUsers, useEncryptedPasswords) {
1875
* @param {Object} userToTest
1976
* @returns {Object} Object with `isAuthenticated` and `appsUserHasAccessTo` properties
2077
*/
21-
function authenticate(userToTest) {
22-
let bcrypt = require('bcryptjs');
23-
78+
function authenticate(userToTest, usernameOnly) {
2479
var appsUserHasAccessTo = null;
80+
var matchingUsername = null;
2581

2682
//they provided auth
2783
let isAuthenticated = userToTest &&
2884
//there are configured users
2985
this.validUsers &&
3086
//the provided auth matches one of the users
3187
this.validUsers.find(user => {
32-
let isAuthenticated = userToTest.name == user.user &&
33-
(this.useEncryptedPasswords ? bcrypt.compareSync(userToTest.pass, user.pass) : userToTest.pass == user.pass);
34-
if (isAuthenticated) {
88+
let isAuthenticated = false;
89+
let usernameMatches = userToTest.name == user.user;
90+
let passwordMatches = this.useEncryptedPasswords ? bcrypt.compareSync(userToTest.pass, user.pass) : userToTest.pass == user.pass;
91+
if (usernameMatches && (usernameOnly || passwordMatches)) {
92+
isAuthenticated = true;
93+
matchingUsername = user.user;
3594
// User restricted apps
3695
appsUserHasAccessTo = user.apps || null;
3796
}
@@ -41,10 +100,12 @@ function authenticate(userToTest) {
41100

42101
return {
43102
isAuthenticated,
103+
matchingUsername,
44104
appsUserHasAccessTo
45105
};
46106
}
47107

108+
Authentication.prototype.initialize = initialize;
48109
Authentication.prototype.authenticate = authenticate;
49110

50111
module.exports = Authentication;

Parse-Dashboard/app.js

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use strict';
22
const express = require('express');
3-
const basicAuth = require('basic-auth');
43
const path = require('path');
54
const packageJson = require('package-json');
5+
const csrf = require('csurf');
6+
const Authentication = require('./Authentication.js');
67
var fs = require('fs');
78

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

62+
const users = config.users;
63+
const useEncryptedPasswords = config.useEncryptedPasswords ? true : false;
64+
const authInstance = new Authentication(users, useEncryptedPasswords);
65+
authInstance.initialize(app);
66+
67+
// CSRF error handler
68+
app.use(function (err, req, res, next) {
69+
if (err.code !== 'EBADCSRFTOKEN') return next(err)
70+
71+
// handle CSRF token errors here
72+
res.status(403)
73+
res.send('form tampered with')
74+
});
75+
6176
// Serve the configuration.
6277
app.get('/parse-dashboard-config.json', function(req, res) {
6378
let response = {
6479
apps: config.apps,
6580
newFeaturesInLatestVersion: newFeaturesInLatestVersion,
6681
};
6782

68-
const users = config.users;
69-
const useEncryptedPasswords = config.useEncryptedPasswords ? true : false;
70-
71-
let auth = null;
72-
//If they provide auth when their config has no users, ignore the auth
73-
if (users) {
74-
auth = basicAuth(req);
75-
}
76-
7783
//Based on advice from Doug Wilson here:
7884
//https://github.com/expressjs/express/issues/2518
7985
const requestIsLocal =
@@ -90,12 +96,10 @@ module.exports = function(config, allowInsecureHTTP) {
9096
return res.send({ success: false, error: 'Configure a user to access Parse Dashboard remotely' });
9197
}
9298

93-
let Authentication = require('./Authentication');
94-
const authInstance = new Authentication(users, useEncryptedPasswords);
95-
const authentication = authInstance.authenticate(auth);
96-
97-
const successfulAuth = authentication.isAuthenticated;
98-
const appsUserHasAccess = authentication.appsUserHasAccessTo;
99+
const authentication = req.user;
100+
101+
const successfulAuth = authentication && authentication.isAuthenticated;
102+
const appsUserHasAccess = authentication && authentication.appsUserHasAccessTo;
99103

100104
if (successfulAuth) {
101105
if (appsUserHasAccess) {
@@ -111,9 +115,8 @@ module.exports = function(config, allowInsecureHTTP) {
111115
return res.json(response);
112116
}
113117

114-
if (users || auth) {
118+
if (users) {
115119
//They provided incorrect auth
116-
res.set('WWW-Authenticate', 'Basic realm=Authorization Required');
117120
return res.sendStatus(401);
118121
}
119122

@@ -146,8 +149,42 @@ module.exports = function(config, allowInsecureHTTP) {
146149
}
147150
}
148151

152+
app.get('/login', csrf(), function(req, res) {
153+
if (!users || (req.user && req.user.isAuthenticated)) {
154+
return res.redirect('/apps');
155+
}
156+
let mountPath = getMount(req);
157+
let errors = req.flash('error');
158+
if (errors && errors.length) {
159+
errors = `<div id="login_errors" style="display: none;">
160+
${errors.join(' ')}
161+
</div>`
162+
}
163+
res.send(`<!DOCTYPE html>
164+
<head>
165+
<link rel="shortcut icon" type="image/x-icon" href="${mountPath}favicon.ico" />
166+
<base href="${mountPath}"/>
167+
<script>
168+
PARSE_DASHBOARD_PATH = "${mountPath}";
169+
</script>
170+
</head>
171+
<html>
172+
<title>Parse Dashboard</title>
173+
<body>
174+
<div id="login_mount"></div>
175+
${errors}
176+
<script id="csrf" type="application/json">"${req.csrfToken()}"</script>
177+
<script src="${mountPath}bundles/login.bundle.js"></script>
178+
</body>
179+
</html>
180+
`);
181+
});
182+
149183
// For every other request, go to index.html. Let client-side handle the rest.
150184
app.get('/*', function(req, res) {
185+
if (users && (!req.user || !req.user.isAuthenticated)) {
186+
return res.redirect('/login');
187+
}
151188
let mountPath = getMount(req);
152189
res.send(`<!DOCTYPE html>
153190
<head>

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,17 @@
3333
"LICENSE"
3434
],
3535
"dependencies": {
36-
"basic-auth": "^1.0.3",
36+
"bcryptjs": "^2.3.0",
37+
"body-parser": "^1.15.2",
3738
"commander": "^2.9.0",
39+
"connect-flash": "^0.1.1",
40+
"cookie-session": "^2.0.0-alpha.1",
41+
"csurf": "^1.9.0",
3842
"express": "^4.13.4",
3943
"json-file-plus": "^3.2.0",
4044
"package-json": "^2.3.1",
41-
"bcryptjs": "^2.3.0"
45+
"passport": "^0.3.2",
46+
"passport-local": "^1.0.0"
4247
},
4348
"devDependencies": {
4449
"babel-core": "~5.8.12",

src/components/CSRFInput/CSRFInput.react.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import React from 'react';
1212
// containing the CSRF token into a form
1313
let CSRFInput = () => (
1414
<div style={{ margin: 0, padding: 0, display: 'inline' }}>
15-
<input name='authenticity_token' type='hidden' value={getToken()} />
15+
<input name='_csrf' type='hidden' value={getToken()} />
1616
</div>
1717
);
1818

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright (c) 2016-present, Parse, LLC
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the LICENSE file in
6+
* the root directory of this source tree.
7+
*/
8+
import LoginForm from 'components/LoginForm/LoginForm.react';
9+
import LoginRow from 'components/LoginRow/LoginRow.react';
10+
import React from 'react';
11+
12+
export const component = LoginForm;
13+
14+
export const demos = [
15+
{
16+
render() {
17+
return (
18+
<div style={{ background: '#06283D', height: 500, position: 'relative' }}>
19+
<LoginForm
20+
header='Access your Dashboard'
21+
footer={<a href='javascript:;'>Forgot something?</a>}
22+
action='Log In'>
23+
<LoginRow
24+
label='Email'
25+
input={<input type='email' />} />
26+
<LoginRow
27+
label='Password'
28+
input={<input type='password' />} />
29+
</LoginForm>
30+
</div>
31+
);
32+
}
33+
}, {
34+
render() {
35+
return (
36+
<div style={{ background: '#06283D', height: 700, position: 'relative' }}>
37+
<LoginForm
38+
header='Sign up with Parse'
39+
footer={
40+
<div>
41+
<span>Signing up signifies that you have read and agree to the </span>
42+
<a href='https://parse.com/about/terms'>Terms of Service</a>
43+
<span> and </span>
44+
<a href='https://parse.com/about/privacy'>Privacy Policy</a>.
45+
</div>
46+
}
47+
action='Sign Up'>
48+
<LoginRow
49+
label='Email'
50+
input={<input type='email' placeholder='email@domain' autoComplete='off' />} />
51+
<LoginRow
52+
label='Password'
53+
input={<input type='password' placeholder='The stronger, the better' autoComplete='off' />} />
54+
<LoginRow
55+
label='App Name'
56+
input={<input type='text' placeholder='Name your first app' />} />
57+
<LoginRow
58+
label='Company'
59+
input={<input type='text' placeholder='(Optional)' />} />
60+
</LoginForm>
61+
</div>
62+
);
63+
}
64+
}
65+
];
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright (c) 2016-present, Parse, LLC
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the LICENSE file in
6+
* the root directory of this source tree.
7+
*/
8+
import CSRFInput from 'components/CSRFInput/CSRFInput.react';
9+
import Icon from 'components/Icon/Icon.react';
10+
import PropTypes from 'lib/PropTypes';
11+
import React from 'react';
12+
import styles from 'components/LoginForm/LoginForm.scss';
13+
import { verticalCenter } from 'stylesheets/base.scss';
14+
15+
// Class-style component, because we need refs
16+
export default class LoginForm extends React.Component {
17+
render() {
18+
return (
19+
<div className={styles.login} style={{ marginTop: this.props.marginTop || '-220px' }}>
20+
<Icon width={80} height={80} name='infinity' fill='#093A59' />
21+
<form method='post' ref='form' action={this.props.endpoint} className={styles.form}>
22+
<CSRFInput />
23+
<div className={styles.header}>{this.props.header}</div>
24+
{this.props.children}
25+
<div className={styles.footer}>
26+
<div className={verticalCenter} style={{ width: '100%' }}>
27+
{this.props.footer}
28+
</div>
29+
</div>
30+
<input
31+
type='submit'
32+
disabled={!!this.props.disableSubmit}
33+
onClick={() => {
34+
if (this.props.disableSubmit) {
35+
return;
36+
}
37+
this.refs.form.submit()
38+
}}
39+
className={styles.submit}
40+
value={this.props.action} />
41+
</form>
42+
</div>
43+
);
44+
}
45+
}

0 commit comments

Comments
 (0)