use chrono::{Duration, Utc};
use jsonwebtoken::{Algorithm, EncodingKey, Header};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DamlCantonTokenError {
#[error("JWT signing failed: {0}")]
Jwt(#[from] jsonwebtoken::errors::Error),
}
pub type DamlCantonTokenResult<T> = std::result::Result<T, DamlCantonTokenError>;
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct DamlCantonClaims {
#[serde(skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aud: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iat: Option<i64>,
pub exp: i64,
}
#[derive(Debug, Clone)]
pub struct DamlCantonTokenBuilder {
claims: DamlCantonClaims,
}
impl DamlCantonTokenBuilder {
pub fn new_with_duration_secs(duration_secs: i64) -> Self {
let now = Utc::now();
let exp = (now + Duration::seconds(duration_secs)).timestamp();
Self {
claims: DamlCantonClaims {
iat: Some(now.timestamp()),
exp,
..DamlCantonClaims::default()
},
}
}
pub fn new_with_expiry(expiry_epoch_secs: i64) -> Self {
Self {
claims: DamlCantonClaims {
iat: Some(Utc::now().timestamp()),
exp: expiry_epoch_secs,
..DamlCantonClaims::default()
},
}
}
pub fn issuer(mut self, iss: impl Into<String>) -> Self {
self.claims.iss = Some(iss.into());
self
}
pub fn subject(mut self, sub: impl Into<String>) -> Self {
self.claims.sub = Some(sub.into());
self
}
pub fn audience(mut self, aud: impl Into<String>) -> Self {
self.claims.aud = Some(aud.into());
self
}
pub fn scope(mut self, scope: impl Into<String>) -> Self {
self.claims.scope = Some(scope.into());
self
}
pub fn claims(&self) -> &DamlCantonClaims {
&self.claims
}
pub fn new_hs256_unsafe_token(self, secret: impl AsRef<[u8]>) -> DamlCantonTokenResult<String> {
let header = Header::new(Algorithm::HS256);
let key = EncodingKey::from_secret(secret.as_ref());
Ok(jsonwebtoken::encode(&header, &self.claims, &key)?)
}
pub fn new_rs256_token(self, pem: impl AsRef<[u8]>) -> DamlCantonTokenResult<String> {
let header = Header::new(Algorithm::RS256);
let key = EncodingKey::from_rsa_pem(pem.as_ref())?;
Ok(jsonwebtoken::encode(&header, &self.claims, &key)?)
}
pub fn new_es256_token(self, pem: impl AsRef<[u8]>) -> DamlCantonTokenResult<String> {
let header = Header::new(Algorithm::ES256);
let key = EncodingKey::from_ec_pem(pem.as_ref())?;
Ok(jsonwebtoken::encode(&header, &self.claims, &key)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use jsonwebtoken::{DecodingKey, Validation, decode};
#[test]
fn hs256_roundtrip() {
let secret = "test-secret";
let token = DamlCantonTokenBuilder::new_with_duration_secs(60)
.issuer("https://example.invalid")
.subject("alice")
.audience("https://daml.com/jwt/aud/participant/sandbox")
.scope("daml_ledger_api")
.new_hs256_unsafe_token(secret)
.expect("sign");
let mut validation = Validation::new(Algorithm::HS256);
validation.set_audience(&["https://daml.com/jwt/aud/participant/sandbox"]);
let decoded = decode::<DamlCantonClaims>(&token, &DecodingKey::from_secret(secret.as_ref()), &validation)
.expect("decode");
let claims = decoded.claims;
assert_eq!(claims.sub.as_deref(), Some("alice"));
assert_eq!(claims.scope.as_deref(), Some("daml_ledger_api"));
assert_eq!(claims.iss.as_deref(), Some("https://example.invalid"));
}
#[test]
fn expiry_is_in_the_future() {
let builder = DamlCantonTokenBuilder::new_with_duration_secs(60);
let now = Utc::now().timestamp();
assert!(builder.claims().exp >= now);
assert!(builder.claims().exp <= now + 60 + 1);
}
}