Skip to content

Commit

Permalink
Show a warning when a token with invalid base64 or padded base64 is b…
Browse files Browse the repository at this point in the history
…eing encoded
  • Loading branch information
Sambego committed Aug 23, 2019
1 parent 9c2994d commit 3fceb9d
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 30 deletions.
2 changes: 2 additions & 0 deletions src/dom-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const payloadElement = document.querySelector('.js-payload');

export const signatureStatusElement =
document.querySelector('.validation-status.js-signature');
export const editorWarnings =
document.querySelector('.js-editor-warnings');

export const algorithmSelect = document.getElementById('algorithm-select');
export const algorithmEs512 =
Expand Down
63 changes: 55 additions & 8 deletions src/editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ import {
encodedTabLink,
decodedTabLink,
encodedTabElement,
decodedTabElement
decodedTabElement,
editorWarnings
} from "../dom-elements.js";

import log from "loglevel";
Expand Down Expand Up @@ -80,6 +81,14 @@ function markAsInvalid() {
}`;
}

function markJWTAsInvalid() {
signatureStatusElement.classList.remove("valid-token");
signatureStatusElement.classList.add("invalid-token");
signatureStatusElement.innerHTML = `<i class="icon-budicon-501"></i> ${
strings.editor.jwtInvalid
}`;
}

function markAsValid() {
const elementsWithError = document.getElementsByClassName("error");
Array.prototype.forEach.call(elementsWithError, element => {
Expand Down Expand Up @@ -212,6 +221,28 @@ function markAsInvalidWithElement(element, clearTokenEditor = true) {
}
}

function showEditorWarnings(warnings) {
editorElement.classList.add("warning");
editorWarnings.classList.remove("hidden");
editorWarnings.innerHTML = '';

warnings.forEach(warning => {
const warningElement = document.createElement('p');
const warningLabel = document.createElement('strong');
const warningMessage = document.createTextNode(warning)
warningLabel.innerText = 'Warning: ';
warningElement.appendChild(warningLabel);
warningElement.appendChild(warningMessage);
editorWarnings.appendChild(warningElement)
});
}

function hideEditorWarnings() {
editorElement.classList.remove("warning");
editorWarnings.classList.add("hidden");
editorWarnings.innerHTML = '';
}

function encodeToken() {
deferToNextLoop(fixEditorHeight);

Expand Down Expand Up @@ -300,6 +331,13 @@ function decodeToken() {
tokenHash: tokenHash
});
} else {
if (decoded.warnings && decoded.warnings.length > 0) {
showEditorWarnings(decoded.warnings);
markJWTAsInvalid();
} else {
hideEditorWarnings();
}

verifyToken();
}
} catch (e) {
Expand Down Expand Up @@ -332,13 +370,22 @@ function verifyToken() {
? secretInput.value
: publicKeyTextArea.value;

verify(jwt, publicKeyOrSecret, secretBase64Checkbox.checked).then(valid => {
if (valid) {
markAsValid();
metrics.track("editor-jwt-verified", {
tokenHash: tokenHash,
secretBase64Checkbox: secretBase64Checkbox.checked
});
verify(jwt, publicKeyOrSecret, secretBase64Checkbox.checked).then(result => {
if (result.validSignature) {
if (!result.validBase64) {
markJWTAsInvalid();
metrics.track("editor-jwt-invalid", {
reason: "invalid base64",
tokenHash: tokenHash,
secretBase64Checkbox: secretBase64Checkbox.checked
});
} else {
markAsValid();
metrics.track("editor-jwt-verified", {
tokenHash: tokenHash,
secretBase64Checkbox: secretBase64Checkbox.checked
});
}
} else {
markAsInvalid();
metrics.track("editor-jwt-invalid", {
Expand Down
30 changes: 19 additions & 11 deletions src/editor/jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import jose from 'node-jose';
import b64u from 'base64url';
import any from 'promise.any';
import { pki } from 'node-forge';

import strings from '../strings';strings
import log from 'loglevel';

// node-jose does not support keys shorter than block size. This is a
Expand Down Expand Up @@ -97,24 +97,27 @@ export function sign(header,

export function verify(jwt, secretOrPublicKeyString, base64Secret = false) {
if(!isToken(jwt)) {
return Promise.resolve(false);
return Promise.resolve({validSignature: false});
}

const decoded = decode(jwt);

if(!decoded.header.alg || decoded.errors) {
return Promise.resolve(false);
return Promise.resolve({validSignature: false});
}

return getJoseKey(decoded.header, secretOrPublicKeyString, base64Secret).then(
key => {
return jose.JWS.createVerify(key)
.verify(jwt)
.then(() => true, () => false);
.then(() => ({
validSignature: true,
validBase64: jwt.split('.').reduce((valid, s) => valid = valid && isValidBase64String(s), true)
}), () => ({validSignature: false}));
}, e => {
log.warn('Could not verify token, ' +
'probably due to bad data in it or the keys: ', e);
return false;
return {validSignature: false};
}
);
}
Expand All @@ -123,7 +126,8 @@ export function decode(jwt) {
const result = {
header: {},
payload: {},
errors: false
errors: false,
warnings: [],
};

if(!jwt) {
Expand All @@ -134,12 +138,12 @@ export function decode(jwt) {
const split = jwt.split('.');

if (!isValidBase64String(split[2])) {
result.errors = true;
result.warnings.push(strings.warnings.signatureBase64Invalid);
}

try {
if (!isValidBase64String(split[0])) {
result.errors = true;
result.warnings.push(strings.warnings.headerBase64Invalid);
}
result.header = JSON.parse(b64u.decode(split[0]));
} catch(e) {
Expand All @@ -148,7 +152,7 @@ export function decode(jwt) {

try {
if (!isValidBase64String(split[1])) {
result.errors = true;
result.warnings.push(strings.warnings.payloadBase64Invalid);
}
result.payload = JSON.parse(b64u.decode(split[1]));
} catch(e) {
Expand All @@ -158,7 +162,11 @@ export function decode(jwt) {
return result;
}

export function isValidBase64String(s) {
export function isValidBase64String(s, allowPadding = false) {
if (allowPadding) {
return /^[a-zA-Z0-9_=-]*$/.test(s);
}

return /^[a-zA-Z0-9_-]*$/.test(s);
}

Expand All @@ -175,7 +183,7 @@ export function isToken(jwt, checkTypClaim = false) {

const split = jwt.split('.');
let valid = true;
split.forEach(s => valid = valid && isValidBase64String(s));
split.forEach(s => valid = valid && isValidBase64String(s, true));

return valid;
}
10 changes: 8 additions & 2 deletions src/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,18 @@ export default {
},
extension: {
alreadyInstalled: 'Already installed',
addToBrowser: 'Add to Browser',
addToBrowser: 'Add to Browser',
noJwtsFound: 'No JWTs found',
saveBackTo: 'Save back to '
},
editor: {
signatureVerified: 'signature verified',
signatureInvalid: 'invalid signature'
signatureInvalid: 'invalid signature',
jwtInvalid: 'Invalid JWT'
},
warnings: {
headerBase64Invalid: 'Looks like your JWT header is not encoded correctly using base64url (https://tools.ietf.org/html/rfc4648#section-5). Note that padding ("=") must be omitted as per https://tools.ietf.org/html/rfc7515#section-2',
payloadBase64Invalid: 'Looks like your JWT payload is not encoded correctly using base64url (https://tools.ietf.org/html/rfc4648#section-5). Note that padding ("=") must be omitted as per https://tools.ietf.org/html/rfc7515#section-2',
signatureBase64Invalid: 'Looks like your JWT signature is not encoded correctly using base64url (https://tools.ietf.org/html/rfc4648#section-5). Note that padding ("=") must be omitted as per https://tools.ietf.org/html/rfc7515#section-2',
}
};
3 changes: 3 additions & 0 deletions stylus/website/codemirror.styl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
.jwt-playground .input
&.error
background pink
&.warning
background #fff7c9
margin: 0
.CodeMirror
font-size 20px
line-height 30px
Expand Down
27 changes: 27 additions & 0 deletions stylus/website/index.styl
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,9 @@ body
.error .CodeMirror
background-color #ffc0cb

.warning .CodeMirror
background-color #fff7c9

.box-content
height 0
overflow hidden
Expand All @@ -536,6 +539,7 @@ body

.input
padding 10px
position relative
+breakpoint("tablet")
padding 20px
// &[data-alg="HS256"]
Expand Down Expand Up @@ -1191,3 +1195,26 @@ footer
margin: 0;
display: inline;
}

.editor-warning
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 1;
background: rgba(127,123,100,0.1)
margin: 0;
border-top: 1px solid rgba(155,155,155,0.5);

&.hidden
display: none

p
margin: 0

p + p
margin: 20px 0 0

.jwt-playground .algorithm-code .tab-content .input:hover
.editor-warning
display: none;
18 changes: 9 additions & 9 deletions test/unit/editor/jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,18 @@ describe('JWT', function() {
const token = b64u.toBase64(tokens.hs256.token);

jwt.isToken(token).should.be.false;
return jwt.verify(token, tokens.hs256.secret).should.eventually.be.false;
return jwt.verify(token, tokens.hs256.secret).should.eventually.include({validBase64: false});
});

describe('verifies valid tokens', function() {
Object.keys(tokens).forEach(alg => {
it(alg.toUpperCase(), function() {
if(alg.indexOf('hs') !== -1) {
return jwt.verify(tokens[alg].token, tokens[alg].secret)
.should.eventually.be.true;
.should.eventually.include({validSignature: true});
} else {
return jwt.verify(tokens[alg].token, tokens[alg].publicKey)
.should.eventually.be.true;
.should.eventually.be.include({validSignature: true});
}
});
});
Expand Down Expand Up @@ -97,7 +97,7 @@ describe('JWT', function() {
return Promise.all(promises.map(p => p.then(v => !v, e => true)))
.then(all => all.every(v => v))
.finally(() => log.enableAll())
.should.eventually.be.true;
.should.eventually.include({validSignature: true});
});

it('signs/verifies tokens (HS256)', function() {
Expand All @@ -118,7 +118,7 @@ describe('JWT', function() {
decoded.header.should.deep.equal(header);
decoded.payload.should.deep.equal(payload);

return jwt.verify(token, 'secret').should.eventually.be.true;
return jwt.verify(token, 'secret').should.eventually.include({validSignature: true});
});
});

Expand All @@ -141,7 +141,7 @@ describe('JWT', function() {
decoded.payload.should.deep.equal(payload);

return jwt.verify(token, tokens.rs256.publicKey)
.should.eventually.be.true;
.should.eventually.include({validSignature: true});
});
});

Expand All @@ -164,7 +164,7 @@ describe('JWT', function() {
decoded.payload.should.deep.equal(payload);

return jwt.verify(token, tokens.es256.publicKey)
.should.eventually.be.true;
.should.eventually.include({validSignature: true});
});
});

Expand All @@ -187,7 +187,7 @@ describe('JWT', function() {
decoded.payload.should.deep.equal(payload);

return jwt.verify(token, tokens.ps256.publicKey)
.should.eventually.be.true;
.should.eventually.include({validSignature: true});
});
});

Expand All @@ -200,7 +200,7 @@ describe('JWT', function() {
};

return jwt.sign(header, payload, tokens.rs256.privateKey).then(token => {
return jwt.verify(token, publicKeyPlainRSA).should.eventually.be.true;
return jwt.verify(token, publicKeyPlainRSA).should.eventually.include({validSignature: true});
});
});

Expand Down
1 change: 1 addition & 0 deletions views/token-editor-common.pug
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
.tab-content
#encoded-jwt.box-content.current
.input.js-input
include token-editor-warnings.pug
#decoded-jwt.box-content
.output
.jwt-explained.jwt-header
Expand Down
1 change: 1 addition & 0 deletions views/token-editor-warnings.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.editor-warning.warning.js-editor-warnings.hidden

0 comments on commit 3fceb9d

Please sign in to comment.