Try It

Try It

Fourteen interactive demos, all running client-side in your browser — nothing is sent anywhere. For full server-backed attack/defense labs, the reference is PortSwigger’s Web Security Academy (linked below).

1. JWT Decoder

A JWT is signed, not encrypted. Decode one and see every claim in the clear — then note the cnf.jkt claim that binds a sender-constrained token to a key.

Header — how it's signed

{ "alg": "ES256", "typ": "JWT" }

Payload — the claims (readable by anyone!)

{ "sub": "user-4829", "iss": "https://auth.bank.example", "aud": "https://api.bank.example", "scope": "accounts payments", "cnf": { "jkt": "vC8kPlaceholderThumbprint" }, "iat": 1752062400, "exp": 1752066000 }

Signature — proves the issuer signed it

SIGNATURE_bytes_are_not_readable_they_prove_the_issuer_signed_this
The lesson: a JWT is signed, not encrypted. Anyone holding the token can read every claim — the payload above is just base64, not a secret. The signature only proves it hasn't been tampered with and that the authorization server issued it. Notice the cnf.jkt claim: that's the key thumbprint a sender-constrained token is bound to. See the Bearer vs PoP demo for why that matters.

2. PKCE Lab

Generate a real code_verifier and watch the S256 code_challenge computed live with the browser’s SubtleCrypto. Regenerate to see the hash change completely. This is the mechanism that kills authorization-code interception — see FAPI 2.0.

3. Bearer vs Proof-of-Possession

Steal a bearer token and the attacker gets in. Steal a sender-constrained (DPoP) token and the attacker fails, because the private key never left the real client. The web analog of the mobile per-request hash — see Sender-Constrained Tokens.

Legitimate client
Holds the access token ✓
Nothing else needed
API call → 200 OK
Attacker who stole the token
Holds the access token
Nothing else needed
The lesson: a bearer token is like cash — whoever holds it spends it, so a stolen one grants full access. A sender-constrained token (DPoP) is bound to a private key the client proves it holds on every call. Steal the token and you still fail, because the key never left the legitimate client. This is the web analog of the mobile per-request hash.

4. Passkey Ceremony

A real navigator.credentials.create() call against this page as the relying party — no backend, nothing stored. See what the browser API actually returns, and why the origin binding it creates is what makes passkeys phishing-resistant. See WebAuthn & Passkeys.

5. Clickjacking

Toggle frame-ancestors on and off and watch a decoy overlay either steal a click on a hidden “confirm transfer” button, or find nothing to sit on top of. See Clickjacking.

The grey checkerboard is attacker.example, framing your bank page beneath a decoy "Claim your prize" layer at ~40% opacity. Click the decoy button — you're really clicking whatever sits at that exact pixel in the frame underneath.

🎉 Click here to claim your prize!
The lesson: clickjacking doesn't forge a request the way CSRF does — it tricks a real, authenticated user into making a real click on a page they can't see. The defense is declarative and server-side: Content-Security-Policy: frame-ancestors (or the older X-Frame-Options) tells the browser which origins, if any, are allowed to frame the page. Set it to 'none' or a specific list and the attacker's iframe never renders your content at all.

6. CSRF

Watch a hidden cross-site form either forge a transfer using your ambient session cookie, or get blocked by SameSite and a CSRF token. See CSRF.

You're logged into your bank in one tab (an ambient session cookie). In another tab, attacker.example auto-submits a hidden form to bank.example/transfer?to=attacker&amount=5000. The browser attaches your cookie automatically — that's the whole attack.

attacker.example

<form action="https://bank.example/transfer" method="POST">
<input name="to" value="attacker">
<input name="amount" value="5000">
<script>document.forms[0].submit()</script>
</form>

bank.example

Ambient session cookie: present ✓

SameSite=Strict: no

CSRF token required: no

The lesson: CSRF doesn't steal anything — it forges a state-changing request and rides on a session the browser attaches automatically. Two independent defenses close it: SameSite=Strict (or Lax) stops the browser from sending the cookie on a cross-site request in the first place; a per-session, per-form CSRF token means the attacker's forged request is missing something it can't guess. Either one alone usually suffices; real deployments often use both.

7. XSS: Escaping vs Raw Insertion

The same attacker-supplied string, shown as inert encoded text vs a simulation of what raw HTML insertion would do. Nothing here ever actually executes. See XSS.

Imagine this is a comment box on a page that echoes back whatever a user typed. Nothing here is ever actually executed — the "unsafe" panel is a simulation of what raw HTML insertion would do, rendered as inert text.

Unsafe — page does el.innerHTML = comment (simulated, not executed here)

→ browser would parse this into a live DOM node:
<img src=x onerror="alert('stolen: ' + document.cookie)">
🚨 simulated: the handler above would fire immediately — attacker JS runs with full access to this page's DOM, cookies (unless HttpOnly), and any bearer token in memory.

Safe — page does el.textContent = comment (or a templating engine that HTML-encodes)

