Search code examples
javascriptreactjsnode.jsdatabasemariadb

Authentification fallback while re-login when a session expires using React, Nodejs and Mariadb database


this is my first issue I am posting here so please bear with me :D I am creating a social app, where currently I am implementing fallback when my database is off. In my Login.jsx component I am fetching my data do the backend like this:

fetch(getApiUrl('/auth/login'), {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(values),
  credentials: 'include',
  })
  .then(res => {
    const jsn = res.json();
    if (res.ok) {
      handleAuthLogin(navigate, revalidate)(jsn);
    } else if (jsn.message) {
     toast.error(<div className='text-lg'>{jsn.message}</div>);
    }
  })
  .catch((err) => {
    console.error(err);
    if (err.message === 'Failed to fetch') {
      navigate('/service-unavailable');
    } else if (err.message === 'Invalid token' || err.message === 'Unauthorized') {
      navigate('/login');
    } else {
      toast.error(<div className='text-lg'>Failed to connect to the server. Please check your connection.</div>);
    }
  });

Then in my backend, I am using JWT to hash the password. Here is just a code snippet from my index.js file:

/**
 * Middleware for handling authentication routes.
 * All routes starting with '/auth' will be handled by the authRouter.
 */
app.use(checkToken);
app.use('/auth', authRouter);
app.use(userRouter);

// Set up the /users endpoint
app.get('/users', async (req, res) => {
    const { name } = req.query.name;

    // Query the database using Prisma
    const users = await prisma.user.findMany({
        where: {
            name: {
                contains: name,
            },
        },
    });

    // Send the result back to the client
    res.json(users);
});

and the checkToken component:

const checkToken = (req, res, next) => {
    const token = req.headers['authorization'];
    console.log('token: ', token);

    if (!token) {
        return res.status(403).send({ message: 'No token provided.' });
    }

    try {
        const decoded = decodeJwt(token);
        req.userId = decoded.id;
        next();
    } catch (error) {
        return res.status(401).send({ message: 'Unauthorized! Session expired.' });
    }
};

as well as the form verification:

const formVerification = (req, res, action) => {
  // Extract the data from the request body
  const data = req.body;
  console.log(data);

  // Validate the data against the schema
  schema
    .validate(data)
    .catch(err => {
      // If validation fails, send a 422 status code and the validation error messages
      res.status(422).json({ status: 422, message: err.errors });
    })
    .then(valid => {
      if (valid) {
        const cred = {
          usr: data.username,
          pwd: data.password
        };

        if (action === "login") {
          // Login handle
          handleLogin(cred, req, res);
        } else if (action === "signup") {
          // Signup handle
          handleSignup(cred, req, res);
        } else {
          throw new Error("Invalid action");
        }
      } else {
        res.status(400).json({ status: 400, message: "Invalid request" });
      }
    });
}

handleLogin component with encoding jwt:

const hashPwd = (pwd) => {
    return createHash("md5").update(pwd).digest("hex");
};

const encodeJwt = ({ id, name }) => {
    return sign({ id, name }, secret, { expiresIn: "1h" });
};

const decodeJwt = (jwt) => {
    try {
        return verify(jwt, secret);
    } catch (e) {
        throw new Error('Failed to decode JWT: ' + e.message);
    }
};
const handleLogin = ({ usr, pwd }, req, res) => {
    console.log('usr: ', usr, 'pwd: ', pwd);
    getUser(usr, (err, user) => {
        if (err) {
            console.error(err);
            return res.status(500).json({ status: 500, message: "Database is offline" });
        }
        console.log('user: ', user);
        if (!user) {
            return res.status(404).json({ status: 404, message: "User not found" });
        } else if (user.credentials.hash !== hashPwd(pwd)) {
            return res.status(401).json({ status: 401, message: "Invalid password" });
        } else {
            return res.status(200).json({ status: 200, jwt: encodeJwt(user), user: { id: user.id, name: user.name } });
        }
    }, true);
};

You may realize now that this is hard to pinpoint where exactly is the error and the code is a little large, so I'll just link my repo for the whole code: https://github.com/Patri22k/social-app.

Finally, what is the error? On the mount I have an error user.js:29 GET http://localhost:5000/user/@me 401 (Unauthorized). Oh, btw this is user.js file:

// No token stored in local store
const NO_TOKEN_ERR = "no_token";

const UserContext = createContext(null);
const UserProvider = ({ children }) => {
    const [user, setUser] = useState(null);
    const [fetching, setFetching] = useState(false);
    const [error, setError] = useState(null);
    
    // Obtain new data
    const revalidate = () => {
        // Obtain local JWT val
        const jwt = localStorage.getItem("jwt");
        if (!jwt) {
            setError(NO_TOKEN_ERR);
            return;
        }
        const init = {
            method: "GET",
            headers: {
                'Authorization': `Bearer ${jwt}`
            }
        }
        // Fetch that thing
        fetch(getApiUrl("/user/@me"), init) // line 29
            .then(res => res.json())
            .then(({ user }) => setUser(user))
            .catch((e) => setError(e.message))
            .finally(() => setFetching(false))
    }
    // rest of the code
}

and after I pass any value to the form, the token is undefined and I got an error POST http://localhost:5000/auth/login 403 (Forbidden). But on the mount there is a hashed token. What I want to achieve? I'd like to have a fallback when my database is off, it will redirect to the /service-unavailable url. That's it. When it's on, it should work as any social app with form validation.


Solution

  • If you need to access the token using const token = req.headers['authorization']; you need to pass the token manually in the POST request headers like this:

    fetch(getApiUrl('/auth/login'), {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${your_token}`
        },
        body: JSON.stringify(values),
        credentials: 'include',
    })
    

    And in your backend you need to remove the 'Bearer ', don't forget it!

    const token = req.headers['authorization'].split(' ')[1]
    

    Just seting: credentials: 'include' will not put the token in its correct place which is why your token is undefined and the http status is 401, like you programed it to be.

    Don't worry about the error being something so simple; I've spent too many days until I realized this kind of thing in my codes... The answers are always in the things that we would never have thought we could get wrong.

    Hope it helps ;)