To understand JWT properly, we first need to understand the problem JWT was created to solve.
HTTP is a stateless protocol.
When we send a request:
GET /productsthe server responds and then forgets about us.
A second later, when we send:
POST /cartthe server has no inherent memory that this request came from the same person.
From the server's perspective:
Request 1 ---> Response
(forget)
Request 2 ---> Response
(forget)
Request 3 ---> Response
(forget)
Each request is independent.
Imagine an e-commerce application.
We log in as:
skyy@example.com
The server verifies our password.
Now suppose we request:
GET /profileHow does the server know this request belongs to us?
The server needs some way to answer:
Who is making this request?
This question is called:
Authentication answers:
Who are we?
Historically, servers used sessions.
We submit:
{
"email": "skyy@example.com",
"password": "secret123"
}The server verifies credentials.
Then the server creates:
Session ID = abc123xyz
and stores:
abc123xyz ---> User #57
inside server memory or a database.
The server then sends:
Set-Cookie: sessionid=abc123xyzto the browser.
Browser stores:
sessionid=abc123xyz
as a cookie.
Every future request automatically includes:
Cookie: sessionid=abc123xyzServer receives:
abc123xyz
looks it up:
abc123xyz ---> User #57
and knows who we are.
Sessions work well, but they have limitations.
The server must remember every active session.
Example:
User A ---> Session A
User B ---> Session B
User C ---> Session C
For millions of users:
Millions of sessions
must be stored somewhere.
Memory usage increases.
Imagine:
Server 1
Server 2
Server 3
A user logs into Server 1.
Session exists only there:
Session A ---> User #57
Now the next request reaches Server 2.
Server 2 says:
I don't know this session.
because it wasn't stored there.
Solutions exist:
- Redis
- Shared databases
- Sticky sessions
But infrastructure becomes more complex.
JWT takes a completely different approach.
Instead of:
Server remembers user
JWT says:
User carries proof of identity
The token itself contains the information.
This is called:
The server does not need to remember logged-in users.
JWT stands for:
JSON Web Token
A JWT is a compact string that contains:
- Data (claims)
- A digital signature
Example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJpZCI6IjEyMyIsIm5hbWUiOiJTa3l5In0
.
k8Jw9s9K9eL1...
Three parts separated by dots:
HEADER.PAYLOAD.SIGNATURE
Example:
{
"alg": "HS256",
"typ": "JWT"
}Meaning:
Algorithm = HS256
Token Type = JWT
Encoded as Base64.
Result:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Contains data.
Example:
{
"id": "123",
"name": "Skyy",
"role": "admin"
}This is called a:
A claim is information asserted by the token.
Encoded as Base64:
eyJpZCI6IjEyMyIsIm5hbWUiOiJTa3l5In0
Most important part.
Created using:
Header
+
Payload
+
Secret Key
Example:
jwt.sign(payload, secret)Produces:
Xk9slf83jfjf8f8...
Final token:
HEADER.PAYLOAD.SIGNATURE
Consider:
const JWT_SECRET = "supersecretkey";This value never leaves the server.
Never.
Clients never see it.
Without a secret:
Anyone could modify:
{
"role": "user"
}to
{
"role": "admin"
}and gain admin privileges.
That would be disastrous.
The signature prevents this.
Imagine:
{
"id": 57,
"role": "user"
}Server signs it:
payload + secret
producing:
ABC123SIGNATURE
Token becomes:
payload.signature
Now suppose an attacker changes:
{
"role": "admin"
}The payload changes.
The old signature becomes invalid.
Server detects tampering.
Authentication fails.
We submit credentials.
{
"email": "skyy@example.com",
"password": "secret123"
}Server verifies credentials.
Server generates JWT.
const token = jwt.sign(
{
userId: user._id,
},
process.env.JWT_SECRET
);Server returns:
{
"token": "eyJhbGc..."
}Frontend stores token.
Often:
Local Storage
or
HttpOnly Cookie
Future requests include:
Authorization: Bearer eyJhbGc...Bearer means:
Whoever possesses this token
is treated as authenticated.
Possession grants access.
Request:
GET /profile
Authorization: Bearer eyJhbGc...Server extracts token.
Verifies signature.
If valid:
Authenticated
If invalid:
Unauthorized
Creates a token.
Example:
const token = jwt.sign(
{
userId: 57
},
process.env.JWT_SECRET
);Think:
Data
+
Secret
=
JWT
Checks validity.
Example:
const payload = jwt.verify(
token,
process.env.JWT_SECRET
);Think:
JWT
+
Secret
=
Verified User Data
Notice:
Login
β
JWT Generated
β
Client Stores JWT
β
Future Requests Carry JWT
The server stores nothing.
No session table.
No memory record.
Nothing.
Every request contains everything needed.
No session storage.
Works across:
Server A
Server B
Server C
All servers can verify the same token.
No database lookup for session information.
Only:
jwt.verify()Especially:
- React
- Next.js
- React Native
- Mobile Apps
- Microservices
Suppose:
JWT expires in 7 days
User gets banned.
Token still works until expiration.
With sessions:
Delete session
Immediate logout.
JWT doesn't work that way by default.
Session:
abc123
JWT:
eyJhbGciOi...
More data transmitted.
A very common misconception.
JWT is:
Signed
not
Encrypted
Anyone can decode:
Header
Payload
using online tools.
Therefore:
Never store passwords, credit card numbers, or sensitive secrets inside a JWT payload.
Many beginners think:
JWT secures data
No.
JWT proves:
This data came from us
and wasn't modified.
It guarantees:
- Integrity
- Authenticity
It does NOT guarantee:
- Secrecy
- Encryption
Think of a JWT as a digitally signed passport.
The payload is:
Name: Skyy
ID: 57
Role: User
The signature is the government's seal.
Anyone can read the passport.
But nobody can forge the seal without the secret.
When our server receives the JWT, it asks:
Is the seal valid?
If yes:
Welcome back.
If no:
Access denied.
That is the essence of JWT authentication: a cryptographically signed identity document that allows stateless authentication across requests without the server having to remember who is logged in.
Before JWT existed, the fundamental problem was:
HTTP is stateless.
The server does not remember previous requests.
Imagine:
Request #1
POST /login
Response
200 OK
Five seconds later:
Request #2
GET /profile
The server has no built-in way to know:
Are these requests from the same person?
Authentication systems exist to solve this problem.
Many beginners confuse these.
Answers:
Who are we?
Example:
Username: skyy
Password: secret123
Server verifies identity.
Answers:
What are we allowed to do?
Example:
User:
Can view products
Admin:
Can delete products
Authorization happens after authentication.
Let's begin from the very start.
We submit:
{
"username": "skyy",
"email": "skyy@gmail.com",
"password": "secret123"
}Server:
- Validates input
- Checks if email already exists
- Hashes password
- Saves user
Example database record:
{
"_id": "123",
"username": "skyy",
"email": "skyy@gmail.com",
"password":
"$2b$10$h4jK...."
}Notice:
Password is hashed.
Never stored in plain text.
Two common approaches exist.
Register
β
Automatically login
β
Generate JWT
Many modern applications do this.
Register
β
Redirect to Login
β
Generate JWT later
Also common.
JWT is not required for registration itself.
JWT is required when we want to authenticate future requests.
This is where JWT becomes important.
Request:
{
"username": "skyy",
"password": "secret123"
}Server:
Find user
Compare password hash
Verify credentials
If credentials are valid:
Authentication successful
Now we must solve:
How do we prove identity
on future requests?
JWT is the answer.
Server creates:
jwt.sign(
{
id: user._id,
username: user.username
},
JWT_SECRET
)This creates:
header.payload.signature
This is the most important JWT concept.
Imagine a token without a signature:
{
"id": 123,
"role": "user"
}Anyone could modify it:
{
"id": 123,
"role": "admin"
}and gain admin privileges.
So we digitally sign it.
Think of a signature like:
Government seal
on a passport
Anyone can read the passport.
Nobody can forge the seal.
JWT works similarly.
Server owns:
JWT_SECRET="super-secret-key"Only the server knows this.
Client never sees:
JWT_SECRET
Never.
When signing:
Payload
+
Secret
=
Signature
Later:
Payload
+
Secret
=
Expected Signature
If signatures match:
Token is authentic
Server responds:
{
"token": "eyJhbGc..."
}At this point:
Authentication complete
The user is logged in.
The client must store it somewhere.
Common options:
localStorage.setItem("token", token)Simple.
Popular in development.
Preferred in many production systems.
More resistant to XSS attacks.
Regardless of storage:
Client now possesses proof of identity.
Now we reach one of the most misunderstood concepts.
Suppose we have:
JWT = abc.xyz.123
Future request:
GET /dashboard
Authorization: Bearer abc.xyz.123The word:
Bearer
means:
Whoever bears
(possesses)
this token
is considered authenticated.
Think of:
Movie ticket
The theater does not care who bought it.
The person carrying it gets entry.
JWT bearer authentication works similarly.
Possession grants access.
Now we visit:
GET /dashboardHeader:
Authorization: Bearer eyJhbGc...Server receives request.
Extracts:
const token =
authHeader.split(" ")[1]Result:
eyJhbGc...
This is where:
jwt.verify()enters.
Many beginners think:
verify()
means:
Ask database
It does not.
Instead:
Take token
Take secret
Recompute signature
Compare signatures
If signatures match:
Valid token
If signatures differ:
Token modified
Authentication fails.
Successful verification returns:
{
id: 123,
username: "skyy",
iat: ...
exp: ...
}This is called:
Decoded payload
We now know:
Who made the request
without querying login credentials again.
Traditional session systems:
Server remembers users
JWT systems:
Token remembers users
Huge difference.
Session:
Server Memory
βββ Session ID
JWT:
Client carries identity
Server stores nothing.
This is where JWT behaves differently from sessions.
Logout:
Delete session
Done.
User immediately loses access.
Server does not store JWT.
Remember:
Stateless
So what can the server delete?
Nothing.
The server has no record of active JWTs.
Usually:
localStorage.removeItem("token")or
Delete cookie
Client loses token.
Future requests:
GET /dashboardcontain:
No token
Authentication fails.
Tokens should never live forever.
Example:
jwt.sign(payload, secret, {
expiresIn: "30d"
})Adds:
{
"exp": 1783514844
}to payload.
During verification:
jwt.verify()checks:
Current Time
<
Expiration Time
If expired:
TokenExpiredError
Authentication fails.
Large applications often use:
Short lifespan:
15 minutes
Long lifespan:
30 days
Flow:
Access Token expires
β
Client sends Refresh Token
β
Server issues new Access Token
This provides:
Security
+
Convenience
REGISTER
β
Create User
β
Store Hashed Password
β
LOGIN
β
Verify Password
β
Sign JWT
β
Return JWT
β
Store JWT
β
Protected Request
β
Authorization: Bearer <token>
β
Verify JWT
β
Decode Payload
β
Authorize User
β
Return Data
β
LOGOUT
β
Delete Token
β
Future Requests Fail
A JWT is not a session.
A JWT is not encrypted user data.
A JWT is essentially:
A digitally signed identity card
The payload contains identity claims:
User ID
Username
Role
The signature proves:
The server created this
and nobody has modified it.
Every authenticated request is simply the client presenting that signed identity card and the server checking whether the signature is still valid. That's the entire JWT authentication model in one sentence.
Excellent question.
The idea of Access Tokens and Refresh Tokens exists because we are trying to balance two competing goals:
Security
vs
Convenience
If we maximize one, we usually weaken the other.
Suppose we issue:
jwt.sign(payload, secret, {
expiresIn: "30d"
});The user logs in once and stays logged in for 30 days.
Sounds great.
But consider what happens if the token is stolen.
Suppose an attacker somehow obtains our JWT.
Maybe through:
- XSS
- Malware
- Shared computer
- Browser extension
- Leaked logs
Now the attacker has:
eyJhbGc...
Remember what "Bearer" means:
Whoever possesses the token
is considered authenticated.
The attacker can now make requests as us.
And because the token expires in:
30 days
the attacker has access for:
30 days
That's a long time.
Instead of:
expiresIn: "30d"we do:
expiresIn: "15m"Now if the token is stolen:
Maximum damage:
15 minutes
Much safer.
But now we have another problem.
Imagine:
Login
β
15 minutes pass
β
Token expires
β
Login again
β
15 minutes pass
β
Login again
Nobody wants this.
Especially on:
- Gmail
- Amazon
- Netflix
We expect to stay logged in.
So we need:
Short-lived authentication
+
Long-lived login
This seems contradictory.
Instead of one token:
JWT
we create two.
Short lifespan:
15 minutes
Contains:
{
"id": 123,
"role": "user"
}Used on every API request.
Long lifespan:
30 days
Used only to obtain new access tokens.
Now the flow looks like:
Login
β
Receive:
Access Token (15m)
Refresh Token (30d)
When we request:
GET /profilewe send:
Authorization: Bearer ACCESS_TOKENOnly the access token.
Never the refresh token.
After 15 minutes:
Access Token expires
Server rejects requests.
Instead of forcing login:
Client sends:
POST /refreshwith refresh token.
Server verifies refresh token.
If valid:
Issue new Access Token
without asking for username/password again.
User never notices.
Imagine an attacker steals:
Access Token
Because it expires quickly:
15 minutes
attacker loses access soon.
Without refresh tokens:
Stolen token
=
30 days access
With refresh tokens:
Stolen access token
=
15 minutes access
Huge improvement.
Many beginners ask:
If refresh tokens last 30 days, why not use them for normal requests?
Because refresh tokens are extremely valuable.
Think of them as:
Master Key
while access tokens are:
Temporary Visitor Pass
We want the master key exposed as little as possible.
So:
Refresh Token
is only sent occasionally.
Maybe:
Once every 15 minutes
or even less.
While:
Access Token
is sent on every request.
Think of a hotel.
Hotel room key.
Valid for one day.
Used constantly.
Identity document at reception.
Valid for whole vacation.
When room key expires:
Show ID
β
Receive new room key
We don't walk around all day carrying our passport.
We keep it safe.
Same idea.
Remember when we discussed JWT being stateless?
One problem was:
We cannot easily revoke JWTs.
Suppose:
JWT valid for 30 days
User changes password.
Attacker still has token.
Bad.
With refresh tokens we often store them in a database.
Example:
RefreshToken
βββ User 123
When user:
Changes password
Logs out
Gets banned
we delete refresh token.
Now:
No new access tokens
can be generated.
Access token may live:
15 minutes
but after that:
Session ends completely.
Access Token
-----------
Lifetime:
15 minutes
Used:
Every API request
Stored:
Memory or cookie
Refresh Token
------------
Lifetime:
30 days
Used:
Only to get new access tokens
Stored:
Secure HttpOnly cookie
Database-backed
If we use only one token:
Long expiration
=
Convenient
=
Less secure
If we use only short-lived tokens:
Secure
=
Annoying user experience
Using Access Tokens + Refresh Tokens gives us:
Short-lived access
+
Long-lived login
+
Ability to revoke sessions
+
Better security
That's why almost every large authentication system todayβwhether from Google, Microsoft, GitHub, or Auth0βuses some variation of the access-token/refresh-token model rather than relying on a single long-lived JWT.
Now we're getting into the architecture that most real-world applications use.
To understand cookies in authentication, we first need to understand that cookies are not authentication themselves.
A cookie is simply a storage and transport mechanism.
Think of it as:
A small piece of data that the browser
automatically stores and sends back
to the server with future requests.
The cookie doesn't care whether it contains:
- A session ID
- A JWT
- A theme preference
- A shopping cart ID
It's just a container.
Suppose the server responds:
Set-Cookie: username=skyyThe browser stores:
username=skyy
Now every request automatically includes:
Cookie: username=skyyWithout us writing:
headers: {
Cookie: ...
}The browser does it automatically.
Remember:
HTTP is stateless.
The browser and server needed a way to maintain state across requests.
Cookies were invented to solve exactly that problem.
Historically:
We send:
{
"username": "skyy",
"password": "secret"
}Server verifies credentials.
Server creates:
Session ID:
abc123xyz
Stores:
abc123xyz -> User #57
inside memory or Redis.
Then sends:
Set-Cookie:
sessionid=abc123xyzBrowser stores it.
Future requests:
Cookie:
sessionid=abc123xyzServer looks it up:
abc123xyz
β
User #57
Authenticated.
Most beginner docs teach:
localStorage.setItem(
"token",
jwtToken
);and later:
axios.get("/dashboard", {
headers: {
Authorization:
`Bearer ${token}`
}
});This works.
But there are drawbacks.
Imagine malicious JavaScript executes:
const token =
localStorage.getItem("token");It can steal the JWT.
Why?
Because:
LocalStorage is accessible
to JavaScript.
Any script can read it.
Instead of:
localStorage.setItem(...)Server does:
Set-Cookie:
token=eyJhbGc...Browser stores JWT inside a cookie.
Now every request automatically sends:
Cookie:
token=eyJhbGc...Server extracts token from cookies.
No need for:
Authorization:
Bearer ...header anymore.
This is one of the most important cookie settings.
Server sends:
Set-Cookie:
token=abc123;
HttpOnlyNow:
document.cookiecannot access it.
Even if malicious JavaScript runs:
const token =
document.cookie;the authentication cookie remains hidden.
This is why modern applications often store JWTs in:
HttpOnly Cookies
instead of:
LocalStorage
Let's walk through the entire lifecycle.
User submits:
{
"username": "skyy",
"password": "secret"
}Server verifies credentials.
Creates JWT:
const token =
jwt.sign(...)Instead of returning:
{
"token": "..."
}Server sends:
Set-Cookie:
token=eyJhbGc...Browser stores it automatically.
User visits:
GET /dashboardBrowser automatically includes:
Cookie:
token=eyJhbGc...Server extracts:
req.cookies.tokenverifies JWT
and authenticates user.
Logout becomes very simple.
Server sends:
Set-Cookie:
token=;
Expires=Thu, 01 Jan 1970Browser deletes cookie.
Future requests:
Cookie:
(nothing)Authentication fails.
Modern systems frequently do:
Access Token
β
Memory
Refresh Token
β
HttpOnly Cookie
Let's understand why.
Access token is sent frequently:
GET /profile
GET /products
GET /orders
GET /cartPossibly dozens of times.
Access token usually lives:
5-15 minutes
Very short.
If stolen:
Damage is limited.
Refresh token lives:
30 days
or more.
Much more valuable.
If stolen:
Attacker can generate
new access tokens.
Very dangerous.
Therefore:
Refresh Token
gets stronger protection.
Usually:
HttpOnly
Secure
SameSitecookie.
These are critical.
Set-Cookie:
token=abc;
HttpOnlyJavaScript cannot read it.
Protects against:
XSS
Set-Cookie:
token=abc;
SecureCookie only travels via:
HTTPS
Never plain HTTP.
Protects against:
CSRF
Cross-Site Request Forgery.
Example:
SameSite=StrictOnly same-site requests get cookie.
Today many systems use:
Login
β
Access Token
(15 minutes)
Refresh Token
(30 days)
Storage:
Access Token
β
Memory
Refresh Token
β
HttpOnly Cookie
Flow:
Login
β
Receive both tokens
β
Use Access Token
β
Expires
β
Send Refresh Token
β
Receive new Access Token
β
Continue working
We could.
Many applications do.
For example:
Cookie:
accessToken=...
refreshToken=...
Both HttpOnly.
This is common in traditional web applications.
Because:
LocalStorage
is readable by JavaScript.
Therefore:
XSS risk increases.
Think of cookies as:
An automatic delivery service
between browser and server.
The browser:
- Stores the cookie.
- Sends it automatically.
- Updates it automatically.
- Deletes it when instructed.
JWTs, session IDs, refresh tokens, and access tokens are simply different kinds of data that can be placed inside that delivery service.
That's why in modern authentication:
Cookie
=
Transport & Storage Mechanism
JWT
=
Identity Document
Access Token
=
Short-lived identity document
Refresh Token
=
Long-lived renewal permit
Cookies and JWTs are not competing technologies. In most production systems, they are often used together, with cookies acting as the secure transport mechanism for JWT-based authentication.
This is one of the most important concepts in security, and many developers confuse them.
At first glance, hashing and encryption seem similar because both transform data into something unreadable. But they solve completely different problems.
Encryption is:
Readable Data
β
Encryption Key
β
Encrypted Data
β
Decryption Key
β
Original Data
The original data can be recovered.
Hashing is:
Readable Data
β
Hash Function
β
Hash
The original data cannot be recovered.
Think of it this way:
Locking a document in a safe.
We can unlock the safe later.
Putting a document into a paper shredder.
We can verify what went in, but we cannot reconstruct the original document from the shredded pieces.
Encryption protects:
Confidentiality
Meaning:
Prevent unauthorized people
from reading data.
Examples:
- Credit card numbers
- Medical records
- Messages
- Banking information
Suppose we have:
Hello Skyy
After encryption:
a8fK9s2LpQ==
Looks meaningless.
But with the correct key:
SecretKey123
we can decrypt it:
Hello Skyy
again.
Hashing protects:
Integrity
and
Verification
not secrecy.
Suppose a user registers with:
password123
We never want to store:
password123
inside the database.
If the database gets hacked:
Attacker gets every password.
Disaster.
Instead we store:
Hash(password123)
Example:
ef92b778ba...
Database stores:
{
"username": "skyy",
"password":
"ef92b778ba..."
}Notice:
Original password is gone.
Many beginners ask:
If we cannot recover the password from the hash, how do we log in?
Excellent question.
During registration:
password123
β
Hash Function
β
ef92b778ba...
Store:
ef92b778ba...
Later during login:
User enters:
password123
Again:
password123
β
Hash Function
β
ef92b778ba...
Compare:
Stored Hash
=
Generated Hash
If equal:
Password is correct.
Notice:
We never decrypted anything.
We simply hashed twice and compared.
A hash function always produces the same output for the same input.
Example:
Hash("hello")
always becomes:
2cf24dba5...
Every time.
This property allows verification.
This is the easiest way to remember the difference.
Two-way process:
Encrypt
β
Decrypt
We can go both directions.
One-way process:
Hash
β
Done
No reverse operation.
Suppose:
MyPassword123
MyPassword123
β
Encrypt
β
X7aP91kLm2
Later:
X7aP91kLm2
β
Decrypt
β
MyPassword123
MyPassword123
β
Hash
β
93bc6f0a...
That's it.
No:
Unhash()
function exists.
A common beginner question.
Suppose we encrypt passwords:
password123
β
Encrypted
β
X7aP91kLm2
To verify login we must decrypt:
X7aP91kLm2
β
Decrypt
β
password123
This means the server must possess:
Decryption Key
somewhere.
If attackers steal:
Database
+
Decryption Key
they get every password.
Hashing avoids this.
Because:
No decryption key exists.
Modern password hashing includes:
Salt
A random value added before hashing.
Example:
password123
+
x8Qk91
becomes:
password123x8Qk91
Then hashed.
Why?
Imagine two users choose:
password123
Without salt:
Hash(password123)
produces:
abc123
for both users.
Database:
User A -> abc123
User B -> abc123
Attackers immediately know:
Same password.
With salt:
User A:
password123 + randomSaltA
User B:
password123 + randomSaltB
Different hashes.
bcrypt is:
A password hashing algorithm
specifically designed for passwords.
Not encryption.
Hashing.
Example:
const hashedPassword =
await bcrypt.hash(password, 10);Produces:
$2b$10$N7K...
Verification:
await bcrypt.compare(
password,
hashedPassword
);bcrypt hashes the input and checks whether it matches.
This is where many people get confused.
JWT uses neither hashing nor encryption for authentication.
JWT primarily uses:
Digital Signatures
which rely on cryptographic hashing internally.
When we do:
jwt.sign(payload, secret)the library:
- Creates payload
- Hashes content
- Uses secret key
- Generates signature
Result:
JWT
When we do:
jwt.verify(token, secret)it:
- Recomputes hash
- Recomputes signature
- Compares signatures
If equal:
Token is authentic.
Used for:
- Passwords
- File integrity
- Checksums
- Digital signatures
- Blockchain
Goal:
Verification
Used for:
- HTTPS
- Credit card storage
- Secure messaging
- Banking systems
- VPNs
Goal:
Confidentiality
Think of:
Encryption
as placing a document into a locked box.
The document still exists.
Anyone with the key can read it.
Think of:
Hashing
as generating a unique fingerprint of the document.
The fingerprint proves:
This document is the same one
we saw earlier.
But the fingerprint does not contain the document itself.
| Feature | Hashing | Encryption |
|---|---|---|
| Purpose | Verification & Integrity | Confidentiality |
| Reversible? | No | Yes |
| Uses Keys? | No (normally) | Yes |
| Can Recover Original Data? | No | Yes |
| Used For Passwords? | Yes | No |
| Used For HTTPS? | No | Yes |
| Used In JWT Signatures? | Yes (internally) | Not usually |
| Example | bcrypt, Argon2, SHA-256 | AES, RSA |
The simplest way to remember it is:
Hashing:
Can we prove data is correct?
Encryption:
Can we hide data from others?
If we need to verify, we hash.
If we need to hide, we encrypt.