Akash Kumar Sinha

Understanding Authentication: From Basics to Token-Based Security

Authentication is one of the most fundamental security problems in modern applications-and one of the easiest to underestimate.

When I first started building applications, authentication seemed straightforward: take a username and password, check whether they match, create a session, and let the user in.

It worked.

But “working” and “being secure” are two very different things.

As I dug deeper, I discovered that authentication isn’t simply about checking a password. It involves protecting credentials, verifying that a user actually controls an account, managing sessions securely, handling tokens, preventing common attacks, and deciding exactly how much trust the application should place in each request.

A simple login form is only the visible part of a much larger security system.

In this blog, I’ll walk through that progression from the ground up-from a basic username-and-password implementation to stronger authentication mechanisms such as email verification, secure password hashing, session management, and token-based authentication inspired by OAuth 2.0.

The goal isn’t to make authentication unnecessarily complicated. It’s to understand why each layer exists, what problem it solves, and what can go wrong when it’s implemented incorrectly.

Because in authentication, the question isn’t just:

“Can this user log in?”

It’s:

“How can the system confidently establish who this user is-and continue protecting that identity after they log in?”

My Initial Approach to Authentication

When I first started building web applications, authentication was one of the most confusing parts of the development process. I understood the basic idea-users provide credentials, the server verifies them, and the application keeps them logged in-but I didn’t yet understand how much security was hidden behind those seemingly simple steps.

Like many beginners, I started with a straightforward approach.

The user would submit a username and password to the server. The server would verify the credentials, and if they were valid, it would create a session and send a session cookie back to the browser. That cookie would have an expiration time, allowing the user to remain authenticated without entering their password on every request.

For a basic application, this approach feels perfectly reasonable. The browser stores the session identifier, sends it with subsequent requests, and the server uses it to determine whether the user has an active session.

But authentication isn’t just about making the login flow work.

Sooner or later, you have to ask harder questions:

What happens if someone obtains the session cookie?

How are passwords stored if the database is compromised?

How do you verify that an email address actually belongs to the person creating the account?

How should authentication work when multiple services or applications need to trust the same user identity?

These questions exposed the limitations of my initial implementation.

The basic session-based approach wasn’t necessarily wrong-it was simply incomplete. Real-world authentication requires multiple layers of protection, because compromising any one of those layers can undermine the entire identity system.

That realization led me to explore what happens beyond a simple username-and-password check.

Email Verification

The initial approach I used felt too naive. Once the server verified a valid username and password, the user was essentially trusted and given an authenticated session.

But that raised an important question: how do we establish that the user actually controls the email address associated with the account?

This led me to explore email verification as an additional layer in the authentication flow.

Instead of treating account creation as complete immediately, the application sends a verification link or a short-lived one-time code to the user’s registered email address. The user must successfully complete that step before the account is considered verified and access to certain authenticated features is granted.

The flow becomes:

Create Account → Verify Credentials/Details → Send Verification Email → User Confirms → Account Verified

This provides an important security boundary. Someone who can submit information to the registration form but cannot access the associated email account cannot complete the verification step.

It also gives the application a reliable way to confirm that an email address is reachable and controlled by the person completing the registration process.

However, email verification has an important limitation: it establishes control of an email account, not necessarily the real-world identity of the person behind it. If an email account is compromised, for example, an attacker may also be able to complete the verification process.

That distinction helped me understand a broader principle of authentication:

Security rarely comes from one perfect mechanism. It comes from combining multiple layers, each designed to address a different failure mode.

Email verification strengthened the account lifecycle, but it didn’t solve another fundamental problem I hadn’t considered deeply enough yet:

What happens to the user’s password if the database containing it is compromised?

That question led to the next layer- secure password hashing .

A Better Way to Handle Authentication

Email verification added another layer of protection, but it quickly became clear that a secure authentication system needed to protect something even more fundamental: the user’s credentials themselves .

When you’re building your own authentication system for a web or mobile application, you can’t rely on the security infrastructure of a third-party identity provider such as Google or Facebook. Your application becomes responsible for protecting passwords, sessions, account recovery flows, and the other pieces that determine how user identity is established and maintained.

One of the most important principles is simple:

Never store passwords in plaintext.

Why Does Password Hashing Matter?

