diff --git a/README.md b/README.md index 6b94616a..d16ee1fe 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,10 @@ import * as jose from 'https://deno.land/x/jose/index.ts' - JSON Web Tokens (JWT) - [Signing](docs/classes/jwt_sign.SignJWT.md#readme) - - [Verification & Claims Set Validation](docs/functions/jwt_verify.jwtVerify.md#readme) + - [Verification & JWT Claims Set Validation](docs/functions/jwt_verify.jwtVerify.md#readme) - Encrypted JSON Web Tokens - [Encryption](docs/classes/jwt_encrypt.EncryptJWT.md#readme) - - [Decryption & Claims Set Validation](docs/functions/jwt_decrypt.jwtDecrypt.md#readme) + - [Decryption & JWT Claims Set Validation](docs/functions/jwt_decrypt.jwtDecrypt.md#readme) - Key Import - [JWK Import](docs/functions/key_import.importJWK.md#readme) - [Public Key Import (SPKI)](docs/functions/key_import.importSPKI.md#readme) @@ -72,6 +72,7 @@ import * as jose from 'https://deno.land/x/jose/index.ts' - [Public Key Export](docs/functions/key_export.exportSPKI.md#readme) - Utilities - [Decoding Token's Protected Header](docs/functions/util_decode_protected_header.decodeProtectedHeader.md#readme) + - [Decoding JWT Claims Set](docs/functions/util_decode_jwt.decodeJwt.md#readme) - [Unsecured JWT](docs/classes/jwt_unsecured.UnsecuredJWT.md#readme) - [JOSE Errors](docs/modules/util_errors.md#readme) diff --git a/docs/README.md b/docs/README.md index a6965d28..a65fcc9d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,10 +25,10 @@ import * as jose from 'https://deno.land/x/jose/index.ts' - JSON Web Tokens (JWT) - [Signing](classes/jwt_sign.SignJWT.md#readme) - - [Verification & Claims Set Validation](functions/jwt_verify.jwtVerify.md#readme) + - [Verification & JWT Claims Set Validation](functions/jwt_verify.jwtVerify.md#readme) - Encrypted JSON Web Tokens - [Encryption](classes/jwt_encrypt.EncryptJWT.md#readme) - - [Decryption & Claims Set Validation](functions/jwt_decrypt.jwtDecrypt.md#readme) + - [Decryption & JWT Claims Set Validation](functions/jwt_decrypt.jwtDecrypt.md#readme) - Key Import - [JWK Import](functions/key_import.importJWK.md#readme) - [Public Key Import (SPKI)](functions/key_import.importSPKI.md#readme) @@ -55,6 +55,7 @@ import * as jose from 'https://deno.land/x/jose/index.ts' - [Public Key Export](functions/key_export.exportSPKI.md#readme) - Utilities - [Decoding Token's Protected Header](functions/util_decode_protected_header.decodeProtectedHeader.md#readme) + - [Decoding JWT Claims Set](functions/util_decode_jwt.decodeJwt.md#readme) - [Unsecured JWT](classes/jwt_unsecured.UnsecuredJWT.md#readme) - [JOSE Errors](modules/util_errors.md#readme) diff --git a/src/index.ts b/src/index.ts index c8d2d8f0..089d03b9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,6 +47,7 @@ export { importSPKI, importPKCS8, importX509, importJWK } from './key/import.js' export type { PEMImportOptions } from './key/import.js' export { decodeProtectedHeader } from './util/decode_protected_header.js' +export { decodeJwt } from './util/decode_jwt.js' export type { ProtectedHeaderParameters } from './util/decode_protected_header.js' export * as errors from './util/errors.js' diff --git a/src/util/decode_jwt.ts b/src/util/decode_jwt.ts new file mode 100644 index 00000000..fb53e3c7 --- /dev/null +++ b/src/util/decode_jwt.ts @@ -0,0 +1,48 @@ +import { decode as base64url } from './base64url.js' +import { decoder } from '../lib/buffer_utils.js' +import isObject from '../lib/is_object.js' +import type { JWTPayload } from '../types.d' +import { JWTInvalid } from './errors.js' + +/** + * Decodes a signed JSON Web Token payload. This does not validate the JWT Claims Set + * types or values. This does not validate the JWS Signature. For a proper + * Signed JWT Claims Set validation and JWS signature verification use `jose.jwtVerify()`. + * For an encrypted JWT Claims Set validation and JWE decryption use `jose.jwtDecrypt()`. + * + * @param jwt JWT token in compact JWS serialization. + * + * @example Usage + * ```js + * const claims = jose.decodeJwt(token) + * console.log(claims) + * ``` + */ +export function decodeJwt(jwt: string) { + if (typeof jwt !== 'string') + throw new JWTInvalid('JWTs must use Compact JWS serialization, JWT must be a string') + + const { 1: payload, length } = jwt.split('.') + + if (length === 5) throw new JWTInvalid('Only JWTs using Compact JWS serialization can be decoded') + if (length !== 3) throw new JWTInvalid('Invalid JWT') + if (!payload) throw new JWTInvalid('JWTs must contain a payload') + + let decoded: Uint8Array + try { + decoded = base64url(payload) + } catch { + throw new JWTInvalid('Failed to parse the base64url encoded payload') + } + + let result: unknown + try { + result = JSON.parse(decoder.decode(decoded)) + } catch { + throw new JWTInvalid('Failed to parse the decoded payload as JSON') + } + + if (!isObject(result)) throw new JWTInvalid('Invalid JWT Claims Set') + + return result +} diff --git a/test/util/decode_jwt.test.mjs b/test/util/decode_jwt.test.mjs new file mode 100644 index 00000000..28dd4148 --- /dev/null +++ b/test/util/decode_jwt.test.mjs @@ -0,0 +1,54 @@ +import test from 'ava' + +const root = !('WEBCRYPTO' in process.env) ? '#dist' : '#dist/webcrypto' +const { decodeJwt, errors, base64url } = await import(root) + +test('invalid inputs', (t) => { + const jwt = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' + + const parts = jwt.split('.') + + t.throws(() => decodeJwt(null), { + instanceOf: errors.JWTInvalid, + message: 'JWTs must use Compact JWS serialization, JWT must be a string', + }) + + t.throws(() => decodeJwt('....'), { + instanceOf: errors.JWTInvalid, + message: 'Only JWTs using Compact JWS serialization can be decoded', + }) + + t.throws(() => decodeJwt('.'), { + instanceOf: errors.JWTInvalid, + message: 'Invalid JWT', + }) + + t.throws(() => decodeJwt([parts[0], '', parts[2]].join('.')), { + instanceOf: errors.JWTInvalid, + message: 'JWTs must contain a payload', + }) + + t.throws(() => decodeJwt([parts[0], base64url.encode('null'), parts[2]].join('.')), { + instanceOf: errors.JWTInvalid, + message: 'Invalid JWT Claims Set', + }) + + t.throws(() => decodeJwt([parts[0], base64url.encode('[]'), parts[2]].join('.')), { + instanceOf: errors.JWTInvalid, + message: 'Invalid JWT Claims Set', + }) + + t.throws(() => decodeJwt([parts[0], base64url.encode('{"notajson'), parts[2]].join('.')), { + instanceOf: errors.JWTInvalid, + message: 'Failed to parse the decoded payload as JSON', + }) + + t.deepEqual(decodeJwt([parts[0], base64url.encode('{}'), parts[2]].join('.')), {}) + + t.deepEqual(decodeJwt(jwt), { + sub: '1234567890', + name: 'John Doe', + iat: 1516239022, + }) +})