Skip to content

Instantly share code, notes, and snippets.

@luishrd
Last active June 5, 2022 21:50
Show Gist options
  • Select an option

  • Save luishrd/d914f89ead933526344cde9eb1f328c8 to your computer and use it in GitHub Desktop.

Select an option

Save luishrd/d914f89ead933526344cde9eb1f328c8 to your computer and use it in GitHub Desktop.

Revisions

  1. luishrd revised this gist May 17, 2018. 1 changed file with 4 additions and 1 deletion.
    5 changes: 4 additions & 1 deletion routes.js
    Original file line number Diff line number Diff line change
    @@ -77,6 +77,7 @@ function makeToken(user) {
    return jwt.sign(payload, secret, options);
    }

    /*
    function checkRole(role) {
    return function(req, res, next) {
    if (role === req.user.role) {
    @@ -86,6 +87,7 @@ function checkRole(role) {
    }
    };
    }
    */

    // routes
    module.exports = function(server) {
    @@ -108,7 +110,8 @@ module.exports = function(server) {
    });

    // 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, checkRole('user admin'), (req, res) => {
    server.get('/users', protected, (req, res) => {
    User.find()
    .select('username')
    .then(users => {
  2. luishrd revised this gist May 17, 2018. 6 changed files with 168 additions and 2 deletions.
    36 changes: 36 additions & 0 deletions App.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,36 @@
    // /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);
    62 changes: 62 additions & 0 deletions Signin.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,62 @@
    // /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;
    41 changes: 41 additions & 0 deletions Users.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,41 @@
    // /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;
    15 changes: 15 additions & 0 deletions index.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,15 @@
    // 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')
    );
    2 changes: 1 addition & 1 deletion notes.md
    Original file line number Diff line number Diff line change
    @@ -113,7 +113,7 @@ The players:

    ## Client Auth

    For routes:
    Routes:

    * /
    * /signup
    14 changes: 13 additions & 1 deletion routes.js
    Original file line number Diff line number Diff line change
    @@ -68,6 +68,7 @@ function makeToken(user) {
    sub: user._id,
    iat: timestamp,
    username: user.username,
    role: user.role,
    };
    const options = {
    expiresIn: '24h',
    @@ -76,6 +77,16 @@ function makeToken(user) {
    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) {
    @@ -96,7 +107,8 @@ module.exports = function(server) {
    res.status(200).json({ token: makeToken(req.user), user: req.user });
    });

    server.get('/users', protected, (req, res) => {
    // 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) => {
    User.find()
    .select('username')
    .then(users => {
  3. luishrd created this gist May 16, 2018.
    35 changes: 35 additions & 0 deletions User.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,35 @@
    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');
    128 changes: 128 additions & 0 deletions notes.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,128 @@
    # Auth JWT Mini Notes

    ## Modules.

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

    [JWTs](https://jwt.io/).

    * 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

    For routes:

    * /
    * /signup
    * /signin
    * /users

    Configure router

    * add react-router-dom
    * add routes

    Build the forms.
    109 changes: 109 additions & 0 deletions routes.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,109 @@
    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,
    };
    const options = {
    expiresIn: '24h',
    };

    return jwt.sign(payload, secret, options);
    }

    // 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 });
    });

    server.get('/users', protected, (req, res) => {
    User.find()
    .select('username')
    .then(users => {
    res.json(users);
    })
    .catch(err => {
    res.status(500).json(err);
    });
    });
    };
    19 changes: 19 additions & 0 deletions server.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,19 @@
    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'));