Skip to content

Commit

Permalink
update Claims to accept any Js record type as payload, and add claim …
Browse files Browse the repository at this point in the history
…opts
  • Loading branch information
volf52 committed Jun 11, 2023
1 parent 8431bda commit 09f9db6
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 98 deletions.
8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions bench/sign.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ suite
defer: true,
minSamples,
fn: function (deferred) {
client.sign(JSON.stringify(payload), 1000);
client.sign(payload, 1000);
deferred.resolve();
},
})
.add('@carbonteq/jwt#signClaims', {
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();
},
Expand Down
23 changes: 19 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>
exp: Number
aud?: string
iat?: Number
iss?: string
jti?: string
nbf?: Number
sub?: string
constructor(data: Record<string, any>, 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<string, any>, expiresInSeconds: number, claimOpts?: ClaimOpts | undefined | null): string
signClaims(claims: Claims): string
verify(token: string): boolean
decode(token: string): Claims
verifyAndDecode(token: string): Claims
}
94 changes: 94 additions & 0 deletions src/claims.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
// Time at which the JWT was issued (as UTC timestamp, seconds from epoch time)
#[serde(skip_serializing_if = "Option::is_none")]
pub iat: Option<Number>,
// Issuer of JWT
#[serde(skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
// [JWT id] Unique identifier
#[serde(skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
// [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<Number>,
// Subject of JWT (the user)
#[serde(skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
}

// 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<String, Value>,
// 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<String>,
// Time at which the JWT was issued (as UTC timestamp, seconds from epoch time)
#[serde(skip_serializing_if = "Option::is_none")]
pub iat: Option<Number>,
// Issuer of JWT
#[serde(skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
// [JWT id] Unique identifier
#[serde(skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
// [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<Number>,
// Subject of JWT (the user)
#[serde(skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
}

#[napi]
impl Claims {
#[napi(constructor)]
pub fn new(data: Map<String, Value>, expires_in_seconds: u32, opts: Option<ClaimOpts>) -> 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,
}
}
}
75 changes: 75 additions & 0 deletions src/jwt_client.rs
Original file line number Diff line number Diff line change
@@ -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<String, serde_json::Value>,
expires_in_seconds: u32,
claim_opts: Option<ClaimOpts>,
) -> 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::<Claims>(&token, &self.decoding_key, &self.validation).is_ok()
}

#[napi]
pub fn verify_and_decode(&self, token: String) -> napi::Result<Claims> {
let decode_res = jsonwebtoken::decode::<Claims>(&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)
}
}
}
}
96 changes: 6 additions & 90 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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::<Claims>(&token, &self.decoding_key, &self.validation).is_ok()
}

#[napi]
pub fn decode(&self, token: String) -> napi::Result<Claims> {
let decode_res = jsonwebtoken::decode::<Claims>(&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;

0 comments on commit 09f9db6

Please sign in to comment.