Security

The story so far

Groovy! We have all of the CRUD. We are the CRUDiest. CRUDtastic.

One thing left: security. More specifically, we'll talk about authorization, or user permissions. This is one part of security, but an important part.

Earlier, you learned how to add Edit and Delete buttons to every record. Trouble is, that lets anyone delete your data. They go to the right URL, and keep clicking the Delete button.

Remember, normally DBs are kept on servers, so if one persons deletes data, it's gone for everyone.

Let's add some authorization.

Flag variable

Let's keep it simple. We'll have a variable called loggedIn. It will have "yes" in it, if a user with admin permissions is logged in.

Pattern

Flag

Situation:
You want to keep track of whether something happened. For example, whether one of several input errors happened.
Actions:
Use a variable as a flag. Write code that initializes it, sets it if some condition occurs, then checks it.

We need four things:

  1. Login form: set loggedIn to "yes" if the user gives the right username and password.
  2. A menu to show the log in and out links.
  3. Logout: erase loggedIn.
  4. On every admin page: read loggedIn. If it isn't "yes", leave the page.

Different pages will be using the variable loggedIn. The login form will use it, the logout mechanism (however we do that), and every admin page. Admin pages include at least:

  • The admin record list page.
  • The add/edit page.
  • The database initialization page.

We need a way to share a variable across pages. We know how to use localStorage, so let's use that again.

Let's make just the authorization part of an app about sloths.

Sloth

Image from Wikipedia.

Our app will track sloths in a sloth sanctuary, also known as a slotel. You can try the app.

Files

Here are our files:

Files

The library files are as usual. So is the main index.html. It's the public list file.

admin has one new file: login.html. That's the login from.

Let's start with that.

Login form

The form looks like this:

A login form

Here's the HTML:

  1. <h1>Log In</h1>
  2. <div class="form-group">
  3.     <label for="user-name">User name</label>
  4.     <input type="text" class="form-control"
  5.         id="user-name"
  6.         aria-describedby="user-name-help"
  7.         placeholder="Enter user name">
  8.     <small id="user-name-help"
  9.         class="form-text text-muted">
  10.         Please enter your user name.</small>
  11. </div>
  12. <div class="form-group">
  13.     <label for="user-password">Password</label>
  14.     <input type="password" class="form-control"
  15.         id="user-password" placeholder="Password">
  16. </div>
  17. <div class="form-group">
  18.     <button type="button" onclick="Slotel.login()"
  19.     class="btn btn-primary" id="login-button"
  20.     >Log in</button>
  21. </div>
Everything should be familiar, except that line 14 uses the password field type:

<input type="password"...

Here's the login button's click code (line 18):

onclick="Slotel.login()"

What should that do? Here's some pseudocode:

If the username and password are valid:
  Set the logged in flag.
  Jump to the main admin page.
Else:
  Show an error message.

Here's the code:

  1. //Get the input user name and password.
  2. var userName = $("#user-name").val().trim().toLocaleLowerCase();
  3. var userPassword = $("#user-password").val().trim();
  4. //Compare with known.
  5. if ( userName == 'buffy' && userPassword == 'giles' ) {
  6.     //Login OK.
  7.     //Store flag in localStorage.
  8.     localStorage.setItem('slotelLoggedIn', "yes");
  9.     //Jump to home page.
  10.     window.location.href = "/";
  11.     //Exit this function.
  12.     return;
  13. }
  14. //Login failed.
  15. alert("Sorry, username and/or password are invalid.");
Line 2 gets the username the user typed:

var userName = $("#user-name").val().trim().toLocaleLowerCase();

It strips off leading and trailing spaces, and converts the username to lowercase. This means that usernames are not case-sensitive. The user could type:

  • BobbyJoe
  • Bobbyjoe
  • BOBBYJOE
  • bobbyjoe

For all of them, the variable userName would have the value bobbyjoe.

Line 3 gets the password:

var userPassword = $("#user-password").val().trim();

It trims spaces, but doesn't convert to lowercase. Passwords are case-sensitive:

  • If the user typed the password "BatmandBegins", the variable userPassword is set to BatmandBegins.
  • If the user typed the password "batmandbegins", the variable userPassword is set to batmandbegins.

OK, so now, userName has whatever the user typed in the username field, and userPassword has whatever the user typed in the password field. Trimmed and (maybe) lower-cased, ready for processing.

The next line of code is:

