JWT Decoder Thursday, March 8, 2018 7:13 PM Base64 JSON JWT 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). JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSmFuZSBEb2UiLCJpYXQiOjE1ODgzMTk4NzZ9.8BEsAHLc0hUaBd38WPPB5DsOhSCYAuKAq16kr8RlV_Y Secret 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 arrays if (Array.isArray(value)) { for(const [index, item] of value.entries()) { renderValue(`${property}[${index}]`, item) } return } // render row const 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/54113881 const 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 = await window.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 history const params = new URLSearchParams() params.set('token', jwt) params.set('secret', secret) history.replaceState(null, document.title, '?' + params.toString()) // clear all previous tables const output = document.querySelector('.output') output.innerHTML = '<p class="text-danger">invalid JWT</p>' // split JWT const parts = jwt.split('.') // extract the header, payload and signature const [header, payload, signature] = [ JSON.parse(atob(parts[0])), JSON.parse(atob(parts[1])), parts[2] // FIXME *can* be base64 encoded ] // render JWT output.textContent = '' render(header, '1. header') render(payload, '2. payload') render({ signature }, '3. signature') // render JSON renderJSON(JSON.stringify(header, null, 4)) renderJSON(JSON.stringify(payload, null, 4)) // verify header + payload verify('', 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>' } // init const 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)