aboutsummaryrefslogtreecommitdiff
path: root/src/tools
diff options
context:
space:
mode:
authorGravatar Corentin Thomasset <corentin.thomasset74@gmail.com> 2023-01-13 13:59:27 +0100
committerGravatar Corentin Thomasset <corentin.thomasset74@gmail.com> 2023-01-13 14:02:44 +0100
commitf52f7a845c34ce7da57b11c17d261733be89554f (patch)
tree6fd78f5d5bfd6bebe4a26367381baef45c7d7a22 /src/tools
parentacc7f0a586c64500c5f720e70cdbccf9bffe76d9 (diff)
downloadit-tools-f52f7a845c34ce7da57b11c17d261733be89554f.tar.gz
it-tools-f52f7a845c34ce7da57b11c17d261733be89554f.tar.zst
it-tools-f52f7a845c34ce7da57b11c17d261733be89554f.zip
refactor(jwt-parser): simplified code
Diffstat (limited to 'src/tools')
-rw-r--r--src/tools/jwt-parser/claim.vue29
-rw-r--r--src/tools/jwt-parser/index.ts20
-rw-r--r--src/tools/jwt-parser/jwt-parser.constants.ts92
-rw-r--r--src/tools/jwt-parser/jwt-parser.service.ts457
-rw-r--r--src/tools/jwt-parser/jwt-parser.vue63
-rw-r--r--src/tools/jwt-parser/value.vue24
6 files changed, 182 insertions, 503 deletions
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>