From 09f9db62b0c9ee9536b2b3b0521c5f0df8745eac Mon Sep 17 00:00:00 2001 From: Arslan Date: Sun, 11 Jun 2023 05:25:04 +0500 Subject: [PATCH] update Claims to accept any Js record type as payload, and add claim opts --- Cargo.toml | 8 +++- bench/sign.mjs | 4 +- index.d.ts | 23 ++++++++++-- src/claims.rs | 94 ++++++++++++++++++++++++++++++++++++++++++++++ src/jwt_client.rs | 75 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 96 +++-------------------------------------------- 6 files changed, 202 insertions(+), 98 deletions(-) create mode 100644 src/claims.rs create mode 100644 src/jwt_client.rs diff --git a/Cargo.toml b/Cargo.toml index d5247b8..46937ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,13 @@ crate-type = ["cdylib"] [dependencies] jsonwebtoken = { version = "8.3.0", default-features = false } # 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"] } +napi = { version = "2.12.2", default-features = false, features = [ + "napi4", + "serde-json", +] } napi-derive = "2.12.2" -serde = { version = "1.0.164", features = ["derive"] } +serde = { version = "1.0.164" } +serde_json = "1.0.96" [build-dependencies] napi-build = "2.0.1" diff --git a/bench/sign.mjs b/bench/sign.mjs index d79bee0..70aa8c4 100644 --- a/bench/sign.mjs +++ b/bench/sign.mjs @@ -52,7 +52,7 @@ suite defer: true, minSamples, fn: function (deferred) { - client.sign(JSON.stringify(payload), 1000); + client.sign(payload, 1000); deferred.resolve(); }, }) @@ -60,7 +60,7 @@ suite defer: true, minSamples, fn: function (deferred) { - const claims = new Claims(JSON.stringify(payload), 1000); + const claims = new Claims(payload, 1000); client.signClaims(claims); deferred.resolve(); }, diff --git a/index.d.ts b/index.d.ts index a7172e6..efac35c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,15 +3,30 @@ /* auto-generated by NAPI-RS */ +export interface ClaimOpts { + aud?: string + iat?: Number + iss?: string + jti?: string + nbf?: Number + sub?: string +} export class Claims { - constructor(data: string, expiresInMs: number) - get data(): string + data: Record + exp: Number + aud?: string + iat?: Number + iss?: string + jti?: string + nbf?: Number + sub?: string + constructor(data: Record, expiresInSeconds: number, opts?: ClaimOpts | undefined | null) } export class JwtClient { constructor(secretKey: string) static fromBufferKey(secretKey: Buffer): JwtClient - sign(payload: string, expiresInMs: number): string + sign(data: Record, expiresInSeconds: number, claimOpts?: ClaimOpts | undefined | null): string signClaims(claims: Claims): string verify(token: string): boolean - decode(token: string): Claims + verifyAndDecode(token: string): Claims } diff --git a/src/claims.rs b/src/claims.rs new file mode 100644 index 0000000..b61d733 --- /dev/null +++ b/src/claims.rs @@ -0,0 +1,94 @@ +use napi_derive::napi; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Number, Value}; + +#[napi(object)] +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct ClaimOpts { + // Recipient for which the JWT is intended + #[serde(skip_serializing_if = "Option::is_none")] + pub aud: 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 Default for ClaimOpts { +// fn default() -> Self { +// Self { +// aud: None, +// iat: None, +// iss: None, +// jti: None, +// nbf: None, +// sub: None, +// // aud: Default::default(), +// // iat: Default::default(), +// // iss: Default::default(), +// // jti: Default::default(), +// // nbf: Default::default(), +// // sub: Default::default(), +// } +// } +// } + +#[napi] +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub data: Map, + // Time after which the JWT expires (as UTC timestamp, seconds from epoch time) + pub exp: Number, + + // Recipient for which the JWT is intended + #[serde(skip_serializing_if = "Option::is_none")] + pub aud: 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, +} + +#[napi] +impl Claims { + #[napi(constructor)] + pub fn new(data: Map, expires_in_seconds: u32, opts: Option) -> Self { + let exp_val = jsonwebtoken::get_current_timestamp() + u64::from(expires_in_seconds); + let exp = Number::from(exp_val); + + let opts = opts.unwrap_or_default(); + + Self { + data, + exp, + aud: opts.aud, + iat: opts.iat, + iss: opts.iss, + jti: opts.jti, + nbf: opts.nbf, + sub: opts.sub, + } + } +} diff --git a/src/jwt_client.rs b/src/jwt_client.rs new file mode 100644 index 0000000..58d2015 --- /dev/null +++ b/src/jwt_client.rs @@ -0,0 +1,75 @@ +use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; +use napi::bindgen_prelude::Buffer; +use napi_derive::napi; + +use crate::claims::{ClaimOpts, Claims}; + +#[napi] +pub struct JwtClient { + encoding_key: EncodingKey, + decoding_key: DecodingKey, + header: Header, + validation: Validation, +} + +#[napi] +impl JwtClient { + #[inline] + fn from_key(key: &[u8]) -> Self { + let encoding_key = EncodingKey::from_secret(key); + let decoding_key = DecodingKey::from_secret(key); + + Self { + encoding_key, + decoding_key, + header: Header::default(), + validation: Validation::default(), + } + } + + #[napi(constructor)] + pub fn new(secret_key: String) -> Self { + let key = secret_key.as_bytes(); + Self::from_key(key) + } + + #[napi(factory)] + pub fn from_buffer_key(secret_key: Buffer) -> Self { + Self::from_key(&secret_key) + } + + #[napi] + pub fn sign( + &self, + data: serde_json::Map, + expires_in_seconds: u32, + claim_opts: Option, + ) -> String { + let claims = Claims::new(data, expires_in_seconds, claim_opts); + + self.sign_claims(&claims) + } + + #[napi] + pub fn sign_claims(&self, claims: &Claims) -> String { + jsonwebtoken::encode(&self.header, claims, &self.encoding_key).unwrap() + } + + #[napi] + pub fn verify(&self, token: String) -> bool { + jsonwebtoken::decode::(&token, &self.decoding_key, &self.validation).is_ok() + } + + #[napi] + pub fn verify_and_decode(&self, token: String) -> napi::Result { + let decode_res = jsonwebtoken::decode::(&token, &self.decoding_key, &self.validation); + + match decode_res { + Ok(token_data) => napi::Result::Ok(token_data.claims), + Err(e) => { + let err = napi::Error::new(napi::Status::Unknown, e.to_string()); + napi::Result::Err(err) + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index fc0bbbd..d780fcf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,94 +1,10 @@ #![deny(clippy::all)] -#[macro_use] -extern crate napi_derive; +// DISCLAIMER: Majority of the code and/or inspiration comes from @node-rs/jsonwebtoken. I have +// just updated and modified the code to meet my own use cases and API design -use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; -use napi::bindgen_prelude::Buffer; -use serde::{Deserialize, Serialize}; +mod claims; +mod jwt_client; -#[napi] -#[derive(Debug, Serialize, Deserialize)] -pub struct Claims { - data: String, - exp: u64, -} - -#[napi] -impl Claims { - #[napi(constructor)] - pub fn new(data: String, expires_in_ms: u32) -> Self { - let exp = jsonwebtoken::get_current_timestamp() + u64::from(expires_in_ms); - Self { data, exp } - } - - #[napi(getter)] - pub fn data(&self) -> String { - self.data.clone() - } -} - -#[napi] -struct JwtClient { - encoding_key: EncodingKey, - decoding_key: DecodingKey, - header: Header, - validation: Validation, -} - -#[napi] -impl JwtClient { - #[inline] - fn from_key(key: &[u8]) -> Self { - let encoding_key = EncodingKey::from_secret(key); - let decoding_key = DecodingKey::from_secret(key); - - Self { - encoding_key, - decoding_key, - header: Header::default(), - validation: Validation::default(), - } - } - - #[napi(constructor)] - pub fn new(secret_key: String) -> Self { - let key = secret_key.as_bytes(); - Self::from_key(key) - } - - #[napi(factory)] - pub fn from_buffer_key(secret_key: Buffer) -> Self { - Self::from_key(&secret_key) - } - - #[napi] - pub fn sign(&self, payload: String, expires_in_ms: u32) -> String { - let claims = Claims::new(payload, expires_in_ms); - - self.sign_claims(&claims) - } - - #[napi] - pub fn sign_claims(&self, claims: &Claims) -> String { - jsonwebtoken::encode(&self.header, claims, &self.encoding_key).unwrap() - } - - #[napi] - pub fn verify(&self, token: String) -> bool { - jsonwebtoken::decode::(&token, &self.decoding_key, &self.validation).is_ok() - } - - #[napi] - pub fn decode(&self, token: String) -> napi::Result { - let decode_res = jsonwebtoken::decode::(&token, &self.decoding_key, &self.validation); - - match decode_res { - Ok(token_data) => napi::Result::Ok(token_data.claims), - Err(e) => { - let err = napi::Error::new(napi::Status::Unknown, e.to_string()); - napi::Result::Err(err) - } - } - } -} +pub use claims::{ClaimOpts, Claims}; +pub use jwt_client::JwtClient;