From b8d53c1049b18aa0285c1c8177259b90921b2ea8 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 18 Mar 2022 11:52:38 +0100 Subject: [PATCH] support unsecured jwt --- src/dom-elements.js | 2 + src/editor/default-tokens.js | 5 +++ src/editor/index.js | 21 ++++++++- src/editor/jwt.js | 22 +++++++--- src/strings.js | 1 + test/unit/editor/jwt.js | 73 +++++++++++++++++++++---------- views/token-editor-algorithms.pug | 1 + 7 files changed, 95 insertions(+), 30 deletions(-) diff --git a/src/dom-elements.js b/src/dom-elements.js index e35beb2d..a0cc7d96 100644 --- a/src/dom-elements.js +++ b/src/dom-elements.js @@ -35,3 +35,5 @@ export const secretInput = document.querySelector('.jwt-signature input[name="secret"]'); export const secretBase64Checkbox = document.getElementById('is-base64-encoded'); +export const signatureArea = + document.querySelector('.jwt-explained.jwt-signature') diff --git a/src/editor/default-tokens.js b/src/editor/default-tokens.js index 3d05e6c2..03bc6d6b 100644 --- a/src/editor/default-tokens.js +++ b/src/editor/default-tokens.js @@ -186,5 +186,10 @@ export default { privateKey: rsaPrivateKey, jwk: rsaJwk, publicKey: rsaPublicKey + }, + none: { + token: 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.', + privateKey: '', + publicKey: '' } }; diff --git a/src/editor/index.js b/src/editor/index.js index d919711a..a94ef19b 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -36,6 +36,7 @@ import { encodedTabElement, decodedTabElement, editorWarnings, + signatureArea, } from "../dom-elements.js"; import log from "loglevel"; @@ -74,6 +75,12 @@ function markAsInvalid(errorMessages = []) { } } +function markAsUnsecuredJwt() { + signatureStatusElement.classList.remove("valid-token"); + signatureStatusElement.classList.add("invalid-token"); + signatureStatusElement.innerHTML = ` ${strings.editor.unsecuredJwt}`; +} + function markJWTAsInvalid() { signatureStatusElement.classList.remove("valid-token"); signatureStatusElement.classList.add("invalid-token"); @@ -99,7 +106,11 @@ function displaySecretOrKeys(algorithm) { hmacShaTextSpan.firstChild.textContent = `HMACSHA${algoSize}`; secretEditorContainer.style.display = ""; keyEditorContainer.style.display = "none"; + signatureArea.style.display = "" + } else if (algorithm === 'none') { + signatureArea.style.display = "none" } else { + signatureArea.style.display = "" const texts = { RS: "RSASHA", PS: "RSAPSSSHA", @@ -157,7 +168,11 @@ export function useDefaultToken(algorithm) { privateKeyTextArea.value = defaults.privateKey; } - markAsValid(); + if (algorithm !== 'none') { + markAsValid(); + } else { + markAsUnsecuredJwt(); + } }); } @@ -333,7 +348,7 @@ function verifyToken() { const jwt = getTrimmedValue(tokenEditor); const decoded = decode(jwt); - if (!decoded.header.alg || decoded.header.alg === "none") { + if (!decoded.header.alg) { markAsInvalid(); return; } @@ -349,6 +364,8 @@ function verifyToken() { } else { markAsValid(); } + } else if (decoded.header.alg === 'none') { + markAsUnsecuredJwt(); } else { markAsInvalid(); } diff --git a/src/editor/jwt.js b/src/editor/jwt.js index c1937b4a..f15631bc 100644 --- a/src/editor/jwt.js +++ b/src/editor/jwt.js @@ -65,12 +65,16 @@ export function sign(header, return Promise.reject(new Error('Missing "alg" claim in header')); } + if (!(typeof payload === 'string' || payload instanceof String)) { + payload = JSON.stringify(payload); + } + + if (header.alg === 'none') { + return Promise.resolve(`${jose.base64url.encode(JSON.stringify(header))}.${jose.base64url.encode(new TextEncoder().encode(payload))}.`) + } + return getJoseKey(header, secretOrPrivateKeyString, base64Secret, types.PRIVATE).then( key => { - if (!(typeof payload === 'string' || payload instanceof String)) { - payload = JSON.stringify(payload); - } - return new jose.CompactSign(new TextEncoder().encode(payload)) .setProtectedHeader(header) .sign(key); @@ -89,12 +93,20 @@ export function verify(jwt, secretOrPublicKeyString, base64Secret = false) { return Promise.resolve({ validSignature: false }); } + if (decoded.header.alg === 'none') { + return Promise.resolve({ + validSignature: false, + unsecuredJwt: true, + validBase64: jwt.split('.').every((s) => isValidBase64String(s)) + }) + } + return getJoseKey(decoded.header, secretOrPublicKeyString, base64Secret, types.PUBLIC).then( key => { return jose.compactVerify(jwt, key) .then(() => ({ validSignature: true, - validBase64: jwt.split('.').reduce((valid, s) => valid = valid && isValidBase64String(s), true) + validBase64: jwt.split('.').every((s) => isValidBase64String(s)) }), (e) => { log.warn('Could not verify token: ', e); return { validSignature: false } diff --git a/src/strings.js b/src/strings.js index d282c6a3..e5a3a613 100644 --- a/src/strings.js +++ b/src/strings.js @@ -46,6 +46,7 @@ export default { editor: { signatureVerified: 'signature verified', signatureInvalid: 'invalid signature', + unsecuredJwt: 'Unsecured JWT', jwtInvalid: 'Invalid JWT' }, warnings: { diff --git a/test/unit/editor/jwt.js b/test/unit/editor/jwt.js index 45673040..fb85d923 100644 --- a/test/unit/editor/jwt.js +++ b/test/unit/editor/jwt.js @@ -81,30 +81,57 @@ describe('JWT', function() { privateKey = publicKey = vector.secret; } - it(`signs/verifies ${alg.toUpperCase()}`, function () { - const header = { alg: alg.toUpperCase(), iat: Date.now() }; - const payload = { sub: 'test' }; - - // test the default token - return jwt.verify(vector.token, publicKey).should.eventually.include({validSignature: true}) - .then(() => { - // test signing - return jwt.sign(header, payload, privateKey).then(token => { - token.should.be.a('string'); - - const split = token.split('.'); - split.should.have.lengthOf(3); - - const decoded = jwt.decode(token); - decoded.header.should.deep.equal(header); - decoded.payload.should.deep.equal(payload); - - // test verifying just signed token - return jwt.verify(token, publicKey) - .should.eventually.include({validSignature: true}); + if (alg !== 'none') { + it(`signs/verifies ${alg.toUpperCase()}`, function () { + const header = { alg: alg.toUpperCase(), iat: Date.now() }; + const payload = { sub: 'test' }; + + // test the default token + return jwt.verify(vector.token, publicKey).should.eventually.include({validSignature: true}) + .then(() => { + // test signing + return jwt.sign(header, payload, privateKey).then(token => { + token.should.be.a('string'); + + const split = token.split('.'); + split.should.have.lengthOf(3); + + const decoded = jwt.decode(token); + decoded.header.should.deep.equal(header); + decoded.payload.should.deep.equal(payload); + + // test verifying just signed token + return jwt.verify(token, publicKey) + .should.eventually.include({validSignature: true}); + }); }); - }); - }); + }); + } else { + it(`encodes/decodes ${alg}`, function () { + const header = { alg, iat: Date.now() }; + const payload = { sub: 'test' }; + + // test the default token + return jwt.verify(vector.token).should.eventually.include({validSignature: false, unsecuredJwt: true}) + .then(() => { + // test signing + return jwt.sign(header, payload).then(token => { + token.should.be.a('string'); + + const split = token.split('.'); + split.should.have.lengthOf(3); + + const decoded = jwt.decode(token); + decoded.header.should.deep.equal(header); + decoded.payload.should.deep.equal(payload); + + // test verifying unsecured jwt + return jwt.verify(token, publicKey) + .should.eventually.include({validSignature: false, unsecuredJwt: true}); + }); + }); + }); + } if (jwk) { it(`signs/verifies ${alg.toUpperCase()} with a JWK`, function () { diff --git a/views/token-editor-algorithms.pug b/views/token-editor-algorithms.pug index 886c5172..8131c39a 100644 --- a/views/token-editor-algorithms.pug +++ b/views/token-editor-algorithms.pug @@ -11,3 +11,4 @@ select#algorithm-select option(name='algorithm',value='PS256') PS256 option(name='algorithm',value='PS384') PS384 option(name='algorithm',value='PS512') PS512 + option(name='algorithm',value='none') none