22. How to achieve simple and reliable authentication? part 2

Previous chapter showed how to implement Rails with AWS Cognito.

This chapter will focus on how you can communicate with your backend for authentication purposes.

Note that there are many ways of achieving this and the ones covered here may not be the ‘best’ for your application.

First of all, I create a basic login page:

To do this I used a page from material ui pro from Creative Tim templates. Try to leverage existing textboxes etc for your login, I will try go through just the main parts. Here are your two main inputs, username and password.

              <CardBody>
                <CustomInput
                  labelText="Username..."
                  id="username"
                  formControlProps={{
                    fullWidth: true
                  }}
                  inputProps={{
                    value: username,
                    onChange: e => { setUsername(e.target.value) },
                    endAdornment: (
                      <InputAdornment position="end">
                        <Face className={classes.inputAdornmentIcon} />
                      </InputAdornment>
                    )
                  }}
                />
                <CustomInput
                  labelText="Password"
                  id="password"
                  formControlProps={{
                    fullWidth: true
                  }}
                  inputProps={{
                    value: password,
                    onChange: e => { setPassword(e.target.value) },
                    endAdornment: (
                      <InputAdornment position="end">
                        <Icon className={classes.inputAdornmentIcon}>
                          lock_outline
                        </Icon>
                      </InputAdornment>
                    ),
                    type: "password",
                    autoComplete: "off"
                  }}
                />
              </CardBody>

The button which will action the request:

                <Button color="rose" simple size="lg" block onClick={() => { handleLoginClick() }}>
                  Let{"'"}s Go
                </Button>

Here’s the interesting part, we handle the login authentication with javascript:

  async function handleLoginClick() {
    const response = await handleSignIn(username, password);
    let errors = response.errors;
    let error = response.error;

    if (errors || error) {
      let notifierMessage =
        "Sorry, there's been a problem processing your request!\n";
      if (error !== undefined) {
        notifierMessage = notifierMessage + " - " + error + "\n";
      } else if (errors !== undefined) {
        for (let key in errors) {
          notifierMessage =
            notifierMessage + key + " - " + errors[key] + "\n";
        }
      }
      setNotificationMessage(notifierMessage);
      setNotificationColor("warning");
      setNotificationOpen(true);

      return;
    }

    setCookies("token", response.access_token, { path: '/' });
    setNotificationMessage("Welcome back!");
    setNotificationColor("info");
    setNotificationOpen(true);
  }

What this shows is that when we try to log in, if there’s any issues with logging in, we display a notification message to the user. If we successfully log in, we enter the access token in our cookies, this enables our other pages to utilise it. This also means that if we duplicate tabs or close the page and re-open it, we are still able to read the access token, which is why its better this way than saving it in state or redux props for instance.

To save it in our cookies, the react cookie library was used.

I have a hook set up on cookies such that if they change we try to authenticate:

  React.useEffect(() => {
    redirectIfAuthenticated();
  }, [cookies]);

  function redirectIfAuthenticated() {
    authenticator.authenticate(cookies.token, () => {
      setRedirectTo(true);
    });
  }

  const redirectPath = { from: { pathname: "/tech-jobs/search" } };

  if (redirectTo === true) {
    return <Redirect to={redirectPath} />;
  }

This shows that when cookies change, we try to authenticate the access token, if successful, we redirect to another page, in this case tech-jobs/search, but it can be dynamic as you need.

Let’s take a look at what the authenticator object is doing:

export const authenticator = {
  isAuthenticated: false,

  async authenticate(authToken, cb) {
    const response = await getCurrentUser(authToken);
    if (response.status < 400) {
      const user = await response.json();
      if (user !== undefined && user.username !== null && user.username !== "") {
        // sanity check that username is present
        // this can easily be 'hacked' but even if bypassed to main page, the backend will not authorise requests.
        this.isAuthenticated = true;
        setTimeout(cb, 100) // small delay before we call the callback

        return user
      } else {
        this.isAuthenticated = false;
      }
    } else {
      this.isAuthenticated = false;
    }
  },

  signout(authToken) {
    this.isAuthenticated = false;
    handleSignOut(authToken);
  }
};

So this authenticator object simply calls our getCurrentUser from backend. If the response is valid and contains a username then this object assumes its ok. This can easily be hacked, but our front-end pages don’t display any sensitive info so there’s no issues here. i.e. a hacker could inject a username into response object and as far as this piece of code would see, it would be a valid response.

If authenticated, this function will then activate the callback function if required. In the previous code block, we saw that if authenticated, we want it to activate: setRedirectTo(true);. This means it will only be executed if this is true.

Let’s now check the getCurrentUser(token) and handleSignOut(token) functions:

export async function getCurrentUser(token) {
  console.log(headers);
  return fetch(rootUrl + "/api/v1/public/current-user", {
    method: "GET",
    headers: { ...headers, Authentication: token },
    credentials: "same-origin"
  });
}

