Users and Security

Authentication with JWT


Learning Objectives

  • You understand the structure of JSON Web Tokens (JWTs) and their three components.
  • You can implement JWT-based authentication with token expiration.

JSON Web Tokens

JSON Web Tokens (JWTs) are a compact way of securely transmitting information as a JSON object, commonly used for authentication and passing information. A JWT consist of three parts: a header, a payload, and a signature:

  • The header typically consists of two parts: the type of the token, which is JWT, and the signing algorithm being used, such as HS256 for HMAC-SHA256
  • The payload contains the claims. Claims are statements about an entity (typically, the user) and additional data such as the expiration time for the token (defined by the exp claim).
  • The signature is used to verify that the token has not been tampered with. The signature is created by taking the encoded header, the encoded payload, a secret, the algorithm specified in the header, and signing that. Only those who have the secret key (typically, the server) can verify (and create) the signature.

The header and payload are Base64 encoded JSON objects, while the signature is an encoded string. Jointly, the three parts form a JWT, which is represented as a string in the format header.payload.signature.

When using JWTs, the server does not need to store session data about the user. Instead, the server can verify the token by recomputing its signature using the secret key. If the signature and claims are valid, the server can trust the data in the payload (such as a user ID or email) without a database lookup.

Note that JWTs are not encrypted by default. Their contents are readable by anyone with the token, so do not include sensitive data like passwords or secrets into the token. This is illustrated in the next exercise, where you use an online service to decode a JWT.

JWT payload

0 / 5 points