When a user creates an account, their password should never be stored directly in the database. Instead, the application should pass the password through a password-hashing function specifically designed to make password guessing computationally expensive.

Algorithms such as bcrypt, scrypt, and Argon2 are designed for this purpose.

The important distinction is between encryption and hashing .

Encrypted data can be decrypted when the correct key is available. Password hashes, on the other hand, are designed to be one-way. During login, the application hashes or verifies the supplied password against the stored password hash rather than decrypting an original password.

Why Salting Matters

Password hashing alone isn’t enough if identical passwords always produce identical hashes.

A salt is a unique random value incorporated into the password-hashing process. With a properly designed password-hashing algorithm, each password gets a unique salt, meaning that two users who choose the same password will still have different stored password hashes.

This makes large-scale precomputed attacks, such as rainbow-table attacks, considerably less useful and makes identical passwords much harder to identify from a stolen database.

Modern password-hashing libraries such as bcrypt and Argon2 handle salt generation and inclusion in the resulting hash, so developers should generally use the library’s standard password-hashing and verification APIs rather than implementing salting manually.

This matters because a database breach should not automatically become a complete credential compromise.

History has repeatedly shown the consequences of treating password storage as a simple hashing problem. Older systems have used fast general-purpose hashing algorithms, inadequate salting, or other weak storage practices, making stolen password databases far easier to attack offline.

A secure password-storage strategy therefore follows a simple principle:

Assume the password database could eventually be exposed, and make the stored representation as resistant to offline guessing as practical.

That changed how I thought about authentication.

The goal isn’t merely to prevent someone from logging in without a password. It’s to design the system so that even when one layer fails, the attacker’s path to the user’s identity remains difficult.

But protecting the password is only one part of the problem.

Once the user has successfully authenticated, the application still needs a secure way to remember that authentication across subsequent requests.

That’s where sessions and tokens enter the picture.

Concepts of OAuth 2.0

As I continued learning about authentication and authorization, I eventually came across OAuth 2.0 -a widely adopted framework for delegated authorization.

OAuth 2.0 is used when one application needs controlled access to resources owned by a user without requiring the user to hand over their credentials to that application. This is the mechanism behind many integrations where a user grants one service permission to access specific resources from another service.

However, understanding OAuth 2.0 also helped me recognize an important distinction that is easy to miss when you’re starting out:

Authentication and authorization are not the same thing.

Authentication answers:

“Who are you?”

Authorization answers:

“What are you allowed to access?”

OAuth 2.0 primarily addresses the second question-delegated authorization.

OAuth 2.0 vs. a Custom Authentication System

OAuth 2.0 is commonly associated with signing in through providers such as Google, GitHub, or other identity platforms, but the underlying framework is broader than a login button.

In my application, I wasn’t implementing a complete OAuth 2.0 authorization server. Instead, I built a custom authentication system using token-based session-management concepts that are also common in OAuth-based architectures , particularly the separation between short-lived access tokens and longer-lived refresh tokens.

This distinction is important because simply generating an access token and a refresh token does not make an authentication system OAuth 2.0 compliant.

What OAuth 2.0 did provide was a useful mental model for thinking about limited-lived credentials, token rotation, delegated access, and reducing the amount of time a compromised credential remains useful.

Implementing Token-Based Authentication

In my custom system, the flow begins when a user submits their credentials.

The server verifies the supplied password against the stored password hash. If authentication succeeds, the server issues two credentials with different purposes: an access token and a refresh token .

The client stores these credentials using a secure storage mechanism appropriate to the platform. On mobile devices, this typically means using the operating system’s secure credential storage rather than treating application storage as inherently trusted. For web applications, secure, appropriately configured cookies are often preferable to exposing long-lived credentials to JavaScript.

Access Token

An access token is a short-lived credential used to access protected resources.

The client presents it when making an authenticated request, commonly through the HTTP Authorization header:

Authorization: Bearer <access-token>

The server validates the token before allowing the request to access protected resources.

Keeping access tokens relatively short-lived limits the window in which a stolen token can be abused. The exact lifetime depends on the application’s threat model and usability requirements.

Refresh Token

A refresh token serves a different purpose.