export async function handleSignOut(token) {
  console.log(headers);
  return fetch(rootUrl + "/api/v1/public/sign_out", {
    method: "POST",
    headers: { ...headers, Authentication: token },
    credentials: "same-origin"
  });
}
export const headers = {
  Accept: "application/json",
  "Content-Type": "application/json",
  // "X-CSRF-TOKEN": auth_token 
};

As you may remember from the previous post, the get current user and sign out methods didn’t require payload, only the headers. This is seen here as these are GET requests, with no URL encoded parameters and the token is applied to the header.

Now let’s take a look at the registration page.

Currently, we just need 3 parameters from the user; email, username and password.

Here’s the code for the form if interested:

<form className={classes.form}>
  <CustomInput
    formControlProps={{
      fullWidth: true,
      className: classes.customFormControlClasses
    }}
    inputProps={{
      startAdornment: (
        <InputAdornment
          position="start"
          className={classes.inputAdornment}
        >
          <Email className={classes.inputAdornmentIcon} />
        </InputAdornment>
      ),
      value: email || "",
      onChange: e => {setEmail(e.target.value);},
      placeholder: "Email..."
    }}
  />
  <CustomInput
      formControlProps={{
        fullWidth: true,
        className: classes.customFormControlClasses
      }}
      inputProps={{
        startAdornment: (
          <InputAdornment
            position="start"
            className={classes.inputAdornment}
          >
            <Face className={classes.inputAdornmentIcon} />
          </InputAdornment>
        ),
        value: username || "",
        onChange: e => {setUsername(e.target.value);},
        placeholder: "Username..."
      }}
    />
  <CustomInput
    formControlProps={{
      fullWidth: true,
      className: classes.customFormControlClasses
    }}      
    inputProps={{
      startAdornment: (
        <InputAdornment
          position="start"
          className={classes.inputAdornment}
        >
          <Icon className={classes.inputAdornmentIcon}>
            lock_outline
          </Icon>
        </InputAdornment>
      ),
      type: (showPassword ? 'text' : 'password'),
      endAdornment: (
        <InputAdornment position="end">
          <IconButton
            aria-label="toggle password visibility"
            onClick={() => { setShowPassword(!showPassword) } }
            onMouseDown={ (event) => { event.preventDefault(); } }
          >
            {showPassword ? <Visibility /> : <VisibilityOff />}
          </IconButton>
        </InputAdornment>
      ),
      value: password || "",
      onChange: e => {setPassword(e.target.value);},
      placeholder: "Password..."
    }}
  />
  <div className={classes.center} onClick={ async () => { handleRegisterClick() }}>
    <Button round color="primary">
      Get started
    </Button>
  </div>
</form>

We handle the registration click with this:

  async function handleRegisterClick() {
    const response = await handleRegistration(username, email, password);

    let errors = response.errors;
    let error = response.error;

    if (errors || error) {
      let notifierMessage =
        "Sorry, there's been a problem processing your request!\n";
      if (error !== undefined) {
        notifierMessage = notifierMessage + " - " + error + "\n";
      } else if (errors !== undefined) {
        for (let key in errors) {
          notifierMessage =
            notifierMessage + key + " - " + errors[key] + "\n";
        }
      }

      setNotificationMessage(notifierMessage);
      setNotificationColor("warning");
      setNotificationOpen(true);

      return;
    }

    let notifierMessage = "Welcome!";
    setNotificationMessage(notifierMessage);
    setNotificationColor("info");
    setNotificationOpen(true);
    setTimeout(setRedirectTo(true), 100)
  }

We just try to call the registration API, and if the response contains errors we display it to the user, otherwise we display a Welcome! message to the user before redirecting them away to another page.

Now let’s check our handleRegistration method:

export async function handleRegistration(username, email, password) {
  const user_params = {
    user: {
      email: email,
      password: password,
      username: username
    }
  };

  const send_params = JSON.stringify(user_params);
  const response = await fetch(rootUrl + "/api/v1/public/register", {
    method: "POST",
    headers: headers,
    body: send_params,
    credentials: "same-origin"
  });

  const jsonRes = await response.json();
  return jsonRes;
}

We just stringify our user parameters and send them to our API register endpoint. We make sure that our backend returns user friendly error messages and just render them if they come up.

This should be enough to get you started!

In our other react pages, we may put some checks to prevent unauthorised users from accessing them.

A good way of doing this is at the routes level – you may want to process your active user (for example with use of redux) and if there isn’t one, or doesn’t have the correct permissions, don’t include the route in your page.

There’s a lot of ways you can protect a page, another is like this:

  async function fetchUser() {
    const data  = await authenticator.authenticate(cookies.token);
    if (authenticator.isAuthenticated) {
      const newUserVals = {...userValues}
      // This is the format cognito will send your information in
      newUserVals.email = data.user_attributes.find(o => o.name == "email").value;
      newUserVals.username = data.username;

      setUserValues(newUserVals);
    } else {
      setRedirectTo(true);
    }
  }

the authenticator.authenticate(cookies.token) returns a user object, so in this case it can directly be used to set the states user object. If the authenticator is indicating it failed, it will redirect the page based on the setup. This way you can have the pages setup in routes (you will not get a 404 error for instance) and determine messages or redirects as you wish.