→ browser renders these exact characters as inert text:
&lt;img src=x onerror=&quot;alert(&#39;stolen: &#39; + document.cookie)&quot;&gt;
The lesson: the payload above is identical in both panels — the difference is entirely in how the page inserts it into the DOM. innerHTML (or any raw HTML sink) parses attacker-controlled strings as markup, so a crafted tag's event handler executes as your page's own JavaScript. textContent, an auto-escaping template, or output encoding turns the same string into inert display text. Content-Security-Policy (next page) is the backstop for whatever escaping misses.

8. CSP Tester

Paste a Content-Security-Policy string and see which of five example resource loads it would allow or block — pure client-side parsing, no real enforcement. See Content Security Policy.

Content-Security-Policy header

Would this load / run?Result
Inline <script>...</script>
needs 'unsafe-inline' (or a matching nonce/hash) in script-src
Blocked 🛡
eval() / new Function()
needs 'unsafe-eval' in script-src
Blocked 🛡
External <script src="https://cdn.example.com/lib.js">
needs the host listed in script-src (or default-src)
Allowed ⚠
Inline style="..." / <style>
needs 'unsafe-inline' in style-src
Allowed ⚠
External <img src="https://images.example.com/logo.png">
needs the host listed in img-src (or default-src)
Allowed ⚠
The lesson: this is a pure client-side parse of the policy string — no real enforcement, just the same directive-matching logic the browser applies. Try deleting 'unsafe-inline' from style-src, or removing https://cdn.example.com from script-src, and watch results flip. A tight CSP (no 'unsafe-inline', no 'unsafe-eval', an explicit allowlist) is the backstop that makes a successful XSS injection inert — the attacker's script tag can exist in the DOM, but the browser refuses to execute it.

9. SRI Hash-Pin

Compute a live SHA-384 integrity hash for a script, then edit one character to see it change completely — the same mismatch that makes a browser refuse a tampered CDN response. See Subresource Integrity.

Third-party script content

The lesson: Subresource Integrity hash-pins a third-party script at the moment you trust it. Even one byte of tampering — a compromised CDN, a Magecart-style supply-chain injection — changes the hash completely, and the browser will refuse to execute a script whose integrity attribute doesn't match what was actually served. It doesn't stop the tamper from happening upstream; it stops the tampered script from ever running in your users' browsers.

10. WebCrypto Keygen

Generate a real, non-extractable P-256 signing key with crypto.subtle — the browser’s closest analog to a mobile secure enclave — then try to export the private key and watch the browser refuse. See Sender-Constrained Tokens.

The lesson: this is the web's closest equivalent to a mobile secure enclave. The private key lives inside crypto.subtle's key store rather than in JavaScript-readable memory; the page can ask it to sign things (see the DPoP proof builder on the Try It hub) but can never read the key itself out, even if an XSS payload runs on the page. That's a materially different security property than a key held as a JavaScript string or localStorage value.

11. DPoP Proof Builder

Build and sign a real DPoP proof JWT with the non-extractable key above, then run the four checks a gateway would: signature, htm/htu match, key-thumbprint binding, and replay. Edit the proof to simulate tampering and watch a specific check fail.

This builds a real, cryptographically signed DPoP proof — using the non-extractable key from WebCrypto Keygen — and runs the same four checks an API gateway would.

The lesson: try three tampers and see which check catches each one — flip a character in the signature (breaks check 1), edit htu in the payload without re-signing (breaks check 1 too, since the signature covers the payload), or click "Build & sign" twice and re-paste an old proof (breaks check 4, replay). Regenerating the key and building a new, validly self-signed proof with a different key breaks check 3 — a valid signature alone proves nothing if the key isn't the one the token was bound to.

12. FAPI Flow Walkthrough

Step through the full PAR → authorize → token → API call sequence, front channel vs back channel at every hop, and where PKCE, the passkey ceremony, and the DPoP binding each land. See FAPI 2.0.

1. Prepare: PKCE verifier + challengeclient-side, nothing sent yet
code_verifier = 'a1B2...' (kept secret)
code_challenge = BASE64URL(SHA256(code_verifier))

PKCE (RFC 7636): the client commits to a secret before the flow starts. Nobody has seen anything cross the network yet.

The lesson: every FAPI 2.0 control targets a specific channel-crossing moment. Front-channel steps (3, 4, 5) carry only opaque handles and short-lived, single-use codes — never anything an interceptor could redeem alone. Back-channel steps (2, 6, 7) are where the client authenticates itself and where long-lived secrets and bindings actually move.

13. Step-Up / Risk Simulator

Toggle risk signals and watch a live score decide allow vs step-up vs block, including the PSD2 TRA exemption logic. See Risk-Based & Adaptive Authentication.

Risk score30 / 100

⚠ Step-up required (SCA)

No TRA exemption requested — SCA is required for this payment by default.

The lesson: PSD2 defaults to requiring Strong Customer Authentication on every payment — the TRA exemption is opt-in per transaction, and only holds if the real-time risk score and the amount both clear the issuer's bar. A single hard signal (a RAT/malware indicator) vetoes the exemption outright, regardless of how low the amount is. This is the compensating control for the web's missing hardware root of trust: where mobile leans on the enclave key, the web leans on this risk decision firing correctly, every time.

14. Device Fingerprint

A handful of ordinary, non-invasive browser signals hashed into a demo fingerprint — illustrative only, not production device intelligence. See Device Fingerprinting.


For full server-backed attack/defense labs (clickjacking with CSRF tokens, and more), the reference is PortSwigger’s Web Security Academy — this portal teaches the concepts and links out rather than rebuilding a live sandbox.