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 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)