Authentication and Cryptography

A visual guide to cryptographic signatures, OpenID Connect, JWT verification, and application sessions.

Trust Is Hard on the Internet

Alice and Bob are never actually in the same room. Every message between them crosses a public network — wires, routers, and servers that neither of them controls.

Anyone on that network can read a message, copy it, or quietly change it before it arrives. So when Bob receives "Hi Bob, it's Alice," he's left holding two unanswered questions.

Who actually sent this? And did it arrive exactly as sent? This talk is about how cryptography lets Bob answer both, without ever trusting the network in between.

Alice
"Hi Bob, it's Alice"
public network
"Hi Bob, it's Alice"
anyone here can read it, copy it, or change it
Bob
"Hi Bob, it's Alice"
really from Alice? unchanged?
? ? ?

Three Cryptographic Jobs

Cryptography gives software three distinct capabilities. Authentication relies primarily on signatures.

  • Hashing creates a tamper-evident fingerprint.
  • Encryption keeps data secret.
  • Signatures prove an issuer approved unchanged data.
#
HashSame input, same fixed-length fingerprint. One-way by design.
EncryptReadable data becomes secret ciphertext that a key can recover.
SignA private key attaches proof that a public key can verify.

Hashing Detects Changes

A cryptographic hash converts arbitrary data into a fixed-size fingerprint. Each character added to this JSON produces a dramatically different result.

Hashes do not hide their input. For passwords, use a dedicated slow password-hashing algorithm such as Argon2id, bcrypt, or scrypt.

SHA-256 ↓

Shared Secrets Authenticate a Payload

Alice and Bob already share a secret. Alice builds the payload and signs it by running it through a keyed hash together with the secret — an HMAC — then sends Bob the payload plus that signature.

Bob runs the payload he received through the same HMAC using the same secret. If his result matches the signature Alice sent, he knows it came from someone who knew the secret and wasn't changed along the way.

This is a message authentication code, not a public-key signature: both parties hold the same secret.

Alice
+
🔒 winter-bridge-47
=
public network
payload + signature
Bob
+
🔒 winter-bridge-47
=
vs. received

A Man in the Middle Is Detected

An intermediary sitting on the public network can read and rewrite Alice's payload. But it doesn't know the shared secret, so it can't produce a new signature to match its edit — it can only forward the old one.

Bob recomputes the HMAC on the payload he actually received. Since that payload was changed, his result no longer matches the signature Alice sent, and he rejects it.

Alice
+
🔒 winter-bridge-47
=
public network
Intermediary (no secret)
Bob
+
🔒 winter-bridge-47
=
vs. received

Public and Private Keys

Asymmetric cryptography uses a matched pair of keys, but the two halves are treated very differently. Alice's private key stays behind her wall and never travels anywhere.

Alice's public key lives out in the open, on the other side of that same wall. Alice doesn't send it to anyone; Bob (or anyone else) simply fetches a copy whenever he needs it. That's fine, because a public key can't do anything a private key can undo.

Alice
🔒 private key
stays behind the wall
public space
🔓 public key
public key
Bob
🔓 public key
fetches a copy whenever he needs it

Encryption: Public Locks, Private Unlocks

Bob first fetches Alice's public key from the public space, same as before. Then he uses it to lock up a message meant only for her.

Once locked, only Alice's private key, still safe behind her wall, can open it back up. Unlike HMAC, the two sides never share a secret — Bob only ever holds the public half.

Alice
🔒 private key
decrypt with private key
public space
🔓 public key
Bob
🔓 public key
encrypt with public key

Signing: Private Signs, Public Verifies

Bob fetches Alice's public key the same way as before. Alice signs her message with her private key, and Bob verifies it with the public key he fetched — no shared secret involved.

Compare that to the HMAC slide: there, verifying required holding the same secret Alice used, so anyone who could check a signature could also forge one. Here, signing and verifying use different keys, so anyone with Alice's public key can verify her signature — and doing so gives them no power to produce a valid one themselves.

A forger sitting out in the public space never touches Alice's wall. Whatever they sign, Bob's verification fails: that's the source of trust.

Alice
🔒 private key
sign with private key
public space
🔓 public key
Bob
🔓 public key
verify with public key

Signatures Bind Data to an Issuer

This is the exact mechanism you just watched Alice and Bob use, generalized: the signer is now called an issuer (an identity provider, for example), and the verifier is your application. Private key signs, public key verifies — nothing else changes.

The signed data is just a claims payload: who it's about (sub), who issued it (iss). Change a single byte after it's signed, and verification fails.

Issuer
🔒 private key
sign with private key
public space
🔓 public key
Application
🔓 public key
verify with public key

JWT: A Signed Envelope

A JSON Web Token has three Base64url-encoded parts — header, payload, signature — joined with dots into one string.

Base64url is an encoding, not encryption: fully reversible, no key required. Decode the header or payload yourself with nothing but a text editor. The signature is what can't be faked, because producing it required the issuer's private key.

header
base64url encode ↓
.
.
anyone can decode the first two parts — only the issuer's private key could produce the third

Claims Need Context

A valid signature only proves who signed the token. Your application still has to confirm the token was made for this app, for this login, and is still valid right now — the signature alone checks none of that.

For identity, store the provider's stable iss and sub pair, not an email address alone.

issexpected issuer
audthis application
expnot expired
nbfvalid now
noncematches login request
substable provider user ID

Finding the Right Public Key

OpenID Connect providers publish discovery metadata at a standard well-known URL. That metadata points to a JSON Web Key Set (JWKS) — a small list of the provider's current public keys.

The token header's kid selects which key in that set to verify against. Providers rotate keys by adding a new one before retiring the old kid — no coordinated application release required.

1Load trusted provider configuration.
2Read its jwks_uri — a set of public keys.
kid: key-2025 kid: key-2026 kid: key-2027
3Token header says kid: key-2026 — match it to one JWK.
4Verify with that key using an explicitly allowed algorithm.

The OpenID Connect Flow

The browser redirects the person to the identity provider. After login, the provider returns a short-lived authorization code to the application.

The application backend exchanges that code, verifies the resulting ID token using the preceding process, and only then treats the identity as trusted.

Browserredirect to identity provider →
Provider← login complete, return one-time code
Applicationexchange code server-side →
Provider← ID token and token response
Applicationverify signature and claims ✓

Issue Your Own Session

After verification, the application maps (iss, sub) to its own user record and creates a local session. A short-lived opaque session cookie gives the application straightforward logout, revocation, and fresh authorization checks.

JWTs are useful assertions. They are not automatically the best browser session format.

Provider verified ID token local user
HttpOnly / Secure / SameSite

Appendix: RSA Math

RSA is one possible asymmetric scheme. It uses three numbers: a public exponent e, a shared modulus n, and a private exponent d. The same modular exponentiation operation runs in either direction, just with different exponents.

Real systems also apply carefully specified padding and hash constructions. Use maintained cryptography and OpenID Connect libraries rather than implementing this yourself.

Encrypt / decrypt
ciphertext = messagee mod n
public key components: e, n — anyone can encrypt
message = ciphertextd mod n
private key component: d — only the owner can decrypt
Sign / verify
hash = SHA-256(message)
signature = hashd mod n
private key component: d — only the owner can sign
verified hash = signaturee mod n
public key components: e, n — anyone can verify
valid signature: verified hash matches a freshly computed message hash