From 6d0d95abd50ddefde808a5189ebe65c37d65ecbf Mon Sep 17 00:00:00 2001 From: Marcos Passos Date: Fri, 16 Dec 2022 10:54:04 -0300 Subject: [PATCH] Fix token deserialization with binary signatures (#298) --- src/base64Url.ts | 36 ++++++++++++++++++++++-------------- src/token/token.ts | 6 +++--- test/base64Url.test.ts | 39 ++++++++++++++++++++++++--------------- test/token/token.test.ts | 10 +++++++--- 4 files changed, 56 insertions(+), 35 deletions(-) diff --git a/src/base64Url.ts b/src/base64Url.ts index cc5fb0e4..65d51006 100644 --- a/src/base64Url.ts +++ b/src/base64Url.ts @@ -9,20 +9,28 @@ function base64Escape(value: string): string { .replace(/=/g, ''); } -export function base64UrlEncode(value: string): string { - return base64Escape( - window.btoa( - window.encodeURIComponent(value) - .replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(Number.parseInt(p1, 16))), - ), - ); +export function base64UrlEncode(value: string, utf8 = false): string { + if (utf8) { + return base64Escape( + window.btoa( + window.encodeURIComponent(value) + .replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(Number.parseInt(p1, 16))), + ), + ); + } + + return base64Escape(window.btoa(value)); } -export function base64UrlDecode(value: string): string { - return window.decodeURIComponent( - Array.prototype.map.call( - window.atob(base64Unescape(value)), - (char: string) => `%${(`00${char.charCodeAt(0).toString(16)}`).slice(-2)}`, - ).join(''), - ); +export function base64UrlDecode(value: string, utf8 = false): string { + if (utf8) { + return window.decodeURIComponent( + Array.prototype.map.call( + window.atob(base64Unescape(value)), + (char: string) => `%${(`00${char.charCodeAt(0).toString(16)}`).slice(-2)}`, + ).join(''), + ); + } + + return window.atob(base64Unescape(value)); } diff --git a/src/token/token.ts b/src/token/token.ts index 62148f5d..f6ba5869 100644 --- a/src/token/token.ts +++ b/src/token/token.ts @@ -79,11 +79,11 @@ export class Token { let signature; try { - headers = JSON.parse(base64UrlDecode(parts[0])); - payload = JSON.parse(base64UrlDecode(parts[1])); + headers = JSON.parse(base64UrlDecode(parts[0], true)); + payload = JSON.parse(base64UrlDecode(parts[1], true)); if (parts.length === 3) { - signature = base64UrlDecode(parts[2]); + signature = base64UrlDecode(parts[2], false); } } catch { throw new Error('The token is corrupted.'); diff --git a/test/base64Url.test.ts b/test/base64Url.test.ts index 69095f0d..ec495f0f 100644 --- a/test/base64Url.test.ts +++ b/test/base64Url.test.ts @@ -1,25 +1,34 @@ import {base64UrlDecode, base64UrlEncode} from '../src/base64Url'; describe('A base64 URL encoder/decoder function', () => { - const encodeTests = [ - ['000000', 'MDAwMDAw'], - ['', ''], - ['f', 'Zg'], - ['fo', 'Zm8'], - ['foo', 'Zm9v'], - ['foob', 'Zm9vYg'], - ['fooba', 'Zm9vYmE'], - ['foobar', 'Zm9vYmFy'], - ['Jacaré', 'SmFjYXLDqQ'], + const encodeTests: Array<[string, string, boolean]> = [ + ['000000', 'MDAwMDAw', false], + ['\0\0\0\0', 'AAAAAA', false], + ['\xff', '_w', false], + ['\xff\xff', '__8', false], + ['\xff\xff\xff', '____', false], + ['\xff\xff\xff\xff', '_____w', false], + ['\xfb', '-w', false], + ['', '', false], + ['f', 'Zg', false], + ['fo', 'Zm8', false], + ['foo', 'Zm9v', false], + ['foob', 'Zm9vYg', false], + ['fooba', 'Zm9vYmE', false], + ['foobar', 'Zm9vYmFy', false], + // UTF-8 tests + ['Jacaré', 'SmFjYXLDqQ', true], + ['\u00e9', 'w6k', true], + ['\u00e9\u00e9', 'w6nDqQ', true], ]; - it.each(encodeTests)('should encode "%s" as "%s"', (decoded: string, encoded: string) => { - expect(base64UrlEncode(decoded)).toBe(encoded); + it.each(encodeTests)('should encode "%s" as "%s"', (decoded: string, encoded: string, utf8: boolean) => { + expect(base64UrlEncode(decoded, utf8)).toBe(encoded); }); - const decodeTests = encodeTests.map(([encoded, decoded]) => [decoded, encoded]); + const decodeTests = encodeTests.map(([encoded, decoded, utf8]) => [decoded, encoded, utf8]); - it.each(decodeTests)('should decode "%s" as "%s"', (encoded: string, decoded: string) => { - expect(base64UrlDecode(encoded)).toBe(decoded); + it.each(decodeTests)('should decode "%s" as "%s"', (encoded: string, decoded: string, utf8: boolean) => { + expect(base64UrlDecode(encoded, utf8)).toBe(decoded); }); }); diff --git a/test/token/token.test.ts b/test/token/token.test.ts index 8d6f8d16..dc2e1039 100644 --- a/test/token/token.test.ts +++ b/test/token/token.test.ts @@ -1,5 +1,5 @@ import {Token, FixedTokenProvider} from '../../src/token'; -import {base64UrlEncode} from '../../src/base64Url'; +import {base64UrlEncode, base64UrlDecode} from '../../src/base64Url'; describe('A token', () => { const appId = '7e9d59a9-e4b3-45d4-b1c7-48287f1e5e8a'; @@ -29,9 +29,13 @@ describe('A token', () => { }); it('may contain a signature', () => { - const token = Token.parse(`${anonymousSerializedToken}${base64UrlEncode('some-signature')}`); + const binarySignature = 'uLvpiRxDrYpU1BO4Y6rLyFv3uj3PuPD3KFg1RA_Wu5S4' + + 'svht8KsdS1WR8Sr-L55e-7_y9Do8LCTo3ZWp92JZDQ'; - expect(token.getSignature()).toBe('some-signature'); + const token = Token.parse(`${anonymousSerializedToken}${binarySignature}`); + + // The result is a binary string + expect(token.getSignature()).toBe(base64UrlDecode(binarySignature, false)); }); it('should have an issue time', () => {