Rather than being sent with every API request, it is used to obtain a new access token when the current access token expires. This allows the application to maintain a user’s session without requiring them to repeatedly enter their password.

A common flow looks like:

Login → Access Token + Refresh Token

Then, when the access token expires:

Refresh Token → New Access Token

For stronger security, refresh tokens can also be rotated . Each time a refresh token is successfully used, the server issues a new refresh token and invalidates the previous one. This allows the system to detect certain forms of token reuse and revoke compromised sessions.

Token-based authentication therefore isn’t simply about replacing cookies with JWTs or another token format.

The real security comes from how the tokens are issued, stored, validated, expired, rotated, revoked, and scoped .

That realization was one of the biggest changes in how I approached authentication: the token itself isn’t the security boundary.

The entire lifecycle of the credential is.

What Happens When the Access Token Expires?

Access tokens are intentionally short-lived. When one expires, the client shouldn’t have to send the user’s password again just to continue an existing session.

Instead, the refresh token can be used to obtain a new access token.

The typical flow looks like this:

  • The client detects that the access token has expired and sends the refresh token to the token endpoint.
  • The server validates the refresh token and checks whether the associated session is still valid.
  • If the refresh token is valid, the server issues a new access token. Depending on the security model, it may also issue a new refresh token.
  • The client securely stores the newly issued credentials.
  • Subsequent API requests use the new access token.

This creates a useful separation between short-lived access and longer-lived session continuity .

Why Issue a New Refresh Token?

At first, rotating refresh tokens can seem unnecessary.

If a refresh token is already valid for a long period, why not simply keep using the same one until it expires?

For simple systems, that approach can work. But refresh token rotation provides an important security advantage.

With rotation, every successful refresh invalidates the previous refresh token and issues a replacement:

Refresh Token A → New Access Token + Refresh Token B

The next refresh uses Token B:

Refresh Token B → New Access Token + Refresh Token C

This means an old refresh token doesn’t remain valid indefinitely.

More importantly, rotation can help detect refresh-token reuse . If an attacker obtains an old refresh token and attempts to use it after the legitimate client has already exchanged it, the server can detect that the token has been reused and revoke the associated session or token family.

Session Lifetime Still Matters

Refresh-token rotation doesn’t mean a user should remain logged in forever.

A production authentication system can combine rotation with:

  • Idle expiration: the session expires if the user hasn’t been active for a defined period.
  • Absolute expiration: the session has a maximum lifetime regardless of activity.
  • Revocation: the server can invalidate a session when the user logs out, changes their password, or when suspicious activity is detected.
  • Token rotation: previously used refresh tokens become invalid after successful rotation.

This gives the system a balance between security and usability.

An active user can continue their session without repeatedly entering their password, while an abandoned or compromised session eventually becomes invalid.

The important lesson is that a refresh token isn’t simply a “long-lived access token.”

It represents an ongoing authentication session, and that session needs its own lifecycle, expiration, rotation, and revocation rules.

Conclusion

Authentication can seem complicated when you first encounter it. At the beginning, it can look like nothing more than checking a username and password. But once you start thinking about what happens when credentials are stolen, sessions are compromised, databases are leaked, or tokens expire, you realize that authentication is really about one thing:

Managing trust securely.

Email verification adds another layer of confidence around account ownership. Strong password hashing protects credentials even if the database is exposed. Short-lived access tokens limit the lifetime of active credentials, while refresh tokens provide a controlled way to maintain a session without repeatedly asking the user for their password.

The most important lesson I learned is that there isn’t a single authentication mechanism that solves every problem. Secure authentication comes from combining multiple layers, understanding the threats each layer addresses, and designing the entire credential lifecycle carefully.

OAuth 2.0 helped me understand many of these concepts, but building my own token-based system also taught me an important lesson: using a token doesn’t automatically make an authentication system secure. The real security comes from how those credentials are generated, stored, transmitted, validated, rotated, expired, and revoked.

There is still a lot more to explore-from multi-factor authentication and passkeys to session revocation, device management, account recovery, and more sophisticated identity architectures.

But this foundation changed how I think about authentication.

It’s no longer just:

“Does the password match?”

It’s:

“How do I establish trust, limit that trust, and safely take it away when it should no longer exist?”

And that, to me, is what secure authentication is really about.