Skip to content

Commit 47917dc

Browse files
committed
feat(new tool): JWT Generator + JWT Signature verification
Fix CorentinTh#1309
1 parent a8a2a0d commit 47917dc

File tree

10 files changed

+307
-9
lines changed

10 files changed

+307
-9
lines changed

components.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ declare module '@vue/runtime-core' {
244244
'NanoMemo.content': typeof import('./src/tools/nano-memo/nano-memo.content.md')['default']
245245
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
246246
NCode: typeof import('naive-ui')['NCode']
247+
NCode: typeof import('naive-ui')['NCode']
247248
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
248249
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
249250
NDivider: typeof import('naive-ui')['NDivider']
@@ -253,6 +254,7 @@ declare module '@vue/runtime-core' {
253254
NH3: typeof import('naive-ui')['NH3']
254255
NIcon: typeof import('naive-ui')['NIcon']
255256
NInputNumber: typeof import('naive-ui')['NInputNumber']
257+
NInputNumber: typeof import('naive-ui')['NInputNumber']
256258
NLayout: typeof import('naive-ui')['NLayout']
257259
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
258260
NMenu: typeof import('naive-ui')['NMenu']

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
129129
"highlight.js": "^11.7.0",
130130
"iarna-toml-esm": "^3.0.5",
131131
"ibantools": "^4.3.3",
132+
"jose": "^5.9.6",
132133
"ical-generator": "^8.0.1",
133134
"ical.js": "^2.1.0",
134135
"iconv-lite": "^0.6.3",

pnpm-lock.yaml

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { tool as angleConverter } from './angle-converter';
2525
import { tool as floatingPointNumberConverter } from './floating-point-number-converter';
2626
import { tool as snowflakeIdExtractor } from './snowflake-id-extractor';
2727
import { tool as emailNormalizer } from './email-normalizer';
28+
import { tool as jwtGenerator } from './jwt-generator';
2829
import { tool as gptTokenEstimator } from './gpt-token-estimator';
2930
import { tool as myIp } from './my-ip';
3031
import { tool as geoDistanceCalculator } from './geo-distance-calculator';
@@ -361,6 +362,7 @@ export const toolsByCategory: ToolCategory[] = [
361362
otpCodeGeneratorAndValidator,
362363
mimeTypes,
363364
jwtParser,
365+
jwtGenerator,
364366
keycodeInfo,
365367
slugifyString,
366368
htmlWysiwygEditor,

src/tools/jwt-generator/index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Key } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'JWT Generator',
6+
path: '/jwt-generator',
7+
description: 'JWT Token generator and editor',
8+
keywords: [
9+
'jwt',
10+
'generator',
11+
'editor',
12+
'encode',
13+
'typ',
14+
'alg',
15+
'iss',
16+
'sub',
17+
'aud',
18+
'exp',
19+
'nbf',
20+
'iat',
21+
'jti',
22+
'json',
23+
'web',
24+
'token',
25+
],
26+
component: () => import('./jwt-generator.vue'),
27+
icon: Key,
28+
createdAt: new Date('2024-08-15'),
29+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const jwsAlgorithms = [
2+
{ alg: 'HS256', keyDesc: '256 bit (32 byte) secret', key: 'secret', verify: 'HMAC-SHA256' },
3+
{ alg: 'HS384', keyDesc: '384 bit (48 byte) secret', key: 'secret', verify: 'HMAC-SHA384' },
4+
{ alg: 'HS512', keyDesc: '512 bit (64 byte) secret', key: 'secret', verify: 'HMAC-SHA512' },
5+
{ alg: 'ES256', keyDesc: 'NIST P-256 elliptic curve key', key: 'keyspair', verify: 'ECDSA-SHA256' },
6+
{ alg: 'ES256K', keyDesc: 'secp256k1 elliptic curve key', key: 'keyspair', verify: 'ECDSA-SHA256(secp256k1)' },
7+
{ alg: 'ES384', keyDesc: 'NIST P-384 elliptic curve key', key: 'keyspair', verify: 'ECDSA-SHA384' },
8+
{ alg: 'ES512', keyDesc: 'NIST P-521 elliptic curve key', key: 'keyspair', verify: 'ECDSA-SHA512' },
9+
{ alg: 'PS256', keyDesc: 'RSA key of at least 2048 bit modulus length', key: 'keyspair', verify: 'RSA-PSS-SHA256' },
10+
{ alg: 'PS384', keyDesc: 'RSA key of at least 2048 bit modulus length', key: 'keyspair', verify: 'RSA-PSS-SHA384' },
11+
{ alg: 'PS512', keyDesc: 'RSA key of at least 2048 bit modulus length', key: 'keyspair', verify: 'RSA-PSS-SHA512' },
12+
{ alg: 'RS256', keyDesc: 'RSA key of at least 2048 bit modulus length', key: 'keyspair', verify: 'RSA-SHA256' },
13+
{ alg: 'RS384', keyDesc: 'RSA key of at least 2048 bit modulus length', key: 'keyspair', verify: 'RSA-SHA384' },
14+
{ alg: 'RS512', keyDesc: 'RSA key of at least 2048 bit modulus length', key: 'keyspair', verify: 'RSA-SHA512' },
15+
];
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<script setup lang="ts">
2+
import * as jose from 'jose';
3+
import { type KeyLike } from 'jose';
4+
import JSON5 from 'json5';
5+
import { jwsAlgorithms } from './jwt-generator.constants';
6+
import { useValidation } from '@/composable/validation';
7+
import { useQueryParamOrStorage } from '@/composable/queryParams';
8+
9+
const payload = ref(`{
10+
"sub": "1234567890",
11+
"name": "John Doe",
12+
"iat": 1516239022
13+
}`);
14+
15+
const alg = useQueryParamOrStorage({ name: 'alg', storageName: 'jwt-gen:alg', defaultValue: 'HS512' });
16+
const algInfo = computed(() => jwsAlgorithms.find(a => a.alg === alg.value) || { alg: 'UNK', keyDesc: '', key: 'secret', verify: '' });
17+
const isSecret = computed(() => algInfo.value.key === 'secret');
18+
19+
const secret = ref('');
20+
21+
const publicKeyPEM = ref('');
22+
const publicKeyJWK = ref('');
23+
const privateKeyJWK = ref('');
24+
const privateKeyPEM = ref('');
25+
26+
watch(publicKeyPEM, async (newValue) => {
27+
try {
28+
publicKeyJWK.value = JSON.stringify(await jose.exportJWK(await jose.importSPKI(newValue, alg.value, { extractable: true })), null, 2);
29+
}
30+
catch {
31+
}
32+
}, { immediate: true });
33+
watch(privateKeyPEM, async (newValue) => {
34+
try {
35+
privateKeyJWK.value = JSON.stringify(await jose.exportJWK(await jose.importPKCS8(newValue, alg.value, { extractable: true })), null, 2);
36+
}
37+
catch {
38+
}
39+
}, { immediate: true });
40+
watch(publicKeyJWK, async (newValue) => {
41+
try {
42+
publicKeyPEM.value = await jose.exportSPKI(await jose.importJWK({ ...JSON.parse(newValue), ...{ ext: true } }, alg.value) as KeyLike);
43+
}
44+
catch {
45+
}
46+
}, { immediate: true });
47+
watch(privateKeyJWK, async (newValue) => {
48+
try {
49+
privateKeyPEM.value = await jose.exportPKCS8(await jose.importJWK({ ...JSON.parse(newValue), ...{ ext: true } }, alg.value) as KeyLike);
50+
}
51+
catch {
52+
}
53+
}, { immediate: true });
54+
55+
watchEffect(async () => {
56+
if (isSecret.value) {
57+
secret.value = 'your-secret';
58+
privateKeyJWK.value = '';
59+
publicKeyJWK.value = '';
60+
privateKeyPEM.value = '';
61+
publicKeyPEM.value = '';
62+
}
63+
else {
64+
const { publicKey, privateKey } = await jose.generateKeyPair(alg.value, { extractable: true });
65+
privateKeyJWK.value = JSON.stringify(await jose.exportJWK(privateKey), null, 2);
66+
publicKeyJWK.value = JSON.stringify(await jose.exportJWK(publicKey), null, 2);
67+
privateKeyPEM.value = await jose.exportPKCS8(privateKey);
68+
publicKeyPEM.value = await jose.exportSPKI(publicKey);
69+
}
70+
});
71+
72+
const header = computed(() => JSON.stringify({ alg: alg.value, typ: 'JWT' }, null, 2));
73+
const encodedJWT = computedAsync(async () => {
74+
const isSecretValue = isSecret.value;
75+
const secretValue = secret.value;
76+
const privateKeyValue = privateKeyJWK.value;
77+
const algValue = alg.value;
78+
const payloadValue = payload.value;
79+
try {
80+
let privateKeyOrSecret;
81+
if (isSecretValue) {
82+
privateKeyOrSecret = new TextEncoder().encode(secretValue);
83+
}
84+
else {
85+
privateKeyOrSecret = await jose.importJWK(JSON.parse(privateKeyValue), algValue);
86+
}
87+
return {
88+
token: await (new jose.SignJWT(JSON5.parse(payloadValue))
89+
.setProtectedHeader({ alg: algValue })
90+
.sign(privateKeyOrSecret)),
91+
error: '',
92+
};
93+
}
94+
catch (e: any) {
95+
return { error: e.toString(), token: '' };
96+
}
97+
});
98+
99+
const jsonInputValidation = useValidation({
100+
source: payload,
101+
rules: [
102+
{
103+
message: 'Invalid JSON string',
104+
validator: value => JSON5.parse(value),
105+
},
106+
],
107+
});
108+
</script>
109+
110+
<template>
111+
<div>
112+
<c-select
113+
v-model:value="alg"
114+
:options="jwsAlgorithms.map(a => ({ value: a.alg, label: `${a.alg}: ${a.verify}` }))"
115+
placeholder="Algorithms"
116+
mb-2
117+
/>
118+
<n-form-item label="Description:" label-placement="left" mb-2>
119+
{{ algInfo.alg }}: {{ algInfo.keyDesc }} (verify with {{ algInfo.verify }})
120+
</n-form-item>
121+
122+
<c-card title="Token Content" mb-2>
123+
<n-form-item label="Header:">
124+
<textarea-copyable :value="header" language="json" />
125+
</n-form-item>
126+
127+
<c-input-text
128+
v-model:value="payload"
129+
label="Payload:"
130+
multiline
131+
rows="5"
132+
autosize
133+
placeholder="JSON payload"
134+
:validation="jsonInputValidation"
135+
/>
136+
</c-card>
137+
138+
<c-card :title="isSecret ? 'Token Secret' : 'Token Keys'" mb-2>
139+
<c-input-text v-if="isSecret" v-model:value="secret" />
140+
141+
<div v-if="!isSecret">
142+
<n-form-item label="Public Key (PEM):">
143+
<c-input-text
144+
v-model:value="publicKeyPEM"
145+
multiline
146+
rows="5"
147+
autosize
148+
/>
149+
</n-form-item>
150+
<n-form-item label="Private Key (PEM):">
151+
<c-input-text
152+
v-model:value="privateKeyPEM"
153+
multiline
154+
rows="5"
155+
autosize
156+
/>
157+
</n-form-item>
158+
159+
<n-divider />
160+
161+
<n-form-item label="Public Key (JWK):">
162+
<c-input-text
163+
v-model:value="publicKeyJWK"
164+
multiline
165+
rows="5"
166+
autosize
167+
/>
168+
</n-form-item>
169+
<n-form-item label="Private Key (JWK):">
170+
<c-input-text
171+
v-model:value="privateKeyJWK"
172+
multiline
173+
rows="5"
174+
autosize
175+
/>
176+
</n-form-item>
177+
</div>
178+
</c-card>
179+
180+
<c-card v-if="encodedJWT" title="Generated JWT Token:" mb-2>
181+
<textarea-copyable v-if="encodedJWT.token" :value="encodedJWT.token" word-wrap />
182+
<c-alert v-if="encodedJWT.error">
183+
{{ encodedJWT.error }}
184+
</c-alert>
185+
</c-card>
186+
</div>
187+
</template>

src/tools/jwt-parser/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { Key } from '@vicons/tabler';
22
import { defineTool } from '../tool';
3-
import { translate } from '@/plugins/i18n.plugin';
43

54
export const tool = defineTool({
6-
name: translate('tools.jwt-parser.title'),
5+
name: 'JWT parser',
76
path: '/jwt-parser',
8-
description: translate('tools.jwt-parser.description'),
7+
description: 'Parse and decode your JSON Web Token (jwt) and display its content.',
98
keywords: [
109
'jwt',
1110
'parser',

src/tools/jwt-parser/jwt-parser.service.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import jwtDecode, { type JwtHeader, type JwtPayload } from 'jwt-decode';
22
import _ from 'lodash';
33
import { ALGORITHM_DESCRIPTIONS, CLAIM_DESCRIPTIONS } from './jwt-parser.constants';
44

5-
export { decodeJwt };
5+
export { decodeJwt, getJwtAlgorithm };
6+
7+
function getJwtAlgorithm({ jwt }: { jwt: string }) {
8+
return jwtDecode<JwtHeader>(jwt, { header: true }).alg;
9+
}
610

711
function decodeJwt({ jwt }: { jwt: string }) {
812
const rawHeader = jwtDecode<JwtHeader>(jwt, { header: true });
@@ -19,7 +23,7 @@ function decodeJwt({ jwt }: { jwt: string }) {
1923

2024
function parseClaims({ claim, value }: { claim: string; value: unknown }) {
2125
const claimDescription = CLAIM_DESCRIPTIONS[claim];
22-
const formattedValue = _.isPlainObject(value) || _.isArray(value) ? JSON.stringify(value, null, 3) : _.toString(value);
26+
const formattedValue = _.isPlainObject(value) ? JSON.stringify(value, null, 3) : _.toString(value);
2327
const friendlyValue = getFriendlyValue({ claim, value });
2428

2529
return {

0 commit comments

Comments
 (0)