Home Dev Blog DevBlog#4: Adding Basic Authentication

DevBlog#4: Adding Basic Authentication

by wforbes

Quick catch-up: In the previous articles I described the web app I started working on – mainly based on this fantastic Docker tutorial. You can find my repository here: github.com/wforbes/node-docker, with ample step-by-step notes in the README file. Then, I added a Vue container into the mix and got it hot-reloading on my local dev environment. In the last post, I added Vuetify to the Vue container, made some initial UI changes and set up the structure of the files in the Vue project.

Ok here’s the meat of the work. The main goal today is to get functionality to:

  • Sign Up for a user account on the app, creating a new User on the API, and a new session on Redis
  • Persist the session with Redis (and understand that system more), so refreshing the page continues your logged in state
  • Display some User-only UI while logged in
  • Log out of the app, destroying the session with Express/Redis, and reverting back to a non-logged in front-end
  • Log in to the app using the credentials we signed up with, persisting the session
  • Test this stuff repeatedly and nail down some initial code for it

But first, I had to do a little ground-work to get started

Not Working Directly On The Vue Container

I’ve decided not to directly develop on the Vue container (the webpack hot-reload just takes too long). I think it’s possible that using Vite might improve that. So, I’ll look into experimenting with it soon. Either way, I turned off the Vue container in the dev docker-compose build. The Nginx container wouldn’t start successfully without it, so I commented out the Vue ‘frontend’ section of the Nginx configuration file.

Commit #59: “Nginx – Comment out frontend conf directives

Global Utility Mixin

I’ve got a utility mixin that I’ve been using throughout the Vue projects I work on that has a few functions I’ve come accustomed with using often.

One of them is an “isEmpty” function which lets me pass in just about any variable or object and get a clear boolean on whether it’s ’empty’. I know that it’s commonplace for JavaScript developers to simply check for truthiness when looking to see if something is null, undefined, or an empty string – but this doesn’t always hold for empty objects, empty arrays, and other scenarios.

Then there’s the use of lodash which is a fantastic utility library that gives you access to functions that do things like checking an object for a property (has), cloning an object with nested properties or arrays of objects (cloneDeep), intuitive equality checking between just about anything like arrays, objects, dates, errors, maps, numbers, regexes, etc. (isEqual), and so much more. I have these functions I commonly use all available in my utility mixin. This keeps me from having to import and use lodash directly, saving the code from refactor in the event that lodash or my use of it changes in the future.

Beyond those, the mixin has functions that I grew accustomed to having access to in other languages, like ucFirst and lcFirst. It has functions to remove spaces, set casing, and regex I commonly use.

I stripped application specific methods out of it and plopped it into this project, to be expanded as the project grows a need for it.

Commit #60: “Vue – add global utility mixin

Adding the Auth page

I had an authentication page (AuthPage.vue) in an old Vue project that would suit the needs here with a little refactoring. It has a Signup Form and a Login Form in a <v-tabs> Vuetify interface along with a bunch of basic validation.

A few extras, too. The Signup Form has a dialog for a ‘privacy policy’ type statement that opens with a link click. It also has a .vue View file for Logout and Verifying the signup with an email – but I won’t get the email verification step tonight.

Added the route for “/auth” that has alias routes to “/login”, “/signup”, “/verify”, “/logout”, along with some route props that the verification page will want. So, no matter which of those routes you navigate to, you’ll be hitting the AuthPage.vue and it will know what to display.

Added buttons to the right side of the App Bar to get to the new routes.

I also added some responsive Vuetify grid resizing on the App.vue <v-main> tag. It’s full width at the smallest size, then scales up to 10 columns at medium, 8 columns at large, and 6 columns at extra large screen size.

It’s all structured in the Feature-Based directory structure I harped on in the previous article:

client-vue
|--src
   |--auth
      |--components
      |  |--LoginForm.vue
      |  |--SignupForm.vue
      |  |--DataStatementDialog.vue
      |--store
      |  |--authModule.js
      |--views
         |--AuthPage.vue
         |--LogoutView.vue
         |--VerifyView.vue

Commit#61: “Vue – Add initial auth page and components

Enable Sign Ups and API calls

Commit#62: “Vue – enable basic signup from client app

Set up the store

