diff --git a/packages/js-auth/src/jwtDecode.spec.ts b/packages/js-auth/src/jwtDecode.spec.ts index 391ded1..7cec50d 100644 --- a/packages/js-auth/src/jwtDecode.spec.ts +++ b/packages/js-auth/src/jwtDecode.spec.ts @@ -277,4 +277,44 @@ describe('jwtDecode', () => { 'RhXDw7UajFr9Bxl18Q8Es82MlbXw1YcJCRe0g1-mPpDaxcBDtvHyu8GQYLpRYm-4xQ5fbg57Pzqxue0jOxv2Kcsl7XhOUHwEs9jTyS5_tfeJTQ_Ab4zKi27EFfb9NmA78xXEa1wyznVDoYvUy-PzemPPchEDezx1qrJkd0zMqnr5CJntSmfPCP22g0ljLscNUtUlbACT7xpIVXAe37XZ6_DBHOuAToleupFoyUKbNH3fRTc3FIrzexWt1m8RQALQ-QGDPljjFpnWjo3aiJQMZAu9FoZgdJn-qlbW0iYRFl91TAu8VAJ8bJJo8o3jbNdlggs9kNYFy3h15Zx3rnOUyA' }) }) + + it('should be able to parse an access token with custom claims and special chars.', () => { + const accessToken = + 'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYTRjYzYyOGQxZmNlM2ZiOTNhM2VlNTU4MjZlNDFjZmFmMThkYzJkZmYzYjA3MjIyNzQwMzgwZTkxOTlkNWQifQ.eyJvcmdhbml6YXRpb24iOnsiaWQiOiJlbldveEZNT25wIiwic2x1ZyI6InRoZS1ibHVlLWJyYW5kLTMiLCJlbnRlcnByaXNlIjpmYWxzZSwicmVnaW9uIjoiZXUtd2VzdC0xIn0sImFwcGxpY2F0aW9uIjp7ImlkIjoiR1J2RGlySmxERyIsImtpbmQiOiJ3ZWJhcHAiLCJwdWJsaWMiOmZhbHNlfSwiY3VzdG9tX2NsYWltIjp7ImN1c3RvbWVyIjp7ImZpcnN0X25hbWUiOiJKw7ZyZyIsImxhc3RfbmFtZSI6IkRvZSJ9fSwic2NvcGUiOiJtYXJrZXQ6YWxsIiwiZXhwIjoxNzI5Nzc5MTQ0LCJ0ZXN0Ijp0cnVlLCJyYW5kIjowLjY5NzY2MjgwNDM2NTg2OTUsImlhdCI6MTcyOTc3MTk0NCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmNvbW1lcmNlbGF5ZXIuY28ifQ.jtOrSfSZMITiiYLqmu5p5oiY3pZnQSGNvDeN7nMQ7vpTByMvy-XQvY9YVCVxCwqDy3I_57c3HiEhgEz47crmv-4g4TpcfFjFqg4i2TQGHf0VNJJ5LOsh_cTUYNf13Q7Vy5jmrJ-sC8MJ0tEcs-f2ACizCJ_uRCqvybB-WwmeIf9A-ExiKN24Ku1Tw9aE0zjuTL3rmwNuR_6H1Umx6XzAQIRjNGL0vOTPTQ52-MrTwV3MYqFxlaWw0HZfr-0RgnUBhXN1LnW4sFYVmLLeYPJ1khYs3blcbKOJo5BQ8Fbnyj7E33Mx2B-3Z63y6uA8vTf45GzucZvIiHBXPeYLtUnglHv9KkhZYksb6xj4WMddZzKGC0r8LVl3ac8ZdWf00epLCBnIDkY8T94NgYF9xCWESB477x8rVLUs8WoKrnvrMzc7OZF2xk22q6Ajc24q9FOhnvymS674N_e1yI5QjNxxZHA5R-W79P-pXbm_nmCRqhKnewZupUKKehbm3SyLYmUndDilJNPkD7qxYZhlDr4rpd5VUpFONAU7qZznGgAak92RY-lJHh-RycyRRf8y-M2Q2jeQfXOrsTXAbK35-c2zvCDXBG7fYTc_fzGZFFMEvMpcfFw4PWfEFZTwxLRWWUHw2WYjg5da94c09z0oUWd2Lmp6P6Yw5vAqeHQn9A5x_y0' + + expect(jwtDecode(accessToken)).toStrictEqual({ + header: { + alg: 'RS512', + typ: 'JWT', + kid: 'aba4cc628d1fce3fb93a3ee55826e41cfaf18dc2dff3b07222740380e9199d5d' + }, + payload: { + organization: { + id: 'enWoxFMOnp', + slug: 'the-blue-brand-3', + enterprise: false, + region: 'eu-west-1' + }, + application: { + id: 'GRvDirJlDG', + kind: 'webapp', + public: false + }, + custom_claim: { + customer: { + first_name: 'Jörg', + last_name: 'Doe' + } + }, + scope: 'market:all', + exp: 1729779144, + test: true, + rand: 0.6976628043658695, + iat: 1729771944, + iss: 'https://auth.commercelayer.co' + }, + signature: + 'jtOrSfSZMITiiYLqmu5p5oiY3pZnQSGNvDeN7nMQ7vpTByMvy-XQvY9YVCVxCwqDy3I_57c3HiEhgEz47crmv-4g4TpcfFjFqg4i2TQGHf0VNJJ5LOsh_cTUYNf13Q7Vy5jmrJ-sC8MJ0tEcs-f2ACizCJ_uRCqvybB-WwmeIf9A-ExiKN24Ku1Tw9aE0zjuTL3rmwNuR_6H1Umx6XzAQIRjNGL0vOTPTQ52-MrTwV3MYqFxlaWw0HZfr-0RgnUBhXN1LnW4sFYVmLLeYPJ1khYs3blcbKOJo5BQ8Fbnyj7E33Mx2B-3Z63y6uA8vTf45GzucZvIiHBXPeYLtUnglHv9KkhZYksb6xj4WMddZzKGC0r8LVl3ac8ZdWf00epLCBnIDkY8T94NgYF9xCWESB477x8rVLUs8WoKrnvrMzc7OZF2xk22q6Ajc24q9FOhnvymS674N_e1yI5QjNxxZHA5R-W79P-pXbm_nmCRqhKnewZupUKKehbm3SyLYmUndDilJNPkD7qxYZhlDr4rpd5VUpFONAU7qZznGgAak92RY-lJHh-RycyRRf8y-M2Q2jeQfXOrsTXAbK35-c2zvCDXBG7fYTc_fzGZFFMEvMpcfFw4PWfEFZTwxLRWWUHw2WYjg5da94c09z0oUWd2Lmp6P6Yw5vAqeHQn9A5x_y0' + }) + }) }) diff --git a/packages/js-auth/src/jwtDecode.ts b/packages/js-auth/src/jwtDecode.ts index e92e137..2cc3652 100644 --- a/packages/js-auth/src/jwtDecode.ts +++ b/packages/js-auth/src/jwtDecode.ts @@ -15,8 +15,8 @@ export function jwtDecode(accessToken: string): CommerceLayerJWT { } return { - header: JSON.parse(decodeBase64URLSafe(encodedHeader)), - payload: JSON.parse(decodeBase64URLSafe(encodedPayload)), + header: JSON.parse(decodeBase64URLSafe(encodedHeader, 'binary')), + payload: JSON.parse(decodeBase64URLSafe(encodedPayload, 'utf-8')), signature } } diff --git a/packages/js-auth/src/jwtEncode.ts b/packages/js-auth/src/jwtEncode.ts index bef222d..b759529 100644 --- a/packages/js-auth/src/jwtEncode.ts +++ b/packages/js-auth/src/jwtEncode.ts @@ -62,13 +62,14 @@ async function jwtEncode( ): Promise { const header = { alg: 'HS512', typ: 'JWT' } - const encodedHeader = encodeBase64URLSafe(JSON.stringify(header)) + const encodedHeader = encodeBase64URLSafe(JSON.stringify(header), 'binary') const encodedPayload = encodeBase64URLSafe( JSON.stringify({ ...payload, iat: Math.floor(new Date().getTime() / 1000) - }) + }), + 'utf-8' ) const unsignedToken = `${encodedHeader}.${encodedPayload}` @@ -96,5 +97,8 @@ async function createSignature(data: string, secret: string): Promise { enc.encode(data) ) - return encodeBase64URLSafe(String.fromCharCode(...new Uint8Array(signature))) + return encodeBase64URLSafe( + String.fromCharCode(...new Uint8Array(signature)), + 'binary' + ) } diff --git a/packages/js-auth/src/jwtVerify.spec.ts b/packages/js-auth/src/jwtVerify.spec.ts index 193c02d..764ab17 100644 --- a/packages/js-auth/src/jwtVerify.spec.ts +++ b/packages/js-auth/src/jwtVerify.spec.ts @@ -45,6 +45,18 @@ describe('jwtVerify', () => { expect(verification).toStrictEqual(jsonwebtokenDecoded) }) + it('should be able to verify a JWT with custom claims and special chars.', async () => { + const jsonwebtokenDecoded = jwt.decode(accessTokenCustomClaims, { + complete: true + }) + + const verification = await jwtVerify(accessTokenCustomClaims, { + ignoreExpiration: true + }) + + expect(verification).toStrictEqual(jsonwebtokenDecoded) + }) + it('should cache in-memory the response from "jwks.json".', async () => { // adds the 'fetchMock' global variable and rewires 'fetch' global to call 'fetchMock' instead of the real implementation fetchMocker.enableMocks() @@ -134,7 +146,7 @@ describe('jwtVerify', () => { const newAccessToken = [ header, - encodeBase64URLSafe(JSON.stringify(newPayload)), + encodeBase64URLSafe(JSON.stringify(newPayload), 'utf-8'), signature ].join('.') @@ -155,7 +167,7 @@ describe('jwtVerify', () => { const newAccessToken = [ header, - encodeBase64URLSafe(JSON.stringify(newPayload)), + encodeBase64URLSafe(JSON.stringify(newPayload), 'utf-8'), signature ].join('.') @@ -177,6 +189,9 @@ const accessTokenIo = const accessTokenCo = 'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYTRjYzYyOGQxZmNlM2ZiOTNhM2VlNTU4MjZlNDFjZmFmMThkYzJkZmYzYjA3MjIyNzQwMzgwZTkxOTlkNWQifQ.eyJvcmdhbml6YXRpb24iOnsiaWQiOiJXWGxFT0ZiT25yIiwic2x1ZyI6ImRyb3AtaW4tanMtc3RnIiwiZW50ZXJwcmlzZSI6dHJ1ZSwicmVnaW9uIjoiZXUtd2VzdC0xIn0sImFwcGxpY2F0aW9uIjp7ImlkIjoiZ3BaYkRpZEtZcCIsImNsaWVudF9pZCI6ImdRTVNJTkx5TW0yVHJabzBVR0VFZHViQzd1U2dtOS1RVEc0ZVRWVVRWMW8iLCJraW5kIjoic2FsZXNfY2hhbm5lbCIsInB1YmxpYyI6dHJ1ZX0sIm1hcmtldCI6eyJpZCI6WyJxZ0xkQmhkbWdBIl0sInN0b2NrX2xvY2F0aW9uX2lkcyI6WyJKblZQZ3VEVmthIiwiQm5hSlF1dndHdyJdLCJnZW9jb2Rlcl9pZCI6bnVsbCwiYWxsb3dzX2V4dGVybmFsX3ByaWNlcyI6ZmFsc2V9LCJzY29wZSI6Im1hcmtldDppZDpxZ0xkQmhkbWdBIiwiZXhwIjoxNzI3MzkzMDM3LCJ0ZXN0Ijp0cnVlLCJyYW5kIjowLjY3MTMxNzc5Mjc4MTc0MjYsImlhdCI6MTcyNzM3ODYzNywiaXNzIjoiaHR0cHM6Ly9hdXRoLmNvbW1lcmNlbGF5ZXIuY28ifQ.kOf-6mwLCjn_dlFxc5SeaTE-4mFSq1JaVW7GCX_afUWSb5FZtb1OAjogqLqOBcm0nLb5XWdl8ZZyTvcgLlQenb8Cg-XX4r1Znd63nkBuHE3cRdbaMqlsGdbzixGzL3-puGCO1RmGBO2GcYoFQQMgSGMeLVLiadu4-NmSelMwQuLMGWmVVUFDZ99tn_6nWGInfBP_slKMwTrF7N3hXJHQIh3ZnwfTxGDC-rA_NZlHdWNMWFvLbfhwv_MrPkv0-sD0sTpndolK95ZKXm7L90dgL2HIrzpdS_gaWbCoqJTKLUPODHRYW6MWLoKwvo1pWT7biZncKF_4REGQiMVW7MivA4B-R5C_GRCEmDChdl9420f5cGXW1tZOge4r7mzYWyy5tIyiSjxg3MTpmCSvMadrtXgZ5d0ZRrQttPlr6B1Fi_6Um8WmImg64UQOYI4GgO3hJ23washNMW3O2M6pQMMcM1OaH3S7p2qtmlmqbYjXqeBrthDHpdjTPdsQzIc33fyg9GPSOIbCUGYzEFRnlXEpJer9E1Rm1FAlX8t5dWTUJcw_73broWzjd6VKwAnVWMNb6WjMc2xkfQu-8bJhM5hScY_Iy1Ui-HRBcoSfmrqXlhgM258ZamU6huiWzqQXUZOqWtupjQUz_K358mpSL_WuMHOfj-pZ70W7cnMxJCa3rbY' +const accessTokenCustomClaims = + 'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYTRjYzYyOGQxZmNlM2ZiOTNhM2VlNTU4MjZlNDFjZmFmMThkYzJkZmYzYjA3MjIyNzQwMzgwZTkxOTlkNWQifQ.eyJvcmdhbml6YXRpb24iOnsiaWQiOiJXWGxFT0ZiT25yIiwic2x1ZyI6ImRyb3AtaW4tanMtc3RnIiwiZW50ZXJwcmlzZSI6dHJ1ZSwicmVnaW9uIjoiZXUtd2VzdC0xIn0sImFwcGxpY2F0aW9uIjp7ImlkIjoiYU5hS21pZW9rTSIsImNsaWVudF9pZCI6IkVELVNYa3gybzJpTUUyM3lXc1dHY1M4VzFyWURrVVRBOW5UYW1TdkFBdEkiLCJraW5kIjoiaW50ZWdyYXRpb24iLCJwdWJsaWMiOmZhbHNlfSwiY3VzdG9tX2NsYWltIjp7ImN1c3RvbWVyIjp7ImZpcnN0X25hbWUiOiJKw7ZyZyIsImxhc3RfbmFtZSI6IkRvZSJ9fSwic2NvcGUiOiJtYXJrZXQ6YWxsIiwiZXhwIjoxNzI5Nzg5Mjk5LCJ0ZXN0Ijp0cnVlLCJyYW5kIjowLjgxMzE0MjI2ODMxNDY2NzcsImlhdCI6MTcyOTc4MjA5OSwiaXNzIjoiaHR0cHM6Ly9hdXRoLmNvbW1lcmNlbGF5ZXIuY28ifQ.Al5NKzAlka2V42c1nx8AQu7xX5DAx-elQvAiEsI5cXEBB3ZoBX4YEolizATj9e3eTuoQ6rolgPmNdR2_WF8-BnDvhiIDpUpdJ7O3DryNPauH0974d6UGBP9lXoIcHkosUtEu88yyPWhRAoIOPcoxgHVbKJCnREBqRSexPLuARImphYPex7VQwDKsoN1KE-fz40pPgFawr-OLx5nd0xQlkh1HW5wV-7WOtFKPX24ofzSy7pna8yiEsWSQ67_2NJ0XZ5_fhRYxUiy6ZSEWwPV8kXJHAYCbWSyX1gcwKrVnfO7-QuTjImZ7LzSTwYoQv1U68h6DPoy0kjd1q7K6htklcW7gjdDjowE8EP0_ZNpzQ1oErP8A70z4bnLV7SqU6zaTzEeJ4r1dj90luoY4l7Zo-12gDOtOZAd32SWDdJk_PvnZyt44050Zkx6a2qXO7EaSq2w-LcOSihDTI-NRQPLdbS4nRvRbM0UEOTGVWZlQ3iUFduImDaek3Vbi-BdRs37Gm-48pBb9_mfSzF4KQVPZM10FIKkX6R27OSw7oJ-_UkTKFfdUE2ifsJMhG7Q_ZqZnKcqICKID5TBL2GBbTQj62i6Nreq2pRtglkNHMoVEaJ4-t_i7J3og4a5Vg2zIujE2MhwSyhQP01YoEt7i61-UhorBA-ngXZTmpT_GkxtJZrk' + /** * Generate private key: openssl genrsa -out ./packages/js-auth/src/private.key 4096 * Generate public key: openssl rsa -in ./packages/js-auth/src/private.key -pubout -outform PEM -out ./packages/js-auth/src/public.key diff --git a/packages/js-auth/src/jwtVerify.ts b/packages/js-auth/src/jwtVerify.ts index 9ecdf1a..539b584 100644 --- a/packages/js-auth/src/jwtVerify.ts +++ b/packages/js-auth/src/jwtVerify.ts @@ -38,7 +38,7 @@ export async function jwtVerify( ) const rawSignature = new Uint8Array( - Array.from(decodeBase64URLSafe(decodedJWT.signature), (c) => + Array.from(decodeBase64URLSafe(decodedJWT.signature, 'binary'), (c) => c.charCodeAt(0) ) ) diff --git a/packages/js-auth/src/utils/base64.spec.ts b/packages/js-auth/src/utils/base64.spec.ts index 26b93de..ed898c7 100644 --- a/packages/js-auth/src/utils/base64.spec.ts +++ b/packages/js-auth/src/utils/base64.spec.ts @@ -35,72 +35,69 @@ describe('Using `btoa` and `atob`', () => { function runTests(): void { describe('encodeBase64UrlSafe', () => { it('should be able to create a Base64 URL safe encoded ASCII string from a binary string.', () => { - expect(encodeBase64URLSafe('')).toEqual('') - expect(encodeBase64URLSafe('Hello, world')).toEqual('SGVsbG8sIHdvcmxk') + expect(encodeBase64URLSafe('', 'utf-8')).toEqual('') + expect(encodeBase64URLSafe('Hello, world', 'utf-8')).toEqual( + 'SGVsbG8sIHdvcmxk' + ) - expect(encodeBase64URLSafe(stringifiedObject)).toEqual( + expect(encodeBase64URLSafe(stringifiedObject, 'utf-8')).toEqual( 'eyJjdXN0b21lciI6eyJmaXJzdF9uYW1lIjoiSm9obiIsImxhc3RfbmFtZSI6IkRvZSJ9fQ' ) - expect(encodeBase64URLSafe(stringifiedObjectWithSpecialChar)).toEqual( + expect( + encodeBase64URLSafe(stringifiedObjectWithSpecialChar, 'utf-8') + ).toEqual( 'eyJjdXN0b21lciI6eyJmaXJzdF9uYW1lIjoiSsO2cmciLCJsYXN0X25hbWUiOiJEb2UifX0' ) - expect( - encodeBase64URLSafe( - '0\x82\x0760\x82\x06\x1E \x03\x02\x01\x02\x02\x10\tW¸\x13HxölÈÐ×\x12¨Ìµú0' - ) - ).toEqual('MIIHNjCCBh6gAwIBAgIQCVe4E0h49mzI0NcSqMy1-jA') - - expect(encodeBase64URLSafe('subjects?_d=1')).toEqual('c3ViamVjdHM_X2Q9MQ') + expect(encodeBase64URLSafe('subjects?_d=1', 'utf-8')).toEqual( + 'c3ViamVjdHM_X2Q9MQ' + ) }) }) describe('decodeBase64UrlSafe', () => { it('should be able to decode a string of data which has been encoded using Base64 encoding.', () => { - expect(decodeBase64URLSafe('')).toEqual('') - expect(decodeBase64URLSafe('SGVsbG8sIHdvcmxk')).toEqual('Hello, world') + expect(decodeBase64URLSafe('', 'utf-8')).toEqual('') + expect(decodeBase64URLSafe('SGVsbG8sIHdvcmxk', 'utf-8')).toEqual( + 'Hello, world' + ) expect( decodeBase64URLSafe( - 'eyJjdXN0b21lciI6eyJmaXJzdF9uYW1lIjoiSm9obiIsImxhc3RfbmFtZSI6IkRvZSJ9fQ==' + 'eyJjdXN0b21lciI6eyJmaXJzdF9uYW1lIjoiSm9obiIsImxhc3RfbmFtZSI6IkRvZSJ9fQ==', + 'utf-8' ) ).toEqual(stringifiedObject) - expect( - decodeBase64URLSafe('MIIHNjCCBh6gAwIBAgIQCVe4E0h49mzI0NcSqMy1+jA=') - ).toEqual( - '0\x82\x0760\x82\x06\x1E \x03\x02\x01\x02\x02\x10\tW¸\x13HxölÈÐ×\x12¨Ìµú0' - ) - expect( decodeBase64URLSafe( - 'eyJjdXN0b21lciI6eyJmaXJzdF9uYW1lIjoiSsO2cmciLCJsYXN0X25hbWUiOiJEb2UifX0' + 'eyJjdXN0b21lciI6eyJmaXJzdF9uYW1lIjoiSsO2cmciLCJsYXN0X25hbWUiOiJEb2UifX0', + 'utf-8' ) ).toEqual(stringifiedObjectWithSpecialChar) - expect(decodeBase64URLSafe('c3ViamVjdHM/X2Q9MQ==')).toEqual( + expect(decodeBase64URLSafe('c3ViamVjdHM/X2Q9MQ==', 'utf-8')).toEqual( 'subjects?_d=1' ) }) it('should be able to decode a string of data which has been encoded using Base64 URL safe encoding.', () => { - expect(decodeBase64URLSafe('')).toEqual('') - expect(decodeBase64URLSafe('SGVsbG8sIHdvcmxk')).toEqual('Hello, world') + expect(decodeBase64URLSafe('', 'utf-8')).toEqual('') + expect(decodeBase64URLSafe('SGVsbG8sIHdvcmxk', 'utf-8')).toEqual( + 'Hello, world' + ) expect( decodeBase64URLSafe( - 'eyJjdXN0b21lciI6eyJmaXJzdF9uYW1lIjoiSm9obiIsImxhc3RfbmFtZSI6IkRvZSJ9fQ' + 'eyJjdXN0b21lciI6eyJmaXJzdF9uYW1lIjoiSm9obiIsImxhc3RfbmFtZSI6IkRvZSJ9fQ', + 'utf-8' ) ).toEqual(stringifiedObject) - expect( - decodeBase64URLSafe('MIIHNjCCBh6gAwIBAgIQCVe4E0h49mzI0NcSqMy1-jA') - ).toEqual( - '0\x82\x0760\x82\x06\x1E \x03\x02\x01\x02\x02\x10\tW¸\x13HxölÈÐ×\x12¨Ìµú0' + expect(decodeBase64URLSafe('c3ViamVjdHM_X2Q9MQ', 'utf-8')).toEqual( + 'subjects?_d=1' ) - - expect(decodeBase64URLSafe('c3ViamVjdHM_X2Q9MQ')).toEqual('subjects?_d=1') }) }) } diff --git a/packages/js-auth/src/utils/base64.ts b/packages/js-auth/src/utils/base64.ts index c39e3ba..3fae466 100644 --- a/packages/js-auth/src/utils/base64.ts +++ b/packages/js-auth/src/utils/base64.ts @@ -9,10 +9,19 @@ * @param stringToEncode The binary string to encode. * @returns An ASCII string containing the Base64 URL safe representation of `stringToEncode`. */ -export function encodeBase64URLSafe(stringToEncode: string): string { +export function encodeBase64URLSafe( + stringToEncode: string, + encoding: 'utf-8' | 'binary' +): string { if (typeof btoa !== 'undefined') { + // Convert the string to a UTF-8 byte sequence before encoding + const utf8String = + encoding === 'utf-8' + ? unescape(encodeURIComponent(stringToEncode)) + : stringToEncode + return ( - btoa(stringToEncode) + btoa(utf8String) // Remove padding equal characters .replaceAll('=', '') // Replace characters according to base64url specifications @@ -21,7 +30,7 @@ export function encodeBase64URLSafe(stringToEncode: string): string { ) } - return Buffer.from(stringToEncode, 'binary').toString('base64url') + return Buffer.from(stringToEncode, encoding).toString('base64url') } /** @@ -35,15 +44,20 @@ export function encodeBase64URLSafe(stringToEncode: string): string { * @param encodedData A binary string (i.e., a string in which each character in the string is treated as a byte of binary data) containing Base64 URL safe -encoded data. * @returns An ASCII string containing decoded data from `encodedData`. */ -export function decodeBase64URLSafe(encodedData: string): string { +export function decodeBase64URLSafe( + encodedData: string, + encoding: 'utf-8' | 'binary' +): string { if (typeof atob !== 'undefined') { - return atob( + const decoded = atob( encodedData // Replace characters according to base64url specifications .replaceAll('-', '+') .replaceAll('_', '/') ) + + return encoding === 'utf-8' ? decodeURIComponent(escape(decoded)) : decoded } - return Buffer.from(encodedData, 'base64url').toString('binary') + return Buffer.from(encodedData, 'base64url').toString(encoding) }