Roll your own authenticator app with KeystoneJS and React – pt 3

In our previous post we got to the point of displaying an OTP in our card component. Now it is time to refactor a bit and implement a countdown functionality to see when this token will expire. For now we will go ahead and add this logic into our Card component. In order to figure out how to build this countdown timer we first need to understand how the TOTP counter is calculated.

In other words, we know that at TOTP token is derived from a secret key and the current time. If we dig into the spec some we can find that time is a reference to Linux epoch time or the number of seconds that have elapsed since January 1st 1970. For a little more clarification check out this Stackexchange article.

So if we know that the time is based on epoch time, we also need to know that most TOTP tokens have a validity period of either 30 seconds or 60 seconds. 30 seconds is the most common standard so we will use that for our implementation. If we put all that together then basically all we need is 2 variables:

  1. Number of seconds since epoch
  2. How many seconds until this token expires

The first one is easy:

let secondsSinceEpoch;
secondsSinceEpoch = Math.ceil(Date.now() / 1000) - 1;

# This gives us a time like so: 1710338609

For the second one we will need to do a little math but it’s pretty straightforward. We need to divide secondsSinceEpoch by 30 seconds and then subtract this number from 30. Here is what that looks like:

let secondsSinceEpoch;
let secondsRemaining;
const period = 30;

secondsSinceEpoch = Math.ceil(Date.now() / 1000) - 1;
secondsRemaining = period - (secondsSinceEpoch % period);

Now let’s put all of that together into a function that we can test out to make sure we are getting the results we expect.

const timer = setInterval(() => {
  countdown()
}, 1000)

function countdown() {
  let secondsSinceEpoch;
  let secondsRemaining;

  const period = 30;
  secondsSinceEpoch = Math.ceil(Date.now() / 1000) - 1;

  secondsRemaining = period - (secondsSinceEpoch % period);
  console.log(secondsSinceEpoch, secondsRemaining)
  if (secondsRemaining == 1) {
    console.log("timer done")
    clearInterval(timer)
  }
}

Running this function should give you output similar to the following. In this example we are stopping the timer once it hits 1 second just to show that everything is working as we expect. In our application we will want this time to keep going forever:

1710339348, 12
1710339349, 11
1710339350, 10
1710339351, 9
1710339352, 8
1710339353, 7
1710339354, 6
1710339355, 5
1710339356, 4
1710339357, 3
1710339358, 2
1710339359, 1
"timer done"

Here is a JSfiddle that shows it in action: https://jsfiddle.net/561vg3k7/

We can go ahead and add this function to our Card component and get it wired up. I am going to skip ahead a bit and add a progress bar to our card that is synced with our countdown timer and changes colors as it drops below 10 seconds. For now we will be using a setInterval function to accomplish this.

Here is what my updated src/Components/Card.tsx looks like:

import { useState } from "react";
import { IToken } from "../App"
import { TOTP } from 'totp-generator';


function Card({ token }: { token: IToken }) {
  const { otp } = TOTP.generate(token.token);
  const [timerStyle, setTimerStyle] = useState("");
  const [timerWidth, setTimerWidth] = useState("");


  function countdown() {
    let secondsSinceEpoch: number;
    let secondsRemaining: number = 30;
    const period = 30;
    secondsSinceEpoch = Math.ceil(Date.now() / 1000) - 1;
    secondsRemaining = period - (secondsSinceEpoch % period);
    setTimerWidth(`${100 - (100 / 30 * (30 - secondsRemaining))}%`)
    setTimerStyle(secondsRemaining < 10 ? "salmon" : "lightgreen")
  }
  setInterval(() => {
    countdown();
  }, 250);
  return (
    <>
      <div className='card'>
        <div className='progressBar'style={{ width: timerWidth, backgroundColor: timerStyle}}></div>
        <span>{token.issuer}</span>
        <span>{token.account}</span>
        <span >{otp}</span>
      </div>
    </>

  )
}
export default Card

Pretty straightforward. I also updated my src/index.css and added a style for our progress bar:

.progressBar {
  height: 10px;
  position: absolute;
  top: 0;
  left: 0;
  right: inherit;
}
// Also be sure to add position:relative to .card which is the parent of this.

Here is what it all looks like in action:

If you look closely you will notice a few interesting things. First is that the color of the progress bar changes from green to red. This is handled by our timerStyle variable. That part is pretty simple, if the timer is less than 10 seconds we set the background color as salmon otherwise we use light green. The width of the progress bar is controlled by `${100 – (100 / 30 * (30 – secondsRemaining))}%`

The other interesting thing to note is that when the timer runs out it automatically restarts at 30 seconds with a new OTP. This is due to the fact that this component is re-rendering every 1/4 second and every time it re-renders it is running the entire function body including: const { otp } = TOTP.generate(token.token);.

This is to be expected since we are using React and we are just using a setInterval. It may be a little unexpected though if you aren’t as familiar with React render cycles. For our purposes this will work just fine for now. Stay tuned for pt 4 of this series where we wire up the backend API.