Lab 3: Connecting front end and back end

In this lab we will connect your front end with your back end. You will also implement a simple authentication and authorization system. Using authentication we ensure that only logged-in users have access to the data we want to protect. With the authorization system we ensure that different users have different permissions, giving them different rights to access and manipulate the data.

AJAX

AJAX makes it possible to fetch and send data even after a web page has loaded. This is done using JavaScript. Read an introduction here.

Preparatory questions

Answer the following questions and write down your answers before the demonstration: 1. What does AJAX stand for? 2. Sketch a solution for how your server could find out which user is sending a particular request.

Part 1: AJAX

Start by following Starting a new lab.

We start by modifying the current client-side implementation so that the client fetches data from your Flask server (from lab 1) asynchronously.

  1. Replace serverStub with AJAX calls in client.js and remove serverStub.js; test whether your new implementation works by removing the file serverStub.js and seeing if everything still works.

    Tip

    Use the following code sketch to replace the call serverStub.getCars(). To replace the other methods the example below can be modified with respect to url and type. Your server’s address should be saved in a variable for easier modification in the future.

    Note that CORS distinguishes between http://127.0.0.1 and http://localhost. Instead of having a hard-coded name you can use:

    host = window.location.protocol + '//' + location.host
    
    fetch(host + '/cars', // url
       { // options
          method: 'PUT', // eller 'GET', 'POST' eller 'DELETE'
          headers: {
             'Content-Type': 'application/json' // Behövs ibland för PUT eller POST
          }
       }
    ).then(response => {
       if (!response.ok) {
          throw new Error('...');
       }
       return response.json();
    }).then(cars => {
       // cars innehåller nu den JSON-data som servern svarar med på /cars
       // Gör någonting med /cars här
    });
    

    The variable cars is not visible after the call and code that runs here cannot see the changes made in the function that has the cars variable - that code runs asynchronously when it has received a response from the server.

    This is the difficulty with AJAX - understanding when code runs.

Note that all calls to the server should be asynchronous, i.e. async: false (if using XMLHttpRequest), await or similar methods must not be used.

Part 2: The user model

We will build an authentication system in Flask where we handle registration, login and access to data.

As a first step we will now make some changes to the current implementation to make it more suited for handling different types of users.

  1. Refactor (rename) your model for Customer to User and add a new field, is_admin. The field should be of type boolean with the default value False.

    There are also other solutions for distinguishing between different types of users. In this lab however this solution is practical since it essentially only requires adding a field to the database.

    As you can see we have set an initial value for the field is_admin; this is solely to ensure that new users are always set as non-admin. Note that you also need to update relations and methods in all places where you have used Customer.

    Tip

    Use Ctrl+Shift+F in VSCode to search for customer in the entire folder.

Part 3: Registration and password handling on the server

The next step in our authentication process is to give the user the ability to identify themselves by providing a password when logging in. A first idea for solving this task is to create a column in our database to store the user’s password as a string. But now we need to be careful - is it reasonable to save a password as plain text in a database? The answer is of course no, no and no again. It is never a good idea to save passwords as plain text in a database. We solve this instead by using a hash function on the password before we store it in the database.

In this lab we will continue using helper libraries just as before. In this case we will use bcrypt which is a hashing library. Read through the bcrypt documentation to find methods that can help us hash passwords before they are stored in the database.

When we then need to validate whether a user is authentic we will run the same hash function on the claimed user’s provided password and compare it with the hashed password we stored in the database.

  1. Install flask-bcrypt in your development environment and add the following two lines at appropriate places in your main.py:

    from flask_bcrypt import Bcrypt
    
    bcrypt = Bcrypt(app)
    
  2. Now add a field to your user model to store a hashed password. Name it password_hash and set the type to String. It can then be convenient to add a method to the model as below to easily hash and set passwords.

    def set_password(self, password):
       self.password_hash = generate_password_hash(password).decode('utf8')
    
  3. Now create functionality for the route /sign-up.
    You should receive JSON with an email address, a name and a password. This data should be used to save a new user in the database. The password should be hashed before it is saved. The server should then respond with status code 200.

    Tip

    Use the method set_password to hash the password before you save it.

Part 4: Token-based authentication

The overall structure we will follow is token-based authentication. In summary this means that a token is generated on the server upon login. This token is then used in all requests to determine who the user is (see the figure below). Read more about token-based authentication here.