With the pages added, I had to get the store initializing the data and objects needed to call the API. I’ve developed a pretty decent pattern in my Vue projects to get this done and it goes something like this:

  1. App.vue created() hook dispatches an action on the root Vuex store to “setupStore”, passing it the Vue instance.
    1. Passing the Vuex store the Vue instance is done because Vuex doesn’t have documented access to the Vue instance. There is a workaround using “this._vm” to do so, but it’s not available in Vuex 4 and since it’s not documented I don’t trust it completely for future-proof design. Instead, it makes sense to me to simply have
  2. setupStore action calls the initial set up functions:
    1. Set the Vue instance to a member of the state
    2. Set the Host so that the front-end knows what it’s address is and the address of any backend, we can also deliniate between development and production environment addresses. For instance, on my dev the frontend is at localhost:8080 and the backend is at localhost.
    3. Set the DataAccess object, and passes the Vue instance to it.
      1. This is a class module that acts as an interface between the store and whatever specific DataAccess class module is being used to interact with the backend, in this case it’s ‘ApiDataAccess’ that communicates with the Node server.
      2. This abstraction might not be required for most projects, but I expect to stand up other webservers with Docker here and it’d be nice to have the frontend flexible enough to work with any of them.
      3. This requires importing the DataAccess module to the store

API DataAccess pattern

Now the store will be able to use or provide the DataAccess object to any of it’s modules for API calls. These are located in /src/data/ directory.

The DataAccess.js class is pretty simple, meant to be as dumb as possible. It may make some decision about which class to use to make data related calls if there’s more than one, but for the most part it simply sets it’s dataContext, enforces the API calls by only exposing methods the store can use, and passes the calls through.

DataAccess.js acts as an interface between the Vuex store and the class making API calls

The ApiDataAccess.js class is what actually makes the API calls. We could have named it NodeDataAccess.js or something to specify which type of server it’s meant for, and that may be what it changes to after we add another web server.. but for now, this works.

It imports the Axios library to use for HTTP calls. It stores the Vue instance into a class member to have access to the store’s getters. It has HTTP related helper methods that use Axios to actually make calls, handle errors, and pass back the data it gets asynchronously via resolved promises.

The ApiDataAccess class that uses Axios to actually make the API calls

And for completeness here’s ApiDataAccess‘s post helper function..

The post() helper function in ApiDataAccess class

You’ll notice that how we handle the results of those GET and POST calls in the .then callback functions may vary between which web server it’s communicating with. Our Node server in this project passes back the “status” property in it’s data, and may have other indicators we can use to check for a successful call – like the HTTP status code itself. Either way, this pattern gives wiggle room to stand up a new dataContext object for a different data source and need to change as little as possible in the rest of the app to handle it.

Using these GET and POST helper methods is very simple. For the two first methods in this commit they simply pass in which endpoint and the data needed for the call.

The use of the helpers in the API call functions make it really easy

Username Taken Validation

Now in the Signup Form, there’s some validation on proper format of the email address along with rules for the username and password fields.

All pretty standard, but I wanted to also validate for whether or not the Username is already taken. That was a bit more tricky because the default Vuetify validation makes it hard to handle validation rules that come from asynchronous sources – like our API calls. This is what I did..

Our Username text-field has the default rules applied to it, along with that usernameIsUnique rule…

I have that and the error message string, the validation message string, and the rule boolean in the component’s data object..

I have a watcher on the newUsername property which will fire whenever the text-field v-model changes..

That validateUsernameExistence method is a bit twisty, but basically it will validate whether or not the username is taken and update that usernameIsUnique field accordingly – along with adding or removing the usernameValidationMsg error message for it. It won’t check if the username is too short to be valid or the store isn’t done setting up. It’s not the prettiest function and I left comments so I can clean it up later, but this is what it looks like…

You can see on line 259 there, it calls isUniqueUsername and awaits it’s resolve. That’s where the store is hit to actually call for the API data…

The rest of it goes through the Vuex store to DataAccess to Node and back again. For the sake of completeness, this it what that looks like…

/src/auth/store/authModule.js has the action
/src/data/ApiDataAccess.js has the API call, using the GET helper there
Node’s index.js picks up the /api/v1/users route
Nodes’ /routes/userRoutes.js picks up the /userExists/ route with the username
Finally, Node’s /controllers/usersController.js handles the .exists method, checking the database and responding accordingly

All of that ends up ensuring that Users can’t easily attempt to create duplicate usernames…

Persisting The Session

Signing up for an account will start a session on the server. It gives the client back a cookie with the session data that will be sent along with any future request by default for as long as that cookie is alive. That’s the goal. However, making that work took a bit more work.

Commit #63: Vue/Node – Persist session after signup and page refresh

CORS Whitelist

First I had to ensure that the client app gets the session cookie. Especially across different origins (localhost:8080 to localhost). This took setting a whitelist of domain origins and giving the Express CORS a couple options settings.

