Skip to content

Instantly share code, notes, and snippets.

@hillal20
Forked from luishrd/App.js
Created May 17, 2018 18:30
Show Gist options
  • Save hillal20/4fe35dc4556402ac965426c1381a0894 to your computer and use it in GitHub Desktop.
Save hillal20/4fe35dc4556402ac965426c1381a0894 to your computer and use it in GitHub Desktop.
JWT Server, all other files remain the same
// /src/App.js
import React, { Component } from 'react';
import { Route, withRouter } from 'react-router-dom';
import logo from './logo.svg';
import './App.css';
import Signin from './auth/Signin';
import Users from './users/Users';
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
{localStorage.getItem('token') && (
<button onClick={this.signout}>Sign out</button>
)}
</header>
<Route path="/signin" component={Signin} />
<Route path="/users" component={Users} />
</div>
);
}
signout = () => {
localStorage.removeItem('token');
this.props.history.push('/signin');
};
}
export default withRouter(App);
// React Application: /client/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById('root')
);

Auth JWT Mini Notes

Modules.

  • server side auth using JWTs with passport and jsonwebtoken npm modules.
  • client side auth with JWT/local storage.

JWTs.

  • An open standard.
  • No need for session store or cookies.

Token is sent every time, because now we're stateless (no session storage on the server) like HTTP.

The token will be stored on the client, possibly inside local storage (key-value store for the browser).

Overview of the Process:

  • user signs up.
  • server hashes user password and store the user in the database.
  • server creates JWT token (encrypted and signed using secret).
  • send JWT token back to the client as part of the response.
  • client stores the token.
  • client sends token on every request.
  • server verifies token and denies or provide access to resource.

Many different ways of doing this with Express and MongoDB.

We'll use Mongoose models. We could use schema.methods. We could use schema.statics. We could use middleware (tied to lifecycle events).

