Notes on adding authentication to blog tutorial app on frontend and backend

Last updated on 22 Mar. 2024

Note: This post is a details-post for the post: Learning web app development through free online tutorials – Organized Notes and Log, https://raviswdev.blogspot.com/2024/03/learning-web-app-development-through.html .

In the Node Express tutorial John Smilga says, if I recall correctly, that it is irresponsible to deploy a backend server without authentication. His words resonated with me, as the backend server provides CRUD operations on cloud database, and a malicious user could create problems even though the probability of such malice happening for the Blog tutorial project is very low. But I did not want to go the whole hog of supporting user registration, user roles (admin and normal) and JWT as I felt it will become an overload for the student-learner.

So I came up with a sort-of middle path of using basic authentication along with https (SSL) and which is supported by cyclic.sh. The Cyclic hosting service allows one to create users and turn on basic authentication (or turn it off). That worked well for me as I did not have to do any coding in the backend.

On the frontend, I had to provide a set user credentials page instead of a login as in basic authentication, the user credentials are passed on every request.

Axios provides a way to set a common authorization header, which I used. Related code snippet (btoa does Base64 encoding of passed string and is used in Basic Auth, https://en.wikipedia.org/wiki/Basic_access_authentication ):

    const authdata = btoa(formUsername + ":" + password);
    axiosAPI.defaults.headers.common["Authorization"] = `Basic ${authdata}`;

I had to also set the authorization header in the useAxiosGet hook. Related code snippet (user.authdata is assigned above authdata):
        const res = await axios.get(url, {
          headers: {
            Authorization: `Basic ${user.authdata}`,
          },
          signal: controller.signal,
        });

That was enough to get the axios code working with Cyclic.sh hosting service provided basic authentication.

Error handling for unauthorized user required some changes. 

To be on safe side, I did not consider storing the password even with btoa on the client (local storage/cookie etc.). So the user has to key in username and password for every browser session (though passwords manager of browser can help out with auto-fill).

Testing out the basic authentication code and related error handling in frontend using local (PC) client to Cyclic.sh server resulted in the free quota of 1000 APIs per month to get exhausted quickly! That led me to explore implementing basic authentication in the Node express server, so that I could do such testing without using up free API quota of Cyclic.sh.

I used express-basic-auth node package and its custom async authorizer option to quite easily implement basic authentication support in the BlogAPI Express server. I also provided an easy way to enable or disable this basic authentication via an environment variable (ENABLE_AUTH). By default, authentication is disabled. If the environment variable is defined and has the value "Y", then authentication is enabled.

Initially, for testing purposes, I used hardcoded user and password in the Express server. Later I changed this to use a Users collection in MongoDB where the password was stored as a bcrypt hash. It needed the bcrypt node package. The associated code snippet is given below:
async function myAsyncAuthorizer(username, password, cb) {
  try {
    const user = await User.findOne({ username: username }).exec();
    if (user) {
      const match = await bcrypt.compare(password, user.password);
      if (match) {
        return cb(null, true);
      } else {
        return cb(null, false);
      }
    } else {
      return cb(null, false);
    }
  } catch (error) {
    return cb(null, false);
  }
}
----
I could have avoided some of the return cb(null, false) statements by using more complex if statement(s) but I chose to keep this code simple.

I added two usercmd app files, createuser.js to create a user&password entry in users collection and a checkuserpwd.js which checks whether a matching user and password entry is present in users collection, with the commands being passed command line arguments (easy to code in node.js instead of reading std. input) of username and password. The key bcrypt related code snippet from createuser.js is given below:
  const bcrypt = require("bcrypt");
  const saltRounds = 10;

  try {
    const hash = await bcrypt.hash(password, saltRounds);
    ...
----
The hash(ed password) is stored in MongoDB users collection.

My createuser.js standalone node program was not exiting after its work was done. The cause seemed to be that Mongoose db connection has to be closed for the node program to exit. I followed code example from a reference article provided in a later section in this post, and it worked. I have given the related code snippet below:

  try {
    const hash = await bcrypt.hash(password, saltRounds);
    const db = await connectDB(process.env.MONGO_URI);
    console.log("Just after await connectDB(). Presumably connected to DB ...");
    await createUser(username, hash);
    db.disconnect();
  } catch (err) {
    console.log(`${err}`);
  }

I later invested some more time in improving the user interface (of frontend) app for network error case which happens when the backend API server is down. To identify the network error case (in addition to identifying the 401 unauthorized  user error), I used the following code:
      try {
        const res = await axios.get(url, {
          headers: {
            Authorization: `Basic ${user.authdata}`,
          },
          signal: controller.signal,
        });
        setData(res.data);
        setAxiosGetError({
          message: "",
          unauthorized: false,
          networkError: false,
        });
        setIsLoading(false);
      } catch (error) {
        if (!controller.signal.aborted) {
          const errData = {
            message: error.message,
            unauthorized: false,
            networkError: false,
          };
          if (error.response && error.response.status === 401)
            errData.unauthorized = true;
          if (!error.response && error.code === "ERR_NETWORK") {
            errData.networkError = true;
          }
          setAxiosGetError(errData);
          setData([]);
          setIsLoading(false);
        }
      }
-----
In this manner, I was able to provide basic authentication for both front end and back end with lesser code and complexity than having JWT, login route, registration route, user roles etc. which I think would have been an overload for the student-learner for this beginner course. Of course, the student would need to learn JWT and the other features mentioned for real-life full stack projects, but he/she can do that on their own, after finishing this beginner course.
----

Additional reading

Input elements should have autocomplete attributes, https://stackoverflow.com/questions/54970352/input-elements-should-have-autocomplete-attributes

Authentication with React Router v6: A complete guide, https://blog.logrocket.com/authentication-react-router-v6/


https://mongoosejs.com/docs/connections.html#connection-events has some mention of connection close and disconnect but it is not very clear like above extract.

https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.connect()

https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.disconnect()

----

Reading input from terminal seems to be a somewhat complicated affair (promise/await) in node.js: https://nodejs.org/api/readline.html

Command line options are much more straightforward: How To Handle Command-line Arguments in Node.js Scripts, https://www.digitalocean.com/community/tutorials/nodejs-command-line-arguments-node-scripts

Getting original password from bcrypt hash is very difficult and so infeasible. See the post: How can I retrieve the real password from a hashed password?, https://stackoverflow.com/questions/39247917/how-can-i-retrieve-the-real-password-from-a-hashed-password .

Set a code/status for "Network Error" #383, https://github.com/axios/axios/issues/383

https://github.com/axios/axios#handling-errors

How to Check if Axios Call Fails due to No Internet Connection?, https://stackoverflow.com/questions/62061642/how-to-check-if-axios-call-fails-due-to-no-internet-connection/73047376

Comments

Archive