Authentication with bearer tokens

Since anyone can use Postman or other tools to send arbitrary requests to a web server it is not enough that the user must log in on the client - the server must always be protected against all conceivable requests.

To create and read tokens we will use the package Flask-Jwt-Extended. This package will be referred to as Flask-JWT going forward.

  1. Install Flask-JWT by running the command:

    (venv) $ pip install flask-jwt-extended
    

    or by using your requirements.txt if you have one.

  2. Configure Flask-JWT in main.py by:

    • Importing JWTManager from flask_jwt_extended

    • Setting app.config['JWT_SECRET_KEY'] to some hard-to-guess string

    • Initializing JWTManager: jwt = JWTManager(app)

  3. Create functionality for POST /login. The method should receive an email and a password. Verify that they are correct and then respond with a token and user data (make sure not to send the password hash). The response should thus look like this:

    _images/lab3-4-3.png

    If the login credentials are not correct the server should respond with HTTP status code 401.

    Tip

    Use create_access_token from Flask-JWT.

  4. The server must also keep track of ensuring the right users get access to the right functionality and data. Modify your server code so that all routes except /sign-up and /login require an authenticated user. If a user tries to access a route and cannot be authenticated, the server should respond with status code 401. Test with Postman both with and without the auth header.

    Tip

    Use the decorator @jwt_required as in the documentation

    In Postman you can include a token in this way:

    _images/lab3-4-4.png

Part 5: Registration, login and logout on the client

  1. Create registration capability:

    1. Skapa menyalternativet “Registrera dig”.

    2. Create a new view containing an HTML form that accepts email, name and password.

    3. Create functionality to send the form data to /sign-up so that a new user is created.

    4. When the user has been created, make sure the welcome view is displayed instead of the form.

  2. Create login capability:

    1. Skapa menyalternativet “Logga in”.

    2. Create a new view containing an HTML form that accepts email and password.

    3. Create functionality to send the form data to /login where the credentials will be validated.

    4. Save the token and user data that the server responds with after a successful login.

    Tip

    sessionStorage can be used to save data in the browser in the following way:

    sessionStorage.setItem('auth', JSON.stringify(loginResponse))
    
    1. When the user has successfully logged in, make sure the welcome view is displayed.

    Tip

    The browser’s developer tools can be used to read the contents of sessionStorage. Values can also be deleted using this. The developer tools in Chrome are opened by pressing the F12 key.

    _images/lab3-5-2-5.png
  3. Dölj menyalternativen “Logga in” och “Registrera dig” om det finns ett token sparat i webbläsaren. Se även till att dessa menyalternativ visas om ett token inte finns sparat.

    Tip

    Use classList.toggle() and the Bootstrap class d-none to control the visibility of an element. This logic can advantageously be placed so that it runs when the page loads.

    var signedIn = sessionStorage.getItem('auth').length > 0;
    document.getElementById("...").classList.toggle('d-none', !signedIn)
    
  4. Create logout capability:

    1. Create the menu item Log out and show it only when a user is logged in.

    2. Remove the token from sessionStorage when the user logs out.

    Tip

    Use sessionStorage.removeItem()

Part 6: Authentication with AJAX

  1. Visa menyalternativet “Bilar” endast om användaren är inloggad.

  2. Send the token you saved at login in every request that needs to be authenticated (not for /login and /sign-up). This can be done by passing the following argument to fetch and so on.

    headers: {"Authorization": "Bearer " + JSON.parse(sessionStorage.getItem('auth')).token}
    
  3. Visa knapparna “Redigera” och “Ta bort” på varje bil endast om användaren är admin.

  4. Skapa och visa en “Boka”-knapp på varje bil som visas om användaren inte är admin. Dock, om bilen redan är bokad, ska “Bokad av namn” visas istället för boka-knappen.

  5. Skapa POST /cars/[int:car_id]/booking som sätter en relation mellan den inloggade användaren och bilen ifall den inte redan är bokad. Servern ska svara med {success: true} om bokningen gick igenom och {success: false} om den redan var bokad av någon annan. Gör så att en förfrågan till denna route skickas när användaren klickar på en bils bokningsknapp och att texten “Bokad av namn” visas om bokningen lyckades.

    Tip

    Use get_jwt_identity to find the logged-in user.

  6. Now create a solution for cancelling car bookings in the same way as cars are booked.

Demonstration

See Demonstration.