We'll use middleware to hash the passwords. We'll hash them on schema.pre('save', function(next) { //here }.

Passport is the go to library for auth in Node.js.

Authentication

  • client sends credentials
  • server verify credentials
  • server sends back token
  • client stores the token
  • client sends token on every request
  • server verifies that token is valid
  • server provides access to resource

Cookies

  • automatically included on every request
  • unique to each domain + device pair
  • cannot be sent to a different domain
  • sent in the cookey header
  • has a body that can have extra identifying information
  • max size around 4KB

Tokens

  • have to be wire them up manually on both server and client
  • sent inside the Authentication header
  • can be sent to any domain. Important when your client and server are deployed to different servers/domains.
  • larger site limit than cookies (research the size)

JWTs

  • on successful register or login, take user id + server secret to generate jwt.
  • on request for protected resource, take jwt + server secret to decode token and optain user id.
  • the tree methods we'll use are: sign, verify and decode.

When signing the token, sub refers to the subject (who is this token about) and iat means issued at time and will be included by default.

Passport

Authentication middleware. More of an ecosystem of strategies.

  • install passport and strategies(passport-local, passport-jwt, etc).
  • configure and use a strategy (a kind of plugin)
    • in the case of passport-jwt we need to tell the strategy where to find the payload and the secret
  • use passport to generate express middleware for the protected routes
    • const protected = passport.authenticate('jwt', { session: false });
  • payload comes from the token payload
  • Add the protected middleware to any endpoints that need it.

for users: server.get('/api/users', protected, function(homies...) {}

To test it:

  • sign up.
  • copy the returned toke (without the quotes)
  • add the token to the Authorization header. Note that removing the Bearer part will make it fail. Seems like postman normalizes the casing, so Bearer or bearer + space + token both work.
  • hitting the users route should now need the token.

Login

  • add a local strategy to let the user authenticate using username and password.
  • install passport-local strategy module/po
  • inside the the local strategy config function use the method to verify password against database (not written yet)
  • add the method to the .methods object on the user model.
  • tell passport to use that strategy for login: passport.use(localStrategy).
  • define another middleware for the local strategy: const authenticate = passport.authenticate('local', { session: false });
  • add it to the login route server.post('/api/login', authenticate, (req, res) => {});. Here we just need to provide the token, because by the time they hit this route, they already authenticated.
  • test it on postman and the token should be returned

Three scenarios:

  • register
  • login
  • access protected resource

The players:

  • Register uses jsonwebtoken module.
  • Login uses the local strategy (passport-local) to verify username and password on login, add the user to the req object and provide a token on success.
  • access a protected resource: The jwt strategy is used to extract token from header and decode it and use the payload to get the user information from the database and add it to req.user.
  • For jwt signing and decoding the tokens we use jsonwebtoken.

Client Auth

Routes:

  • /
  • /signup
  • /signin
  • /users

Configure router

  • add react-router-dom
  • add routes

Build the forms.

const jwt = require('jsonwebtoken');
const passport = require('passport');
const LocalStrategy = require('passport-local');
const JwtStrategy = require('passport-jwt').Strategy;
const { ExtractJwt } = require('passport-jwt');
const User = require('../users/User');
const secret = 'that is what I shared yesterday lol';
const localStrategy = new LocalStrategy(function(username, password, done) {
User.findOne({ username })
.then(user => {
if (!user) {
done(null, false);
} else {
user
.validatePassword(password)
.then(isValid => {
if (isValid) {
const { _id, username } = user;
return done(null, { _id, username }); // this ends in req.user
} else {
return done(null, false);
}
})
.catch(err => {
return done(err);
});
}
})
.catch(err => done(err));
});
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: secret,
};
const jwtStrategy = new JwtStrategy(jwtOptions, function(payload, done) {
// here the token was decoded successfully
User.findById(payload.sub)
.then(user => {
if (user) {
done(null, user); // this is req.user
} else {
done(null, false);
}
})
.catch(err => {
done(err);
});
});
// passport global middleware
passport.use(localStrategy);
passport.use(jwtStrategy);
// passport local middleware
const passportOptions = { session: false };
const authenticate = passport.authenticate('local', passportOptions);
const protected = passport.authenticate('jwt', passportOptions);
// helpers
function makeToken(user) {
const timestamp = new Date().getTime();
const payload = {
sub: user._id,
iat: timestamp,
username: user.username,
role: user.role,
};
const options = {
expiresIn: '24h',
};
return jwt.sign(payload, secret, options);
}
/*
function checkRole(role) {
return function(req, res, next) {
if (role === req.user.role) {
next();
} else {
res.status(403).send('you have no power here');
}
};
}
*/
// routes
module.exports = function(server) {
server.get('/', function(req, res) {
res.send({ api: 'up and running' });
});
server.post('/register', function(req, res) {
User.create(req.body) // new User + user.save
.then(user => {
const token = makeToken(user);
res.status(201).json({ user, token });
})
.catch(err => res.status(500).json(err));
});
server.post('/login', authenticate, (req, res) => {
// if we're here the user logged in correctly
res.status(200).json({ token: makeToken(req.user), user: req.user });
});
// having checkRole here will not let you access that unless the user has the role of user admin.
// server.get('/users', protected, checkRole('user admin'), (req, res) => {
server.get('/users', protected, (req, res) => {
User.find()
.select('username')
.then(users => {
res.json(users);
})
.catch(err => {
res.status(500).json(err);
});
});
};
const express = require('express');
const db = require('./_config/db');
const setupMiddleware = require('./_config/middleware');
const setupRoutes = require('./_config/routes');
const server = express();
db
.connectTo('jwtauth')
.then(() => console.log('\n... API Connected to jwtauth Database ...\n'))
.catch(err => {
console.log('\n*** ERROR Connecting to MongoDB, is it running? ***\n', err);
});
setupMiddleware(server);
setupRoutes(server);
server.listen(5500, () => console.log('\n=== API running on port 5500 ===\n'));
// /src/auth/Signin.js
import React from 'react';
import axios from 'axios';
class Signin extends React.Component {
state = {
username: 'samwise',
password: 'pass',
};
render() {
return (
<form onSubmit={this.submitHandler}>
<div>
<label htmlFor="username" />
<input
name="username"
value={this.state.username}
onChange={this.inputChangeHandler}
type="text"
/>
</div>
<div>
<label htmlFor="password" />
<input
name="password"
value={this.state.password}
onChange={this.inputChangeHandler}
type="password"
/>
</div>
<div>
<button>Sign in</button>
</div>
</form>
);
}
inputChangeHandler = event => {
const { name, value } = event.target;
this.setState({ [name]: value });
};
submitHandler = event => {
event.preventDefault();
axios
.post('http://localhost:5500/login', this.state)
.then(response => {
localStorage.setItem('token', response.data.token);
this.props.history.push('/users');
})
.catch(err => {
localStorage.removeItem('token');
});
};
}
export default Signin;
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
lowercase: true,
},
password: {
type: String,
required: true,
minlength: 4, // make this at least 12 in production
},
});
userSchema.pre('save', function(next) {
return bcrypt
.hash(this.password, 10)
.then(hash => {
this.password = hash;
return next();
})
.catch(err => {
return next(err);
});
});
userSchema.methods.validatePassword = function(passwordGuess) {
return bcrypt.compare(passwordGuess, this.password);
};
module.exports = mongoose.model('User', userSchema, 'users');
// /src/users/Users.js
import React from 'react';
import axios from 'axios';
class Users extends React.Component {
state = {
users: [],
};
render() {
return (
<ul>
{this.state.users.map(user => <li key={user._id}>{user.username}</li>)}
</ul>
);
}
componentDidMount = event => {
const token = localStorage.getItem('token');
const authToken = `Bearer ${token}`;
const requestOptions = {
headers: {
Authorization: authToken,
},
};
axios
.get('http://localhost:5500/users', requestOptions)
.then(response => {
this.setState({ users: response.data });
})
.catch(err => {
this.props.history.push('/signin');
});
};
}
export default Users;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment