my last tutorial on passportjs had some glaring inadequacies so i am rewriting it now for a MERN app with local strategy with the help of the Auther workshop from FSA.
my first attempt at this involved using passport-local-mongoose
but it kept dying horribly due to bad documentation so i ripped it out.
Step 1
npm i express-session passport passport-local bcrypt-nodejs
install body-parser
if you need it
Step 2
add UserSchema.js
:
const mongoose = require('mongoose');
var bcrypt = require('bcrypt-nodejs'); // works better on windows https://scotch.io/tutorials/easy-node-authentication-setup-and-local
const Schema = mongoose.Schema;
const userSchema = new Schema({
username : String,
password : String
});
// methods ======================
// generating a hash
userSchema.methods.generateHash = function(password) {
return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null);
};
// checking if password is valid
userSchema.methods.validPassword = function(password) {
return bcrypt.compareSync(password, this.password);
};
module.exports = mongoose.model('User', userSchema);
add auth.js
:
const passport = require('passport');
const LocalStrategy = require('passport-local');
const session = require('express-session');
const router = require('express').Router();
const User = require('./UserSchema')
router.use(session({
// this mandatory configuration ensures that session IDs are not predictable
secret: 'SUPERSECRETSECRET', // or whatever you like
// this option is recommended and reduces session concurrency issues
resave: false,
saveUninitialized: true,
cookie: { secure: true } // requires HTTPS
}));
router.use(passport.initialize());
router.use(passport.session());
// http://www.passportjs.org/docs/username-password/
passport.use(new LocalStrategy(
function(username, password, done) {
User.findOne({ username: username }, function(err, user) {
if (err) { return done(err); }
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
console.log('user', user)
if (!user.validPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
}
));
passport.serializeUser(function(user, done) {
console.log('serialized', user)
done(null, user._id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
console.log('deserialized', user)
console.log('deserialized err', err)
done(err, user);
});
});
router.get('/auth/me', (req, res) => {
console.log('req.user', req.user)
res.json(req.user)
})
router.post('/auth/signup',
async (req, res, next) => {
const newUser = new User(req.body)
newUser.password = newUser.generateHash(newUser.password)
const result = await newUser.save()
.then(user => req.login(user, err => err ? next(err) : res.json(user)))
.catch(next)
},
passport.authenticate('local'),
(req, res) => res.json(req.user)// success
);
router.post('/auth/login',
passport.authenticate('local'),
(req, res) => res.json(req.user) // success
);
router.post('/auth/logout',
(req, res) => {
req.logout()
res.json(null)
}
);
module.exports = router;
in index.js
:
app.use(require('./auth')) // needs to be after bodyParser
Step 3
now for the clientside. a very simple implementation in React in App.js:
import React, { Component } from "react";
import logo from "./logo.svg";
import { ToastContainer, toast } from 'react-toastify';
import "./App.css";
class App extends Component {
state = { polls: null, message: "", newpollname: "", user: null };
componentDidMount() {
fetch("/api/hello")
.then(res => res.json())
.then(res => this.setState({ message: res.express }));
fetch("/auth/me")
.then(res => res.json())
.then(user => this.setState({ user: user }))
.catch(err => console.log('no current user ', err))
this.getPolls()
}
getPolls = () => fetch("/api/polls")
.then(res => res.json())
.then(res => (res ? this.setState({ polls: res }) : null));
clickHandler = poll => () => {
console.log('poll', poll)
fetch("/api/polls", {
method: "POST",
body: JSON.stringify({poll}),
headers: new Headers({
'Content-Type': 'application/json'
})
}).then(this.getPolls)
};
deletePoll = poll => () => {
fetch("/api/polls?_id=" + poll._id, { method: "DELETE"});
this.setState({polls: this.state.polls && this.state.polls.filter(p => p._id !== poll._id)})
toast.success( "Deleted poll!")
}
textchange = e => {
e.preventDefault()
this.setState({newpollname: e.target.value})
}
addNewPoll = () => {
this.setState({polls: this.state.polls.concat({
name: this.state.newpollname,
counter: 0,
id: null
})})
}
textHandler = fieldName => e => this.setState({[fieldName]: e.target.value})
authHandler = authAction => () => {
fetch('/auth/' + authAction,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username:this.state.username,
password: this.state.password
})
}
)
.then(res => res.json())
.then(user => {
this.setState({ user })
toast.success(authAction + " success!")
})
.catch(console.log)
}
render() {
const {polls, user} = this.state
return (
<div className="App">
<ToastContainer />
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<hr />
{user ?
<div>Logged in as: {user.username} <button onClick={this.authHandler('logout')}>Logout</button></div>:
<div>
<h1>login</h1>
<div>
<label>Username:</label>
<input type="text" name="username" onChange={this.textHandler("username")}/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password" onChange={this.textHandler("password")}/>
</div>
<div>
<button onClick={this.authHandler('login')}>Log In</button>
<button onClick={this.authHandler('signup')}>Sign Up</button>
</div>
</div>
}
<hr />
<p>Message from the server: {this.state.message}</p>
<p className="App-intro">SunburstJS</p>
<ul>
{polls && polls.map((poll, i) => {
return <li key={i}>{poll.name}: {poll.counter}
<button onClick={this.clickHandler(poll)}>click me</button>
<button onClick={this.deletePoll(poll)}>delete me</button>
</li>
})}
</ul>
<input onChange={this.textchange} value={this.state.newpollname} placeholder="name of poll" />
<button onClick={this.addNewPoll}>add new poll</button>
</div>
);
}
}
export default App
i ran into a gnarly problem where the app wasnt saving down my session cookies. urgh.