diff --git a/.gitattributes b/.gitattributes index 69cf88b5..912c804d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -20,6 +20,8 @@ packages/deno-lint/index.js linguist-detectable=false packages/deno-lint/index.d.ts linguist-detectable=false packages/jieba/index.js linguist-detectable=false packages/jieba/index.d.ts linguist-detectable=false +packages/jsonwebtoken/index.js linguist-detectable=false +packages/jsonwebtoken/index.d.ts linguist-detectable=false packages/xxhash/index.js linguist-detectable=false packages/xxhash/index.d.ts linguist-detectable=false .yarn/releases/*.js linguist-detectable=false \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cf21dd63..2b2c294d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -91,7 +91,7 @@ jobs: yarn lerna exec "yarn build --target aarch64-unknown-linux-musl" --concurrency 1 --stream --no-prefix - host: windows-latest target: 'aarch64-pc-windows-msvc' - build: yarn lerna exec "yarn build --target aarch64-pc-windows-msvc" --concurrency 1 --stream --no-prefix + build: yarn lerna exec "yarn build --target aarch64-pc-windows-msvc" --ignore @node-rs/jsonwebtoken --concurrency 1 --stream --no-prefix name: stable - ${{ matrix.settings.target }} - node@18 runs-on: ${{ matrix.settings.host }} @@ -111,7 +111,7 @@ jobs: uses: dtolnay/rust-toolchain@stable if: ${{ !matrix.settings.docker }} with: - toolchain: nightly-2023-01-11 + toolchain: nightly-2023-03-01 targets: ${{ matrix.settings.target }} - name: Cache cargo registry @@ -132,7 +132,7 @@ jobs: - uses: goto-bus-stop/setup-zig@v2 if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' }} with: - version: 0.10.0 + version: 0.10.1 - name: Setup node x86 if: matrix.settings.target == 'i686-pc-windows-msvc' @@ -276,6 +276,7 @@ jobs: yarn test packages/bcrypt yarn test packages/crc32 yarn test packages/jieba + yarn test packages/jsonwebtoken yarn test packages/xxhash test-linux-x64-gnu-binding: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 031134fb..0a616ffe 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -23,7 +23,7 @@ jobs: - name: Install uses: dtolnay/rust-toolchain@stable with: - toolchain: nightly-2023-01-11 + toolchain: nightly-2023-03-01 components: rustfmt, clippy - name: 'Install dependencies' diff --git a/Cargo.toml b/Cargo.toml index a08a0573..8da920b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "./packages/crc32", "./packages/deno-lint", "./packages/jieba", + "./packages/jsonwebtoken", "./packages/xxhash", ] diff --git a/packages/jsonwebtoken/Cargo.toml b/packages/jsonwebtoken/Cargo.toml new file mode 100644 index 00000000..a3c86938 --- /dev/null +++ b/packages/jsonwebtoken/Cargo.toml @@ -0,0 +1,23 @@ +[package] +authors = ["Francesco Benedetto"] +edition = "2021" +name = "node-rs-jsonwebtoken" +version = "0.1.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +global_alloc = { path = "../../crates/alloc" } +jsonwebtoken = { version = "8" } +napi = { version = "2", default-features = false, features = [ + "napi3", + "serde-json", +] } +napi-derive = { version = "2" } +serde = "1.0" +serde_json = "1.0" +rand = "0.8" + +[build-dependencies] +napi-build = "2" diff --git a/packages/jsonwebtoken/README.md b/packages/jsonwebtoken/README.md new file mode 100644 index 00000000..7b80fe95 --- /dev/null +++ b/packages/jsonwebtoken/README.md @@ -0,0 +1,61 @@ +# `@node-rs/jsonwebtoken` + +![](https://github.com/napi-rs/node-rs/workflows/CI/badge.svg) +![](https://img.shields.io/npm/dm/@node-rs/jsonwebtoken.svg?sanitize=true) + +🚀 Fastest jsonwebtoken in Node.js + +## Support matrix + +| | node12 | node14 | node16 | node18 | +| ---------------- | ------ | ------ | ------ | ------ | +| Windows x64 | ✓ | ✓ | ✓ | ✓ | +| Windows x32 | ✓ | ✓ | ✓ | ✓ | +| Windows arm64 | ✓ | ✓ | ✓ | ✓ | +| macOS x64 | ✓ | ✓ | ✓ | ✓ | +| macOS arm64 | ✓ | ✓ | ✓ | ✓ | +| Linux x64 gnu | ✓ | ✓ | ✓ | ✓ | +| Linux x64 musl | ✓ | ✓ | ✓ | ✓ | +| Linux arm gnu | ✓ | ✓ | ✓ | ✓ | +| Linux arm64 gnu | ✓ | ✓ | ✓ | ✓ | +| Linux arm64 musl | ✓ | ✓ | ✓ | ✓ | +| Android arm64 | ✓ | ✓ | ✓ | ✓ | +| Android armv7 | ✓ | ✓ | ✓ | ✓ | +| FreeBSD x64 | ✓ | ✓ | ✓ | ✓ | + +## Usage + +See [tests](__tests__/jsonwebtoken.spec.ts) and [types](index.d.ts) + +## Bench + +``` +Model Name: MacBook Pro +Model Identifier: MacBookPro18,2 +Processor Name: Apple M1 Max +Processor Speed: 2.6 GHz +Number of Processors: 1 +Total Number of Cores: 8 +L2 Cache (per Core): 256 KB +L3 Cache: 12 MB +Hyper-Threading Technology: Disabled +Memory: 64 GB +``` + +```text +@node-rs/jsonwebtoken x 17,491 ops/sec ±0.39% (92 runs sampled) +node-jsonwebtoken x 6,899 ops/sec ±0.62% (88 runs sampled) +Async sign bench suite: Fastest is @node-rs/jsonwebtoken + +@node-rs/jsonwebtoken x 17,393 ops/sec ±1.57% (87 runs sampled) +node-jsonwebtoken x 5,256 ops/sec ±0.74% (85 runs sampled) +Async verify bench suite: Fastest is @node-rs/jsonwebtoken + +@node-rs/jsonwebtoken x 264,001 ops/sec ±0.08% (101 runs sampled) +node-jsonwebtoken x 71,785 ops/sec ±1.01% (97 runs sampled) +Sync sign bench suite: Fastest is @node-rs/jsonwebtoken + +@node-rs/jsonwebtoken x 278,657 ops/sec ±0.08% (99 runs sampled) +node-jsonwebtoken x 54,462 ops/sec ±0.53% (100 runs sampled) +Sync verify bench suite: Fastest is @node-rs/jsonwebtoken +``` diff --git a/packages/jsonwebtoken/__tests__/jsonwebtoken.spec.ts b/packages/jsonwebtoken/__tests__/jsonwebtoken.spec.ts new file mode 100644 index 00000000..642d13c3 --- /dev/null +++ b/packages/jsonwebtoken/__tests__/jsonwebtoken.spec.ts @@ -0,0 +1,114 @@ +import { promises as fs } from 'node:fs' +import { join } from 'node:path' + +import test from 'ava' +import { decode as nodeJwtDecode } from 'jsonwebtoken' + +import { Algorithm, sign, signSync, verifySync, verify } from '../index.js' + +const getUtcTimestamp = () => Math.floor(new Date().getTime() / 1000) +const oneDayInSeconds = 86400 + +test('signSync and sign (async) should produce the same result', async (t) => { + const claims = { + data: { + id: 'f81d4fae-7dec-11d0-a765-00a0c91e6bf6', + pr: 33, + isM: true, + set: ['KL', 'TV', 'JI'], + nest: { id: 'poly' }, + }, + exp: getUtcTimestamp() + oneDayInSeconds, + } + const secretKey = 'secret' + const resSync = signSync(claims, secretKey) + const resAsync = await sign(claims, secretKey) + + t.is(resSync, resAsync) + t.truthy(nodeJwtDecode(resAsync)) +}) + +test('verify should return the decoded claims', async (t) => { + const data = { + id: 'f81d4fae-7dec-11d0-a765-00a0c91e6bf6', + pr: 33, + isM: true, + set: ['KL', 'TV', 'JI'], + nest: { id: 'poly' }, + } + const claims = { data, exp: getUtcTimestamp() + oneDayInSeconds } + const secretKey = 'secret' + const headers = { algorithm: Algorithm.HS384 } + + const token = await sign(claims, secretKey, headers) + + const validation = { algorithms: [Algorithm.HS384] } + const decodedToken = await verify(token, secretKey, validation) + + t.like(decodedToken, claims) +}) + +test('verify sync should return the decoded claims', async (t) => { + const data = { + id: 'f81d4fae-7dec-11d0-a765-00a0c91e6bf6', + pr: 33, + isM: true, + set: ['KL', 'TV', 'JI'], + nest: { id: 'poly' }, + } + const claims = { data, exp: getUtcTimestamp() + oneDayInSeconds } + const publicKeyPem = + '-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzq7L/V03tpy3QTYOP51CT0fY2Sp5spejcja9brkEZoLYFcvLSeNnsXtPg/Sr7PwbykiXoY++xo7+6o2VfPnbiEFV8fNap+4tWDmxeZfPifmCEA58BFncnK8z5luxR+syeRuI/9IUHllGxsKoQAbFECZoNCR+I5H/ynqhm9rvk89iNsh5EGxknOq2GmMaKRZ3nHZtVuwUj3BlwgsmP28ZAofMN/xM8bugHS1nNNHmRh6Ubg0Od3r2FH0+3df86ZzJ013M/LG1189aGNPXDOH4guBh7TPficw9nUnhIghiEFrxhAvIOQjClbhFud931T+UqD5BsF/ZarJ1VkaUa3UjxwIDAQAB-----END PUBLIC KEY-----' + const privateKeyPem = + '-----BEGIN RSA PRIVATE KEY-----MIIEowIBAAKCAQEAzq7L/V03tpy3QTYOP51CT0fY2Sp5spejcja9brkEZoLYFcvLSeNnsXtPg/Sr7PwbykiXoY++xo7+6o2VfPnbiEFV8fNap+4tWDmxeZfPifmCEA58BFncnK8z5luxR+syeRuI/9IUHllGxsKoQAbFECZoNCR+I5H/ynqhm9rvk89iNsh5EGxknOq2GmMaKRZ3nHZtVuwUj3BlwgsmP28ZAofMN/xM8bugHS1nNNHmRh6Ubg0Od3r2FH0+3df86ZzJ013M/LG1189aGNPXDOH4guBh7TPficw9nUnhIghiEFrxhAvIOQjClbhFud931T+UqD5BsF/ZarJ1VkaUa3UjxwIDAQABAoIBAQDGYRB7B9ZJ+PIMLY5PkOnsntGM4DAfM102a0Q32m5W1pABm7JsIVGOEQWpalb7CKDD8BlagVZjzyzuhSdO5aPJjKyppyMEvJ/ZZsbqJsSVcl9cegqfQoF2AtSV7ryigyXXCI7evQ2Cc75zWLOVgOn1LmgmZECOc7xI5JvptKLwAwrIuLE4wnuLgSdxxVZ8uwJHW7+hTCQ8x0cSID1POy3q39kEEdqi+yNOrVFZV7DGJ6T5gYWDe53fWpks++tr7D6Wbq1mRdX5T62IdG/G4q9/vA2tSR+5hZWMxMqZ+GUBmIH2zPU16yc4hfwne/C5WQkRUaPBIl/u5swFLHwxNIqBAoGBAPsCOB5T7/oO9Y/LyD6SCDLiKpKQhwPPZJ76/Nu9yNXM2sLINGDq6RUXmaflifoKSRxFqApBHXqcP8NRzrYT+eY5Q0/m2Nvt4MvoMRoNDx2FVnQY8yo4AdSpQl2fNhMdXc1R2Wc3EJdWZd+2J9xGBTbLZ5nUem9zdVdZr0YbMrwpAoGBANLK7txwi/YSYfHo+S0KZqqO32CAN8m0s6Clnz1SomZY4TX1nQQyfbzT06AG/7vtVf5roc4t1JrX08Qelu7VBOCH2Y2jEYyX1M6e7sJbl+Z5LYqOQkiAW+GBF3gvn/IvQ1Irjzd8MF/5wfyafaeE5mxoAtDOGW/BfcwORIoAOt5vAoGAHHjx+K64x/qubDNHcaGLAIqbHaj7R7lcxpPd3uc2QtpL7lBbcKr06YmVym/FKPHFvUlBeHhOabwTl4pOEmVNsYnJUuTysG/ZUgfymevlTQn09pJl8uILgx34AzquHZj1LPcd3BFo9mG8iJXXC6t9p+uGwvJRORc1tkTcFu264ZECgYB9sygXakH8PmAL6vrUQhSQ9tv75tndvZU0Yi+AWQug7rV2AP5eJ2HVvZfAIQxVW6VhL3vwwGG86KFOnVMyHvNmlXxFOw3XAh+UCzCj1AzUEkT3D/g01d50rg95yySdPlPt5y3jT3plcUGdyd7Oi7EAylGLhKukegTzLzrt9E8mnwKBgBx+31YGB/sxdLXKN7CKvkB9+PUQ1ywDZshzuXfSL+lEcgls6MT/AjMP49eEu14698S4VHnhMu/671TwJXS6NpCTCGjrUJoKymuaBGYvgFRqcqjVtHzyz+YMkFQISvi/DurN5CN4C1Yiv7EDFQC+69fcOo4tP9S9EFya189IvJsJ-----END RSA PRIVATE KEY-----' + const headers = { algorithm: Algorithm.RS256 } + + const token = await sign(claims, privateKeyPem, headers) + + const validation = { algorithms: [Algorithm.RS256] } + const decodedToken = verifySync(token, publicKeyPem, validation) + + t.like(decodedToken, claims) +}) + +test('verify should throw in case the token is expired', async (t) => { + const data = { + id: 'f81d4fae-7dec-11d0-a765-00a0c91e6bf6', + pr: 33, + isM: true, + set: ['KL', 'TV', 'JI'], + nest: { id: 'poly' }, + } + const claims = { data, iat: getUtcTimestamp() - oneDayInSeconds * 2, exp: getUtcTimestamp() - oneDayInSeconds } + const secretKey = 'secret' + const headers = { algorithm: Algorithm.HS256 } + + const token = await sign(claims, secretKey, headers) + + t.throws(() => verifySync(token, secretKey)) + await t.throwsAsync(() => verify(token, secretKey)) +}) + +test('should allow to use a buffer as sign/verify key', async (t) => { + const [publicKeyPem, privateKeyPem] = await Promise.all([ + fs.readFile(join(__dirname, 'public-key.pem')), + fs.readFile(join(__dirname, 'private-key.pem')), + ]) + + const data = { + id: 'f81d4fae-7dec-11d0-a765-00a0c91e6bf6', + pr: 33, + isM: true, + set: ['KL', 'TV', 'JI'], + nest: { id: 'poly' }, + } + const claims = { data, exp: getUtcTimestamp() + oneDayInSeconds } + const headers = { algorithm: Algorithm.PS256 } + + const token = await sign(claims, privateKeyPem.toString(), headers) + + const validation = { algorithms: [Algorithm.PS256] } + const decodedToken = await verify(token, publicKeyPem.toString(), validation) + + t.like(decodedToken, claims) +}) diff --git a/packages/jsonwebtoken/__tests__/private-key.pem b/packages/jsonwebtoken/__tests__/private-key.pem new file mode 100644 index 00000000..7b9e3d57 --- /dev/null +++ b/packages/jsonwebtoken/__tests__/private-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAowpInj/GxkrOtNVXG89JXmsglD495jzijM2xOEs4jIlKa+B2 +QVIY296+GJUJtnvUboEIGBYwWZUVxBnvVpGYB+0nYTnZSn7A9+KpJ8Lpt+ui/Ibc +kfx6gw5CfSIeKUz5cIuaPxNIZByEUL4FI+6dQGVLc4eVWDCDRpR2RuOW1EcrbIEm +CKrhQgpWrmpc7v8JYj9bzcE63R0iWM8EM9fawbu6yJxFxqQ9vwZFZBYw7vsdj+Pa +9dNyhqTAtUr3c1+fZEDifYSEdGRJZld+2z7Pm2LEI7EFWRKC8LTHA4rC4N6U2B1k +wRWZ2gk7aXu1W4gu9qD79OIjcuhkuVx8OrJZMwIDAQABAoIBAQCgBcPorqgmj936 +Vzq8LOPSLEs5tS2EAVZK5MiAfDPwm//TiegHjNChXSovbniuBzQlkbekDINAKbfH +Vb03tocFoJr6LpE7MNWtd2aXhBNpVXoPaT6seqa0YxaXQxlfaBGbiSnHpuFygRrN +NPROpDDrt4Aq0HSgrlzqtWSxh0fO6MqFrCZPfLelY+etDHfr+JkMG2qbEKYqv7ZF +e18Pvf97VwFebNTolWCKE8A+G8Ps7nUrsl5OiDYNGK42kPZNbM0uImgGgIzvVdYs +458WVwi4rA9A5vrzrANY9U+SMQg3A9Cr1DXMQBHb2MC0W/AxB5UWISEtNe99p7UH +uqtoQUwhAoGBAM1vOWgksEhoJJUAW3UWknAP/SOEqMxDYFTXx2y9q1F7g9Dhe9gC ++wqQQsXP0w2pGyBslFx8RWcP55uc/r1kcjbQbF0i2yWv8ZgVsloFh2WukBODNikp +CDyj8YHSHHi1bojwBeIv32sLt1awBKaUKuJhYgonpliJfvf9Edpm4HYPAoGBAMsr +uPHaWf1XjRjd4SpStwzIMBOtS49twriAZvqEBQcAHYhLldZJFpwLxYrwLk6eZK9E +fQgQDANCl4ez5bfgNR11QCjLiziX1K++e1sERbSGTIV/U6rHeDveaCU+rGG672MZ +P1mOl+trXtFcryfQ7tNyBfjD3E4jF00/yXs+OO6dAoGAMs/58QpyF9a6hahK9tEY +c2NhB3H+lldr8lBU4U6gm0zjs7yx9yH1mg1IlsjquQxEy2ZP4/hQ6kcC0HiqgYng +vjIbO4YtkkrMhQOI079eWAYvWMQxl0iw4t7iE2w24pxttK05p1KT/lQtiuGKpPEt +EkVoDH72JBwOLaSIz+52Qn0CgYA1B7qEViv69mk7vl5RP7nLukziNe9tBoc2xT0b +0m3FgAA1XRVFE1q1bFUpiLttheZd4RCJlDaueyk2IHyrW/hBMiOHAmnaYbcAEEX+ +YcUX853xkmRyRRJa/hhM8GjqMXLeeO6SH6gDqMjc+MY3LE/KHQ71+Zl9Q6eHYEjk +xD7z+QKBgGC7MNG1EARfA4kgp7w+j5JBiCakZdXEzJFUzk994ZSBpY39bftecd/i +0OKuVPXX1qcN3en8p+qAPOriukBITkG4IVQvH+SJVEpV61E3+C+bs9jmp3RtBoiS +1/DWZxnwoqFYnzYLHEsKt4BwJ2fiSE0uUDGJdv+ZJhjSGt1R/fbJ +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/packages/jsonwebtoken/__tests__/public-key.pem b/packages/jsonwebtoken/__tests__/public-key.pem new file mode 100644 index 00000000..fad3e909 --- /dev/null +++ b/packages/jsonwebtoken/__tests__/public-key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAowpInj/GxkrOtNVXG89J +XmsglD495jzijM2xOEs4jIlKa+B2QVIY296+GJUJtnvUboEIGBYwWZUVxBnvVpGY +B+0nYTnZSn7A9+KpJ8Lpt+ui/Ibckfx6gw5CfSIeKUz5cIuaPxNIZByEUL4FI+6d +QGVLc4eVWDCDRpR2RuOW1EcrbIEmCKrhQgpWrmpc7v8JYj9bzcE63R0iWM8EM9fa +wbu6yJxFxqQ9vwZFZBYw7vsdj+Pa9dNyhqTAtUr3c1+fZEDifYSEdGRJZld+2z7P +m2LEI7EFWRKC8LTHA4rC4N6U2B1kwRWZ2gk7aXu1W4gu9qD79OIjcuhkuVx8OrJZ +MwIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/packages/jsonwebtoken/benchmark/jsonwebtoken.js b/packages/jsonwebtoken/benchmark/jsonwebtoken.js new file mode 100644 index 00000000..4c67c6b4 --- /dev/null +++ b/packages/jsonwebtoken/benchmark/jsonwebtoken.js @@ -0,0 +1,161 @@ +const { cpus } = require('os') + +const { Suite } = require('benchmark') +const chalk = require('chalk') +const nodeJsonwebtoken = require('jsonwebtoken') +const { range } = require('lodash') + +const { sign, verify, verifySync, signSync } = require('../index') + +const getUtcTimestamp = () => Math.floor(new Date().getTime() / 1000) +const oneDayInSeconds = 86400 + +function nodeJwtSignAsync(jwtPayload, signKey) { + return new Promise((resolve, reject) => { + nodeJsonwebtoken.sign(jwtPayload, signKey, (err, token) => { + if (err) { + return reject(err) + } + resolve(token) + }) + }) +} +function nodeJwtVerifyAsync(jwt, verifyKey) { + return new Promise((resolve, reject) => { + nodeJsonwebtoken.verify(jwt, verifyKey, (err, token) => { + if (err) { + return reject(err) + } + resolve(token) + }) + }) +} +const nodeJwtSignSync = nodeJsonwebtoken.sign +const nodeJwtVerifySync = nodeJsonwebtoken.verify + +const secretKey = 'qwertyuiopasdfghjklzxcvbnm123456' +const jwtClaims = { + data: { + id: 'f81d4fae-7dec-11d0-a765-00a0c91e6bf6', + pr: 33, + isM: true, + set: ['KL', 'TV', 'JI'], + nest: { id: 'poly' }, + }, + exp: getUtcTimestamp() + oneDayInSeconds, +} +const token = nodeJwtSignSync(jwtClaims, secretKey) + +const numOfCores = cpus().length + +function runSignAsync() { + return new Promise((resolve) => { + const suite = new Suite('Async sign') + + return suite + .add('@node-rs/jsonwebtoken', { + defer: true, + fn: (deferred) => { + const asyncJobs = range(numOfCores).map(() => sign(jwtClaims, secretKey)) + Promise.all(asyncJobs).then(() => deferred.resolve()) + }, + }) + .add('node-jsonwebtoken', { + defer: true, + fn: (deferred) => { + const asyncJobs = range(numOfCores).map(() => nodeJwtSignAsync(jwtClaims, secretKey)) + Promise.all(asyncJobs).then(() => deferred.resolve()) + }, + }) + .on('cycle', function (event) { + console.info(String(event.target)) + }) + .on('complete', function () { + console.info(`${this.name} bench suite: Fastest is ${chalk.green(this.filter('fastest').map('name'))}`) + resolve() + }) + .run() + }) +} + +function runVerifyAsync() { + return new Promise((resolve) => { + const suite = new Suite('Async verify') + + return suite + .add('@node-rs/jsonwebtoken', { + defer: true, + fn: (deferred) => { + const asyncJobs = range(numOfCores).map(() => verify(token, secretKey)) + Promise.all(asyncJobs).then(() => deferred.resolve()) + }, + }) + .add('node-jsonwebtoken', { + defer: true, + fn: (deferred) => { + const asyncJobs = range(numOfCores).map(() => nodeJwtVerifyAsync(token, secretKey)) + Promise.all(asyncJobs).then(() => deferred.resolve()) + }, + }) + .on('cycle', function (event) { + console.info(String(event.target)) + }) + .on('complete', function () { + console.info(`${this.name} bench suite: Fastest is ${chalk.green(this.filter('fastest').map('name'))}`) + resolve() + }) + .run() + }) +} + +function runSignSync() { + return new Promise((resolve) => { + const suite = new Suite('Sync sign') + + return suite + .add('@node-rs/jsonwebtoken', () => { + signSync(jwtClaims, secretKey) + }) + .add('node-jsonwebtoken', () => { + nodeJwtSignSync(jwtClaims, secretKey) + }) + .on('cycle', function (event) { + console.info(String(event.target)) + }) + .on('complete', function () { + console.info(`${this.name} bench suite: Fastest is ${chalk.green(this.filter('fastest').map('name'))}`) + resolve() + }) + .run() + }) +} + +function runVerifySync() { + return new Promise((resolve) => { + const suite = new Suite('Sync verify') + + return suite + .add('@node-rs/jsonwebtoken', () => { + verifySync(token, secretKey) + }) + .add('node-jsonwebtoken', () => { + nodeJwtVerifySync(token, secretKey) + }) + .on('cycle', function (event) { + console.info(String(event.target)) + }) + .on('complete', function () { + console.info(`${this.name} bench suite: Fastest is ${chalk.green(this.filter('fastest').map('name'))}`) + resolve() + }) + .run() + }) +} + +// Run suites in series +;(async () => { + const suiteRunners = [runSignAsync, runVerifyAsync, runSignSync, runVerifySync] + for (const run of suiteRunners) { + await run() + } +})() diff --git a/packages/jsonwebtoken/build.rs b/packages/jsonwebtoken/build.rs new file mode 100644 index 00000000..1f866b6a --- /dev/null +++ b/packages/jsonwebtoken/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/packages/jsonwebtoken/index.d.ts b/packages/jsonwebtoken/index.d.ts new file mode 100644 index 00000000..eb0dae6f --- /dev/null +++ b/packages/jsonwebtoken/index.d.ts @@ -0,0 +1,177 @@ +/* tslint:disable */ +/* eslint-disable */ + +/* auto-generated by NAPI-RS */ + +export const enum Algorithm { + /** HMAC using SHA-256 */ + HS256 = 0, + /** HMAC using SHA-384 */ + HS384 = 1, + /** HMAC using SHA-512 */ + HS512 = 2, + /** ECDSA using SHA-256 */ + ES256 = 3, + /** ECDSA using SHA-384 */ + ES384 = 4, + /** RSASSA-PKCS1-v1_5 using SHA-256 */ + RS256 = 5, + /** RSASSA-PKCS1-v1_5 using SHA-384 */ + RS384 = 6, + /** RSASSA-PKCS1-v1_5 using SHA-512 */ + RS512 = 7, + /** RSASSA-PSS using SHA-256 */ + PS256 = 8, + /** RSASSA-PSS using SHA-384 */ + PS384 = 9, + /** RSASSA-PSS using SHA-512 */ + PS512 = 10, + /** Edwards-curve Digital Signature Algorithm (EdDSA) */ + EdDSA = 11, +} +export interface Claims { + data: Record + aud?: string + exp?: Number + iat?: Number + iss?: string + jti?: string + nbf?: Number + sub?: string +} +export interface Header { + /** + * The algorithm used + * + * Defined in [RFC7515#4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1). + * Default to `HS256` + */ + algorithm?: Algorithm + /** + * Content type + * + * Defined in [RFC7519#5.2](https://tools.ietf.org/html/rfc7519#section-5.2). + */ + contentType?: string + /** + * JSON Key URL + * + * Defined in [RFC7515#4.1.2](https://tools.ietf.org/html/rfc7515#section-4.1.2). + */ + jsonKeyUrl?: string + /** + * JSON Web Key + * + * Defined in [RFC7515#4.1.3](https://tools.ietf.org/html/rfc7515#section-4.1.3). + * Key ID + * + * Defined in [RFC7515#4.1.4](https://tools.ietf.org/html/rfc7515#section-4.1.4). + */ + keyId?: string + /** + * X.509 URL + * + * Defined in [RFC7515#4.1.5](https://tools.ietf.org/html/rfc7515#section-4.1.5). + */ + x5Url?: string + /** + * X.509 certificate chain. A Vec of base64 encoded ASN.1 DER certificates. + * + * Defined in [RFC7515#4.1.6](https://tools.ietf.org/html/rfc7515#section-4.1.6). + */ + x5CertChain?: Array + /** + * X.509 SHA1 certificate thumbprint + * + * Defined in [RFC7515#4.1.7](https://tools.ietf.org/html/rfc7515#section-4.1.7). + */ + x5CertThumbprint?: string + /** + * X.509 SHA256 certificate thumbprint + * + * Defined in [RFC7515#4.1.8](https://tools.ietf.org/html/rfc7515#section-4.1.8). + * + * This will be serialized/deserialized as "x5t#S256", as defined by the RFC. + */ + x5TS256CertThumbprint?: string +} +export function sign( + claims: Claims, + key: string | Buffer, + header?: Header | undefined | null, + abortSignal?: AbortSignal | undefined | null, +): Promise +export function signSync(claims: Claims, key: string | Buffer, header?: Header | undefined | null): string +export interface Validation { + /** + * If it contains a value, the validation will check that the `aud` field is a member of the + * audience provided and will error otherwise. + * + * Defaults to an empty collection. + */ + aud?: Array + /** + * Which claims are required to be present before starting the validation. + * This does not interact with the various `validate_*`. If you remove `exp` from that list, you still need + * to set `validate_exp` to `false`. + * The only value that will be used are "exp", "nbf", "aud", "iss", "sub". Anything else will be ignored. + * + * Defaults to `exp`. + */ + requiredSpecClaims?: Array + /** + * Add some leeway (in seconds) to the `exp` and `nbf` validation to + * account for clock skew. + * + * Defaults to `60`. + */ + leeway?: Number + /** + * Whether to validate the `exp` field. + * + * Defaults to `true`. + */ + validateExp?: boolean + /** + * Whether to validate the `nbf` field. + * + * It will return an error if the current timestamp is before the time in the `nbf` field. + * + * Defaults to `false`. + */ + validateNbf?: boolean + /** + * If it contains a value, the validation will check that the `sub` field is the same as the + * one provided and will error otherwise. + * + * Turned off by default. + */ + sub?: string + /** + * The algorithm used to verify the signature. + * + * Defaults to `HS256`. + */ + algorithms?: Array + /** + * If it contains a value, the validation will check that the `iss` field is a member of the + * iss provided and will error otherwise. + * Use `set_issuer` to set it + * + * Defaults to an empty collection. + */ + iss?: Array + /** + * Whether to validate the JWT signature. + * + * Defaults to `true`. + */ + validateSignature?: boolean +} +export function verify( + token: string, + key: string | Buffer, + validation?: Validation | undefined | null, + abortSignal?: AbortSignal | undefined | null, +): Promise +export function verifySync(token: string, key: string | Buffer, validation?: Validation | undefined | null): Claims diff --git a/packages/jsonwebtoken/index.js b/packages/jsonwebtoken/index.js new file mode 100644 index 00000000..4ca0ecee --- /dev/null +++ b/packages/jsonwebtoken/index.js @@ -0,0 +1,237 @@ +const { existsSync, readFileSync } = require('fs') +const { join } = require('path') + +const { platform, arch } = process + +let nativeBinding = null +let localFileExisted = false +let loadError = null + +function isMusl() { + // For Node 10 + if (!process.report || typeof process.report.getReport !== 'function') { + try { + const lddPath = require('child_process').execSync('which ldd').toString().trim() + return readFileSync(lddPath, 'utf8').includes('musl') + } catch (e) { + return true + } + } else { + const { glibcVersionRuntime } = process.report.getReport().header + return !glibcVersionRuntime + } +} + +switch (platform) { + case 'android': + switch (arch) { + case 'arm64': + localFileExisted = existsSync(join(__dirname, 'jsonwebtoken.android-arm64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./jsonwebtoken.android-arm64.node') + } else { + nativeBinding = require('@node-rs/jsonwebtoken-android-arm64') + } + } catch (e) { + loadError = e + } + break + case 'arm': + localFileExisted = existsSync(join(__dirname, 'jsonwebtoken.android-arm-eabi.node')) + try { + if (localFileExisted) { + nativeBinding = require('./jsonwebtoken.android-arm-eabi.node') + } else { + nativeBinding = require('@node-rs/jsonwebtoken-android-arm-eabi') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Android ${arch}`) + } + break + case 'win32': + switch (arch) { + case 'x64': + localFileExisted = existsSync(join(__dirname, 'jsonwebtoken.win32-x64-msvc.node')) + try { + if (localFileExisted) { + nativeBinding = require('./jsonwebtoken.win32-x64-msvc.node') + } else { + nativeBinding = require('@node-rs/jsonwebtoken-win32-x64-msvc') + } + } catch (e) { + loadError = e + } + break + case 'ia32': + localFileExisted = existsSync(join(__dirname, 'jsonwebtoken.win32-ia32-msvc.node')) + try { + if (localFileExisted) { + nativeBinding = require('./jsonwebtoken.win32-ia32-msvc.node') + } else { + nativeBinding = require('@node-rs/jsonwebtoken-win32-ia32-msvc') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync(join(__dirname, 'jsonwebtoken.win32-arm64-msvc.node')) + try { + if (localFileExisted) { + nativeBinding = require('./jsonwebtoken.win32-arm64-msvc.node') + } else { + nativeBinding = require('@node-rs/jsonwebtoken-win32-arm64-msvc') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Windows: ${arch}`) + } + break + case 'darwin': + localFileExisted = existsSync(join(__dirname, 'jsonwebtoken.darwin-universal.node')) + try { + if (localFileExisted) { + nativeBinding = require('./jsonwebtoken.darwin-universal.node') + } else { + nativeBinding = require('@node-rs/jsonwebtoken-darwin-universal') + } + break + } catch {} + switch (arch) { + case 'x64': + localFileExisted = existsSync(join(__dirname, 'jsonwebtoken.darwin-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./jsonwebtoken.darwin-x64.node') + } else { + nativeBinding = require('@node-rs/jsonwebtoken-darwin-x64') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync(join(__dirname, 'jsonwebtoken.darwin-arm64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./jsonwebtoken.darwin-arm64.node') + } else { + nativeBinding = require('@node-rs/jsonwebtoken-darwin-arm64') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on macOS: ${arch}`) + } + break + case 'freebsd': + if (arch !== 'x64') { + throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) + } + localFileExisted = existsSync(join(__dirname, 'jsonwebtoken.freebsd-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./jsonwebtoken.freebsd-x64.node') + } else { + nativeBinding = require('@node-rs/jsonwebtoken-freebsd-x64') + } + } catch (e) { + loadError = e + } + break + case 'linux': + switch (arch) { + case 'x64': + if (isMusl()) { + localFileExisted = existsSync(join(__dirname, 'jsonwebtoken.linux-x64-musl.node')) + try { + if (localFileExisted) { + nativeBinding = require('./jsonwebtoken.linux-x64-musl.node') + } else { + nativeBinding = require('@node-rs/jsonwebtoken-linux-x64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync(join(__dirname, 'jsonwebtoken.linux-x64-gnu.node')) + try { + if (localFileExisted) { + nativeBinding = require('./jsonwebtoken.linux-x64-gnu.node') + } else { + nativeBinding = require('@node-rs/jsonwebtoken-linux-x64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm64': + if (isMusl()) { + localFileExisted = existsSync(join(__dirname, 'jsonwebtoken.linux-arm64-musl.node')) + try { + if (localFileExisted) { + nativeBinding = require('./jsonwebtoken.linux-arm64-musl.node') + } else { + nativeBinding = require('@node-rs/jsonwebtoken-linux-arm64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync(join(__dirname, 'jsonwebtoken.linux-arm64-gnu.node')) + try { + if (localFileExisted) { + nativeBinding = require('./jsonwebtoken.linux-arm64-gnu.node') + } else { + nativeBinding = require('@node-rs/jsonwebtoken-linux-arm64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm': + localFileExisted = existsSync(join(__dirname, 'jsonwebtoken.linux-arm-gnueabihf.node')) + try { + if (localFileExisted) { + nativeBinding = require('./jsonwebtoken.linux-arm-gnueabihf.node') + } else { + nativeBinding = require('@node-rs/jsonwebtoken-linux-arm-gnueabihf') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Linux: ${arch}`) + } + break + default: + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) +} + +if (!nativeBinding) { + if (loadError) { + throw loadError + } + throw new Error(`Failed to load native binding`) +} + +const { Algorithm, sign, signSync, verify, verifySync } = nativeBinding + +module.exports.Algorithm = Algorithm +module.exports.sign = sign +module.exports.signSync = signSync +module.exports.verify = verify +module.exports.verifySync = verifySync diff --git a/packages/jsonwebtoken/npm/android-arm-eabi/README.md b/packages/jsonwebtoken/npm/android-arm-eabi/README.md new file mode 100644 index 00000000..ae150cbb --- /dev/null +++ b/packages/jsonwebtoken/npm/android-arm-eabi/README.md @@ -0,0 +1,3 @@ +# `@node-rs/jsonwebtoken-android-arm-eabi` + +This is the **armv7-linux-androideabi** binary for `@node-rs/jsonwebtoken` diff --git a/packages/jsonwebtoken/npm/android-arm-eabi/package.json b/packages/jsonwebtoken/npm/android-arm-eabi/package.json new file mode 100644 index 00000000..9cc5a6a8 --- /dev/null +++ b/packages/jsonwebtoken/npm/android-arm-eabi/package.json @@ -0,0 +1,29 @@ +{ + "name": "@node-rs/jsonwebtoken-android-arm-eabi", + "version": "0.1.0", + "os": [ + "android" + ], + "cpu": [ + "arm" + ], + "main": "jsonwebtoken.android-arm-eabi.node", + "files": [ + "jsonwebtoken.android-arm-eabi.node" + ], + "description": "Rust jsonwebtoken binding for Node.js", + "keywords": [ + "jsonwebtoken", + "jwt", + "napi-rs", + "node-rs" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + } +} diff --git a/packages/jsonwebtoken/npm/android-arm64/README.md b/packages/jsonwebtoken/npm/android-arm64/README.md new file mode 100644 index 00000000..8bab1f73 --- /dev/null +++ b/packages/jsonwebtoken/npm/android-arm64/README.md @@ -0,0 +1,3 @@ +# `@node-rs/jsonwebtoken-android-arm64` + +This is the **aarch64-linux-android** binary for `@node-rs/jsonwebtoken` diff --git a/packages/jsonwebtoken/npm/android-arm64/package.json b/packages/jsonwebtoken/npm/android-arm64/package.json new file mode 100644 index 00000000..a8e374cf --- /dev/null +++ b/packages/jsonwebtoken/npm/android-arm64/package.json @@ -0,0 +1,29 @@ +{ + "name": "@node-rs/jsonwebtoken-android-arm64", + "version": "0.1.0", + "os": [ + "android" + ], + "cpu": [ + "arm64" + ], + "main": "jsonwebtoken.android-arm64.node", + "files": [ + "jsonwebtoken.android-arm64.node" + ], + "description": "Rust jsonwebtoken binding for Node.js", + "keywords": [ + "jsonwebtoken", + "jwt", + "napi-rs", + "node-rs" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + } +} diff --git a/packages/jsonwebtoken/npm/darwin-arm64/README.md b/packages/jsonwebtoken/npm/darwin-arm64/README.md new file mode 100644 index 00000000..9d86b021 --- /dev/null +++ b/packages/jsonwebtoken/npm/darwin-arm64/README.md @@ -0,0 +1,3 @@ +# `@node-rs/jsonwebtoken-darwin-arm64` + +This is the **aarch64-apple-darwin** binary for `@node-rs/jsonwebtoken` diff --git a/packages/jsonwebtoken/npm/darwin-arm64/package.json b/packages/jsonwebtoken/npm/darwin-arm64/package.json new file mode 100644 index 00000000..fd648b95 --- /dev/null +++ b/packages/jsonwebtoken/npm/darwin-arm64/package.json @@ -0,0 +1,29 @@ +{ + "name": "@node-rs/jsonwebtoken-darwin-arm64", + "version": "0.1.0", + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "main": "jsonwebtoken.darwin-arm64.node", + "files": [ + "jsonwebtoken.darwin-arm64.node" + ], + "description": "Rust jsonwebtoken binding for Node.js", + "keywords": [ + "jsonwebtoken", + "jwt", + "napi-rs", + "node-rs" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + } +} diff --git a/packages/jsonwebtoken/npm/darwin-x64/README.md b/packages/jsonwebtoken/npm/darwin-x64/README.md new file mode 100644 index 00000000..2e5f1d35 --- /dev/null +++ b/packages/jsonwebtoken/npm/darwin-x64/README.md @@ -0,0 +1,3 @@ +# `@node-rs/jsonwebtoken-darwin-x64` + +This is the **x86_64-apple-darwin** binary for `@node-rs/jsonwebtoken` diff --git a/packages/jsonwebtoken/npm/darwin-x64/package.json b/packages/jsonwebtoken/npm/darwin-x64/package.json new file mode 100644 index 00000000..1312ecd0 --- /dev/null +++ b/packages/jsonwebtoken/npm/darwin-x64/package.json @@ -0,0 +1,29 @@ +{ + "name": "@node-rs/jsonwebtoken-darwin-x64", + "version": "0.1.0", + "os": [ + "darwin" + ], + "cpu": [ + "x64" + ], + "main": "jsonwebtoken.darwin-x64.node", + "files": [ + "jsonwebtoken.darwin-x64.node" + ], + "description": "Rust jsonwebtoken binding for Node.js", + "keywords": [ + "jsonwebtoken", + "jwt", + "napi-rs", + "node-rs" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + } +} diff --git a/packages/jsonwebtoken/npm/freebsd-x64/README.md b/packages/jsonwebtoken/npm/freebsd-x64/README.md new file mode 100644 index 00000000..dd8b3e16 --- /dev/null +++ b/packages/jsonwebtoken/npm/freebsd-x64/README.md @@ -0,0 +1,3 @@ +# `@node-rs/jsonwebtoken-freebsd-x64` + +This is the **x86_64-unknown-freebsd** binary for `@node-rs/jsonwebtoken` diff --git a/packages/jsonwebtoken/npm/freebsd-x64/package.json b/packages/jsonwebtoken/npm/freebsd-x64/package.json new file mode 100644 index 00000000..aac35a2e --- /dev/null +++ b/packages/jsonwebtoken/npm/freebsd-x64/package.json @@ -0,0 +1,29 @@ +{ + "name": "@node-rs/jsonwebtoken-freebsd-x64", + "version": "0.1.0", + "os": [ + "freebsd" + ], + "cpu": [ + "x64" + ], + "main": "jsonwebtoken.freebsd-x64.node", + "files": [ + "jsonwebtoken.freebsd-x64.node" + ], + "description": "Rust jsonwebtoken binding for Node.js", + "keywords": [ + "jsonwebtoken", + "jwt", + "napi-rs", + "node-rs" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + } +} diff --git a/packages/jsonwebtoken/npm/linux-arm-gnueabihf/README.md b/packages/jsonwebtoken/npm/linux-arm-gnueabihf/README.md new file mode 100644 index 00000000..033fe003 --- /dev/null +++ b/packages/jsonwebtoken/npm/linux-arm-gnueabihf/README.md @@ -0,0 +1,3 @@ +# `@node-rs/jsonwebtoken-linux-arm-gnueabihf` + +This is the **armv7-unknown-linux-gnueabihf** binary for `@node-rs/jsonwebtoken` diff --git a/packages/jsonwebtoken/npm/linux-arm-gnueabihf/package.json b/packages/jsonwebtoken/npm/linux-arm-gnueabihf/package.json new file mode 100644 index 00000000..2dd81790 --- /dev/null +++ b/packages/jsonwebtoken/npm/linux-arm-gnueabihf/package.json @@ -0,0 +1,29 @@ +{ + "name": "@node-rs/jsonwebtoken-linux-arm-gnueabihf", + "version": "0.1.0", + "os": [ + "linux" + ], + "cpu": [ + "arm" + ], + "main": "jsonwebtoken.linux-arm-gnueabihf.node", + "files": [ + "jsonwebtoken.linux-arm-gnueabihf.node" + ], + "description": "Rust jsonwebtoken binding for Node.js", + "keywords": [ + "jsonwebtoken", + "jwt", + "napi-rs", + "node-rs" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + } +} diff --git a/packages/jsonwebtoken/npm/linux-arm64-gnu/README.md b/packages/jsonwebtoken/npm/linux-arm64-gnu/README.md new file mode 100644 index 00000000..dc4d642b --- /dev/null +++ b/packages/jsonwebtoken/npm/linux-arm64-gnu/README.md @@ -0,0 +1,3 @@ +# `@node-rs/jsonwebtoken-linux-arm64-gnu` + +This is the **aarch64-unknown-linux-gnu** binary for `@node-rs/jsonwebtoken` diff --git a/packages/jsonwebtoken/npm/linux-arm64-gnu/package.json b/packages/jsonwebtoken/npm/linux-arm64-gnu/package.json new file mode 100644 index 00000000..e3eddad8 --- /dev/null +++ b/packages/jsonwebtoken/npm/linux-arm64-gnu/package.json @@ -0,0 +1,32 @@ +{ + "name": "@node-rs/jsonwebtoken-linux-arm64-gnu", + "version": "0.1.0", + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "main": "jsonwebtoken.linux-arm64-gnu.node", + "files": [ + "jsonwebtoken.linux-arm64-gnu.node" + ], + "description": "Rust jsonwebtoken binding for Node.js", + "keywords": [ + "jsonwebtoken", + "jwt", + "napi-rs", + "node-rs" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + } +} diff --git a/packages/jsonwebtoken/npm/linux-arm64-musl/README.md b/packages/jsonwebtoken/npm/linux-arm64-musl/README.md new file mode 100644 index 00000000..008fff96 --- /dev/null +++ b/packages/jsonwebtoken/npm/linux-arm64-musl/README.md @@ -0,0 +1,3 @@ +# `@node-rs/jsonwebtoken-linux-arm64-musl` + +This is the **aarch64-unknown-linux-musl** binary for `@node-rs/jsonwebtoken` diff --git a/packages/jsonwebtoken/npm/linux-arm64-musl/package.json b/packages/jsonwebtoken/npm/linux-arm64-musl/package.json new file mode 100644 index 00000000..c92ee899 --- /dev/null +++ b/packages/jsonwebtoken/npm/linux-arm64-musl/package.json @@ -0,0 +1,32 @@ +{ + "name": "@node-rs/jsonwebtoken-linux-arm64-musl", + "version": "0.1.0", + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "main": "jsonwebtoken.linux-arm64-musl.node", + "libc": [ + "musl" + ], + "files": [ + "jsonwebtoken.linux-arm64-musl.node" + ], + "description": "Rust jsonwebtoken binding for Node.js", + "keywords": [ + "jsonwebtoken", + "jwt", + "napi-rs", + "node-rs" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + } +} diff --git a/packages/jsonwebtoken/npm/linux-x64-gnu/README.md b/packages/jsonwebtoken/npm/linux-x64-gnu/README.md new file mode 100644 index 00000000..f4480c8f --- /dev/null +++ b/packages/jsonwebtoken/npm/linux-x64-gnu/README.md @@ -0,0 +1,3 @@ +# `@node-rs/jsonwebtoken-linux-x64-gnu` + +This is the **x86_64-unknown-linux-gnu** binary for `@node-rs/jsonwebtoken` diff --git a/packages/jsonwebtoken/npm/linux-x64-gnu/package.json b/packages/jsonwebtoken/npm/linux-x64-gnu/package.json new file mode 100644 index 00000000..e97bdef6 --- /dev/null +++ b/packages/jsonwebtoken/npm/linux-x64-gnu/package.json @@ -0,0 +1,32 @@ +{ + "name": "@node-rs/jsonwebtoken-linux-x64-gnu", + "version": "0.1.0", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "main": "jsonwebtoken.linux-x64-gnu.node", + "libc": [ + "glibc" + ], + "files": [ + "jsonwebtoken.linux-x64-gnu.node" + ], + "description": "Rust jsonwebtoken binding for Node.js", + "keywords": [ + "jsonwebtoken", + "jwt", + "napi-rs", + "node-rs" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + } +} diff --git a/packages/jsonwebtoken/npm/linux-x64-musl/README.md b/packages/jsonwebtoken/npm/linux-x64-musl/README.md new file mode 100644 index 00000000..b53fa6b6 --- /dev/null +++ b/packages/jsonwebtoken/npm/linux-x64-musl/README.md @@ -0,0 +1,3 @@ +# `@node-rs/jsonwebtoken-linux-x64-musl` + +This is the **x86_64-unknown-linux-musl** binary for `@node-rs/jsonwebtoken` diff --git a/packages/jsonwebtoken/npm/linux-x64-musl/package.json b/packages/jsonwebtoken/npm/linux-x64-musl/package.json new file mode 100644 index 00000000..981751ff --- /dev/null +++ b/packages/jsonwebtoken/npm/linux-x64-musl/package.json @@ -0,0 +1,32 @@ +{ + "name": "@node-rs/jsonwebtoken-linux-x64-musl", + "version": "0.1.0", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "main": "jsonwebtoken.linux-x64-musl.node", + "files": [ + "jsonwebtoken.linux-x64-musl.node" + ], + "description": "Rust jsonwebtoken binding for Node.js", + "keywords": [ + "jsonwebtoken", + "jwt", + "napi-rs", + "node-rs" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + } +} diff --git a/packages/jsonwebtoken/npm/win32-ia32-msvc/README.md b/packages/jsonwebtoken/npm/win32-ia32-msvc/README.md new file mode 100644 index 00000000..328546b3 --- /dev/null +++ b/packages/jsonwebtoken/npm/win32-ia32-msvc/README.md @@ -0,0 +1,3 @@ +# `@node-rs/jsonwebtoken-win32-ia32-msvc` + +This is the **i686-pc-windows-msvc** binary for `@node-rs/jsonwebtoken` diff --git a/packages/jsonwebtoken/npm/win32-ia32-msvc/package.json b/packages/jsonwebtoken/npm/win32-ia32-msvc/package.json new file mode 100644 index 00000000..fa96a897 --- /dev/null +++ b/packages/jsonwebtoken/npm/win32-ia32-msvc/package.json @@ -0,0 +1,29 @@ +{ + "name": "@node-rs/jsonwebtoken-win32-ia32-msvc", + "version": "0.1.0", + "os": [ + "win32" + ], + "cpu": [ + "ia32" + ], + "main": "jsonwebtoken.win32-ia32-msvc.node", + "files": [ + "jsonwebtoken.win32-ia32-msvc.node" + ], + "description": "Rust jsonwebtoken binding for Node.js", + "keywords": [ + "jsonwebtoken", + "jwt", + "napi-rs", + "node-rs" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + } +} diff --git a/packages/jsonwebtoken/npm/win32-x64-msvc/README.md b/packages/jsonwebtoken/npm/win32-x64-msvc/README.md new file mode 100644 index 00000000..4cfa6d37 --- /dev/null +++ b/packages/jsonwebtoken/npm/win32-x64-msvc/README.md @@ -0,0 +1,3 @@ +# `@node-rs/jsonwebtoken-win32-x64-msvc` + +This is the **x86_64-pc-windows-msvc** binary for `@node-rs/jsonwebtoken` diff --git a/packages/jsonwebtoken/npm/win32-x64-msvc/package.json b/packages/jsonwebtoken/npm/win32-x64-msvc/package.json new file mode 100644 index 00000000..2443fa53 --- /dev/null +++ b/packages/jsonwebtoken/npm/win32-x64-msvc/package.json @@ -0,0 +1,29 @@ +{ + "name": "@node-rs/jsonwebtoken-win32-x64-msvc", + "version": "0.1.0", + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "main": "jsonwebtoken.win32-x64-msvc.node", + "files": [ + "jsonwebtoken.win32-x64-msvc.node" + ], + "description": "Rust jsonwebtoken binding for Node.js", + "keywords": [ + "jsonwebtoken", + "jwt", + "napi-rs", + "node-rs" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + } +} diff --git a/packages/jsonwebtoken/package.json b/packages/jsonwebtoken/package.json new file mode 100644 index 00000000..2f9fae53 --- /dev/null +++ b/packages/jsonwebtoken/package.json @@ -0,0 +1,67 @@ +{ + "name": "@node-rs/jsonwebtoken", + "version": "0.0.0", + "description": "Rust jsonwebtoken binding for Node.js", + "main": "index.js", + "types": "index.d.ts", + "files": [ + "index.js", + "index.d.ts" + ], + "scripts": { + "artifacts": "napi artifacts -d ../../artifacts", + "bench": "cross-env NODE_ENV=production node benchmark/jsonwebtoken.js", + "build": "napi build --platform --release --pipe \"prettier -w\"", + "build:debug": "napi build --platform --pipe \"prettier -w\"", + "prepublishOnly": "napi prepublish", + "version": "napi version" + }, + "napi": { + "name": "jsonwebtoken", + "triples": { + "defaults": true, + "additional": [ + "aarch64-apple-darwin", + "aarch64-linux-android", + "aarch64-unknown-linux-gnu", + "aarch64-unknown-linux-musl", + "armv7-unknown-linux-gnueabihf", + "x86_64-unknown-linux-musl", + "x86_64-unknown-freebsd", + "i686-pc-windows-msvc", + "armv7-linux-androideabi" + ] + } + }, + "repository": { + "type": "git", + "url": "git+https://github.com/napi-rs/node-rs.git" + }, + "keywords": [ + "jsonwebtoken", + "jwt", + "napi-rs", + "node-rs" + ], + "author": { + "name": "Francesco Benedetto", + "url": "https://github.com/nebarf", + "email": "francesco.benedetto263@gmail.com" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/napi-rs/node-rs/issues" + }, + "homepage": "https://github.com/napi-rs/node-rs#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + }, + "engines": { + "node": ">= 10" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.1", + "jsonwebtoken": "^9.0.0" + } +} diff --git a/packages/jsonwebtoken/src/algorithm.rs b/packages/jsonwebtoken/src/algorithm.rs new file mode 100644 index 00000000..ec26ed3a --- /dev/null +++ b/packages/jsonwebtoken/src/algorithm.rs @@ -0,0 +1,56 @@ +use napi::bindgen_prelude::*; +use napi_derive::napi; + +#[napi] +pub enum Algorithm { + /// HMAC using SHA-256 + HS256, + /// HMAC using SHA-384 + HS384, + /// HMAC using SHA-512 + HS512, + /// ECDSA using SHA-256 + ES256, + /// ECDSA using SHA-384 + ES384, + /// RSASSA-PKCS1-v1_5 using SHA-256 + RS256, + /// RSASSA-PKCS1-v1_5 using SHA-384 + RS384, + /// RSASSA-PKCS1-v1_5 using SHA-512 + RS512, + /// RSASSA-PSS using SHA-256 + PS256, + /// RSASSA-PSS using SHA-384 + PS384, + /// RSASSA-PSS using SHA-512 + PS512, + /// Edwards-curve Digital Signature Algorithm (EdDSA) + EdDSA, +} + +impl From for jsonwebtoken::Algorithm { + #[inline] + fn from(value: Algorithm) -> Self { + match value { + Algorithm::ES256 => jsonwebtoken::Algorithm::ES256, + Algorithm::ES384 => jsonwebtoken::Algorithm::ES384, + Algorithm::EdDSA => jsonwebtoken::Algorithm::EdDSA, + Algorithm::HS256 => jsonwebtoken::Algorithm::HS256, + Algorithm::HS384 => jsonwebtoken::Algorithm::HS384, + Algorithm::HS512 => jsonwebtoken::Algorithm::HS512, + Algorithm::PS256 => jsonwebtoken::Algorithm::PS256, + Algorithm::PS384 => jsonwebtoken::Algorithm::PS384, + Algorithm::PS512 => jsonwebtoken::Algorithm::PS512, + Algorithm::RS256 => jsonwebtoken::Algorithm::RS256, + Algorithm::RS384 => jsonwebtoken::Algorithm::RS384, + Algorithm::RS512 => jsonwebtoken::Algorithm::RS512, + } + } +} + +impl Default for Algorithm { + fn default() -> Self { + Self::HS256 + } +} diff --git a/packages/jsonwebtoken/src/claims.rs b/packages/jsonwebtoken/src/claims.rs new file mode 100644 index 00000000..1df6cc39 --- /dev/null +++ b/packages/jsonwebtoken/src/claims.rs @@ -0,0 +1,62 @@ +use napi_derive::napi; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Number, Value}; + +#[napi(object)] +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub data: Map, + // Recipient for which the JWT is intended + #[serde(skip_serializing_if = "Option::is_none")] + pub aud: Option, + // Time after which the JWT expires (as UTC timestamp, seconds from epoch time) + #[serde(skip_serializing_if = "Option::is_none")] + pub exp: Option, + // Time at which the JWT was issued (as UTC timestamp, seconds from epoch time) + #[serde(skip_serializing_if = "Option::is_none")] + pub iat: Option, + // Issuer of JWT + #[serde(skip_serializing_if = "Option::is_none")] + pub iss: Option, + // [JWT id] Unique identifier + #[serde(skip_serializing_if = "Option::is_none")] + pub jti: Option, + // [not-before-time] Time before which the JWT must not be accepted for processing (as UTC timestamp, seconds from epoch time) + #[serde(skip_serializing_if = "Option::is_none")] + pub nbf: Option, + // Subject of JWT (the user) + #[serde(skip_serializing_if = "Option::is_none")] + pub sub: Option, +} + +impl Claims { + #[inline] + pub fn merge(self, other: Self) -> Self { + Self { + data: self.data, + aud: self.aud.or(other.aud), + exp: self.exp.or(other.exp), + iat: self.iat.or(other.iat), + iss: self.iss.or(other.iss), + jti: self.jti.or(other.jti), + nbf: self.nbf.or(other.nbf), + sub: self.sub.or(other.sub), + } + } +} + +impl Default for Claims { + #[inline] + fn default() -> Self { + Self { + data: Map::new(), + aud: None, + exp: None, + iat: Some(jsonwebtoken::get_current_timestamp().into()), + iss: None, + jti: None, + nbf: None, + sub: None, + } + } +} diff --git a/packages/jsonwebtoken/src/header.rs b/packages/jsonwebtoken/src/header.rs new file mode 100644 index 00000000..34da8da3 --- /dev/null +++ b/packages/jsonwebtoken/src/header.rs @@ -0,0 +1,108 @@ +use napi_derive::napi; + +use crate::algorithm::Algorithm; + +#[napi(object)] +pub struct Header { + /// The algorithm used + /// + /// Defined in [RFC7515#4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1). + /// Default to `HS256` + pub algorithm: Option, + + /// Content type + /// + /// Defined in [RFC7519#5.2](https://tools.ietf.org/html/rfc7519#section-5.2). + pub content_type: Option, + + /// JSON Key URL + /// + /// Defined in [RFC7515#4.1.2](https://tools.ietf.org/html/rfc7515#section-4.1.2). + pub json_key_url: Option, + + /// JSON Web Key + /// + /// Defined in [RFC7515#4.1.3](https://tools.ietf.org/html/rfc7515#section-4.1.3). + // TODO: support jwk + // pub jwk: Option, + + /// Key ID + /// + /// Defined in [RFC7515#4.1.4](https://tools.ietf.org/html/rfc7515#section-4.1.4). + pub key_id: Option, + + /// X.509 URL + /// + /// Defined in [RFC7515#4.1.5](https://tools.ietf.org/html/rfc7515#section-4.1.5). + pub x5_url: Option, + + /// X.509 certificate chain. A Vec of base64 encoded ASN.1 DER certificates. + /// + /// Defined in [RFC7515#4.1.6](https://tools.ietf.org/html/rfc7515#section-4.1.6). + pub x5_cert_chain: Option>, + + /// X.509 SHA1 certificate thumbprint + /// + /// Defined in [RFC7515#4.1.7](https://tools.ietf.org/html/rfc7515#section-4.1.7). + pub x5_cert_thumbprint: Option, + + /// X.509 SHA256 certificate thumbprint + /// + /// Defined in [RFC7515#4.1.8](https://tools.ietf.org/html/rfc7515#section-4.1.8). + /// + /// This will be serialized/deserialized as "x5t#S256", as defined by the RFC. + pub x5t_s256_cert_thumbprint: Option, +} + +impl From<&Header> for jsonwebtoken::Header { + #[inline] + fn from(value: &Header) -> Self { + jsonwebtoken::Header { + typ: Some(String::from("JWT")), + alg: value.algorithm.unwrap_or(Algorithm::ES256).into(), + cty: value.content_type.clone(), + jku: value.json_key_url.clone(), + kid: value.key_id.clone(), + x5u: value.x5_url.clone(), + x5c: value.x5_cert_chain.clone(), + x5t: value.x5_cert_thumbprint.clone(), + x5t_s256: value.x5t_s256_cert_thumbprint.clone(), + // TODO: support jwk + jwk: None, + } + } +} + +impl Header { + #[inline] + pub fn merge(self, other: Self) -> Self { + Self { + algorithm: self.algorithm.or(other.algorithm), + content_type: self.content_type.or(other.content_type), + json_key_url: self.json_key_url.or(other.json_key_url), + key_id: self.key_id.or(other.key_id), + x5_url: self.x5_url.or(other.x5_url), + x5_cert_chain: self.x5_cert_chain.or(other.x5_cert_chain), + x5_cert_thumbprint: self.x5_cert_thumbprint.or(other.x5_cert_thumbprint), + x5t_s256_cert_thumbprint: self + .x5t_s256_cert_thumbprint + .or(other.x5t_s256_cert_thumbprint), + } + } +} + +impl Default for Header { + #[inline] + fn default() -> Self { + Self { + algorithm: Some(Algorithm::HS256), + content_type: None, + json_key_url: None, + key_id: None, + x5_url: None, + x5_cert_chain: None, + x5_cert_thumbprint: None, + x5t_s256_cert_thumbprint: None, + } + } +} diff --git a/packages/jsonwebtoken/src/lib.rs b/packages/jsonwebtoken/src/lib.rs new file mode 100644 index 00000000..01f2c49a --- /dev/null +++ b/packages/jsonwebtoken/src/lib.rs @@ -0,0 +1,19 @@ +#![deny(clippy::all)] +#![allow(dead_code)] + +/// Explicit extern crate to use allocator. +extern crate global_alloc; + +mod algorithm; +mod claims; +mod header; +mod sign; +mod validation; +mod verify; + +pub use algorithm::Algorithm; +pub use claims::Claims; +pub use header::Header; +pub use sign::{sign, sign_sync}; +pub use validation::Validation; +pub use verify::{verify, verify_sync}; diff --git a/packages/jsonwebtoken/src/sign.rs b/packages/jsonwebtoken/src/sign.rs new file mode 100644 index 00000000..e94968fc --- /dev/null +++ b/packages/jsonwebtoken/src/sign.rs @@ -0,0 +1,134 @@ +use jsonwebtoken::{self}; +use napi::{bindgen_prelude::*, JsBuffer, JsBufferValue, Ref}; +use napi_derive::napi; +use std::borrow::Borrow; + +use crate::{claims::Claims, header::Header}; + +pub enum AsyncKeyInput { + String(String), + Buffer(Ref), +} + +impl AsyncKeyInput { + #[inline] + pub fn from_either(input: Either) -> Result { + match input { + Either::A(s) => Ok(Self::String(s)), + Either::B(b) => Ok(Self::Buffer(b.into_ref()?)), + } + } +} + +impl AsRef<[u8]> for AsyncKeyInput { + #[inline] + fn as_ref(&self) -> &[u8] { + match self { + Self::String(s) => s.as_bytes(), + Self::Buffer(b) => b.as_ref(), + } + } +} + +#[inline] +fn into_encoding_key( + value: &[u8], + alg: &jsonwebtoken::Algorithm, +) -> Result { + let encoding_key = match alg { + // HMAC family + jsonwebtoken::Algorithm::HS256 + | jsonwebtoken::Algorithm::HS384 + | jsonwebtoken::Algorithm::HS512 => Ok(jsonwebtoken::EncodingKey::from_secret(value)), + // RSA family + jsonwebtoken::Algorithm::RS256 + | jsonwebtoken::Algorithm::RS384 + | jsonwebtoken::Algorithm::RS512 + | jsonwebtoken::Algorithm::PS256 + | jsonwebtoken::Algorithm::PS384 + | jsonwebtoken::Algorithm::PS512 => jsonwebtoken::EncodingKey::from_rsa_pem(value), + // EC family + jsonwebtoken::Algorithm::ES256 | jsonwebtoken::Algorithm::ES384 => { + jsonwebtoken::EncodingKey::from_ec_pem(value) + } + + // ED family + jsonwebtoken::Algorithm::EdDSA => jsonwebtoken::EncodingKey::from_ed_pem(value), + }; + + encoding_key.map_err(|err| Error::new(Status::InvalidArg, format!("{err}"))) +} + +pub struct SignTask { + key: AsyncKeyInput, + header: Header, + claims: Claims, +} + +impl SignTask { + #[inline] + pub fn sign(claims: &Claims, header: &Header, key: &[u8]) -> Result { + let header: &jsonwebtoken::Header = &header.borrow().into(); + let claims = &claims; + let sign_key = &into_encoding_key(key, &header.alg)?; + + jsonwebtoken::encode(header, claims, sign_key) + .map_err(|err| Error::new(Status::GenericFailure, format!("{err}"))) + } +} + +#[napi] +impl Task for SignTask { + type Output = String; + type JsValue = String; + + fn compute(&mut self) -> Result { + Self::sign(&self.claims, &self.header, self.key.as_ref()) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } + + fn finally(&mut self, env: Env) -> Result<()> { + if let AsyncKeyInput::Buffer(buf) = &mut self.key { + buf.unref(env)?; + } + Ok(()) + } +} + +#[napi] +pub fn sign( + claims: Claims, + key: Either, + header: Option
, + abort_signal: Option, +) -> Result> { + Ok(AsyncTask::with_optional_signal( + SignTask { + header: match header { + Some(h) => h.merge(Header::default()), + _ => Header::default(), + }, + claims: claims.merge(Claims::default()), + key: AsyncKeyInput::from_either(key)?, + }, + abort_signal, + )) +} + +#[napi] +pub fn sign_sync( + claims: Claims, + key: Either, + header: Option
, +) -> Result { + let header = match header { + Some(h) => h.merge(Header::default()), + _ => Header::default(), + }; + let claims = claims.merge(Claims::default()); + + SignTask::sign(&claims, &header, key.as_ref()) +} diff --git a/packages/jsonwebtoken/src/validation.rs b/packages/jsonwebtoken/src/validation.rs new file mode 100644 index 00000000..21d0db50 --- /dev/null +++ b/packages/jsonwebtoken/src/validation.rs @@ -0,0 +1,92 @@ +use napi_derive::napi; +use serde_json::Number; + +use crate::algorithm::Algorithm; + +#[napi(object)] +#[derive(Default)] +pub struct Validation { + /// If it contains a value, the validation will check that the `aud` field is a member of the + /// audience provided and will error otherwise. + /// + /// Defaults to an empty collection. + pub aud: Option>, + /// Which claims are required to be present before starting the validation. + /// This does not interact with the various `validate_*`. If you remove `exp` from that list, you still need + /// to set `validate_exp` to `false`. + /// The only value that will be used are "exp", "nbf", "aud", "iss", "sub". Anything else will be ignored. + /// + /// Defaults to `exp`. + pub required_spec_claims: Option>, + /// Add some leeway (in seconds) to the `exp` and `nbf` validation to + /// account for clock skew. + /// + /// Defaults to `60`. + pub leeway: Option, + /// Whether to validate the `exp` field. + /// + /// Defaults to `true`. + pub validate_exp: Option, + /// Whether to validate the `nbf` field. + /// + /// It will return an error if the current timestamp is before the time in the `nbf` field. + /// + /// Defaults to `false`. + pub validate_nbf: Option, + /// If it contains a value, the validation will check that the `sub` field is the same as the + /// one provided and will error otherwise. + /// + /// Turned off by default. + pub sub: Option, + /// The algorithm used to verify the signature. + /// + /// Defaults to `HS256`. + pub algorithms: Option>, + /// If it contains a value, the validation will check that the `iss` field is a member of the + /// iss provided and will error otherwise. + /// Use `set_issuer` to set it + /// + /// Defaults to an empty collection. + pub iss: Option>, + /// Whether to validate the JWT signature. + /// + /// Defaults to `true`. + pub validate_signature: Option, +} + +impl From<&Validation> for jsonwebtoken::Validation { + #[inline] + fn from(value: &Validation) -> Self { + let mut validation = Self::new(jsonwebtoken::Algorithm::HS256); + + if let Some(aud) = &value.aud { + validation.set_audience(aud); + } + if let Some(required_spec_claims) = &value.required_spec_claims { + validation.set_required_spec_claims(required_spec_claims); + } + if let Some(leeway) = value.leeway.clone().and_then(|l| l.as_u64()) { + validation.leeway = leeway; + } + if let Some(validate_exp) = value.validate_exp { + validation.validate_exp = validate_exp; + } + if let Some(validate_nbf) = value.validate_nbf { + validation.validate_nbf = validate_nbf; + } + if let Some(sub) = &value.sub { + validation.sub = Some(sub.to_string()); + } + if let Some(algorithms) = &value.algorithms { + validation.algorithms = algorithms.iter().map(|alg| alg.to_owned().into()).collect(); + } + if let Some(iss) = &value.iss { + validation.set_issuer(iss); + } + if let Some(false) = value.validate_signature { + validation.insecure_disable_signature_validation() + } + + validation + } +} diff --git a/packages/jsonwebtoken/src/verify.rs b/packages/jsonwebtoken/src/verify.rs new file mode 100644 index 00000000..26708c5c --- /dev/null +++ b/packages/jsonwebtoken/src/verify.rs @@ -0,0 +1,129 @@ +use napi::{bindgen_prelude::*, JsBuffer, JsBufferValue, Ref}; +use napi_derive::napi; +use std::borrow::Borrow; + +use crate::{claims::Claims, validation::Validation}; + +pub enum AsyncKeyInput { + String(String), + Buffer(Ref), +} + +impl AsyncKeyInput { + #[inline] + pub fn from_either(input: Either) -> Result { + match input { + Either::A(s) => Ok(Self::String(s)), + Either::B(b) => Ok(Self::Buffer(b.into_ref()?)), + } + } +} + +impl AsRef<[u8]> for AsyncKeyInput { + #[inline] + fn as_ref(&self) -> &[u8] { + match self { + Self::String(s) => s.as_bytes(), + Self::Buffer(b) => b.as_ref(), + } + } +} + +#[inline] +fn into_decoding_key( + value: &[u8], + alg: &jsonwebtoken::Algorithm, +) -> Result { + let encoding_key = match alg { + // HMAC family + jsonwebtoken::Algorithm::HS256 + | jsonwebtoken::Algorithm::HS384 + | jsonwebtoken::Algorithm::HS512 => Ok(jsonwebtoken::DecodingKey::from_secret(value)), + // RSA family + jsonwebtoken::Algorithm::RS256 + | jsonwebtoken::Algorithm::RS384 + | jsonwebtoken::Algorithm::RS512 + | jsonwebtoken::Algorithm::PS256 + | jsonwebtoken::Algorithm::PS384 + | jsonwebtoken::Algorithm::PS512 => jsonwebtoken::DecodingKey::from_rsa_pem(value), + // EC family + jsonwebtoken::Algorithm::ES256 | jsonwebtoken::Algorithm::ES384 => { + jsonwebtoken::DecodingKey::from_ec_pem(value) + } + + // ED family + jsonwebtoken::Algorithm::EdDSA => jsonwebtoken::DecodingKey::from_ed_pem(value), + }; + + encoding_key.map_err(|err| Error::new(Status::InvalidArg, format!("{err}"))) +} + +pub struct VerifyTask { + token: String, + key: AsyncKeyInput, + validation: Validation, +} + +impl VerifyTask { + pub fn verify(token: &str, key: &[u8], validation: &Validation) -> Result { + let validation: &jsonwebtoken::Validation = &validation.borrow().into(); + + let first_alg = validation.algorithms.first().ok_or(Error::new( + Status::InvalidArg, + "Validation `algorithms` should contain at least one valid algorithm".to_string(), + ))?; + let verify_key = &into_decoding_key(key, first_alg)?; + + jsonwebtoken::decode(token, verify_key, validation) + .map_err(|err| Error::new(Status::GenericFailure, format!("{err}"))) + .map(|token_data| token_data.claims) + } +} + +#[napi] +impl Task for VerifyTask { + type Output = Claims; + type JsValue = Claims; + + fn compute(&mut self) -> Result { + VerifyTask::verify(&self.token, self.key.as_ref(), &self.validation) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } + + fn finally(&mut self, env: Env) -> Result<()> { + if let AsyncKeyInput::Buffer(buf) = &mut self.key { + buf.unref(env)?; + } + Ok(()) + } +} + +#[napi] +pub fn verify( + token: String, + key: Either, + validation: Option, + abort_signal: Option, +) -> Result> { + Ok(AsyncTask::with_optional_signal( + VerifyTask { + token, + key: AsyncKeyInput::from_either(key)?, + validation: validation.unwrap_or(Validation::default()), + }, + abort_signal, + )) +} + +#[napi] +pub fn verify_sync( + token: String, + key: Either, + validation: Option, +) -> Result { + let validation = validation.unwrap_or(Validation::default()); + VerifyTask::verify(&token, key.as_ref(), &validation) +} diff --git a/rust-toolchain b/rust-toolchain index 78c5fdbb..b6f2add3 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -nightly-2023-01-11 +nightly-2023-03-01 diff --git a/tsconfig.json b/tsconfig.json index ac27dd4c..3b79f7b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,7 @@ "importsNotUsedAsValues": "remove", "baseUrl": "./packages", "paths": { + "jsonwebtoken": ["node_modules/*"], "@node-rs/*": ["./*/src"] }, "lib": ["dom", "ES5", "ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020", "esnext"] diff --git a/yarn.lock b/yarn.lock index 36d6fc38..2235d00a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1062,6 +1062,15 @@ __metadata: languageName: unknown linkType: soft +"@node-rs/jsonwebtoken@workspace:packages/jsonwebtoken": + version: 0.0.0-use.local + resolution: "@node-rs/jsonwebtoken@workspace:packages/jsonwebtoken" + dependencies: + "@types/jsonwebtoken": ^9.0.1 + jsonwebtoken: ^9.0.0 + languageName: unknown + linkType: soft + "@node-rs/xxhash@workspace:packages/xxhash": version: 0.0.0-use.local resolution: "@node-rs/xxhash@workspace:packages/xxhash" @@ -1728,6 +1737,15 @@ __metadata: languageName: node linkType: hard +"@types/jsonwebtoken@npm:^9.0.1": + version: 9.0.1 + resolution: "@types/jsonwebtoken@npm:9.0.1" + dependencies: + "@types/node": "*" + checksum: a7f0925e9a42ad3ae970364c63c5986d40da5c83d51d3f4e624eb0f064a380376f9e3fb3f2f837390a9ab80143f5d75fd51866da30e110f6b486a3379e1c768f + languageName: node + linkType: hard + "@types/minimatch@npm:^3.0.3": version: 3.0.5 resolution: "@types/minimatch@npm:3.0.5" @@ -2682,6 +2700,13 @@ __metadata: languageName: node linkType: hard +"buffer-equal-constant-time@npm:1.0.1": + version: 1.0.1 + resolution: "buffer-equal-constant-time@npm:1.0.1" + checksum: 80bb945f5d782a56f374b292770901065bad21420e34936ecbe949e57724b4a13874f735850dd1cc61f078773c4fb5493a41391e7bda40d1fa388d6bd80daaab + languageName: node + linkType: hard + "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" @@ -3698,6 +3723,15 @@ __metadata: languageName: node linkType: hard +"ecdsa-sig-formatter@npm:1.0.11": + version: 1.0.11 + resolution: "ecdsa-sig-formatter@npm:1.0.11" + dependencies: + safe-buffer: ^5.0.1 + checksum: 207f9ab1c2669b8e65540bce29506134613dd5f122cccf1e6a560f4d63f2732d427d938f8481df175505aad94583bcb32c688737bb39a6df0625f903d6d93c03 + languageName: node + linkType: hard + "ejs@npm:^3.1.7": version: 3.1.8 resolution: "ejs@npm:3.1.8" @@ -5649,6 +5683,18 @@ __metadata: languageName: node linkType: hard +"jsonwebtoken@npm:^9.0.0": + version: 9.0.0 + resolution: "jsonwebtoken@npm:9.0.0" + dependencies: + jws: ^3.2.2 + lodash: ^4.17.21 + ms: ^2.1.1 + semver: ^7.3.8 + checksum: b9181cecf9df99f1dc0253f91ba000a1aa4d91f5816d1608c0dba61a5623726a0bfe200b51df25de18c1a6000825d231ad7ce2788aa54fd48dcb760ad9eb9514 + languageName: node + linkType: hard + "just-diff-apply@npm:^5.2.0": version: 5.5.0 resolution: "just-diff-apply@npm:5.5.0" @@ -5663,6 +5709,27 @@ __metadata: languageName: node linkType: hard +"jwa@npm:^1.4.1": + version: 1.4.1 + resolution: "jwa@npm:1.4.1" + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: ^5.0.1 + checksum: ff30ea7c2dcc61f3ed2098d868bf89d43701605090c5b21b5544b512843ec6fd9e028381a4dda466cbcdb885c2d1150f7c62e7168394ee07941b4098e1035e2f + languageName: node + linkType: hard + +"jws@npm:^3.2.2": + version: 3.2.2 + resolution: "jws@npm:3.2.2" + dependencies: + jwa: ^1.4.1 + safe-buffer: ^5.0.1 + checksum: f0213fe5b79344c56cd443428d8f65c16bf842dc8cb8f5aed693e1e91d79c20741663ad6eff07a6d2c433d1831acc9814e8d7bada6a0471fbb91d09ceb2bf5c2 + languageName: node + linkType: hard + "kind-of@npm:^6.0.2, kind-of@npm:^6.0.3": version: 6.0.3 resolution: "kind-of@npm:6.0.3" @@ -7785,7 +7852,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 @@ -7857,7 +7924,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7": +"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8": version: 7.3.8 resolution: "semver@npm:7.3.8" dependencies: