# Authentication and Authorization in Backend

So far in this series, we've talked about HTTP, routing, serialization, and how data moves between clients and servers.

But eventually, every backend application reaches two very important questions:

```text
Who are you?

and

What are you allowed to do?
```

These two questions sound similar, but they represent two completely different concepts.

**Authentication** answers:

> Who are you?

**Authorization** answers:

> What are you allowed to do?

Authentication happens first.

The server needs to establish your identity before it can decide whether you're allowed to access a particular resource.

For example:

![](https://cdn.hashnode.com/uploads/covers/611d5567a285e422d5a131ac/6f17daf2-3d92-40cc-bab6-06fea9db4721.png align="center")

Just because someone has successfully logged in doesn't mean they should automatically have access to everything in the system.

A regular user probably shouldn't be able to call:

```http
DELETE /api/death-star
```

Some endpoints deserve a little more protection.

* * *

## Authentication

Authentication is the process of verifying the identity of a user, service, or application.

But passwords are only one way of authenticating users.

Modern systems can use:

*   Username and password
    
*   Sessions
    
*   Tokens
    
*   API keys
    
*   OAuth 2.0
    
*   OpenID Connect
    
*   Passwordless authentication
    
*   Multi-factor authentication
    

You'll also hear terms such as **stateful authentication** and **stateless authentication**.

Let's understand those first.

* * *

## Stateful Authentication

In stateful authentication, the server maintains information about the user's authenticated session.

Imagine a user logs into an application.

The flow could look something like this:

```text
User logs in
     ↓
Server verifies credentials
     ↓
Server creates session
     ↓
Session stored on server
     ↓
Session ID sent to browser
     ↓
Browser sends session ID
with future requests
```

The actual session data might be stored in:

```text
Database
Redis
In-memory session store
```

The browser doesn't need to store all the authentication information.

It usually just stores something like:

```text
session_id = abc123
```

Then every subsequent request contains that identifier.

The server receives it:

```text
session_id = abc123
       ↓
Session Store
       ↓
User = 42
Role = admin
Authenticated = true
```

This gives the backend a lot of control.

If we want to immediately log a user out, we can simply delete that session from the server.

### Advantages

Stateful authentication gives us:

*   Centralized session control
    
*   Easy revocation
    
*   Simple logout behaviour
    
*   Ability to track active sessions
    
*   Fine-grained session management
    

If a user's account becomes compromised, we can invalidate their session immediately.

### The downside

The server now needs to maintain state.

With a single backend server, this isn't too difficult.

But imagine:

If the user's session only exists on Server A and the next request reaches Server C, Server C won't know who the user is.

We can solve this using a shared session store such as Redis

But that introduces another piece of infrastructure we need to operate.

Stateful authentication isn't inherently unscalable, it scales perfectly well when designed correctly but maintaining centralized session state introduces additional infrastructure and coordination.

* * *

## Cookies

This is also where cookies become useful. A cookie is a small piece of data that a server can instruct a browser to store.

For example, after a successful login the server might respond with:

```http
Set-Cookie: session_id=abc123
```

The browser stores it.

Then when the browser makes another request to the appropriate site, it can automatically send:

```http
Cookie: session_id=abc123
```

So the flow becomes:

```text
Login
  ↓
Server creates session
  ↓
Set-Cookie
  ↓
Browser stores cookie
  ↓
Future Request
  ↓
Cookie automatically sent
  ↓
Server identifies session
```

This makes cookies extremely convenient for browser-based authentication.

One important distinction:

> Cookies are not authentication themselves.

They're simply one way of storing and transporting authentication information in a browser.

The cookie could contain:

```text
Session ID
JWT
Other opaque token
```

* * *

## Stateless Authentication

Now imagine we don't want the server to maintain a session entry for every logged-in user. Instead, what if the client carried the information required to prove its identity?

That's the idea behind **stateless authentication**.

A common example uses JWTs.

The flow might look like:

```text
User logs in
     ↓
Server verifies credentials
     ↓
Server creates token
     ↓
Token sent to client
     ↓
Client stores token
     ↓
Token included with future requests
```

When the request arrives, the server verifies the token without necessarily looking up a session from centralized storage.

This can be useful in distributed systems.

* * *

## JWT

JWT stands for **JSON Web Token**.

A JWT is a token format used to represent a set of claims.

A typical JWT looks something like:

```text
xxxxx.yyyyy.zzzzz
```

It contains three parts:

```text
Header
Payload
Signature
```

The payload may contain claims such as:

```json
{
  "sub": "user_123",
  "role": "jedi",
  "exp": 1760000000
}
```

The signature allows the server to verify that the token wasn't modified after it was issued.

One thing that's important to understand:

> A JWT is usually signed, not encrypted.

That means the payload can often be decoded by anyone who possesses the token. So you shouldn't put passwords or sensitive secrets inside it. The signature protects the token's **integrity**, not necessarily its confidentiality.

* * *

## JWT Authentication Flow

Let's say the user logs in:

```http
POST /api/login
```

with:

```json
{
  "email": "obiwan@example.com",
  "password": "highground"
}
```

The server verifies the credentials. If everything is correct, it generates a JWT. The client can then include it with subsequent requests.

One common approach is an HTTP header:

```http
Authorization: Bearer <token>
```

The token could also be placed inside a secure cookie depending on the architecture.

* * *

## Why use JWTs?

JWT-based authentication can provide some useful properties.

### Stateless verification

The server can verify the token without maintaining a session record for every user.

### Scalability

Multiple backend instances can verify the same token as long as they have access to the appropriate signing key or verification key.

### Portability

Tokens work well when APIs are being consumed by:

*   Mobile applications
    
*   Multiple frontend clients
    
*   Distributed services
    
*   Service-to-service APIs
    

But JWTs also create some challenges.

* * *

## The Problem With JWTs

Imagine we issue a token that is valid for one hour.

Five minutes later, we discover that the user's account has been compromised.

We want to immediately revoke access.

With a stateful session:

```text
Delete Session
      ↓
Access Gone
```

Easy.

But with a stateless JWT, the token has already been issued.

As long as:

```text
Signature = valid
Expiration = valid
```

the server may continue accepting it.

This is one of the main challenges with stateless tokens.

### Token theft

If an attacker steals a valid bearer token, they may be able to use it until it expires. This is why token storage, transport security, expiry times, and refresh-token design matter so much.

### Revocation

Stateless tokens are deliberately designed to avoid central session lookups. But sometimes we need centralized control anyway. Which leads to an interesting compromise.

* * *

## Hybrid Authentication

Suppose we still use JWTs, but we maintain a blacklist or revocation list.

The revocation store could be:

```text
Redis
Database
```

If a token needs to be invalidated, we add its identifier to the revocation list.

Now we can revoke tokens before they naturally expire.

But there is an obvious question here.

> If we're querying centralized storage again, aren't we losing the whole benefit of stateless authentication?

Partially, yes. That's the trade-off. Authentication design often isn't about finding a perfect solution. It's about choosing the right trade-offs for your system.

In practice, another common pattern is:

```text
Short-lived Access Token
          +
Long-lived Refresh Token
```

The access token expires quickly. The refresh token can be tracked, rotated, or revoked more carefully. This limits how long a stolen access token remains useful while avoiding a session lookup on every API request.

* * *

## So should I use Sessions or JWTs?

Neither is universally better.

For a traditional web application, server-side sessions with secure cookies can be a perfectly good and often simpler choice.

If you're building:

```text
Browser → Backend
```

and both are part of the same application, sessions can be extremely convenient.

JWTs become more attractive when you're dealing with:

```text
Mobile Clients
Multiple APIs
Distributed services
Third-party consumers
Service-to-service communication
```

The important lesson is:

> Don't use JWT simply because it sounds more modern.

Choose it because the properties of your architecture actually benefit from it.

* * *

# API Keys

Another authentication mechanism you'll encounter frequently is an API key.

An API key is usually a long, randomly generated string:

```text
sk_live_xxxxxxxxxxxxxxxxx
```

A client includes this key while making a request:

```http
Authorization: Bearer <api-key>
```

or sometimes:

```http
X-API-Key: <api-key>
```

The server looks up the key and determines which client or application owns it.

API keys are commonly used for:

```text
Developer APIs
Service integrations
Machine-to-machine communication
```

For example:

Your backend might identify itself to the payment provider using an API key.

API keys are generally better suited to identifying **applications or services** than implementing full end-user login systems. They also need to be treated like passwords. If someone steals the API key, they may gain the permissions associated with it.

So they should never casually appear inside:

```text
GitHub repositories
Frontend JavaScript
Logs
Screenshots
```

* * *

# OAuth

Now things get more interesting.

Imagine you build an application that wants access to someone's Google Calendar.

One possible solution would be:

> Give me your Google username and password.

Which is obviously terrible.

Your application shouldn't need the user's Google credentials just to access their calendar.

This is the **delegation problem**.

We need a way for a user to say:

> I allow this application to access some of my data without giving it my password.

OAuth was created to solve this problem.

* * *

## OAuth 1.0

OAuth 1.0 introduced standardized delegated authorization. Instead of sharing passwords, the user could authorize a third-party application to access certain resources. OAuth 1.0 provided strong request authentication using cryptographic signatures. But implementing it correctly was relatively complicated. Each request had to be signed, and clients had to carefully generate and calculate signatures.

That complexity made implementations harder.

OAuth 2.0 significantly redesigned and simplified the model.

* * *

# OAuth 2.0

OAuth 2.0 is an **authorization framework**.

That distinction is important.

OAuth 2.0 primarily answers:

> What is this application allowed to access on behalf of the user?

Not necessarily:

> Who exactly is the user?

Let's say we're building an application that wants access to someone's Calendar.

Instead of giving us their credentials:

```text
User
 ↓
Authorization Server
 ↓
"Allow this app to read my calendar?"
 ↓
User approves
 ↓
Application receives access token
 ↓
Application accesses Calendar API
```

The client never needs the user's actual password.

OAuth introduces concepts such as:

```text
Resource Owner
Client
Authorization Server
Resource Server
Access Token
Scopes
```

### Scopes

Scopes define what the application is allowed to do.

For example:

```text
calendar.read
calendar.write
profile.read
```

The user might authorize:

```text
calendar.read
```

without allowing:

```text
calendar.write
```

That gives us fine-grained delegated access.

* * *

## A Simplified OAuth 2.0 Flow

For a modern application using the Authorization Code flow:

```text
User
 ↓
Client Application
 ↓
Redirect to Authorization Server
 ↓
User authenticates and approves
 ↓
Authorization Code returned
 ↓
Client exchanges code for token
 ↓
Access Token
 ↓
Resource Server
```

Modern public clients typically use **PKCE** with the Authorization Code flow to protect the authorization-code exchange.

But there was still a problem.

Developers also wanted a standardized way to answer:

> Who is the user that just logged in?

OAuth 2.0 alone wasn't designed to provide a standardized identity layer.

Enter **OpenID Connect**.

* * *

# OpenID Connect

OpenID Connect, commonly called **OIDC**, extends OAuth 2.0 with authentication.

If OAuth answers:

```text
What can this application access?
```

OIDC adds:

```text
Who is the user?
```

OIDC introduces something called an **ID Token**.

The ID Token is typically represented as a JWT and contains claims about the authenticated user.

For example:

```json
{
  "sub": "user_123",
  "name": "Leia Organa",
  "email": "leia@example.com"
}
```

The simplified flow becomes:

```text
User
 ↓
Application
 ↓
Identity Provider
 ↓
User authenticates
 ↓
Authorization Code
 ↓
Application exchanges code
 ↓
Access Token + ID Token
```

The tokens serve different purposes.

### Access Token

Used to access protected APIs.

```text
"What can I access?"
```

### ID Token

Used by the client to learn about the authenticated user's identity.

```text
"Who authenticated?"
```

This is why buttons such as:

```text
Continue with Google
Sign in with Microsoft
```

are commonly implemented using OAuth 2.0 together with OpenID Connect.

* * *

# Authentication Providers

Once authentication starts involving:

```plaintext
OAuth
OIDC
MFA
Password resets
Email verification
Passkeys
Token rotation
Account recovery
Session management
```

things become surprisingly complicated.

And authentication is one of those systems where subtle mistakes can have serious security consequences.

That's why many production systems use dedicated identity providers.

They handle much of the authentication infrastructure while your application focuses on its actual business logic.

This doesn't mean every production system must outsource authentication.

But it's worth remembering that implementing authentication securely is significantly harder than building a `/login` endpoint.

* * *

# Passwordless Authentication

Passwords create a lot of problems.

People:

```text
Reuse them
Forget them
Choose weak ones
Get phished
```

Passwordless authentication attempts to remove the traditional password entirely.

Some common approaches include:

```text
Magic Links
One-Time Codes
Passkeys
Hardware Security Keys
Biometrics backed by device credentials
```

Passkeys are particularly interesting because they use public-key cryptography.

Instead of the server storing a password-derived secret, the user authenticates using a cryptographic credential tied to their device or credential manager.

This also makes them much more resistant to traditional phishing attacks.

* * *

# What about Zero Trust?

You might also hear **Zero Trust** mentioned in conversations about authentication.

Zero Trust isn't another login mechanism like JWT or OAuth.

It's a broader security model.

The basic idea is:

> Never automatically trust a request just because it came from inside your network.

Traditionally, systems sometimes treated the internal company network as trusted.

Zero Trust assumes that every request should instead be continuously evaluated based on things such as:

```text
Identity
Device
Permissions
Context
Risk
```

In other words:

```text
Never trust.
Always verify.
```

* * *

# Which Authentication Approach Should You Use?

There isn't one authentication strategy that works for everything.

A rough mental model might look like this.

### Traditional Web Application

```text
Browser
   ↓
Backend
```

A strong default can be:

```text
Server-side session
+
Secure HttpOnly cookie
```

Simple, controlled, and easy to revoke.

### Single Page Application + API

Possible approaches include:

```text
Secure cookie-based session
```

or carefully designed token-based authentication depending on the architecture.

The important part is avoiding unnecessary exposure of long-lived credentials to browser JavaScript.

### Mobile Application

Token-based authentication is commonly used because mobile clients don't behave exactly like browsers.

```text
Short-lived Access Token
+
Refresh Token
```

is a common architecture.

### Multiple Services / Distributed APIs

JWTs or other signed tokens can be useful where multiple services need to verify identity without constantly querying a centralized session database.

### Third-Party Login

Use:

```text
OAuth 2.0
+
OpenID Connect
```

### Third-Party API Access

If your application needs delegated access to another user's resources:

```text
OAuth 2.0
```

### Machine-to-Machine APIs

Depending on the system:

```text
API Keys
OAuth Client Credentials
mTLS
Signed Tokens
```

may all be appropriate.

The answer is always:

> It depends on what you're authenticating and what threat model you're designing for.

* * *

# Authorization

So far we've established identity.

Now comes the second question:

> What are you allowed to do?

Imagine three users:

```text
User
Moderator
Admin
```

All three are authenticated. But they shouldn't have the same access.

Maybe:

```text
User
→ Read own profile
→ Update own profile

Moderator
→ Everything above
→ Moderate posts

Admin
→ Everything above
→ Manage users
→ Configure system
```

This is authorization.

* * *

# Role-Based Access Control

One common authorization model is **RBAC - Role-Based Access Control**.

Instead of assigning every permission directly to every user, we assign users a role.

```text
User
 ↓
Role
 ↓
Permissions
```

For example:

```text
Luke
 ↓
JEDI
 ↓
read_missions
update_profile
```

while:

```text
Palpatine
 ↓
ADMIN
 ↓
read_users
delete_users
modify_system
```

I'm sure nothing bad could possibly happen there, giving palps admin access.

The backend can then protect routes:

```text
DELETE /api/users/:id
```

with something conceptually like:

```text
Authenticated?
      ↓
Role = ADMIN?
      ↓
Yes → Continue
No  → 403 Forbidden
```

This separation is very important.

Authentication might tell us:

```text
User ID = 123
```

Authorization tells us:

```text
User 123 is NOT allowed to perform this operation.
```

* * *

# 401 vs 403

This distinction also appears in HTTP status codes.

### 401 Unauthorized

Despite the slightly confusing name, `401` generally means:

> You have not successfully authenticated.

For example:

```text
Missing token
Invalid token
Expired credentials
```

### 403 Forbidden

`403` generally means:

> I know who you are, but you're not allowed to do this.

For example:

```text
Authenticated User
        ↓
DELETE /api/users/42
        ↓
Role = USER
        ↓
403 Forbidden
```

So:

```text
401 → Who are you?

403 → I know who you are. You still can't do that.
```

* * *

# Security Details That Are Easy to Miss

Authentication isn't just about tokens and login forms.

Sometimes very small implementation details can leak information to attackers.

Two good examples are:

1.  Error messages
    
2.  Timing attacks
    

* * *

## Authentication Error Messages

Imagine someone attempts to log in with:

```text
email: luke@example.com
password: wrongpassword
```

The server replies:

```text
Incorrect password.
```

Sounds helpful.

But it accidentally tells an attacker:

> This email address exists.

Now imagine another attempt:

```text
email: random@example.com
```

and the server replies:

```text
User not found.
```

The attacker can now determine which accounts exist in the system.

This is called **user enumeration**.

Instead, authentication endpoints should usually return a generic response such as:

```text
Invalid email or password.
```

Regardless of whether:

```text
User doesn't exist
Password is incorrect
Account state prevents login
```

Internally, the server can log the exact reason.

But externally, we don't need to give attackers hints.

* * *

# Timing Attacks

Now imagine our login code works like this:

```text
Find User
   ↓
Does user exist?
   ↓
No → Return immediately

Yes
 ↓
Run expensive password hash verification
 ↓
Return result
```

There's a subtle problem.

Password hashing algorithms are intentionally computationally expensive.

So:

```text
Unknown Username
→ Fast response

Existing Username + Wrong Password
→ Slower response
```

An attacker making many carefully measured requests might be able to infer whether an account exists based on the response time.

This creates another form of user enumeration.

One mitigation is to make both paths perform similar work.

Constant-time comparison functions are also important when comparing sensitive cryptographic values because they avoid leaking information based on how early two values differ.

But simply adding an arbitrary:

```text
sleep(500ms)
```

isn't a particularly robust solution.

Real networks already contain latency variation, and manually chosen delays can still leave measurable differences. A better goal is to make authentication code paths perform approximately equivalent security-sensitive work.

* * *

# Putting Everything Together

A modern authentication and authorization flow might look something like:

![](https://cdn.hashnode.com/uploads/covers/611d5567a285e422d5a131ac/cf8a861d-373a-43c8-85c7-78c53be4b41b.png align="center")

Authentication establishes identity.

Authorization determines permissions.

And surrounding both of them are security mechanisms such as:

```text
Secure cookies
Token expiration
MFA
Password hashing
Rate limiting
Generic error messages
Session revocation
Audit logging
```

Because authentication isn't really one feature.

It's an entire security boundary around your application.

* * *

# Wrapping it up

The easiest way to remember everything we've discussed is:

![](https://cdn.hashnode.com/uploads/covers/611d5567a285e422d5a131ac/5a3e9ed9-cccf-4fad-8acd-bb1c2a9861dd.png align="center")

From there, different systems solve the authentication problem differently.

![](https://cdn.hashnode.com/uploads/covers/611d5567a285e422d5a131ac/9a7d8b89-6580-47c6-a59b-361c223790f2.png align="center")

And once the user's identity is established:  

![](https://cdn.hashnode.com/uploads/covers/611d5567a285e422d5a131ac/40b92120-bfd3-467f-8f29-34b3ad9876f9.png align="center")

That's where mechanisms like RBAC enter the picture.

Authentication and authorization seem straightforward when reduced to login screens and role checks.

But once you start thinking about sessions, cookies, tokens, revocation, OAuth flows, password storage, timing leaks, and permission models, you realize how deep the rabbit hole actually goes.

Which is probably why authentication code deserves a lot more respect than:

```javascript
if (password === "password") {
    login();
}
```

Please don't do that.

Not even in a galaxy far, far away.

Thanks for reading.
