Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion src/lib/isJWT.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
import assertString from './util/assertString';
import isBase64 from './isBase64';

function isValidJSONObject(str) {
// Base64 URL decode
// Replace URL-safe chars and add padding if needed
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
while (base64.length % 4) {
base64 += '=';
}

try {
// Use atob for browser or Buffer for Node.js
let decoded;
if (typeof atob === 'function') {
decoded = atob(base64);
} else if (typeof Buffer !== 'undefined') {
decoded = Buffer.from(base64, 'base64').toString('utf8');
} else {
return false;
}

// Parse as JSON and verify it's an object (not array, string, number, etc.)
const parsed = JSON.parse(decoded);
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed);
} catch (e) {
return false;
}
}

export default function isJWT(str) {
assertString(str);

Expand All @@ -11,5 +38,17 @@ export default function isJWT(str) {
return false;
}

return dotSplit.reduce((acc, currElem) => acc && isBase64(currElem, { urlSafe: true }), true);
// All three parts must be valid Base64 URL
const allBase64 = dotSplit.reduce(
(acc, currElem) => acc && isBase64(currElem, { urlSafe: true }),
true
);

if (!allBase64) {
return false;
}

// Header (first part) and Payload (second part) must be valid JSON objects
// Signature (third part) does not need to be valid JSON
return isValidJSONObject(dotSplit[0]) && isValidJSONObject(dotSplit[1]);
}
54 changes: 54 additions & 0 deletions test/validators.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5532,6 +5532,19 @@ describe('Validators', () => {
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTYxNjY1Mzg3Mn0.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwiaWF0IjoxNjE2NjUzODcyLCJleHAiOjE2MTY2NTM4ODJ9.a1jLRQkO5TV5y5ERcaPAiM9Xm2gBdRjKrrCpHkGr_8M',
'$Zs.ewu.su84',
'ks64$S/9.dy$§kz.3sd73b',
// Issue #2511: Base64-valid but not valid JSON when decoded
'foo.bar.baz', // "foo" decodes to invalid JSON
'.babelrc.cjs', // empty string and non-JSON
'..', // empty parts
'.t.', // empty header and signature
// Issue #2511: Valid JSON but not an object (array, null, primitive types)
'WyJhIiwiYiJd.eyJzdWIiOiIxMjM0In0.rRpe04zbWbbJjwM43VnHzAboDzszJtGrNsUxaqQ-GQ8', // array as header
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.WyJhIiwiYiJd.rRpe04zbWbbJjwM43VnHzAboDzszJtGrNsUxaqQ-GQ8', // array as payload
'bnVsbA.eyJzdWIiOiIxMjM0In0.rRpe04zbWbbJjwM43VnHzAboDzszJtGrNsUxaqQ-GQ8', // null as header
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.bnVsbA.rRpe04zbWbbJjwM43VnHzAboDzszJtGrNsUxaqQ-GQ8', // null as payload
'ImhlbGxvIg.eyJzdWIiOiIxMjM0In0.rRpe04zbWbbJjwM43VnHzAboDzszJtGrNsUxaqQ-GQ8', // string as header
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.MTIz.rRpe04zbWbbJjwM43VnHzAboDzszJtGrNsUxaqQ-GQ8', // number as payload
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.dHJ1ZQ.rRpe04zbWbbJjwM43VnHzAboDzszJtGrNsUxaqQ-GQ8', // boolean as payload
],
error: [
[],
Expand All @@ -5542,6 +5555,47 @@ describe('Validators', () => {
});
});

it('should validate JWT tokens using Buffer fallback (when atob is unavailable)', () => {
// Test the Buffer.from() fallback path in isValidJSONObject by temporarily removing atob
const originalAtob = global.atob;
global.atob = undefined;

try {
test({
validator: 'isJWT',
valid: [
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI',
],
invalid: [
'foo.bar.baz', // Invalid JSON when decoded
],
});
} finally {
global.atob = originalAtob;
}
});

it('should reject JWT tokens when no decoder is available (neither atob nor Buffer)', () => {
// Test the fallback return false path when both atob and Buffer are unavailable
const originalAtob = global.atob;
const originalBuffer = global.Buffer;
global.atob = undefined;
global.Buffer = undefined;

try {
test({
validator: 'isJWT',
invalid: [
// Valid Base64 JWT structure but should fail since no decoder available
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI',
],
});
} finally {
global.atob = originalAtob;
global.Buffer = originalBuffer;
}
});

it('should validate null strings', () => {
test({
validator: 'isEmpty',
Expand Down
Loading