joriszwart.nl

I code dreams™

Tools

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:

  1. header
  2. payload
  3. 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)
jwt-decoder.js