From acc7f0a586c64500c5f720e70cdbccf9bffe76d9 Mon Sep 17 00:00:00 2001 From: bastantoine Date: Tue, 27 Dec 2022 09:38:35 +0100 Subject: [PATCH] feat(new-tool): jwt parser (#262) * npm install jwt-decode * added base tool structure * added function to decode JWT and display header and payload * use a table to display the data * show human readable values * added switch to toggle display of parsed values * lint * replaced basic package-lock.json with pnpm-lock.json * change the icon of the tool * simplify return * use camelCase * added description of the tool * always parse the values * use camelCase... --- package.json | 1 + pnpm-lock.yaml | 6 + src/plugins/naive.plugin.ts | 2 + src/tools/index.ts | 2 + src/tools/jwt-parser/claim.vue | 29 ++ src/tools/jwt-parser/index.ts | 11 + src/tools/jwt-parser/jwt-parser.service.ts | 429 +++++++++++++++++++++ src/tools/jwt-parser/jwt-parser.vue | 63 +++ src/tools/jwt-parser/value.vue | 24 ++ 9 files changed, 567 insertions(+) create mode 100644 src/tools/jwt-parser/claim.vue create mode 100644 src/tools/jwt-parser/index.ts create mode 100644 src/tools/jwt-parser/jwt-parser.service.ts create mode 100644 src/tools/jwt-parser/jwt-parser.vue create mode 100644 src/tools/jwt-parser/value.vue diff --git a/package.json b/package.json index 551044b2..6c8aa1c3 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "fuse.js": "^6.6.2", "highlight.js": "^11.6.0", "json5": "^2.2.1", + "jwt-decode": "^3.1.2", "lodash": "^4.17.21", "mathjs": "^10.6.4", "mime-types": "^2.1.35", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7f2a228..5398bd43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,7 @@ specifiers: highlight.js: ^11.6.0 jsdom: ^19.0.0 json5: ^2.2.1 + jwt-decode: ^3.1.2 less: ^4.1.3 lodash: ^4.17.21 mathjs: ^10.6.4 @@ -85,6 +86,7 @@ dependencies: fuse.js: 6.6.2 highlight.js: 11.6.0 json5: 2.2.1 + jwt-decode: 3.1.2 lodash: 4.17.21 mathjs: 10.6.4 mime-types: 2.1.35 @@ -4838,6 +4840,10 @@ packages: promise: 7.3.1 dev: true + /jwt-decode/3.1.2: + resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} + dev: false + /kind-of/6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} diff --git a/src/plugins/naive.plugin.ts b/src/plugins/naive.plugin.ts index 2a9ac821..077abf97 100644 --- a/src/plugins/naive.plugin.ts +++ b/src/plugins/naive.plugin.ts @@ -53,6 +53,7 @@ import { NTooltip, NUpload, NUploadDragger, + NPopover, NCheckbox, } from 'naive-ui'; @@ -111,6 +112,7 @@ const components = [ NIcon, NSwitch, NCollapseTransition, + NPopover, ]; export const naive = create({ components }); diff --git a/src/tools/index.ts b/src/tools/index.ts index 3b94cb4c..69b81e3b 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,3 +1,4 @@ +import { tool as jwtParser } from './jwt-parser'; import { tool as chmodCalculator } from './chmod-calculator'; import { tool as mimeTypes } from './mime-types'; import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator'; @@ -63,6 +64,7 @@ export const toolsByCategory: ToolCategory[] = [ metaTagGenerator, otpCodeGeneratorAndValidator, mimeTypes, + jwtParser, ], }, { diff --git a/src/tools/jwt-parser/claim.vue b/src/tools/jwt-parser/claim.vue new file mode 100644 index 00000000..3f298a2b --- /dev/null +++ b/src/tools/jwt-parser/claim.vue @@ -0,0 +1,29 @@ + + + diff --git a/src/tools/jwt-parser/index.ts b/src/tools/jwt-parser/index.ts new file mode 100644 index 00000000..dcce4f1f --- /dev/null +++ b/src/tools/jwt-parser/index.ts @@ -0,0 +1,11 @@ +import { Key } from '@vicons/tabler'; +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'], + component: () => import('./jwt-parser.vue'), + icon: Key, +}); diff --git a/src/tools/jwt-parser/jwt-parser.service.ts b/src/tools/jwt-parser/jwt-parser.service.ts new file mode 100644 index 00000000..37b6eccf --- /dev/null +++ b/src/tools/jwt-parser/jwt-parser.service.ts @@ -0,0 +1,429 @@ +import jwt_decode, { InvalidTokenError } from 'jwt-decode'; + +interface JWT { + header: Map; + payload: Map; +} + +export function safeJwtDecode(rawJwt: string): JWT { + try { + const header = jwt_decode(rawJwt, { header: true }) as Map; + const payload = jwt_decode(rawJwt) as Map; + return { header, payload }; + } catch (e) { + if (e instanceof InvalidTokenError) { + return { header: new Map(), payload: new Map() }; + } else { + throw e; + } + } +} + +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: '' }; +} + +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 }; + } +} + +// 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', +}; + +// 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]', + }, +]; diff --git a/src/tools/jwt-parser/jwt-parser.vue b/src/tools/jwt-parser/jwt-parser.vue new file mode 100644 index 00000000..e0b53896 --- /dev/null +++ b/src/tools/jwt-parser/jwt-parser.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/src/tools/jwt-parser/value.vue b/src/tools/jwt-parser/value.vue new file mode 100644 index 00000000..82c10426 --- /dev/null +++ b/src/tools/jwt-parser/value.vue @@ -0,0 +1,24 @@ + + +