Newer
Older
labs / tiddlers / content / reference / Web / _Reference_Web_HTTP Authentication.md
@Mark George Mark George on 26 Sep 11 KB Lab 11

Note that adding HTTP authentication is a bonus task. It is one of the easier tasks however, since you pretty much just need to copy and paste the code in this section into your project.

At the moment we have two problems with the system relating to authentication and authorisation:

  • The authentication is only checking the username. The password is currently ignored.

  • There is no authorisation --- if a user knows the URL for a page or API operation then they can navigate to it directly despite not signing in (a path traversal vulnerability)

We can fix these issues by adding a filter to the Jooby filter chain that checks the authentication for all requests. The advantage of using a filter is that our existing API (the routes defined in the Jooby modules) does not need to be modified to add checks to all of the operations.

We will be using HTTP Basic Access Authentication (BAA) which is the simplest form of token based authentication. Generally you will be using token based authentication when using web service APIs because all you need to do to authenticate every request is to make sure that the token has been added to each request (usually via a header).

https://en.wikipedia.org/wiki/Basic_access_authentication

We are not able to easily secure the web pages or any of the other static assets (CSS, JavaScript, etc) since the browser is downloading these for us and we are not able to add authentication headers to the HTTP requests that are made by the browser --- we can only control the requests that we make via JavaScript using Axios or whatever HTTP library we are using. We can do this if we use the Vue router to manage the pages rather than using window.location for redirecting the browser, however this requires diving a lot deeper into the Vue ecosystem and we don't have sufficient time to cover that in this course.

Not securing the static assets is not such a big problem --- all of the data that is being displayed is coming from the API, so if we protect the API then our site is still secure.

Note that most forms of token based authentication are not secure when using plain HTTP (CSRF vulnerability). You need to be using HTTPS if deploying a real system that uses token based authentication. We will show you how to add transport encryption (HTTPS) in lab 13.

We will be managing the token and sign out functions ourselves rather than relying on the browser to do it, so the problems with cached credentials and signing out as mentioned in the BAA Wikipedia page are not going to be a problem here.

Server Side (Jooby Service)

CredentialsValidator Interface

We should create a new interface to decouple the authentication from the Customer DAO --- the class that we will be using for performing the BAA should not need to know anything about any of the DAO classes.

  1. Add a Java interface to the dao package named CredentialsValidator that contains the following code:

     package dao;
     
     public interface CredentialsValidator {
         Boolean validateCredentials(String username, String password);
     }
  2. Make your existing CustomerDAO interface extend this interface. Your existing DAO implementations should already have an equivalent of the validateCredentials method (a method that takes a username and password and returns a Boolean) --- rename the validateCredentials method to match your method name.

BasicAccessAuth Class