const whitelist = ["http://localhost", "http://localhost:8080"]
app.use(
	cors({ 
		origin: (origin, callback) => {
			if (whitelist.indexOf(origin) !== -1 || !origin) {
				callback(null, true);
			} else {
				callback(new Error("Not allowed CORS origin"));
			}
		},
		credentials: true
	})
);

So what’s going on there? I set up an array of whitelisted domains that should be able to access the app. That’s localhost and localhost port 8080 … the vue client app.

Here we need to set the ‘origin‘ property of the object handed to the cors() function as an anonymous function that matches the ‘origin’ of the request with a domain from the whitelist array. If it matches we call the ‘callback’ parameter function with a true value. If not, we call it with an Error object.

Credentials settings

You may also noticed the ‘credentials‘ property is set to to true there. This ensures that the JavaScript code in the vue client can see the server response credentials which are things like cookies, authorization headers, and TLS certs.

With the server acknowledging ‘credentials’ in the requests, the vue client needs to do the same. We do that by changing a setting on the axios object:

axios.defaults.withCredentials = true;

I added this line to the constructor of the ApiDataAccess class and just like that, the vue-client app could make Cross-Origin requests to the node server.

Read more about Credentials here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials

Add User Dialog Component

Now that our session is persisting after signing up, I’m adding a User Dialog component which opens when the little ‘user’ icon in the app bar is clicked:

It doesn’t do anything yet aside from opening and closing, but it will soon!

Checking for an active session

To make sure the vue client app knows that the user is logged in after refreshing the page or closing the browser and reopening it.. it makes sense to have it check to see if the server has an active session for it when it first starts up.

To do that, I added a vuex action that’s called during the app’s setup procedure named “initSession” which calls a dataaccess method called “checkSession” that returns a user object if there’s an active session on the server for this client.

...

initSession({ commit, dispatch, getters, rootState }) {
	commit({
		type: "setLoginStatus",
		status: getters.LOGIN_STATES.LOADING
	});
	return rootState.da.checkSession().then((responseData) => {
		if (!U.isEmpty(responseData.user)) {
			dispatch({
				type: "loginUser",
				user: responseData.user
			});
		} else {
			dispatch({
				type: "setLoginStatus",
				status: getters.LOGIN_STATES.OUT
			});
		}
		return Promise.resolve();
	});
},

...

Basically, if the user is already logged in with a session, we want the app to log the user in. On node, this also required adding a GET route for “/checkSession”, and an authController method that checks for a user object on the request’s session object

Router View Key To Force Component Reloads

I noticed that when on the Auth page (/signup or /login route), the URL in the browser bar didn’t always match up with the active tab on the page. Sometimes I’d be on the /login route, but the Sign Up tab was open. After a little experimenting, I found that if I add a :key attribute to the router-view tag of App.vue with a value consistent with the router’s path, it forces the view to reload correctly.

Commit #64: Vue – add key to router-view for auth page reloads

Then.. I fixed some little buggy mistakes (#65, #66, #67) …oops!

Add Dashboard route and NotLoggedIn view

Commit #68: Vue – Add dashboard and notloggedinview

A few commits ago, I added the “UserDialog” component which opens when you click the little ‘user’ icon in the right of the app bar, after signing up. Now I’ve added the Dashboard route and the “src/user/views/DashboardPage.vue” view. The “User Dashboard” link will take you to the Dashboard:

The DashboardPage.vue displaying from the /dashboard route

But what happens when you try to navigate to the /dashboard route when you aren’t logged in? I’ve added the src/app/components/NotLoggedInView which the DashboardPage.vue will display if a session isn’t active:

The NotLoggedInView.vue displaying from the /dashboard route when the user isn’t logged in

Redis Rolling and Resaving Options

Do we really want the user to have to log back in every time their session duration is over? That might be optimal for some apps, but in this one it makes the most sense to keep the session active as long as the user is actively using the app. We don’t want the user to be in the middle of something only to be forcibly logged out after the 30 minute session time is over, possibly losing progress in whatever they were doing!

To implement this I had to really read into the documentation for express-session. I found the rolling option, which “Force(s) the session identifier cookie to be set on every response”. However, since our saveUninitialized option is set to false, our session won’t be reset unless we initialize it in the node app during the request cycle. So, in my testing I found that setting the resave option to true will end up extending the session time as we’d like.

Commit #69 (nice): Node – implement rolling / resaving sessions

Implement Login/Logout Features

Commit #71: Vue/Node – Implement Login/Logout features

Pretty self-explanatory, you can check the commit changes for details and browse the files. I was getting pretty tired by this point, but the login page will now log you in to the app… the logout button in the User Dialog will log you out.

That’s All For Now… thanks for reading – I hope it helped!

You may also like

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.