Skip to content

Commit

Permalink
add header/validation opts, algo selection and asymmetric key based s…
Browse files Browse the repository at this point in the history
…ignatures
  • Loading branch information
volf52 committed Jun 26, 2023
1 parent 4c4caff commit c9295eb
Show file tree
Hide file tree
Showing 13 changed files with 661 additions and 121 deletions.
5 changes: 5 additions & 0 deletions .changeset/mean-kangaroos-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@carbonteq/jwt": minor
---

Add asymmetric key based signatures, algorithm selection and other header & validation options
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ version = "0.0.0"
crate-type = ["cdylib"]

[dependencies]
jsonwebtoken = { version = "8.3.0", default-features = false }
jsonwebtoken = { version = "8.3.0" }
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
napi = { version = "2.12.2", default-features = false, features = [
"napi4",
Expand Down
15 changes: 1 addition & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Benchmarks were performed using [benchmark](https://www.npmjs.com/package/benchm
Machine Details:

- OS: Ubuntu 22.04
- CPU: Intel i7
- Processor: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz - Threads: 6 * 2
- Memory: 16G

**DISCLAIMER: Always take benchmarks like this with a grain of salt, as they may not always be indicative of good performance. And performance may not be the top thing to consider when choosing a package for solving your problem (unless the problem is that of performance itself). It would be best to perform these benchmarks on your own machine/deployment environment before making any decision.**
Expand All @@ -36,16 +36,3 @@ fast-jwt x 68,474 ops/sec ±0.79% (182 runs sampled)
SUITE <Verify Token>: Fastest is @carbonteq/jwt
```

### Decoding Token without Verification ([bench/decode.mjs](./bench/decode.mjs))

This package performs the worst here, as I am not actually bypassing verification completely, but you'll probably not use this method much, seeing as you would likely be performing decoding with verification

```
jsonwebtoken x 257,054 ops/sec ±1.27% (190 runs sampled)
jose x 921,471 ops/sec ±0.64% (188 runs sampled)
fast-jwt x 519,612 ops/sec ±1.71% (186 runs sampled)
@carbonteq/jwt x 137,408 ops/sec ±2.11% (184 runs sampled)
SUITE <Decode (No Verification)>: Fastest is jose
```
48 changes: 6 additions & 42 deletions __test__/jwt-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,54 +16,18 @@ test('should create a valid token from payload', async (t) => {
t.deepEqual(joseVerifyRes.payload?.data, testPayload);
});

test('should create a valid token from claims', async (t) => {
const claims = new Claims(testPayload, normalExpiresIn);
const token = client.signClaims(claims);
const joseVerifyRes = await jose.jwtVerify(token, secretEnc);

t.deepEqual(joseVerifyRes.payload?.data, testPayload);
});

test('created token should be valid', (t) => {
const token = client.sign(testPayload, normalExpiresIn);

t.deepEqual(client.verify(token).data, testPayload);
});

test('created (claims) token should be valid', (t) => {
const claims = new Claims(testPayload, normalExpiresIn);
const token = client.signClaims(claims);

t.deepEqual(client.verify(token).data, testPayload);
});
test('verifying after exp should throw error', (t) => {
const claims = new Claims(testPayload, 1);
claims.exp = 10;

test('verifying after exp should return false', (t) => {
const claims = new Claims(testPayload, 2);
claims.exp = 10; // In the past
const token = client.signClaims(claims);

t.throws(() => client.verify(token));
});

test('decode output should give the correct payload data', (t) => {
const token = client.sign(testPayload, normalExpiresIn);
const decoded = client.decode(token);

t.deepEqual(decoded.data, testPayload);
});

test('decode output should give the correct payload data for claims', (t) => {
const claims = new Claims(testPayload, normalExpiresIn);
const token = client.signClaims(claims);
const decoded = client.decode(token);

t.deepEqual(decoded.data, testPayload);
});

test('decoding after exp should return payload', (t) => {
const claims = new Claims(testPayload, 2);
claims.exp = 10; // In the past
const token = client.signClaims(claims);

t.deepEqual(client.decode(token).data, testPayload);
t.throws(() => {
client.verify(token);
});
});
171 changes: 170 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,196 @@

/* 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 ClaimOpts {
/** Recipient for which the JWT is intended */
aud?: string
/** Time at which the JWT was issued (as UTC timestamp, seconds from epoch time) */
iat?: Number
/** Issuer of JWT */
iss?: string
/** [JWT id] Unique identifier */
jti?: string
/** [not-before-time] Time before which the JWT must not be accepted for processing (as UTC timestamp, seconds from epoch time) */
nbf?: Number
/** Subject of JWT (the user) */
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<string>
/**
* 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 interface JwtClientInitOpts {
header?: Header
validation?: Validation
}
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<string>
/**
* 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<string>
/**
* 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<Algorithm>
/**
* 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<string>
/**
* Whether to validate the JWT signature.
*
* Defaults to `true`.
*/
validateSignature?: boolean
}
export class Claims {
data: Record<string, any>
/** Time after which the JWT expires (as UTC timestamp, seconds from epoch time) */
exp: Number
/** Recipient for which the JWT is intended */
aud?: string
/** Time at which the JWT was issued (as UTC timestamp, seconds from epoch time) */
iat?: Number
/** Issuer of JWT */
iss?: string
/** [JWT id] Unique identifier */
jti?: string
/** [not-before-time] Time before which the JWT must not be accepted for processing (as UTC timestamp, seconds from epoch time) */
nbf?: Number
/** Subject of JWT (the user) */
sub?: string
constructor(data: Record<string, any>, expiresInSeconds: number, opts?: ClaimOpts | undefined | null)
}
export class JwtClient {
constructor(secretKey: string | Buffer)
/** For symetric key based signatures */
constructor(secretKey: string | Buffer, opts?: JwtClientInitOpts | undefined | null)
/** For assymetric key based signatures */
static withPubPrivKeys(pubKey: string | Buffer, privKey: string | Buffer, opts?: JwtClientInitOpts | undefined | null): JwtClient
sign(data: Record<string, any>, expiresInSeconds: number, claimOpts?: ClaimOpts | undefined | null): string
signClaims(claims: Claims): string
verify(token: string): Claims
}
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}

const { Claims, JwtClient } = nativeBinding
const { Algorithm, Claims, JwtClient } = nativeBinding

module.exports.Algorithm = Algorithm
module.exports.Claims = Claims
module.exports.JwtClient = JwtClient
56 changes: 56 additions & 0 deletions src/algorithm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use napi::bindgen_prelude::{FromNapiValue, ToNapiValue};
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<Algorithm> 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
}
}
Loading

0 comments on commit c9295eb

Please sign in to comment.