diff options
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | pnpm-lock.yaml | 75 | ||||
-rw-r--r-- | src/tools/jwt-parser/claim.vue | 29 | ||||
-rw-r--r-- | src/tools/jwt-parser/index.ts | 20 | ||||
-rw-r--r-- | src/tools/jwt-parser/jwt-parser.constants.ts | 92 | ||||
-rw-r--r-- | src/tools/jwt-parser/jwt-parser.service.ts | 457 | ||||
-rw-r--r-- | src/tools/jwt-parser/jwt-parser.vue | 63 | ||||
-rw-r--r-- | src/tools/jwt-parser/value.vue | 24 |
8 files changed, 203 insertions, 558 deletions
diff --git a/package.json b/package.json index 6c8aa1c..522aacc 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "qrcode": "^1.5.1", "randombytes": "^2.1.0", "sql-formatter": "^8.2.0", + "ts-pattern": "^4.1.3", "uuid": "^8.3.2", "vue": "^3.2.45", "vue-router": "^4.1.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5398bd4..f826628 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,7 @@ specifiers: sql-formatter: ^8.2.0 standard-version: ^9.5.0 start-server-and-test: ^1.14.0 + ts-pattern: ^4.1.3 typescript: ~4.5.5 uuid: ^8.3.2 vite: ^2.9.15 @@ -96,6 +97,7 @@ dependencies: qrcode: 1.5.1 randombytes: 2.1.0 sql-formatter: 8.2.0 + ts-pattern: 4.1.3 uuid: 8.3.2 vue: 3.2.45 vue-router: 4.1.6_vue@3.2.45 @@ -122,7 +124,7 @@ devDependencies: eslint: 8.27.0 eslint-config-prettier: 8.5.0_eslint@8.27.0 eslint-import-resolver-typescript: 3.5.2_dcpv4nbdr5ks2h5677xdltrk6e - eslint-plugin-import: 2.26.0_gbipkkcbnjmysmpjttq6vkmfqq + eslint-plugin-import: 2.26.0_eslint@8.27.0 eslint-plugin-vue: 8.7.1_eslint@8.27.0 jsdom: 19.0.0 less: 4.1.3 @@ -132,7 +134,7 @@ devDependencies: typescript: 4.5.5 vite: 2.9.15_less@4.1.3 vite-plugin-md: 0.12.4_vite@2.9.15 - vite-plugin-pwa: 0.11.13_7mbbuzxp22mje5bxdolj2b6yg4 + vite-plugin-pwa: 0.11.13_vite@2.9.15 vite-svg-loader: 3.6.0 vitest: 0.13.1_uwxj23d3xojfwkqpytqc7pyhry vue-tsc: 0.31.4_typescript@4.5.5 @@ -2066,10 +2068,6 @@ packages: peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 eslint-plugin-vue: ^8.0.1 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true dependencies: '@typescript-eslint/eslint-plugin': 5.42.1_vfr6z4qvdp6defk3ked6x75zyi '@typescript-eslint/parser': 5.42.1_4rqwsplhh2ekz63wktwk7d7ht4 @@ -2079,6 +2077,7 @@ packages: vue-eslint-parser: 8.3.0_eslint@8.27.0 transitivePeerDependencies: - supports-color + - typescript dev: true /@vue/reactivity-transform/3.2.45: @@ -2107,6 +2106,7 @@ packages: '@vue/runtime-core': 3.2.45 '@vue/shared': 3.2.45 csstype: 2.6.21 + dev: false /@vue/server-renderer/3.2.45_vue@3.2.45: resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==} @@ -2116,6 +2116,7 @@ packages: '@vue/compiler-ssr': 3.2.45 '@vue/shared': 3.2.45 vue: 3.2.45 + dev: false /@vue/shared/3.2.45: resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==} @@ -2860,8 +2861,8 @@ packages: engines: {node: '>=10'} hasBin: true dependencies: - JSONStream: 1.3.5 is-text-path: 1.0.1 + JSONStream: 1.3.5 lodash: 4.17.21 meow: 8.1.2 split2: 3.2.2 @@ -2990,6 +2991,7 @@ packages: /csstype/2.6.21: resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} + dev: false /csstype/3.0.11: resolution: {integrity: sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==} @@ -3028,22 +3030,12 @@ packages: /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true dependencies: ms: 2.0.0 dev: true /debug/3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true dependencies: ms: 2.1.3 dev: true @@ -3582,8 +3574,6 @@ packages: dependencies: debug: 3.2.7 resolve: 1.22.1 - transitivePeerDependencies: - - supports-color dev: true /eslint-import-resolver-typescript/3.5.2_dcpv4nbdr5ks2h5677xdltrk6e: @@ -3596,7 +3586,7 @@ packages: debug: 4.3.4 enhanced-resolve: 5.10.0 eslint: 8.27.0 - eslint-plugin-import: 2.26.0_gbipkkcbnjmysmpjttq6vkmfqq + eslint-plugin-import: 2.26.0_eslint@8.27.0 get-tsconfig: 4.2.0 globby: 13.1.2 is-core-module: 2.11.0 @@ -3606,54 +3596,32 @@ packages: - supports-color dev: true - /eslint-module-utils/2.7.4_idrr6ghswzssuopqxluk4kfum4: + /eslint-module-utils/2.7.4_eslint@8.27.0: resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} peerDependencies: - '@typescript-eslint/parser': '*' eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true eslint: optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true dependencies: - '@typescript-eslint/parser': 5.42.1_4rqwsplhh2ekz63wktwk7d7ht4 debug: 3.2.7 eslint: 8.27.0 - eslint-import-resolver-node: 0.3.6 - eslint-import-resolver-typescript: 3.5.2_dcpv4nbdr5ks2h5677xdltrk6e - transitivePeerDependencies: - - supports-color dev: true - /eslint-plugin-import/2.26.0_gbipkkcbnjmysmpjttq6vkmfqq: + /eslint-plugin-import/2.26.0_eslint@8.27.0: resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==} engines: {node: '>=4'} peerDependencies: - '@typescript-eslint/parser': '*' eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true dependencies: - '@typescript-eslint/parser': 5.42.1_4rqwsplhh2ekz63wktwk7d7ht4 array-includes: 3.1.6 array.prototype.flat: 1.3.1 debug: 2.6.9 doctrine: 2.1.0 eslint: 8.27.0 eslint-import-resolver-node: 0.3.6 - eslint-module-utils: 2.7.4_idrr6ghswzssuopqxluk4kfum4 + eslint-module-utils: 2.7.4_eslint@8.27.0 has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 @@ -3661,10 +3629,6 @@ packages: object.values: 1.1.6 resolve: 1.22.1 tsconfig-paths: 3.14.1 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color dev: true /eslint-plugin-prettier/4.2.1_v7o5sx5x3wbs57ifz6wc4f76we: @@ -4870,8 +4834,6 @@ packages: mime: 1.6.0 needle: 3.1.0 source-map: 0.6.1 - transitivePeerDependencies: - - supports-color dev: true /leven/3.1.0: @@ -5214,8 +5176,6 @@ packages: debug: 3.2.7 iconv-lite: 0.6.3 sax: 1.2.4 - transitivePeerDependencies: - - supports-color dev: true optional: true @@ -6495,6 +6455,10 @@ packages: engines: {node: '>=8'} dev: true + /ts-pattern/4.1.3: + resolution: {integrity: sha512-8beXMWTGEv1JfDjSxfNhe4uT5jKYdhmEUKzt4gZW9dmHlquq3b+IbEyA7vX9LjBfzHmvKnM4HiomAUCyaW2Pew==} + dev: false + /tsconfig-paths/3.14.1: resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==} dependencies: @@ -6578,6 +6542,7 @@ packages: resolution: {integrity: sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==} engines: {node: '>=4.2.0'} hasBin: true + dev: true /uc.micro/1.0.6: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} @@ -6734,11 +6699,10 @@ packages: vite: 2.9.15_less@4.1.3 dev: true - /vite-plugin-pwa/0.11.13_7mbbuzxp22mje5bxdolj2b6yg4: + /vite-plugin-pwa/0.11.13_vite@2.9.15: resolution: {integrity: sha512-Ssj14m3TRVLfkFEAWSMcFE2d1cSdEZyrVTzfY2lSL+umHYvcIFHVDAY143sygtBCb44OPczsAOmWwBTxwOvh7g==} peerDependencies: vite: ^2.0.0 - workbox-window: ^6.4.0 dependencies: debug: 4.3.4 fast-glob: 3.2.12 @@ -7004,6 +6968,7 @@ packages: '@vue/runtime-dom': 3.2.45 '@vue/server-renderer': 3.2.45_vue@3.2.45 '@vue/shared': 3.2.45 + dev: false /vueuc/0.4.49_vue@3.2.45: resolution: {integrity: sha512-WarAC44a/Yx78CxkAgROYLq+LkAeCGA/6wHidVoFmHLbzyF3SiP2nzRNGD/8zJeJInXv18EnWK6A//eGgMMq8w==} diff --git a/src/tools/jwt-parser/claim.vue b/src/tools/jwt-parser/claim.vue deleted file mode 100644 index 3f298a2..0000000 --- a/src/tools/jwt-parser/claim.vue +++ /dev/null @@ -1,29 +0,0 @@ -<template> - <n-space> - <em>{{ claim }}</em> - <span v-if="label.label !== claim"> - <n-popover placement="right" trigger="hover"> - <template #trigger> - <n-icon :component="InfoCircle" trigger /> - </template> - {{ label.label }} - <template v-if="label.ref !== ''" #footer> {{ label.ref }} </template> - </n-popover> - </span> - </n-space> -</template> - -<script setup lang="ts"> -import { computed } from 'vue'; -import { InfoCircle } from '@vicons/tabler'; -import { getClaimLabel } from './jwt-parser.service'; - -const props = defineProps({ - claim: { - type: String, - default: '', - }, -}); - -const label = computed(() => getClaimLabel(props.claim ? props.claim : '')); -</script> diff --git a/src/tools/jwt-parser/index.ts b/src/tools/jwt-parser/index.ts index dcce4f1..7249ace 100644 --- a/src/tools/jwt-parser/index.ts +++ b/src/tools/jwt-parser/index.ts @@ -4,8 +4,24 @@ import { defineTool } from '../tool'; export const tool = defineTool({ name: 'JWT parser', path: '/jwt-parser', - description: 'Parse a JWT (JSON Web Token) to display its content.', - keywords: ['jwt', 'parser'], + description: 'Parse and decode your JSON Web Token (jwt) and display its content.', + keywords: [ + 'jwt', + 'parser', + 'decode', + 'typ', + 'alg', + 'iss', + 'sub', + 'aud', + 'exp', + 'nbf', + 'iat', + 'jti', + 'json', + 'web', + 'token', + ], component: () => import('./jwt-parser.vue'), icon: Key, }); diff --git a/src/tools/jwt-parser/jwt-parser.constants.ts b/src/tools/jwt-parser/jwt-parser.constants.ts new file mode 100644 index 0000000..a5150a0 --- /dev/null +++ b/src/tools/jwt-parser/jwt-parser.constants.ts @@ -0,0 +1,92 @@ +// From https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 +export const ALGORITHM_DESCRIPTIONS: { [k: string]: string } = { + HS256: 'HMAC using SHA-256', + HS384: 'HMAC using SHA-384', + HS512: 'HMAC using SHA-512', + RS256: 'RSASSA-PKCS1-v1_5 using SHA-256', + RS384: 'RSASSA-PKCS1-v1_5 using SHA-384', + RS512: 'RSASSA-PKCS1-v1_5 using SHA-512', + ES256: 'ECDSA using P-256 and SHA-256', + ES384: 'ECDSA using P-384 and SHA-384', + ES512: 'ECDSA using P-521 and SHA-512', + PS256: 'RSASSA-PSS using SHA-256 and MGF1 with SHA-256', + PS384: 'RSASSA-PSS using SHA-384 and MGF1 with SHA-384', + PS512: 'RSASSA-PSS using SHA-512 and MGF1 with SHA-512', + none: 'No digital signature or MAC performed', +}; + +// List extracted from IANA: https://www.iana.org/assignments/jwt/jwt.xhtml +export const CLAIM_DESCRIPTIONS: Record<string, string> = { + typ: 'Type', + alg: 'Algorithm', + iss: 'Issuer', + sub: 'Subject', + aud: 'Audience', + exp: 'Expiration Time', + nbf: 'Not Before', + iat: 'Issued At', + jti: 'JWT ID', + name: 'Full name', + given_name: 'Given name(s) or first name(s)', + family_name: 'Surname(s) or last name(s)', + middle_name: 'Middle name(s)', + nickname: 'Casual name', + preferred_username: 'Shorthand name by which the End-User wishes to be referred to', + profile: 'Profile page URL', + picture: 'Profile picture URL', + website: 'Web page or blog URL', + email: 'Preferred e-mail address', + email_verified: 'True if the e-mail address has been verified; otherwise false', + gender: 'Gender', + birthdate: 'Birthday', + zoneinfo: 'Time zone', + locale: 'Locale', + phone_number: 'Preferred telephone number', + phone_number_verified: 'True if the phone number has been verified; otherwise false', + address: 'Preferred postal address', + updated_at: 'Time the information was last updated', + azp: 'Authorized party - the party to which the ID Token was issued', + nonce: 'Value used to associate a Client session with an ID Token', + auth_time: 'Time when the authentication occurred', + at_hash: 'Access Token hash value', + c_hash: 'Code hash value', + acr: 'Authentication Context Class Reference', + amr: 'Authentication Methods References', + sub_jwk: 'Public key used to check the signature of an ID Token', + cnf: 'Confirmation', + sip_from_tag: 'SIP From tag header field parameter value', + sip_date: 'SIP Date header field value', + sip_callid: 'SIP Call-Id header field value', + sip_cseq_num: 'SIP CSeq numeric header field parameter value', + sip_via_branch: 'SIP Via branch header field parameter value', + orig: 'Originating Identity String', + dest: 'Destination Identity String', + mky: 'Media Key Fingerprint String', + events: 'Security Events', + toe: 'Time of Event', + txn: 'Transaction Identifier', + rph: 'Resource Priority Header Authorization', + sid: 'Session ID', + vot: 'Vector of Trust value', + vtm: 'Vector of Trust trustmark URL', + attest: 'Attestation level as defined in SHAKEN framework', + origid: 'Originating Identifier as defined in SHAKEN framework', + act: 'Actor', + scope: 'Scope Values', + client_id: 'Client Identifier', + may_act: 'Authorized Actor - the party that is authorized to become the actor', + jcard: 'jCard data', + at_use_nbr: 'Number of API requests for which the access token can be used', + div: 'Diverted Target of a Call', + opt: 'Original PASSporT (in Full Form)', + vc: 'Verifiable Credential as specified in the W3C Recommendation', + vp: 'Verifiable Presentation as specified in the W3C Recommendation', + sph: 'SIP Priority header field', + ace_profile: 'ACE profile a token is supposed to be used with.', + cnonce: 'Client nonce', + exi: 'Expires in', + roles: 'Roles', + groups: 'Groups', + entitlements: 'Entitlements', + token_introspection: 'Token introspection response', +}; diff --git a/src/tools/jwt-parser/jwt-parser.service.ts b/src/tools/jwt-parser/jwt-parser.service.ts index 37b6ecc..9bc994c 100644 --- a/src/tools/jwt-parser/jwt-parser.service.ts +++ b/src/tools/jwt-parser/jwt-parser.service.ts @@ -1,429 +1,46 @@ -import jwt_decode, { InvalidTokenError } from 'jwt-decode'; +import jwtDecode, { type JwtHeader, type JwtPayload } from 'jwt-decode'; +import _ from 'lodash'; +import { match } from 'ts-pattern'; +import { ALGORITHM_DESCRIPTIONS, CLAIM_DESCRIPTIONS } from './jwt-parser.constants'; -interface JWT { - header: Map<string, unknown>; - payload: Map<string, unknown>; -} +export { decodeJwt }; + +function decodeJwt({ jwt }: { jwt: string }) { + const rawHeader = jwtDecode<JwtHeader>(jwt, { header: true }); + const rawPayload = jwtDecode<JwtPayload>(jwt); + + const header = _.map(rawHeader, (value, claim) => parseClaims({ claim, value })); + const payload = _.map(rawPayload, (value, claim) => parseClaims({ claim, value })); -export function safeJwtDecode(rawJwt: string): JWT { - try { - const header = jwt_decode(rawJwt, { header: true }) as Map<string, unknown>; - const payload = jwt_decode(rawJwt) as Map<string, unknown>; - return { header, payload }; - } catch (e) { - if (e instanceof InvalidTokenError) { - return { header: new Map<string, unknown>(), payload: new Map<string, unknown>() }; - } else { - throw e; - } - } + return { + header, + payload, + }; } -export function getClaimLabel(claim: string): { label: string; ref: string } { - const infos = STANDARD_CLAIMS.find((info) => info.name === claim); - if (infos) { - return { label: infos.long_name, ref: infos.ref }; - } - switch (claim) { - case 'typ': - return { label: 'Type', ref: '' }; - case 'alg': - return { label: 'Algorithm', ref: '' }; - } - return { label: claim, ref: '' }; +function parseClaims({ claim, value }: { claim: string; value: unknown }) { + const claimDescription = CLAIM_DESCRIPTIONS[claim]; + const formattedValue = _.toString(value); + const friendlyValue = getFriendlyValue({ claim, value }); + + return { + value: formattedValue, + friendlyValue, + claim, + claimDescription, + }; } -export function parseClaimValue(claim: string, value: unknown): { value: unknown; extension?: unknown } { - switch (claim) { - case 'exp': - case 'nbf': - case 'iat': { - // Convert to milliseconds, JWT specs says it should be in seconds, JS - // works with milliseconds - value = typeof value === 'string' ? parseInt(value) : value; - const date = new Date((value as number) * 1000); - return { value: `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`, extension: value }; - } - case 'alg': - return { value: AlgorithmKeyDescriptionMapping[value as string], extension: value }; - default: - if (typeof value === 'boolean') { - // Perhaps there's a better way to do this? - return { value: value ? 'true' : 'false' }; - } - return { value: value }; - } +function getFriendlyValue({ claim, value }: { claim: string; value: unknown }) { + return match(claim) + .with('exp', 'nbf', 'iat', () => dateFormatter(value)) + .with('alg', () => (_.isString(value) ? ALGORITHM_DESCRIPTIONS[value] : undefined)) + .otherwise(() => undefined); } -// From https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 -const AlgorithmKeyDescriptionMapping: { [k: string]: string } = { - HS256: 'HMAC using SHA-256', - HS384: 'HMAC using SHA-384', - HS512: 'HMAC using SHA-512', - RS256: 'RSASSA-PKCS1-v1_5 using SHA-256', - RS384: 'RSASSA-PKCS1-v1_5 using SHA-384', - RS512: 'RSASSA-PKCS1-v1_5 using SHA-512', - ES256: 'ECDSA using P-256 and SHA-256', - ES384: 'ECDSA using P-384 and SHA-384', - ES512: 'ECDSA using P-521 and SHA-512', - PS256: 'RSASSA-PSS using SHA-256 and MGF1 with SHA-256', - PS384: 'RSASSA-PSS using SHA-384 and MGF1 with SHA-384', - PS512: 'RSASSA-PSS using SHA-512 and MGF1 with SHA-512', - none: 'No digital signature or MAC performed', -}; +const dateFormatter = (value: unknown) => { + if (_.isNil(value)) return undefined; -// List extracted from IANA: https://www.iana.org/assignments/jwt/jwt.xhtml -const STANDARD_CLAIMS = [ - { - name: 'iss', - long_name: 'Issuer', - ref: '[RFC7519 - Section 4.1.1]', - }, - { - name: 'sub', - long_name: 'Subject', - ref: '[RFC7519 - Section 4.1.2]', - }, - { - name: 'aud', - long_name: 'Audience', - ref: '[RFC7519 - Section 4.1.3]', - }, - { - name: 'exp', - long_name: 'Expiration Time', - ref: '[RFC7519 - Section 4.1.4]', - }, - { - name: 'nbf', - long_name: 'Not Before', - ref: '[RFC7519 - Section 4.1.5]', - }, - { - name: 'iat', - long_name: 'Issued At', - ref: '[RFC7519 - Section 4.1.6]', - }, - { - name: 'jti', - long_name: 'JWT ID', - ref: '[RFC7519 - Section 4.1.7]', - }, - { - name: 'name', - long_name: 'Full name', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'given_name', - long_name: 'Given name(s) or first name(s)', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'family_name', - long_name: 'Surname(s) or last name(s)', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'middle_name', - long_name: 'Middle name(s)', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'nickname', - long_name: 'Casual name', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'preferred_username', - long_name: 'Shorthand name by which the End-User wishes to be referred to', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'profile', - long_name: 'Profile page URL', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'picture', - long_name: 'Profile picture URL', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'website', - long_name: 'Web page or blog URL', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'email', - long_name: 'Preferred e-mail address', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'email_verified', - long_name: 'True if the e-mail address has been verified; otherwise false', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'gender', - long_name: 'Gender', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'birthdate', - long_name: 'Birthday', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'zoneinfo', - long_name: 'Time zone', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'locale', - long_name: 'Locale', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'phone_number', - long_name: 'Preferred telephone number', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'phone_number_verified', - long_name: 'True if the phone number has been verified; otherwise false', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'address', - long_name: 'Preferred postal address', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'updated_at', - long_name: 'Time the information was last updated', - ref: '[OpenID Connect Core 1.0 - Section 5.1]', - }, - { - name: 'azp', - long_name: 'Authorized party - the party to which the ID Token was issued', - ref: '[OpenID Connect Core 1.0 - Section 2]', - }, - { - name: 'nonce', - long_name: 'Value used to associate a Client session with an ID Token', - ref: '[OpenID Connect Core 1.0 - Section 2]', - }, - { - name: 'auth_time', - long_name: 'Time when the authentication occurred', - ref: '[OpenID Connect Core 1.0 - Section 2]', - }, - { - name: 'at_hash', - long_name: 'Access Token hash value', - ref: '[OpenID Connect Core 1.0 - Section 2]', - }, - { - name: 'c_hash', - long_name: 'Code hash value', - ref: '[OpenID Connect Core 1.0 - Section 3.3.2.11]', - }, - { - name: 'acr', - long_name: 'Authentication Context Class Reference', - ref: '[OpenID Connect Core 1.0 - Section 2]', - }, - { - name: 'amr', - long_name: 'Authentication Methods References', - ref: '[OpenID Connect Core 1.0 - Section 2]', - }, - { - name: 'sub_jwk', - long_name: 'Public key used to check the signature of an ID Token', - ref: '[OpenID Connect Core 1.0 - Section 7.4]', - }, - { - name: 'cnf', - long_name: 'Confirmation', - ref: '[RFC7800 - Section 3.1]', - }, - { - name: 'sip_from_tag', - long_name: 'SIP From tag header field parameter value', - ref: '[RFC8055][RFC3261]', - }, - { - name: 'sip_date', - long_name: 'SIP Date header field value', - ref: '[RFC8055][RFC3261]', - }, - { - name: 'sip_callid', - long_name: 'SIP Call-Id header field value', - ref: '[RFC8055][RFC3261]', - }, - { - name: 'sip_cseq_num', - long_name: 'SIP CSeq numeric header field parameter value', - ref: '[RFC8055][RFC3261]', - }, - { - name: 'sip_via_branch', - long_name: 'SIP Via branch header field parameter value', - ref: '[RFC8055][RFC3261]', - }, - { - name: 'orig', - long_name: 'Originating Identity String', - ref: '[RFC8225 - Section 5.2.1]', - }, - { - name: 'dest', - long_name: 'Destination Identity String', - ref: '[RFC8225 - Section 5.2.1]', - }, - { - name: 'mky', - long_name: 'Media Key Fingerprint String', - ref: '[RFC8225 - Section 5.2.2]', - }, - { - name: 'events', - long_name: 'Security Events', - ref: '[RFC8417 - Section 2.2]', - }, - { - name: 'toe', - long_name: 'Time of Event', - ref: '[RFC8417 - Section 2.2]', - }, - { - name: 'txn', - long_name: 'Transaction Identifier', - ref: '[RFC8417 - Section 2.2]', - }, - { - name: 'rph', - long_name: 'Resource Priority Header Authorization', - ref: '[RFC8443 - Section 3]', - }, - { - name: 'sid', - long_name: 'Session ID', - ref: '[OpenID Connect Front-Channel Logout 1.0 - Section 3]', - }, - { - name: 'vot', - long_name: 'Vector of Trust value', - ref: '[RFC8485]', - }, - { - name: 'vtm', - long_name: 'Vector of Trust trustmark URL', - ref: '[RFC8485]', - }, - { - name: 'attest', - long_name: 'Attestation level as defined in SHAKEN framework', - ref: '[RFC8588]', - }, - { - name: 'origid', - long_name: 'Originating Identifier as defined in SHAKEN framework', - ref: '[RFC8588]', - }, - { - name: 'act', - long_name: 'Actor', - ref: '[RFC8693 - Section 4.1]', - }, - { - name: 'scope', - long_name: 'Scope Values', - ref: '[RFC8693 - Section 4.2]', - }, - { - name: 'client_id', - long_name: 'Client Identifier', - ref: '[RFC8693 - Section 4.3]', - }, - { - name: 'may_act', - long_name: 'Authorized Actor - the party that is authorized to become the actor', - ref: '[RFC8693 - Section 4.4]', - }, - { - name: 'jcard', - long_name: 'jCard data', - ref: '[RFC8688][RFC7095]', - }, - { - name: 'at_use_nbr', - long_name: 'Number of API requests for which the access token can be used', - ref: '[ETSI GS NFV-SEC 022 V2.7.1]', - }, - { - name: 'div', - long_name: 'Diverted Target of a Call', - ref: '[RFC8946]', - }, - { - name: 'opt', - long_name: 'Original PASSporT (in Full Form)', - ref: '[RFC8946]', - }, - { - name: 'vc', - long_name: 'Verifiable Credential as specified in the W3C Recommendation', - ref: '[W3C Recommendation Verifiable Credentials Data Model 1.0 - Expressing verifiable information on the Web (19 November 2019) - Section 6.3.1]', - }, - { - name: 'vp', - long_name: 'Verifiable Presentation as specified in the W3C Recommendation', - ref: '[W3C Recommendation Verifiable Credentials Data Model 1.0 - Expressing verifiable information on the Web (19 November 2019) - Section 6.3.1]', - }, - { - name: 'sph', - long_name: 'SIP Priority header field', - ref: '[RFC9027]', - }, - { - name: 'ace_profile', - long_name: 'The ACE profile a token is supposed to be used with.', - ref: '[RFC-ietf-ace-oauth-authz-46 - Section 5.10]', - }, - { - name: 'cnonce', - long_name: - 'client-nonce. A nonce previously provided to the AS by the RS via the client. Used to verify token freshness when the RS cannot synchronize its clock with the AS.', - ref: '[RFC-ietf-ace-oauth-authz-46 - Section 5.10]', - }, - { - name: 'exi', - long_name: - 'Expires in. Lifetime of the token in seconds from the time the RS first sees it. Used to implement a weaker from of token expiration for devices that cannot synchronize their internal clocks.', - ref: '[RFC-ietf-ace-oauth-authz-46 - Section 5.10.3]', - }, - { - name: 'roles', - long_name: 'Roles', - ref: '[RFC7643 - Section 4.1.2][RFC9068 - Section 2.2.3.1]', - }, - { - name: 'groups', - long_name: 'Groups', - ref: '[RFC7643 - Section 4.1.2][RFC9068 - Section 2.2.3.1]', - }, - { - name: 'entitlements', - long_name: 'Entitlements', - ref: '[RFC7643 - Section 4.1.2][RFC9068 - Section 2.2.3.1]', - }, - { - name: 'token_introspection', - long_name: 'Token introspection response', - ref: '[RFC-ietf-oauth-jwt-introspection-response-12 - Section 5]', - }, -]; + const date = new Date(Number(value) * 1000); + return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; +}; diff --git a/src/tools/jwt-parser/jwt-parser.vue b/src/tools/jwt-parser/jwt-parser.vue index e0b5389..0e81feb 100644 --- a/src/tools/jwt-parser/jwt-parser.vue +++ b/src/tools/jwt-parser/jwt-parser.vue @@ -4,48 +4,59 @@ <n-input v-model:value="rawJwt" type="textarea" placeholder="Put your token here..." rows="5" /> </n-form-item> - <n-table> + <n-table v-if="validation.isValid"> <tbody> - <td colspan="2" class="table-header"><strong>Header</strong></td> - <tr v-for="[key, value] in Object.entries(decodedJWT.header)" :key="key"> - <td class="claims"><claim-vue :claim="key" /></td> - <td> - <value-vue :claim="key" :value="value" /> - </td> - </tr> - <td colspan="2" class="table-header"><strong>Payload</strong></td> - <tr v-for="[key, value] in Object.entries(decodedJWT.payload)" :key="key"> - <td class="claims"><claim-vue :claim="key" /></td> - <td> - <value-vue :claim="key" :value="value" /> - </td> - </tr> + <template v-for="section of sections" :key="section.key"> + <th colspan="2" class="table-header">{{ section.title }}</th> + <tr v-for="{ claim, claimDescription, friendlyValue, value } in decodedJWT[section.key]" :key="claim + value"> + <td class="claims"> + <n-space> + <n-text strong>{{ claim }}</n-text> + <template v-if="claimDescription"> + <n-text depth="3">({{ claimDescription }})</n-text> + </template> + </n-space> + </td> + <td> + <n-space> + <n-text>{{ value }}</n-text> + <template v-if="friendlyValue"> + <n-text depth="3">({{ friendlyValue }})</n-text> + </template> + </n-space> + </td> + </tr> + </template> </tbody> </n-table> </n-card> </template> <script setup lang="ts"> -import { computed, ref } from 'vue'; -import jwt_decode from 'jwt-decode'; import { useValidation } from '@/composable/validation'; import { isNotThrowing } from '@/utils/boolean'; -import { safeJwtDecode } from './jwt-parser.service'; -import claimVue from './claim.vue'; -import valueVue from './value.vue'; +import { withDefaultOnError } from '@/utils/defaults'; +import { computed, ref } from 'vue'; +import { decodeJwt } from './jwt-parser.service'; const rawJwt = ref( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', ); -const decodedJWT = computed(() => { - return safeJwtDecode(rawJwt.value); -}); +const decodedJWT = computed(() => + withDefaultOnError(() => decodeJwt({ jwt: rawJwt.value }), { header: [], payload: [] }), +); + +const sections = [ + { key: 'header', title: 'Header' }, + { key: 'payload', title: 'Payload' }, +] as const; + const validation = useValidation({ source: rawJwt, rules: [ { - validator: (value) => value.length > 0 && isNotThrowing(() => jwt_decode(value, { header: true })), + validator: (value) => value.length > 0 && isNotThrowing(() => decodeJwt({ jwt: rawJwt.value })), message: 'Invalid JWT', }, ], @@ -56,8 +67,4 @@ const validation = useValidation({ .table-header { text-align: center; } - -.claims { - width: 20%; -} </style> diff --git a/src/tools/jwt-parser/value.vue b/src/tools/jwt-parser/value.vue deleted file mode 100644 index 82c1042..0000000 --- a/src/tools/jwt-parser/value.vue +++ /dev/null @@ -1,24 +0,0 @@ -<template> - <n-space> - {{ value.value }} - <em v-if="value.extension">({{ value.extension }})</em> - </n-space> -</template> - -<script setup lang="ts"> -import { computed } from 'vue'; -import { parseClaimValue } from './jwt-parser.service'; - -const props = defineProps({ - claim: { - type: String, - default: '', - }, - value: { - type: String, - default: '', - }, -}); - -const value = computed(() => parseClaimValue(props.claim, props.value)); -</script> |