0

I am adding JWT authentication to a blog app I'm working on. On the server side (built with Nodejs) I am creating the token and sending it back with a successful login. On the client side I am saving the token in LocalStorage. When I log in and check the application tab in dev tools I can see the token. On the server route where blogs are posted to I check authentication. If the token is authenticated the blog posts to the database, but if I delete the token or change it and then make the post request the request fails, as expected.

So far so good.

What I'm confused about is how to restrict access to the page where the blog editor resides on the client. If people aren't authenticated they should not be able to access this page at all, even though if not authenticated they can't post anyway.

Login route on server:

router.post('/login', async (req, res, next) => {
    const cursor = User.collection.find({username: req.body.username}, {username: 1, _id: 1, password: 1});
    if(!(await cursor.hasNext())) {
        return res.status(401).json({ message: 'Cannot find user with that username' });
    }
    const user = await cursor.next();
    try {
    if(await bcrypt.compare(req.body.password, user.password)) {
        const token = jwt.sign({
            email: user.email,
            userId: user._id
        }, process.env.JWT_SECRET, { expiresIn: "1h" })
        return res.status(201).json({
            message: 'User Authenticated',
            token: token
        });
    } else {
        return res.status(400).json({ 
            authenticated: false,
            username: req.body.username,
            password: req.body.password
        })
    }
    } catch (err) {
        return res.status(500).json({ message: err })
    }
});

How I'm checking the token authentication on the server:

const jwt = require('jsonwebtoken');

module.exports = (req, res, next) => {
    try {
        const token = req.headers.authorization;
        console.log(token);
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.userData = decoded;
        next();
    } catch (error) {
        return res.status(401).json({ message: 'Auth Failed' })
    }

}

My client side login route fetch:

handleSubmit(event) {
        event.preventDefault();
        const formData = {
            username: event.target.username.value,
            password: event.target.password.value
        }
        fetch('http://localhost:4000/user/login', {
            method: "POST",
            mode: "cors",
            body: JSON.stringify(formData),
            headers: {
                "Content-Type": "application/json"
            }
        })
        .then(res => res.json())
        .then(res => {
            localStorage.setItem('authorization', res.token);
            console.log(res);
        })
        .catch(err => console.error(err)) 
    }

And here is my fetch call from the client on the blog posting route where the editor resides:

handleSubmit = (event) => {
      event.preventDefault();
      const data = new FormData(event.target);
      const body = event.target.postBody.value;
      const postTitle = event.target.title.value;

      console.log(event.target);
      console.log(data);
      console.log(event.target.postBody.value);

      fetch('http://localhost:4000/blog', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          "Authorization": localStorage.getItem('authorization')
        },
        mode: 'cors',
        body: JSON.stringify({
          title: postTitle,
          postBody: body
        })
      })
      .then(res => res.json())
      .then(err => console.error(err))
    }

So, like I said, everything is working as expected but I don't want people to be able to access the editor page if they are not authenticated. I guess I would check to see if the token exists in localstorage and then redirect? But wouldn't I also need to check to see if the token on the client can be authenticated on the server as well? So would I essentially need to post to the server to do the check whenever someone navigates to that page, or any other page I want to restrict access to? Come to think of it, if a user is already authenticated I don't want them to be able to access the login page either.

I have heard that people use Redux to manage state across components, but I really don't want to go down that road, at least not yet because this project is for learning purposes and I don't really want to start with Redux or anything else like that until I have a better grasp of React on it's own. I don't know if I need Redux or not and from what I understand, that's enough to know that I probably don't need it.

This is just such a different flow than I'm used to from PHP sessions and I'm having some trouble wrapping my head around it.

I realize that you folks may not really need to see all this code, but I also would like some more experienced eyes to see it and point out anywhere I might be making mistakes or where I could improve here.

Isaac
  • 59
  • 8
  • I'm not sure because I'm from Vue but does React have navigation guards ? If so, upon logging in, you can declare set variable in your state management of choice which shows what type of user the authenticated one is. And in the navigation guards you can put a meta of whether this route is for a normal user or a user with access to the editor's page – Karma Blackshaw Jan 14 '20 at 02:23
  • The easiest path would be Redux (or Context) if you plan on having multiple protected routes spread across your application tree. Otherwise, once the editor page is loaded, it should conditionally render a spinner (`loading...`) and send the token to the backend to be authenticated. Then, the backend can respond to the client with a simple `Boolean` (`true` =user validated or `false`=unauthorized user). The client can then consume this Boolean response and either load the page or redirect user to the login page. – Matt Carlotta Jan 14 '20 at 02:24
  • See examples here: https://stackoverflow.com/questions/53197248/how-to-handle-authentication-with-react-router/53197429#53197429 (local state and redux) or here: https://codesandbox.io/s/protected-route-root-context-ho0uj (context) – Matt Carlotta Jan 14 '20 at 02:24

1 Answers1

0

So this is what I have come up with for now, if anyone knows a better way, I'm definitely open to suggestions.

I created a class called CheckAuth which essentially just makes a GET request to the server and sends the jwt along with it.

checkAuth.js:

class CheckAuth {
    constructor() {
        this.auth = false;
    }

    async checkLogin() {
        console.log(localStorage.getItem("authorization"));
        let data = await fetch('http://localhost:4000/auth', {
            method: "GET",
            mode: "cors",
            headers: {
                "Content-Type": "application/json",
                "authorization": localStorage.getItem("authorization")
            }
        })
        return data.json();

    }

    logout(cb) {
        localStorage.removeItem('authenticated')
        this.auth = false;
        cb();
    }

    async isAuthenticated() {
        const data = await this.checkLogin()
        return data;
    }

}

export default new CheckAuth();

Then on pages that only logged in users should see I am doing a simple check to see if they have the token and if it's valid inside of componentDidMount().

componentDidMount() {
        const check = checkAuth.isAuthenticated();
        console.log(check);
        check.then(res => {
            console.log(res);
            if(res.authenticated !== true) {
                this.props.history.push("/login");
            }
        })
        .catch(err => { console.error(err) })
    }
Isaac
  • 59
  • 8