Building Authentication Service with TOTP (Time-Based OTP) Part 1. The Server

Uncategorized
5.2k words

Series

Pre-requisite

  • Java 17
  • Micronaut

Goal

auth-qr

In this two-part series, we are going to build a web application and a REST API service.

After finishing this series hopefully, we get a better understanding of what TOTP is and how to implement it.

We are going to build the REST API using Java with Micronaut framework and the Web App using the good old html and jQuery.

What is a Time-Based One-Time Password (TOTP)?

Simply put TOTP is a mechanism in which the user is required to enter a token (usually a six-digit numeric), the token itself will refresh after a set of interval defined by the service.

The user can generate the token using 3rd party application such as Google Authenticator.

Rest API

Start a micronaut project using this command

1
mn create-app totp-service --features=yaml

--features=yaml is telling micronaut that we are using the YAML format for our configuration file.

here is what our project’s structure will look like.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
src
├── main
│ ├── java
│ │ └── totp
│ │ └── service
│ │ ├── Application.java
│ │ ├── AuthController.java
│ │ ├── AuthService.java
│ │ ├── Storage.java
│ │ ├── TOTP.java
│ │ └── model
│ │ ├── LoginForm.java
│ │ ├── RegisterForm.java
│ │ └── User.java

You can see the complete code in this github repo

We need to add Apache common codec dependency so we can decode string to base 32.

1
2
//build.gradle
implementation ("commons-codec:commons-codec:1.16.1")

Let’s take a look at TOTP.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package totp.service;

import org.apache.commons.codec.binary.Base32;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.stream.IntStream;

//credit : https://gist.github.com/rakeshopensource/def80fac825c3e65804e0d080d2fa9a7
public class TOTP {

private static final String HMAC_ALGO = "HmacSHA1";
private static final int TOTP_LENGTH = 6;
private static final int TIME_STEP = 30;

public static boolean validate(String secret, String token) {
long timeInterval = System.currentTimeMillis() / 1000 / TIME_STEP;
boolean matches = IntStream.of(-1, 0, 1)
.anyMatch(i -> generateTOTP(secret, timeInterval + i).equals(token));
if (matches) {
return true;
}
return false;
}

public static String generateTOTP(String secret) {
long timeInterval = System.currentTimeMillis() / 1000 / TIME_STEP;
return generateTOTP(secret, timeInterval);
}

private static String generateTOTP(String secret, long timeInterval) {
try {
byte[] decodedKey = decode32byte(secret);
byte[] timeIntervalBytes = new byte[8];

for (int i = 7; i >= 0; i--) {
// Extract the least significant byte from timeInterval
timeIntervalBytes[i] = (byte) (timeInterval & 0xFF);
// Right shift to process the next byte
timeInterval >>= 8;
}

Mac hmac = Mac.getInstance(HMAC_ALGO);
hmac.init(new SecretKeySpec(decodedKey, HMAC_ALGO));
byte[] hash = hmac.doFinal(timeIntervalBytes);

/*
* The line offset = hash[hash.length - 1] & 0xF; is used to determine the offset into the HMAC hash
* from which a 4-byte dynamic binary code will be extracted to generate the TOTP.
* This method of determining the offset is specified in the TOTP (RFC 6238) and HOTP (RFC 4226) standards.
*/
int offset = hash[hash.length - 1] & 0xF;

/*
* The expression hash[offset] & 0x7F uses the hexadecimal value 0x7F to mask
* the most significant bit (MSB) of the byte at hash[offset],
* ensuring it's set to 0. The reason for this is to make sure that the resulting 32-bit integer
* (binaryCode) is treated as a positive number. Reference TOTP (RFC 6238)
*/
long mostSignificantByte = (hash[offset] & 0x7F) << 24;
long secondMostSignificantByte = (hash[offset + 1] & 0xFF) << 16;
long thirdMostSignificantByte = (hash[offset + 2] & 0xFF) << 8;
long leastSignificantByte = hash[offset + 3] & 0xFF;

long binaryCode = mostSignificantByte
| secondMostSignificantByte
| thirdMostSignificantByte
| leastSignificantByte;

int totp = (int) (binaryCode % Math.pow(10, TOTP_LENGTH));
return String.format("%0" + TOTP_LENGTH + "d", totp); // Making sure length is equal to TOTP_LENGTH
} catch (Exception e) {
return null;
}
}

//it has to comply with RFC 3548
private static byte[] decode32byte(String input) {
Base32 base32 = new Base32();
return base32.decode(input);
}
}

above code contains a way for us to generate and validate TOTP Token, It is based on RFC-6238

It’s a derivation of HOTP (HMAC Based One Time Password)

1
2
3
TOTP = HOTP(K, T)

Where K is a secret key, and T is the interval time between each valid token

Try running the application

1
./gradlew run

If you see something like this then the server is running.

1
2
3
4
5
6
7
8
9
10
Starting a Gradle Daemon, 2 incompatible and 2 stopped Daemons could not be reused, use --status for details

> Task :run
__ __ _ _
| \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| | | | | (__| | | (_) | | | | (_| | |_| | |_
|_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
21:41:04.346 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 619ms. Server Running: http://localhost:8081
<==========---> 80% EXECUTING [28s]

Now to test it try registering by sending a request.

1
2
3
4
5
curl -XPOST -H "Content-type: application/json" -d '{
"username":"test",
"password":"123"
}' 'http://localhost:8081/auth/register'

1
{"username":"test","secretKey":"GK5gLdu841LBT4c8dfnYFovGhioUjDiL"}

if you get the above response, then congratulations you’ve finished the REST API.

Now on to the next part.

to be continued.

You can see the full code here