1
- import jwt from "jsonwebtoken " ;
1
+ import crypto from "node:crypto " ;
2
2
3
3
import * as config from "#src/config.js" ;
4
4
import { Logger } from "#src/utils/utils.js" ;
5
5
import { AuthenticationError } from "#src/utils/errors.js" ;
6
6
7
7
let jwtKey ;
8
8
const logger = new Logger ( "AUTH" ) ;
9
+ const ALGORITHM = {
10
+ HS256 : "HS256" ,
11
+ } ;
12
+ const ALGORITHM_FUNCTIONS = {
13
+ [ ALGORITHM . HS256 ] : ( data , key ) => crypto . createHmac ( "sha256" , key ) . update ( data ) . digest ( ) ,
14
+ } ;
9
15
10
16
/**
11
17
* @param {WithImplicitCoercion<string> } [key] buffer/b64 str
12
18
*/
13
- export async function start ( key ) {
19
+ export function start ( key ) {
14
20
const keyB64str = key || config . AUTH_KEY ;
15
21
jwtKey = Buffer . from ( keyB64str , "base64" ) ;
16
22
logger . info ( `auth key set` ) ;
@@ -20,18 +26,119 @@ export function close() {
20
26
jwtKey = undefined ;
21
27
}
22
28
29
+ /**
30
+ * @param {Buffer|string } data - The data to encode
31
+ * @returns {string } - base64 encoded string
32
+ */
33
+ function base64Encode ( data ) {
34
+ if ( typeof data === "string" ) {
35
+ data = Buffer . from ( data ) ;
36
+ }
37
+ return data . toString ( "base64" ) ;
38
+ }
39
+
40
+ /**
41
+ * @param {string } str base64 encoded string
42
+ * @returns {Buffer }
43
+ */
44
+ function base64Decode ( str ) {
45
+ let output = str ;
46
+ const paddingLength = 4 - ( output . length % 4 ) ;
47
+ if ( paddingLength < 4 ) {
48
+ output += "=" . repeat ( paddingLength ) ;
49
+ }
50
+ return Buffer . from ( output , "base64" ) ;
51
+ }
52
+
53
+ /**
54
+ * Signs and creates a JWT token
55
+ *
56
+ * @param {Object } payload - The payload to include in the token
57
+ * @param {WithImplicitCoercion<string> } [key] - Optional key, defaults to the configured jwtKey
58
+ * @param {Object } [options]
59
+ * @param {string } [options.algorithm] - The algorithm to use, defaults to HS256
60
+ * @returns {string } - The signed JWT token
61
+ * @throws {AuthenticationError }
62
+ */
63
+ export function sign ( payload , key = jwtKey , { algorithm = ALGORITHM . HS256 } = { } ) {
64
+ if ( ! key ) {
65
+ throw new AuthenticationError ( "JWT signing key is not set" ) ;
66
+ }
67
+ const keyBuffer = Buffer . isBuffer ( key ) ? key : Buffer . from ( key , "base64" ) ;
68
+ const headerB64 = base64Encode ( JSON . stringify ( { alg : algorithm , typ : "JWT" } ) ) ;
69
+ const payloadB64 = base64Encode ( JSON . stringify ( payload ) ) ;
70
+ const signedData = `${ headerB64 } .${ payloadB64 } ` ;
71
+ const signature = ALGORITHM_FUNCTIONS [ algorithm ] ?. ( signedData , keyBuffer ) ;
72
+ if ( ! signature ) {
73
+ throw new AuthenticationError ( "Unsupported algorithm" ) ;
74
+ }
75
+ const signatureB64 = base64Encode ( signature ) ;
76
+ return `${ headerB64 } .${ payloadB64 } .${ signatureB64 } ` ;
77
+ }
78
+
79
+ /**
80
+ * Parses a JWT token into its components
81
+ *
82
+ * @param {string } token
83
+ * @returns {{header: object, payload: object, signature: Buffer, signedData: string} }
84
+ */
85
+ function parseJwt ( token ) {
86
+ const parts = token . split ( "." ) ;
87
+ if ( parts . length !== 3 ) {
88
+ throw new AuthenticationError ( "Invalid JWT format" ) ;
89
+ }
90
+ const [ headerB64 , payloadB64 , signatureB64 ] = parts ;
91
+ const header = JSON . parse ( base64Decode ( headerB64 ) . toString ( ) ) ;
92
+ const payload = JSON . parse ( base64Decode ( payloadB64 ) . toString ( ) ) ;
93
+ const signature = base64Decode ( signatureB64 ) ;
94
+ const signedData = `${ headerB64 } .${ payloadB64 } ` ;
95
+
96
+ return { header, payload, signature, signedData } ;
97
+ }
98
+
99
+ function safeEqual ( a , b ) {
100
+ if ( a . length !== b . length ) {
101
+ return false ;
102
+ }
103
+ try {
104
+ return crypto . timingSafeEqual ( a , b ) ;
105
+ } catch {
106
+ return false ;
107
+ }
108
+ }
109
+
23
110
/**
24
111
* @param {string } jsonWebToken
25
112
* @param {WithImplicitCoercion<string> } [key] buffer/b64 str
26
- * @returns {Promise< any> } json serialized data
113
+ * @returns {any } json serialized data
27
114
* @throws {AuthenticationError }
28
115
*/
29
- export async function verify ( jsonWebToken , key = jwtKey ) {
116
+ export function verify ( jsonWebToken , key = jwtKey ) {
117
+ const keyBuffer = Buffer . isBuffer ( key ) ? key : Buffer . from ( key , "base64" ) ;
118
+ let parsedJWT ;
30
119
try {
31
- return jwt . verify ( jsonWebToken , key , {
32
- algorithms : [ "HS256" ] ,
33
- } ) ;
120
+ parsedJWT = parseJwt ( jsonWebToken ) ;
34
121
} catch {
35
- throw new AuthenticationError ( "JsonWebToken verification error" ) ;
122
+ throw new AuthenticationError ( "Invalid JWT format" ) ;
123
+ }
124
+ const { header, payload, signature, signedData } = parsedJWT ;
125
+ const expectedSignature = ALGORITHM_FUNCTIONS [ header . alg ] ?. ( signedData , keyBuffer ) ;
126
+ if ( ! expectedSignature ) {
127
+ throw new AuthenticationError ( "Unsupported algorithm" ) ;
128
+ }
129
+ if ( ! safeEqual ( signature , expectedSignature ) ) {
130
+ throw new AuthenticationError ( "Invalid signature" ) ;
131
+ }
132
+ // `exp`, `iat` and `nbf` are in seconds (`NumericDate` per RFC7519)
133
+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
134
+ if ( payload . exp && payload . exp < now ) {
135
+ throw new AuthenticationError ( "Token expired" ) ;
136
+ }
137
+ if ( payload . nbf && payload . nbf > now ) {
138
+ throw new AuthenticationError ( "Token not valid yet" ) ;
139
+ }
140
+ if ( payload . iat && payload . iat > now + 60 ) {
141
+ throw new AuthenticationError ( "Token issued in the future" ) ;
36
142
}
143
+ return payload ;
37
144
}
0 commit comments