Search for an online service for decoding JWT tokens (for example, https://jwt.io/). Use the service to decode the following JWT token and study its payload:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXQiOiI0MzVkIn0.fzgIhRpiCrtEq2sENrrMH4xWVvO6-j62GLwRJFRql8k

What is the value associated with the key secret in the payload of the token?

Authentication with JWTs

Here, we modify the authentication API from the last chapter to use JWTs. The API will create a JWT token when the user logs in, and return the token with the user information to the client.

Hono JWT Helpers

Hono comes with JWT Helpers that make it easier to work with JWTs on the server. To use the helpers, we import the functions exposed from @hono/hono/jwt, giving them the alias jwt.

import * as jwt from "@hono/hono/jwt";

The helper exposes three asynchronous functions that we’ll use:

  • sign(payload, secret) that creates a signed JWT
  • verify(token, secret) that verifies a JWT
  • decode(token) that decodes a JWT without verifying it

The example below shows how to use the helper to create a JWT token.

import * as jwt from "@hono/hono/jwt";
const payload = {
message: "Hello world!",
};
const token = await jwt.sign(payload, "SECRET KEY");
console.log(token);

The output of the above program would be as follows:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiSGVsbG8gd29ybGQhIn0.ZIGQVXBhPBOdz_yucR6j-ULF3TvCgNrmWU9Y6iI_DTk

Authentication controller

Let’s modify the authentication controller to use JWTs. First, we import the JWT helpers as shown above. Then, when the user successfully authenticates, we create a payload that contains the user email, and sign the payload using the secret key to create a token. Finally, we respond with the token and the user information.

In this example, we store the secret key in a constant JWT_SECRET with the value “jwt_secret”. In practice though, the secret would not be hardcoded in the source code, but instead be stored in an environment variable. The secret would also be more complex than “jwt_secret”.

The following example outlines the changed authController.js.

import * as jwt from "@hono/hono/jwt";
import { hash, verify } from "scrypt";
import * as authRepository from "./authRepository.js";
const JWT_SECRET = "jwt_secret";
const register = async (c) => {
const user = await c.req.json();
user.password_hash = hash(user.password);
const newUser = await authRepository.create(user);
return c.json(newUser);
};
const login = async (c) => {
const user = await c.req.json();
const foundUser = await authRepository.findByEmail(user.email);
if (!foundUser) {
return c.json({ error: "Invalid email or password" }, 401);
}
const isValid = verify(user.password, foundUser.password_hash);
if (!isValid) {
return c.json({ error: "Invalid email or password" }, 401);
}
const payload = { email: foundUser.email };
const token = await jwt.sign(payload, JWT_SECRET);
return c.json({
message: "Login successful",
user: { email: foundUser.email },
token
});
};
export { login, register };

Now, when we try authenticating, the response has a cookie with the JWT token as the value.

Terminal window
curl -X POST -d '{"email": "test@test.com", "password": "secret"}' localhost:8000/api/auth/login
{"message":"Login successful","user":{"email":"test@test.com"},"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJzZWNyZXQiOiJJIGFtIGhhcHB5IHRoYXQgeW91IGxvb2tlZCBpbnRvIHRoaXMgOikuIn0.v9652aAhEoCyLXCUX7QpHEXX4wO77-QujN9LYHBRkUs"}
Duplicate information?

In the above, the user email is included in both the user object and the token. This way the client can use the user object directly without needing to decode the token. However, the client can also decode the token to get the user email.


Secret key storage

0 / 5 points

What is the best practice for storing the JWT secret key in a production application?

JWT Expiration

By default, the JWT tokens do not expire. They should not be valid forever, though, as that would be a security risk — if a token is compromised, it could be used indefinitely.

Expiration time

To improve security, we can set an expiration time for the tokens.

The expiration of a token can be set when the token is created using an exp property in the payload. The exp property should be set to a timestamp (in seconds) that indicates when the token should expire. As an example, if we would wish to create a token that expires in 60 seconds, we could set the exp property to the current time plus 60 seconds.

const payload = {
email: foundUser.email,
exp: Math.floor(Date.now() / 1000) + 60,
};

Normally, the expiration time would be longer than 60 seconds, for example, one day or one week.

Token expiration time

0 / 5 points

Which code correctly sets a JWT token to expire in 3600 seconds (1 hour) from the current time?

const payload = {
email: user.email,
// expiration time here
};

Access and refresh tokens

Contemporary applications use two tokens to manage authentication: access tokens and refresh tokens. Access tokens are short-lived tokens used to access protected resources, while refresh tokens are long-lived tokens used to obtain new access tokens when the current access token expires.

Access and refresh tokens are used e.g. in OAuth 2.0 authorization framework.

With access and refresh tokens, the flow is as follows: When the user logs in, they receive both an access token and a refresh token. Then, when the user interacts with the system, the client uses the access token to authenticate API requests. If the access token expires, the client can use the refresh token to request a new access token without requiring the user to log in again. If the refresh token is also expired, the user must log in again.

The rationale for using both access and refresh tokens is twofold: first, access tokens are only valid for a short time, and if compromised, the risk of misuse is lower. This is important as access tokens are primarly the tokens that are sent with API requests. Second, refresh tokens improve the user experience, as the user does not need to log in frequently.

In this course, we will not implement refresh tokens. However, you should be aware of their existence and purpose.

Access and refresh tokens

0 / 5 points

Which of the following statements about access tokens and refresh tokens are true? (Select all that apply)

Summary

In summary:

  • JSON Web Tokens (JWTs) consist of three parts: header, payload, and signature, formatted as header.payload.signature:
    • The header contains metadata about the token, including the token type and signing algorithm.
    • The payload contains claims about the user and additional data, such as email and expiration time.
    • The signature ensures the token hasn’t been tampered with and can only be created and verified by those with the secret key (e.g. the server).
  • JWTs are not encrypted by default so anyone who gains access to the tokens can decode and read the payload.
  • Contemporary applications often use two types of tokens: short-lived access tokens for accessing resources and long-lived refresh tokens for obtaining new access tokens without re-authentication.

AI Study Aid

Create a chapter diagram

We're looking into to what extent AI-generated diagrams could help with studying.

Use this study aid to generate an AI-generated visual summary of the material you just studied. Each diagram style emphasizes the content in a different way, so you can choose the focus that feels most useful.

Using the diagram generator is voluntary and does not affect your course progress. We encourage you to try it out and see if it helps, but it's totally up to you! Your answers help us understand how to make better study aids in the future.

Diagrams tried: 0

Authentication API with JWT

0 / 20 points

Follow along the example in this chapter, building on top of your solution to the earlier “Authentication API” exercise. The schema for users is still as follows:

CREATE TABLE users (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL,
password_hash TEXT NOT NULL
);
CREATE UNIQUE INDEX ON users (lower(trim(email)));

Modify the example that you have followed to match the following:

  • POST /api/auth/login for user login

If the login is successful, the response body should be a JSON object with the following properties:

  • message: “Welcome back ${email}!”
  • user: { id: number, email: string }
  • token: string (a JWT token)

Note that the message is different and the user property has the user identifier id. Also include the user identifier id and email email into the payload of the token.

If the login fails (for example, if the email does not exist or the password is incorrect), the response body should be as before.

Note! When signing the JWT token, use “jwt_secret” as the secret key.

No changes are needed to the POST /api/auth/register endpoint for user registration.

Once ready, return your project as a zip file. The expected files in the zip are as follows:

.
├── authController.js
├── authRepository.js
├── app-run.js
└── app.js

The zip can also contain other files (e.g., deno.json), but the above files must be present. If you omit files that are in your own setup, make sure that they are not necessary for running the application.