1
0

optimizations

This commit is contained in:
2022-06-26 17:02:55 +02:00
parent 210b733591
commit 81dde823d3
3 changed files with 178 additions and 200 deletions

View File

@@ -24,9 +24,9 @@ npm i -D @tsndr/cloudflare-worker-jwt
### Basic Example ### Basic Example
```javascript ```typescript
async () => { async () => {
const jwt = require('@tsndr/cloudflare-worker-jwt') import jwt from '@tsndr/cloudflare-worker-jwt'
// Creating a token // Creating a token
const token = await jwt.sign({ name: 'John Doe', email: 'john.doe@gmail.com' }, 'secret') const token = await jwt.sign({ name: 'John Doe', email: 'john.doe@gmail.com' }, 'secret')
@@ -45,9 +45,9 @@ async () => {
### Restrict Timeframe ### Restrict Timeframe
```javascript ```typescript
async () => { async () => {
const jwt = require('@tsndr/cloudflare-worker-jwt') import jwt from '@tsndr/cloudflare-worker-jwt'
// Creating a token // Creating a token
const token = await jwt.sign({ const token = await jwt.sign({

View File

@@ -3,14 +3,25 @@ Object.defineProperty(global, 'crypto', {
value: { subtle } value: { subtle }
}) })
const jwt = require('./index.js').default const jwt = require('./index.js')
const oneDay = (60 * 60 * 24) const oneDay = (60 * 60 * 24)
const now = Date.now() / 1000 const now = Date.now() / 1000
const algorithms = [
'ES256',
'ES384',
'ES512',
'HS256',
'HS384',
'HS512',
'RS256',
'RS384',
'RS512'
]
const secrets = {} const secrets = {}
// Keypairs // Keypairs
for (const algorithm of Object.keys(jwt.algorithms)) { for (const algorithm of algorithms) {
if (algorithm.startsWith('HS')) if (algorithm.startsWith('HS'))
secrets[algorithm] = 'secret' secrets[algorithm] = 'secret'
else if (algorithm.startsWith('RS')) { else if (algorithm.startsWith('RS')) {

View File

@@ -1,3 +1,12 @@
if (typeof crypto === 'undefined' || !crypto.subtle)
throw new Error('SubtleCrypto not supported!')
/**
* @typedef JwtAlgorithm
* @type {'ES256'|'ES384'|'ES512'|'HS256'|'HS384'|'HS512'|'RS256'|'RS384'|'RS512'}
*/
type JwtAlgorithm = 'ES256'|'ES384'|'ES512'|'HS256'|'HS384'|'HS512'|'RS256'|'RS384'|'RS512'
/** /**
* @typedef JwtAlgorithms * @typedef JwtAlgorithms
*/ */
@@ -5,22 +14,6 @@ interface JwtAlgorithms {
[key: string]: SubtleCryptoImportKeyAlgorithm [key: string]: SubtleCryptoImportKeyAlgorithm
} }
/**
* @typedef JwtAlgorithm
* @enum {string}
*/
enum JwtAlgorithm {
ES256 = 'ES256',
ES384 = 'ES384',
ES512 = 'ES512',
HS256 = 'HS256',
HS384 = 'HS384',
HS512 = 'HS512',
RS256 = 'RS256',
RS384 = 'RS384',
RS512 = 'RS512'
}
/** /**
* @typedef JwtHeader * @typedef JwtHeader
* @prop {string} [typ] Type * @prop {string} [typ] Type
@@ -91,7 +84,7 @@ interface JwtSignOptions extends JwtOptions {
/** /**
* @typedef JwtVerifyOptions * @typedef JwtVerifyOptions
* @extends JwtOptions * @extends JwtOptions
* @prop {boolean=false} [throwError] If `true` throw error if checks fail. (default: `false`) * @prop {boolean} [throwError=false] If `true` throw error if checks fail. (default: `false`)
*/ */
interface JwtVerifyOptions extends JwtOptions { interface JwtVerifyOptions extends JwtOptions {
/** /**
@@ -112,43 +105,19 @@ interface JwtData {
payload: JwtPayload payload: JwtPayload
} }
/** function base64UrlParse(s: string): Uint8Array {
* Base64Url
*
* @public
* @class
*/
class Base64Url {
/**
* @param {string} s
* @returns {Uint8Array}
*/
public static parse(s: string): Uint8Array {
// @ts-ignore // @ts-ignore
return new Uint8Array(Array.prototype.map.call(atob(s.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '')), c => c.charCodeAt(0))) return new Uint8Array(Array.prototype.map.call(atob(s.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '')), c => c.charCodeAt(0)))
// return new Uint8Array(Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, ''))).map(c => c.charCodeAt(0))) // return new Uint8Array(Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, ''))).map(c => c.charCodeAt(0)))
} }
/** function base64UrlStringify(a: Uint8Array): string {
* @param {Uint8Array} a
* @returns {string}
*/
public static stringify(a: Uint8Array): string {
// @ts-ignore // @ts-ignore
return btoa(String.fromCharCode.apply(0, a)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') return btoa(String.fromCharCode.apply(0, a)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
// return btoa(String.fromCharCode.apply(0, Array.from(a))).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') // return btoa(String.fromCharCode.apply(0, Array.from(a))).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
}
} }
/** const algorithms: JwtAlgorithms = {
* Jwt
*
* @public
* @class
*/
class Jwt {
protected algorithms: JwtAlgorithms = {
ES256: { name: 'ECDSA', namedCurve: 'P-256', hash: { name: 'SHA-256' } }, ES256: { name: 'ECDSA', namedCurve: 'P-256', hash: { name: 'SHA-256' } },
ES384: { name: 'ECDSA', namedCurve: 'P-384', hash: { name: 'SHA-384' } }, ES384: { name: 'ECDSA', namedCurve: 'P-384', hash: { name: 'SHA-384' } },
ES512: { name: 'ECDSA', namedCurve: 'P-521', hash: { name: 'SHA-512' } }, ES512: { name: 'ECDSA', namedCurve: 'P-521', hash: { name: 'SHA-512' } },
@@ -158,18 +127,13 @@ class Jwt {
RS256: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } }, RS256: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } },
RS384: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-384' } }, RS384: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-384' } },
RS512: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-512' } } RS512: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-512' } }
} }
constructor() { function _utf8ToUint8Array(str: string): Uint8Array {
if (typeof crypto === 'undefined' || !crypto.subtle) return base64UrlParse(btoa(unescape(encodeURIComponent(str))))
throw new Error('SubtleCrypto not supported!') }
}
protected _utf8ToUint8Array(str: string): Uint8Array { function _str2ab(str: string): ArrayBuffer {
return Base64Url.parse(btoa(unescape(encodeURIComponent(str))))
}
protected _str2ab(str: string): ArrayBuffer {
str = atob(str) str = atob(str)
const buf = new ArrayBuffer(str.length); const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf); const bufView = new Uint8Array(buf);
@@ -177,9 +141,9 @@ class Jwt {
bufView[i] = str.charCodeAt(i); bufView[i] = str.charCodeAt(i);
} }
return buf; return buf;
} }
protected _decodePayload(raw: string): JwtHeader | JwtPayload | null { function _decodePayload(raw: string): JwtHeader | JwtPayload | null {
switch (raw.length % 4) { switch (raw.length % 4) {
case 0: case 0:
break break
@@ -197,9 +161,9 @@ class Jwt {
} catch { } catch {
return null return null
} }
} }
/** /**
* Signs a payload and returns the token * Signs a payload and returns the token
* *
* @param {JwtPayload} payload The payload object. To use `nbf` (Not Before) and/or `exp` (Expiration Time) add `nbf` and/or `exp` to the payload. * @param {JwtPayload} payload The payload object. To use `nbf` (Not Before) and/or `exp` (Expiration Time) add `nbf` and/or `exp` to the payload.
@@ -208,47 +172,47 @@ class Jwt {
* @throws {Error} If there's a validation issue. * @throws {Error} If there's a validation issue.
* @returns {Promise<string>} Returns token as a `string`. * @returns {Promise<string>} Returns token as a `string`.
*/ */
public async sign(payload: JwtPayload, secret: string, options: JwtSignOptions | JwtAlgorithm = { algorithm: JwtAlgorithm.HS256, header: { typ: 'JWT' } }): Promise<string> { export async function sign(payload: JwtPayload, secret: string, options: JwtSignOptions | JwtAlgorithm = { algorithm: 'HS256', header: { typ: 'JWT' } }): Promise<string> {
if (typeof options === 'string') if (typeof options === 'string')
options = { algorithm: options, header: { typ: 'JWT' } } options = { algorithm: options, header: { typ: 'JWT' } }
options = { algorithm: JwtAlgorithm.HS256, header: { typ: 'JWT' }, ...options } options = { algorithm: 'HS256', header: { typ: 'JWT' }, ...options }
if (payload === null || typeof payload !== 'object') if (payload === null || typeof payload !== 'object')
throw new Error('payload must be an object') throw new Error('payload must be an object')
if (typeof secret !== 'string') if (typeof secret !== 'string')
throw new Error('secret must be a string') throw new Error('secret must be a string')
if (typeof options.algorithm !== 'string') if (typeof options.algorithm !== 'string')
throw new Error('options.algorithm must be a string') throw new Error('options.algorithm must be a string')
const algorithm: SubtleCryptoImportKeyAlgorithm = this.algorithms[options.algorithm] const algorithm: SubtleCryptoImportKeyAlgorithm = algorithms[options.algorithm]
if (!algorithm) if (!algorithm)
throw new Error('algorithm not found') throw new Error('algorithm not found')
payload.iat = Math.floor(Date.now() / 1000) payload.iat = Math.floor(Date.now() / 1000)
const payloadAsJSON = JSON.stringify(payload) const payloadAsJSON = JSON.stringify(payload)
const partialToken = `${Base64Url.stringify(this._utf8ToUint8Array(JSON.stringify({ ...options.header, alg: options.algorithm })))}.${Base64Url.stringify(this._utf8ToUint8Array(payloadAsJSON))}` const partialToken = `${base64UrlStringify(_utf8ToUint8Array(JSON.stringify({ ...options.header, alg: options.algorithm })))}.${base64UrlStringify(_utf8ToUint8Array(payloadAsJSON))}`
let keyFormat = 'raw' let keyFormat = 'raw'
let keyData let keyData
if (secret.startsWith('-----BEGIN')) { if (secret.startsWith('-----BEGIN')) {
keyFormat = 'pkcs8' keyFormat = 'pkcs8'
keyData = this._str2ab(secret.replace(/-----BEGIN.*?-----/g, '').replace(/-----END.*?-----/g, '').replace(/\s/g, '')) keyData = _str2ab(secret.replace(/-----BEGIN.*?-----/g, '').replace(/-----END.*?-----/g, '').replace(/\s/g, ''))
} else } else
keyData = this._utf8ToUint8Array(secret) keyData = _utf8ToUint8Array(secret)
const key = await crypto.subtle.importKey(keyFormat, keyData, algorithm, false, ['sign']) const key = await crypto.subtle.importKey(keyFormat, keyData, algorithm, false, ['sign'])
const signature = await crypto.subtle.sign(algorithm, key, this._utf8ToUint8Array(partialToken)) const signature = await crypto.subtle.sign(algorithm, key, _utf8ToUint8Array(partialToken))
return `${partialToken}.${Base64Url.stringify(new Uint8Array(signature))}` return `${partialToken}.${base64UrlStringify(new Uint8Array(signature))}`
} }
/** /**
* Verifies the integrity of the token and returns a boolean value. * Verifies the integrity of the token and returns a boolean value.
* *
* @param {string} token The token string generated by `jwt.sign()`. * @param {string} token The token string generated by `jwt.sign()`.
* @param {string} secret The string which was used to sign the payload. * @param {string} secret The string which was used to sign the payload.
* @param {JWTVerifyOptions | JWTAlgorithm | string} options The options object or the algorithm. * @param {JWTVerifyOptions | JWTAlgorithm} options The options object or the algorithm.
* @throws {Error | string} Throws an error `string` if the token is invalid or an `Error-Object` if there's a validation issue. * @throws {Error | string} Throws an error `string` if the token is invalid or an `Error-Object` if there's a validation issue.
* @returns {Promise<boolean>} Returns `true` if signature, `nbf` (if set) and `exp` (if set) are valid, otherwise returns `false`. * @returns {Promise<boolean>} Returns `true` if signature, `nbf` (if set) and `exp` (if set) are valid, otherwise returns `false`.
*/ */
async verify(token: string, secret: string, options: JwtVerifyOptions | JwtAlgorithm = { algorithm: JwtAlgorithm.HS256, throwError: false }): Promise<boolean> { export async function verify(token: string, secret: string, options: JwtVerifyOptions | JwtAlgorithm = { algorithm: 'HS256', throwError: false }): Promise<boolean> {
if (typeof options === 'string') if (typeof options === 'string')
options = { algorithm: options, throwError: false } options = { algorithm: options, throwError: false }
options = { algorithm: JwtAlgorithm.HS256, throwError: false, ...options } options = { algorithm: 'HS256', throwError: false, ...options }
if (typeof token !== 'string') if (typeof token !== 'string')
throw new Error('token must be a string') throw new Error('token must be a string')
if (typeof secret !== 'string') if (typeof secret !== 'string')
@@ -258,10 +222,10 @@ class Jwt {
const tokenParts = token.split('.') const tokenParts = token.split('.')
if (tokenParts.length !== 3) if (tokenParts.length !== 3)
throw new Error('token must consist of 3 parts') throw new Error('token must consist of 3 parts')
const algorithm: SubtleCryptoImportKeyAlgorithm = this.algorithms[options.algorithm] const algorithm: SubtleCryptoImportKeyAlgorithm = algorithms[options.algorithm]
if (!algorithm) if (!algorithm)
throw new Error('algorithm not found') throw new Error('algorithm not found')
const { payload } = this.decode(token) const { payload } = decode(token)
if (!payload) { if (!payload) {
if (options.throwError) if (options.throwError)
throw 'PARSE_ERROR' throw 'PARSE_ERROR'
@@ -281,25 +245,28 @@ class Jwt {
let keyData let keyData
if (secret.startsWith('-----BEGIN')) { if (secret.startsWith('-----BEGIN')) {
keyFormat = 'spki' keyFormat = 'spki'
keyData = this._str2ab(secret.replace(/-----BEGIN.*?-----/g, '').replace(/-----END.*?-----/g, '').replace(/\s/g, '')) keyData = _str2ab(secret.replace(/-----BEGIN.*?-----/g, '').replace(/-----END.*?-----/g, '').replace(/\s/g, ''))
} else } else
keyData = this._utf8ToUint8Array(secret) keyData = _utf8ToUint8Array(secret)
const key = await crypto.subtle.importKey(keyFormat, keyData, algorithm, false, ['verify']) const key = await crypto.subtle.importKey(keyFormat, keyData, algorithm, false, ['verify'])
return await crypto.subtle.verify(algorithm, key, Base64Url.parse(tokenParts[2]), this._utf8ToUint8Array(`${tokenParts[0]}.${tokenParts[1]}`)) return await crypto.subtle.verify(algorithm, key, base64UrlParse(tokenParts[2]), _utf8ToUint8Array(`${tokenParts[0]}.${tokenParts[1]}`))
} }
/** /**
* Returns the payload **without** verifying the integrity of the token. Please use `jwt.verify()` first to keep your application secure! * Returns the payload **without** verifying the integrity of the token. Please use `jwt.verify()` first to keep your application secure!
* *
* @param {string} token The token string generated by `jwt.sign()`. * @param {string} token The token string generated by `jwt.sign()`.
* @returns {JwtData} Returns an `object` containing `header` and `payload`. * @returns {JwtData} Returns an `object` containing `header` and `payload`.
*/ */
public decode(token: string): JwtData { export function decode(token: string): JwtData {
return { return {
header: this._decodePayload(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')) as JwtHeader, header: _decodePayload(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')) as JwtHeader,
payload: this._decodePayload(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')) as JwtPayload payload: _decodePayload(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')) as JwtPayload
}
} }
} }
export default new Jwt export default {
sign,
verify,
decode
}