JWT Decoder
Input
Paste your JWT here. Everything is client-side, so nothing to worry about.
This page can be added to your favorites anytime (see up-to-date URL).
Output
JWTs have three parts:
- header
- payload
- signature
The Code
const descriptions = {alg: 'Message authentication code (MAC) algorithm',auth_time: 'Time of Authentication',amr: 'Authentication Methods References',aud: 'Audience',client_id: 'Client Identifier',cty: 'Content Type',exp: 'Expiration Time',iat: 'Issued At',idp: 'Identity Provider',iss: 'Issuer',jwt: 'JWT ID',kid: 'Key Identifier',kty: 'Key Type ()',name: 'Full Name',nbf: 'Not Before',scope: 'Scope Values',sub: 'Subject',typ: 'Token type',use: 'sig(nature) / enc(ryption)',x5c: 'X.509 Certificate Chain',x5t: 'X.509 Certificate Thumbprint',e: 'Public key component RSA',n: 'Public key component RSA',x: 'Public key component Elliptic Curve',y: 'Public key component Elliptic Curve',}const render = (data, part) => {// TODO value, property -> tuple, entry or something (pass as tuple?)const renderValue = (property, value) => {// detect dates (educated guess)if (Number.isInteger(value)&& value * 1000 > new Date(1974, 10 - 1, 11)&& value * 1000 < new Date(2050, 0, 1)) {value = new Date(value * 1000).toUTCString()}// render arraysif (Array.isArray(value)) {for(const [index, item] of value.entries()) {renderValue(`${property}[${index}]`, item)}return}// render rowconst row = table.insertRow()const $property = row.insertCell()$property.innerHTML = `<strong></strong><br><small></small>`$property.querySelector('strong').textContent = property$property.querySelector('small').textContent = descriptions[property] || ''// TODO object detection broken, i see no <pre> in the output?const $value = row.insertCell()if(typeof value == 'object') {$value.innerHTML = '<pre></pre>'$value.querySelector('pre').textContent = JSON.stringify(value, null, 4)} else {$value.innerText = value}}const table = document.createElement('table')table.innerHTML = `<caption>${part}</caption><tr><th>Property</th><th>Value</th></tr>`table.classList.add('jwt-decoder')for (let [property, value] of Object.entries(data)) {renderValue(property, value)}const output = document.querySelector('.output')output.appendChild(table)}// TODO renderHTML, renderMarkdown (see CSV converter)const renderJSON = data => {const pre = document.createElement('pre')const lines = data.split('\n')for (const line of lines) {const code = document.createElement('code')code.textContent = line + '\n'pre.appendChild(code)}const output = document.querySelector('.output')output.appendChild(pre)}// const base64url = e => btoa().replace('+', '')// https://stackoverflow.com/a/54113881const verify = async (jwsObject, jwKey) => {const jwsSigningInput = jwsObject.split('.').slice(0, 2).join('.')const jwsSignature = jwsObject.split('.')[2]const key = await window.crypto.subtle.importKey('jwk', jwKey, {name: 'RSASSA-PKCS1-v1_5',hash: { name: 'SHA-256' }}, false, ['verify'])const valid = awaitwindow.crypto.subtle.verify({ name: 'RSASSA-PKCS1-v1_5' },key,// TODO use rfc4648.js / base64url// base64url.parse(jwsSignature, { loose: true }),atob(jwsSignature),new TextEncoder().encode(jwsSigningInput))return valid}const update = async jwt => {const secret = document.querySelector('input[name="secret"]').value// push historyconst params = new URLSearchParams()params.set('token', jwt)params.set('secret', secret)history.replaceState(null, document.title, '?' + params.toString())// clear all previous tablesconst output = document.querySelector('.output')output.innerHTML = '<p class="text-danger">invalid JWT</p>'// split JWTconst parts = jwt.split('.')// extract the header, payload and signatureconst [header, payload, signature] = [JSON.parse(atob(parts[0])),JSON.parse(atob(parts[1])),parts[2] // FIXME *can* be base64 encoded]// render JWToutput.textContent = ''render(header, '1. header')render(payload, '2. payload')render({ signature }, '3. signature')// render JSONrenderJSON(JSON.stringify(header, null, 4))renderJSON(JSON.stringify(payload, null, 4))// verify header + payloadverify('', secret)// const data = new TextEncoder().encode(`${parts[0]}.${parts[1]}`)// const key = await crypto.subtle.importKey('jwk', secret, {// name: 'RSASSA-PKCS1-v1_5', // or HMAC?// hash: { name: header.alg }// }, false, ['verify'])// const isValid = await crypto.subtle.verify({ name: 'prRSASSA-PKCS1-v1_5' }, key, signature, data)// document.querySelector('.signature td-something').textContent =// valid ? '<span class="text-success">✔</span>' : '<span class="text-danger">✖</span>'}// initconst params = new URLSearchParams(document.location.search)const jwt = params.get('jwt') || params.get('token')if (jwt) {document.querySelector('textarea').value = jwt}const secret = params.get('secret')if (secret) {document.querySelector('input[name="secret"]').value = secret}update(document.querySelector('textarea').value)