|
| 1 | +/** |
| 2 | + * Ported to Typescript from original implementation below: |
| 3 | + * https://github.com/hokaccha/node-jwt-simple -- MIT licensed |
| 4 | + */ |
| 5 | + |
| 6 | +/** |
| 7 | + * module dependencies |
| 8 | + */ |
| 9 | +import { bytesToUtf8, utf8ToBytes } from '@ethereumjs/util' |
| 10 | +import { base64url } from '@scure/base' |
| 11 | +import crypto from 'crypto' |
| 12 | + |
| 13 | +/** |
| 14 | + * support algorithm mapping |
| 15 | + */ |
| 16 | +export type TAlgorithm = 'HS256' | 'HS384' | 'HS512' | 'RS256' |
| 17 | +const algorithmMap: Record<string, string> = { |
| 18 | + HS256: 'sha256', |
| 19 | + HS384: 'sha384', |
| 20 | + HS512: 'sha512', |
| 21 | + RS256: 'RSA-SHA256', |
| 22 | +} |
| 23 | + |
| 24 | +/** |
| 25 | + * Map algorithm to hmac or sign type, to determine which crypto function to use |
| 26 | + */ |
| 27 | +const typeMap: Record<string, string> = { |
| 28 | + HS256: 'hmac', |
| 29 | + HS384: 'hmac', |
| 30 | + HS512: 'hmac', |
| 31 | + RS256: 'sign', |
| 32 | +} |
| 33 | + |
| 34 | +/** |
| 35 | + * expose object |
| 36 | + */ |
| 37 | + |
| 38 | +/** |
| 39 | + * private util functions |
| 40 | + */ |
| 41 | + |
| 42 | +function assignProperties(dest: any, source: any) { |
| 43 | + for (const [attr] of Object.entries(source)) { |
| 44 | + if (Object.prototype.hasOwnProperty.call(source, attr)) { |
| 45 | + dest[attr] = source[attr] |
| 46 | + } |
| 47 | + } |
| 48 | +} |
| 49 | + |
| 50 | +function assertAlgorithm(alg: any): asserts alg is Algorithm { |
| 51 | + if (!['HS256', 'HS384', 'HS512', 'RS256'].includes(alg)) { |
| 52 | + throw new Error('Algorithm not supported') |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +function base64urlUnescape(str: string) { |
| 57 | + str += new Array(5 - (str.length % 4)).join('=') |
| 58 | + return str.replace(/-/g, '+').replace(/_/g, '/') |
| 59 | +} |
| 60 | + |
| 61 | +function base64urlEscape(str: string) { |
| 62 | + return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') |
| 63 | +} |
| 64 | + |
| 65 | +function sign(input: any, key: string, method: string, type: string) { |
| 66 | + let base64str |
| 67 | + if (type === 'hmac') { |
| 68 | + base64str = crypto.createHmac(method, key).update(input).digest('base64') |
| 69 | + } else if (type === 'sign') { |
| 70 | + base64str = crypto.createSign(method).update(input).sign(key, 'base64') |
| 71 | + } else { |
| 72 | + throw new Error('Algorithm type not recognized') |
| 73 | + } |
| 74 | + |
| 75 | + return base64urlEscape(base64str) |
| 76 | +} |
| 77 | + |
| 78 | +function verify(input: any, key: string, method: string, type: string, signature: string) { |
| 79 | + if (type === 'hmac') { |
| 80 | + return signature === sign(input, key, method, type) |
| 81 | + } else if (type === 'sign') { |
| 82 | + return crypto |
| 83 | + .createVerify(method) |
| 84 | + .update(input) |
| 85 | + .verify(key, base64urlUnescape(signature), 'base64') |
| 86 | + } else { |
| 87 | + throw new Error('Algorithm type not recognized') |
| 88 | + } |
| 89 | +} |
| 90 | + |
| 91 | +/** |
| 92 | + * Decode jwt |
| 93 | + * |
| 94 | + * @param {Object} token |
| 95 | + * @param {String} key |
| 96 | + * @param {Boolean} [noVerify] |
| 97 | + * @param {String} [algorithm] |
| 98 | + * @return {Object} payload |
| 99 | + * @api public |
| 100 | + */ |
| 101 | +const decode = function jwt_decode( |
| 102 | + token: string, |
| 103 | + key: string, |
| 104 | + noVerify: boolean = false, |
| 105 | + algorithm: string = '' |
| 106 | +) { |
| 107 | + // check token |
| 108 | + if (!token) { |
| 109 | + throw new Error('No token supplied') |
| 110 | + } |
| 111 | + // check segments |
| 112 | + const segments = token.split('.') |
| 113 | + if (segments.length !== 3) { |
| 114 | + throw new Error('Not enough or too many segments') |
| 115 | + } |
| 116 | + |
| 117 | + // All segment should be base64 |
| 118 | + const headerSeg = segments[0] |
| 119 | + const payloadSeg = segments[1] |
| 120 | + const signatureSeg = segments[2] |
| 121 | + |
| 122 | + // base64 decode and parse JSON |
| 123 | + const header = JSON.parse(bytesToUtf8(base64url.decode(headerSeg))) |
| 124 | + const payload = JSON.parse(bytesToUtf8(base64url.decode(payloadSeg))) |
| 125 | + |
| 126 | + if (!noVerify) { |
| 127 | + if (!algorithm && /BEGIN( RSA)? PUBLIC KEY/.test(key.toString())) { |
| 128 | + algorithm = 'RS256' |
| 129 | + } |
| 130 | + |
| 131 | + algorithm = algorithm || header.alg |
| 132 | + |
| 133 | + assertAlgorithm(algorithm) |
| 134 | + const signingMethod = algorithmMap[algorithm] |
| 135 | + const signingType = typeMap[algorithm] |
| 136 | + |
| 137 | + // verify signature. `sign` will return base64 string. |
| 138 | + const signingInput = [headerSeg, payloadSeg].join('.') |
| 139 | + if (verify(signingInput, key, signingMethod, signingType, signatureSeg) === false) { |
| 140 | + throw new Error('Signature verification failed') |
| 141 | + } |
| 142 | + |
| 143 | + // Support for nbf and exp claims. |
| 144 | + // According to the RFC, they should be in seconds. |
| 145 | + if (payload.nbf !== undefined && Date.now() < payload.nbf * 1000) { |
| 146 | + throw new Error('Token not yet active') |
| 147 | + } |
| 148 | + |
| 149 | + if (payload.exp !== undefined && Date.now() > payload.exp * 1000) { |
| 150 | + throw new Error('Token expired') |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + return payload |
| 155 | +} |
| 156 | + |
| 157 | +/** |
| 158 | + * Encode jwt |
| 159 | + * |
| 160 | + * @param {Object} payload |
| 161 | + * @param {String} key |
| 162 | + * @param {String} algorithm |
| 163 | + * @param {Object} options |
| 164 | + * @return {String} token |
| 165 | + * @api public |
| 166 | + */ |
| 167 | +const encode = function jwt_encode( |
| 168 | + payload: any, |
| 169 | + key: string, |
| 170 | + algorithm: string = '', |
| 171 | + options: any = undefined |
| 172 | +) { |
| 173 | + // Check key |
| 174 | + if (!key) { |
| 175 | + throw new Error('Require key') |
| 176 | + } |
| 177 | + |
| 178 | + // Check algorithm, default is HS256 |
| 179 | + if (!algorithm) { |
| 180 | + algorithm = 'HS256' |
| 181 | + } |
| 182 | + |
| 183 | + assertAlgorithm(algorithm) |
| 184 | + const signingMethod = algorithmMap[algorithm] |
| 185 | + const signingType = typeMap[algorithm] |
| 186 | + |
| 187 | + // header, typ is fixed value. |
| 188 | + const header = { typ: 'JWT', alg: algorithm } |
| 189 | + if (options !== undefined && options.header !== undefined) { |
| 190 | + assignProperties(header, options.header) |
| 191 | + } |
| 192 | + |
| 193 | + // create segments, all segments should be base64 string |
| 194 | + const segments = [] |
| 195 | + segments.push(base64url.encode(utf8ToBytes(JSON.stringify(header)))) |
| 196 | + segments.push(base64url.encode(utf8ToBytes(JSON.stringify(payload)))) |
| 197 | + segments.push(sign(segments.join('.'), key, signingMethod, signingType)) |
| 198 | + |
| 199 | + return segments.join('.') |
| 200 | +} |
| 201 | + |
| 202 | +export const jwt = { encode, decode } |
0 commit comments