Understanding Simple Login Systems

18 Feb, 2026    

While it is fairly common to simplify login systems through the use of OAuth from major providers like Google and Azure, or even use APIs like Privy, I wanted to explore how JWT works and how to implement a fairly simple login system, just for learning purposes.

To begin, let’s start with a naive login system.

In a very basic setup, we would have a database that stores a mapping of username -> password hash, typically something like argon2id( salt || password ). When a user logins to the system, they send their credentials over (hopefully) a HTTPS session, and the server could simply recompute the password hash and check if it matches what is stored in the database. If there’s a successful match, the server can then permit the user to perform some other protected action.

Can we do better?

This naive approach can be problematic in a high-throughput system where we may want to scale horizontally across a few nodes, as the database becomes a contention point, not to mention the latency involved in making a call to the database. A very simple level up to this problem could be to introduce the use of a cache, but this incurs the cost of memory usage.

However, there’s another way to approach this with just the use of some crypto in Json Web Tokens (JWTs).

JWTs

A typical JWT has the following 3 parts in the structure: <header>.<payload>.<signature>, each part separated by a ..

The header typically consists of the following json: { "alg": "hs256", "typ": "JWT" }. Algorithm is typically HMAC-SHA256 or RSA. To keep things simple in my implementation, I went with HMAC-SHA256, which is a symmetric key algorithm. Each instance of the server must thus know a known shared secret, which will be used for signing the token and also verifying the token.

The next part, the payload, consists of claims, embedded in a json structure as well. It can consist of anything you want to sign, but typically when using a library, some common fields are already defined by default, such as:

  • Issuer (iss)
  • Expiry time (exp) : This is optional, and you can choose not to include it if the system doesn’t handle sensitive data.
  • Subject (sub)
  • etc…

For the final part, the signature is simply computed as HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret).

Lastly, each part is then base64-url encoded and joined together with a ., and returned to the user.

The Revised Login Flow

Now, we can have a login flow as follows.

On fresh login/expired token

  • Check that the provided credentials matches what is stored in the database.
  • On success, generate a JWT and return it to the user

Subsequent requests

  • User sends over the JWT as part of the Authorization headers.
  • Server simply needs to check that:
    • Signing the payload gives back the provided signature
    • Check that the payload is not expired if needed
    • Pull out the required user details from the payload, such as the username, and continue with its usual operations.