if ( userName == 'buffy' && userPassword == 'giles' ) {

There is only one username/password pair in this app.

What happens if that's true? Here's the pseudocode again:

If the username and password are valid:
  Set the logged in flag.
  Jump to the main admin page.
Else:
  Show an error message.

Here's the code:

  1. //Login OK.
  2. //Store flag in localStorage.
  3. localStorage.setItem("slotelLoggedIn", "yes");
  4. //Jump to home page.
  5. window.location.href = "index.html";
  6. //Exit this function.
  7. return;
Line 8 is the important one:

localStorage.setItem("slotelLoggedIn", "yes");

It stores "yes" into localStorage, under the key slotelLoggedIn. It's called slotelLoggedIn rather than loggedIn, for namespacing. It's possible that more than one app wants to use the key loggedIn in the same localStorage. Unlikely, given the way we organize things. Still, adding the app name slotel to the front of the key means that we never have to think about it.

Here's the pseudocode again:

If the username and password are valid:
  Set the logged in flag.
  Jump to the main admin page.
Else:
  Show an error message.  UP TO HERE

Here's the JS for the last line:

alert("Sorry, username and/or password are invalid.");

That's the login form done. You can try it, and grab the code.

This is terrible security, of course. People can see the username and password by pressing Ctrl+U, and looking at the page's source code. In a real app, the username and password would be checked on a server. The valid usernames and passwords would be in a database on the server. Still, the principle is the same as we have here. A login form. The user types in a username and password. If it's valid, set a flag, and jump to the main admin page.

Let's go back to our to-do list:

  1. Login form: set loggedIn to "yes" if the user gives the right username and password.
  2. A menu to show the log in and out links.
  3. Logout: erase loggedIn.
  4. On every admin page: read loggedIn. If it isn't "yes", leave the page.

We've done the first one. The second we'll do with the navbar.

Navbar menu

Let's make the navbar help the user log in and out. The menu will look like this when the user has not logged in:

Logged out

(Let's push the menu to the right of the navbar, just to try something different.)

So, when the user is logged out, the menu shows them a Login link.

What is the user is logged in? Here's the menu:

Logged in

The menu is part of the navbar. The code for the navbar is in library/includes/navbar.html. Recall the our standard template loads the navbar into every page:

$("#navbar").load("library/includes/navbar.html");

Here's some HTML from navbar.html. There are three links:

  1. <li class="nav-item hide-on-load" id="login-link">
  2.     <a class="nav-link"
  3.         href="/crud/sloths/admin/login.html"> Log in </a>
  4. </li>
  5. <li class="nav-item hide-on-load" id="admin-link">
  6.     <a class="nav-link"
  7.         href="/crud/sloths/admin/index.html"> Admin </a>
  8. </li>
  9. <li class="nav-item hide-on-load" id="logout-link">
  10.     <a class="nav-link"
  11.         href="#" onclick="Slotel.logout()"> Log out </a>
  12. </li>
All of them are hidden to begin with:

  1. <li class="nav-item hide-on-load" id="login-link">
  2.     <a class="nav-link"
  3.         href="/crud/sloths/admin/login.html">Log in</a>
  4. </li>
  5. <li class="nav-item hide-on-load" id="admin-link">
  6.     <a class="nav-link"
  7.         href="/crud/sloths/admin/index.html">Admin</a>
  8. </li>
  9. <li class="nav-item hide-on-load" id="logout-link">
  10.     <a class="nav-link"
  11.         href="#" onclick="Slotel.logout()">Log out</a>
  12. </li>
The page load code shows some of the links, depending on whether the user is logged in.

$(document).ready(function () {
    if ( localStorage.getItem('slotelLoggedIn') == "yes") {
        $("#admin-link").show();
        $("#logout-link").show();
    }
    else {
        $("#login-link").show();
    }
});

Adela
Adela
I noticed that all of the URLs in the menu are root relative. Like: /crud/sloths/index.html.

Is that because they have to work on every page?

Right! Good thinking!

navbar.html is loaded into every page in the app. These pages:

Where the navbar is loaded

The menu links have to work for all of the files, no matter where they are in the directory tree. That's why relative links won't do the job. Root relative links work, because they always start at the root of the site (which in this case is https://webappexamples.cybercour.se):

/crud/sloths/index.html

Logging out

We've done the first two things on the to-do list:

  1. Login form: set loggedIn to "yes" if the user gives the right username and password.
  2. A menu to show the log in and out links.
  3. Logout: erase loggedIn. UP TO HERE
  4. On every admin page: read loggedIn. If it isn't "yes", leave the page.

Here's the HTML for the logout link:

<li class="nav-item hide-on-load" id="logout-link">
    <a class="nav-link"
        href="#" onclick="Slotel.logout()">Log out</a>
</li>

The link runs the function Slotel.logout(). Notice the href is just #. That means it doesn't actually go anywhere.

Here's Slotel.logout():

Slotel.logout = function(){
    //Erase the logged in flag.
    localStorage.setItem("slotelLoggedIn", "no");
    //Jump to home page.
    window.location.href = "/crud/sloths/index.html";
};

The comments explain what happens.

Checking the login flag

Here's our to-do list:

  1. Login form: set loggedIn to "yes" if the user gives the right username and password.
  2. A menu to show the log in and out links.
  3. Logout: erase loggedIn.
  4. On every admin page: read loggedIn. If it isn't "yes", leave the page. UP TO HERE

Only the last one is left.

We want to add code to every admin page. These ones:

Admin pages

These pages should only be shown to logged in users.

This pseudocode will work:

If not logged in:
    Jump to log in page

This is convenient for users. If they are not logged in, take them to the page where they can log in.

Here's the JS, added to the page load event:

if ( localStorage.getItem('slotelLoggedIn') != "yes" ) {
    window.location.href = "login.html";
}

That's it!

Summary

  • We added a variable to localStorage to be a flag, to show whether the user is logged in.
  • There's a page with a login form, that sets the flag to "yes" if the user gives the right username and password.
  • The navbar has a menu that's loaded on every page. All of the links are hidden at first. Code in the page load event shows some of them. Which ones are shown depends on whether the user is logged in or not
  • Every admin page checks to see whether the user is logged in. If not, the browser jumps to the log in page.