summary refs log tree commit diff
path: root/src/util/jwtUtils.js
blob: ad97666ac2b2fb9a46444f59a3d753692980f7f8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import { existsSync } from 'fs';
import { readFile, writeFile } from 'node:fs/promises';
import {
    generateKeyPairSync,
    createHash,
    createPublicKey,
    createPrivateKey
} from 'node:crypto';
import jwt from 'jsonwebtoken';

let privateKey, publicKey, fingerprint;

/**
 *
 * @returns {Promise<JwtData>}
 */
export async function initJwt() {
    const secretPath = process.env.JWT_SECRET_PATH;
    if (!secretPath || !existsSync(secretPath)) {
        throw new Error(
            'JWT secret path is not defined in environment variables, or the directory does not exist.'
        );
    }

    console.log(`[JWT] Initializing JWT with secret path: ${secretPath}`);

    const privateKeyPath = `${secretPath}/jwt.key`;
    const publicKeyPath = `${secretPath}/jwt.key.pub`;

    if (!existsSync(privateKeyPath)) {
        console.log('[JWT] Generating new keypair');
        const keyPair = generateKeyPairSync('ec', {
            namedCurve: 'secp521r1'
        });

        privateKey = keyPair.privateKey;
        publicKey = keyPair.publicKey;

        await Promise.all([
            writeFile(
                privateKeyPath,
                privateKey.export({ format: 'pem', type: 'sec1' })
            ),
            writeFile(
                publicKeyPath,
                publicKey.export({ format: 'pem', type: 'spki' })
            )
        ]);
        console.log('[JWT] Keypair generated successfully.');
    } else {
        console.log('[JWT] Using existing keypair');
        const loadedPrivateKey = await readFile(privateKeyPath, 'utf8');
        const loadedPublicKey = await readFile(publicKeyPath, 'utf8');

        privateKey = createPrivateKey(loadedPrivateKey);
        publicKey = createPublicKey(loadedPublicKey);
    }

    fingerprint = createHash('sha256')
        .update(publicKey.export({ format: 'pem', type: 'spki' }))
        .digest('hex');
}

/**
 * @type {import('jsonwebtoken').JwtOptions}
 */
const jwtOptions = {
    algorithm: 'ES512'
};

/**
 *
 * @param data {JwtData}
 * @returns {Promise<unknown>}
 */
export async function generateJwtToken(data) {
    if (!privateKey) {
        throw new Error(
            'JWT private key is not initialized. Please call initJwt() first.'
        );
    }

    return new Promise((resolve, reject) => {
        jwt.sign(data, privateKey, jwtOptions, (err, token) => {
            if (err) {
                console.error('[JWT] Error generating token:', err);
                return reject(err);
            }
            resolve(token);
        });
    });
}

export async function validateJwtToken(token) {
    if (!publicKey) {
        throw new Error(
            'JWT public key is not initialized. Please call initJwt() first.'
        );
    }

    return new Promise((resolve, reject) => {
        jwt.verify(token, publicKey, jwtOptions, (err, decoded) => {
            if (err) {
                console.error('[JWT] Token validation failed:', err);
                return reject(err);
            }
            resolve(decoded);
        });
    });
}

export class JwtData {
    sub;
    type;
    iat = Math.floor(Date.now() / 1000);
}