The Gap Between Using Something and Understanding It
Early in my career, I was debugging an authentication issue when a senior developer asked, "Is the JWT expired?"
I said yes, I'd checked. What I didn't admit: I'd pasted the token into a random website, seen the expiration timestamp, and still wasn't entirely sure what I was looking at. Was this safe? Could anyone read this token? Was it encrypted? Why could any website decode it if it was "signed" and "secure"?
This is for that version of me. Three concepts that appear constantly in web development, that most tutorials assume you already understand, and that are actually straightforward once explained clearly.
Base64: Not Encryption. Not Compression. Just Translation.
The most important thing to know first: Base64 is not a security mechanism. It doesn't hide your data. Anyone can decode it instantly. If you've ever thought "this looks encoded so it's probably safe to put in a URL" — it isn't.
Base64 is a way to represent binary data using only printable ASCII characters. That's the entire point. The practical reason it exists: binary data breaks things. Emails, HTTP headers, URLs, HTML attributes — these systems were built for text. Raw binary data, with its null bytes and control characters, can corrupt or get mangled when passed through text-based systems.
Base64 converts binary to a safe 64-character alphabet: A–Z, a–z, 0–9, +, and /. The output is about 33% larger than the input but guaranteed to travel intact through any text-based system.
Where you'll actually see it
HTTP Basic Authentication: When you authenticate with username:password, the credentials are Base64-encoded and sent in the Authorization: Basic header. This is why Basic Auth over plain HTTP is dangerous — anyone who intercepts the request can decode the credentials immediately. HTTPS is mandatory.
JWT tokens: Each of the three sections of a JWT is Base64url-encoded (a variant that replaces + and / with - and _ to be safe in URLs).
Data URIs: <img src="data:image/png;base64,iVBORw0KGgo..."> — embedding image data directly in HTML or CSS instead of making a separate HTTP request.
Email attachments: MIME encoding uses Base64 so binary files can travel through mail servers designed only for text.
Try it yourself: paste any text into the Base64 Encoder and look at the output. Then paste the encoded string back and decode it. Notice the encoded version conceals nothing — it just uses a different alphabet.
JWT: Three Base64 Strings and a Signature
A JSON Web Token looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJleHAiOjE3MTM5MzYwMDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Exactly two dots. Three sections.
Header — the part before the first dot. Decodes to metadata about the token: which signing algorithm was used, what type of token it is.
Payload — the middle section. Decodes to the actual claims: user ID, email, roles, expiration time. This is what your backend reads to identify the user.
Signature — the part after the second dot. A cryptographic proof that the token wasn't tampered with.
The thing that surprises most developers
The payload is Base64url-encoded. It is not encrypted. Anyone who holds the token can decode the payload and read its contents — user ID, email, roles, expiration, whatever you put in there.
Paste any JWT into the JWT Decoder and see exactly what's inside. No key required.
This surprises people because the string looks opaque and gibberish-like. It isn't. The security model of JWT is: "I can verify this token was genuinely issued by my server (signature check), so I can trust the claims it contains. But those claims themselves are not secret."
The practical implication: don't put passwords, payment data, or sensitive personal information in JWT payloads. Put only what's needed to identify and authorize the user. User ID, plan tier, and roles are all fine.
What the signature actually does
The signature is created by taking the header + payload + a secret key that only your server knows, and running them through a hash function. When your server receives a token, it recomputes the signature using the same secret. If the recomputed signature matches the token's signature, the token is genuine and unchanged.
If someone modifies the payload to change user ID 123 to 456 (to impersonate another user), the signature check fails — the modified payload produces a different signature than what's in the token.
This is why your JWT signing secret needs to be genuinely secret. If an attacker gets it, they can forge tokens and impersonate any user.
Hashing: One-Way Functions
A hash function takes any input and produces a fixed-size output. Two properties make it useful for security:
Deterministic: the same input always produces the same output.
One-way: you cannot reverse the hash to get the original input. There's no unhash().
SHA-256("password123") → ef92b778bafe771207...
SHA-256("password124") → 88d4266fd4e6338d13...
One character changed. Completely different hash. No mathematical relationship between similar inputs and similar outputs.
Why passwords are hashed, not encrypted
If passwords were encrypted, you could decrypt them with the right key. If an attacker steals your database and your encryption key, every user's password is exposed.
With hashing: you store the hash, not the password. When a user logs in, you hash what they typed and compare it to the stored hash. If they match, the password is correct. You never need to recover the original — you only need to verify a guess.
Even if an attacker gets your database, they only have hashes. To get a password, they have to guess and verify, which is much slower than just reading.
MD5, SHA-256, bcrypt — why it matters which you use
MD5 was the standard in the early 2000s. It's now broken for security purposes. A modern GPU can compute billions of MD5 hashes per second, which means an attacker with your database can crack simple passwords in minutes using brute force. Never use MD5 for passwords.
SHA-256 is appropriate for integrity verification — confirming a file wasn't corrupted or tampered with, generating HMAC signatures for API webhooks. But it's too fast for passwords. Fast hashing is bad for passwords because it makes brute-force attacks cheap.
bcrypt is designed specifically for passwords. It's intentionally slow — you can tune how many computation rounds it takes — and it includes a built-in "salt" (random data) that ensures two identical passwords produce completely different hashes. This defeats rainbow table attacks (pre-computed hash databases).
The rule: use bcrypt (or Argon2, or scrypt) for passwords. Use SHA-256 for everything else that needs integrity verification.
Experiment with the Hash Generator — type "password" into MD5, SHA-256, and SHA-512 and compare the output lengths. Then change one character and see how completely the hash changes.
Quick Reference
| Concept | What it is | Can you reverse it? | Use for |
|---|---|---|---|
| Base64 | Binary → text encoding | Yes, trivially | Sending binary data through text systems |
| JWT | Signed JSON token | Payload: yes. Signature: no | Stateless authentication |
| SHA-256 | Hash function | No | File integrity, HMAC webhook signatures |
| bcrypt | Password hash | No | Storing user passwords |
Understanding these three things correctly prevents a class of security mistakes that look fine in code review and only surface when something goes wrong — at which point the damage is already done.
The tools: Base64, JWT Decoder, Hash Generator. Use them to test your understanding, not just to get results.