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 @@
+
+
+ {{ claim }}
+
+
+
+
+
+ {{ label.label }}
+ {{ label.ref }}
+
+
+
+
+
+
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 @@
+
+
+ {{ value.value }}
+ ({{ value.extension }})
+
+
+
+