We need a class that we can install into the Jooby filter chain that will perform the BAA:

  1. Create a Java class named BasicAccessAuth in the web package that contains the following:

     package web;
     
     import dao.CredentialsValidator;
     import io.jooby.Extension;
     import io.jooby.Jooby;
     import io.jooby.StatusCode;
     import io.jooby.exception.MissingValueException;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
     
     import java.util.Base64;
     import java.util.Set;
     import java.util.regex.Matcher;
     import java.util.regex.Pattern;
     
     /**
      * <p>A Jooby extension that adds a HTTP Basic Access Authentication filter to
      * the filter chain.</p>
      *
      * <p>We intentionally omit sending a realm with the 401 response to stop the
      * browser from trying to handle the authentication itself.</p>
      *
      * <p>Install in Jooby using:</p>
      *
      * <pre><code>install(new BasicAccessAuth(credsValidator, Set.of("/path/to/protect"), Set.of("/path/to/exclude")));</code></pre>
      *
      * @author Mark George
      */
     public class BasicAccessAuth implements Extension {
     
         private final CredentialsValidator validator;
         private final Set<String> protect;
         private final Set<String> exclude;
     
         private final Logger logger = LoggerFactory.getLogger(BasicAccessAuth.class);
     
         /**
          * @param validator The validator to use to check the credentials.
          * @param protect   A Set that contains paths that should be protected.  Each
          *                  path is a string that can include regular expressions.
          *                  <p>Example path:</p> <pre><code>/api/.*</code></pre>
          * @param exclude   A Set that contains paths that should NOT be protected.
          *                  Use this to exclude paths that would otherwise be
          *                  included via wildcard paths in the <code>protect</code>
          *                  set.  Each path is a string that can include regular
          *                  expressions.
          *                  <p>Any paths not included in either set will be ignored
          *                  and not protected.</p>
          */
         public BasicAccessAuth(CredentialsValidator validator, Set<String> protect, Set<String> exclude) {
             this.validator = validator;
             this.protect = protect;
             this.exclude = exclude;
         }
     
         @Override
         public void install(Jooby application) throws Exception {
     
             application.decorator(next -> ctx -> {
     
                 // get requested path
                 String path = ctx.getRequestPath();
     
                 // check if path is in exclude set
                 for (String ex : exclude) {
                     if (path.matches(ex)) {
                         logger.debug("EXCLUDE - {}", path);
     
                         // if so, no auth required so continue to the next route
                         return next.apply(ctx);
                     }
                 }
     
                 // check if path is in the protect set
                 for (String pro : protect) {
                     if (path.matches(pro)) {
                         logger.debug("PROTECT - {}", path);
     
                         String authToken;
                         try {
                             authToken = ctx.header("Authorization").value();
                         } catch (MissingValueException ex) {
                             // Authorization header missing - send 401/Unauthorized response
                             return ctx.setResponseHeader("WWW-Authenticate", "None").send(StatusCode.UNAUTHORIZED);
                         }
     
                         // strip off the "Basic " part
                         String stripped = authToken.replace("Basic ", "");
     
                         Base64.Decoder decoder = Base64.getDecoder();
                         String authDetails = new String(decoder.decode(stripped));
     
                         // split the decoded string into username and password
                         Matcher matcher = Pattern.compile("(?<username>.+?):(?<password>.*)").matcher(authDetails);
     
                         if (!matcher.matches()) {
                             // token is not in the expected format so is likely invalid - send 401/Unauthorized response
                             return ctx.setResponseHeader("WWW-Authenticate", "None").send(StatusCode.UNAUTHORIZED);
                         }
     
                         String username = matcher.group("username");
                         String password = matcher.group("password");
     
                         // check the credentials
                         if (validator.validateCredentials(username, password)) {
     
                             // add username to context
                             ctx.setUser(username);
     
                             // continue on to next route
                             return next.apply(ctx);
     
                         } else {
                             // bad credentials - send 401/Unauthorized response
                             return ctx.setResponseHeader("WWW-Authenticate", "None").send(StatusCode.UNAUTHORIZED);
                         }
     
                     }
                 }
     
                 // path is not in protect or exclude set, so carry on to next route
                 logger.debug("IGNORE - {}", path);
                 return next.apply(ctx);
             });
         }
     
     }

    Modify the line of code that calls validateCredentials (which is likely showing an error at the moment) to use your method name.

  2. Add the following to the Server class to add the BasicAccessAuth filter to the filter chain:

     install(new BasicAccessAuth(custDao, Set.of("/api/.*"), Set.of("/api/register")));

    The first parameter is the Customer DAO (which is also a CredentialsValidator). The second parameter is a set of paths that we want to be protected by BAA --- any requests sent to these paths will need a valid BAA token in the Authorization header or they will be rejected with a 401/Unauthorized response. In this case we are protecting everything under the /api/ path (the .* is a regular expression that is acting as a wildcard). The third parameter is set containing any paths that we want to explicitly exclude from BAA. We don't want to protect the /api/register operation since the user would need to authenticate before they could create an account which is a chicken and egg problem.

  3. The order that the Jooby modules are installed/mounted is important since the order of the filters in the chain is determined by the order which they are added in the Server class. We aren't protecting the static resources with BAA, so there is no point in adding the BAA filter before the StaticAssetModule, but we do want to add it before any of the API modules are added. The correct order is:

    • StaticAssetModule
    • GsonModule
    • BasicAccessAuth
    • ProductModule
    • CustomerModule
    • SaleModule

    Reorder the code in Serverso that the modules are loaded in the correct order.

Client Side (Vue)

We need to generate a BAA authentication token, store it, and add it to the HTTP headers for all requests.

  1. Create a JavaScript file named authentication that contains the following module:

     import { dataStore } from './data-store.js'
     
     export const BasicAccessAuthentication = {
     
         computed: Vuex.mapState({
             authToken: 'authToken'
         }),
     
         mounted() {
             // add default header to axios
             axios.defaults.headers.common = {'Authorization': `Basic ${this.authToken}`};
         },
     
         methods: {
             createToken(username, password) {
                 let authToken = window.btoa(username + ":" + password);
                 dataStore.commit("authToken", authToken);
     
                 // add header to current axios instance since mounted has probably already been executed
                 axios.defaults.headers.common = {'Authorization': `Basic ${authToken}`};
             }
         }
     };

    This code provides a createToken function that can be used to create the BAA token from the username and password entered by the user and store it in the data store. The token is retrieved from the data store in the computed section, and added as a default header to Axios in the mounted section so that all HTTP requests will include the token.

  2. This module needs to be imported into view-products.js and the other page controller JS files (you don't need to do this for the create-account, navigation or data store files). Add the following to the bottom of each file, just above the app.mount call:

    // import authentication module
    import { BasicAccessAuthentication } from './authentication.js';
  3. Add the BasicAccessAuthentication module to the mixins array of each of the controllers:

    mixins: [BasicAccessAuthentication]

    If there is already a mixin for the NumberFormatter then add the BasicAccessAuthentication to the existing array:

    mixins: [NumberFormatter, BasicAccessAuthentication]
  4. Add the following to the state section of data-store.js:

     // basic access authentication token
     authToken: null;
  5. Add the following mutation to data-store.js:

     // store basic access token
     authToken(state, token) {
         state.authToken = token;
     }
  6. Add the following to the signIn function in customer.js. It should go at the top of the function, before the axios call:

     this.createToken(this.customer.username, this.customer.password);

That should be it. Restart the server and test everything to make sure it all works --- the password is now being checked, so you need to enter the correct password for the user in the sign in page.