1use chrono::Utc;
2use itertools::Itertools;
3use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7#[derive(Default, Clone)]
76pub struct DamlSandboxTokenBuilder {
77 ledger_id: Option<String>,
78 participant_id: Option<String>,
79 application_id: Option<String>,
80 admin: bool,
81 act_as: Vec<String>,
82 read_as: Vec<String>,
83 expiry: i64,
84}
85
86impl DamlSandboxTokenBuilder {
87 pub fn new_with_duration_secs(secs: i64) -> Self {
89 Self {
90 expiry: Utc::now().timestamp() + secs,
91 ..Self::default()
92 }
93 }
94
95 pub fn new_with_expiry(timestamp: i64) -> Self {
97 Self {
98 expiry: timestamp,
99 ..Self::default()
100 }
101 }
102
103 pub fn ledger_id(self, ledger_id: impl Into<String>) -> Self {
105 Self {
106 ledger_id: Some(ledger_id.into()),
107 ..self
108 }
109 }
110
111 pub fn participant_id(self, participant_id: impl Into<String>) -> Self {
113 Self {
114 participant_id: Some(participant_id.into()),
115 ..self
116 }
117 }
118
119 pub fn application_id(self, application_id: impl Into<String>) -> Self {
121 Self {
122 application_id: Some(application_id.into()),
123 ..self
124 }
125 }
126
127 pub fn admin(self, admin: bool) -> Self {
129 Self {
130 admin,
131 ..self
132 }
133 }
134
135 pub fn act_as(self, act_as: Vec<String>) -> Self {
137 Self {
138 act_as,
139 ..self
140 }
141 }
142
143 pub fn read_as(self, read_as: Vec<String>) -> Self {
145 Self {
146 read_as,
147 ..self
148 }
149 }
150
151 pub fn new_hs256_unsafe_token(self, secret: impl AsRef<[u8]>) -> DamlSandboxAuthResult<String> {
157 let encoding_key = &EncodingKey::from_secret(secret.as_ref());
158 self.generate_token(Algorithm::HS256, encoding_key)
159 }
160
161 pub fn new_rs256_token(self, rsa_pem: impl AsRef<[u8]>) -> DamlSandboxAuthResult<String> {
165 let encoding_key = &EncodingKey::from_rsa_pem(rsa_pem.as_ref())?;
166 self.generate_token(Algorithm::RS256, encoding_key)
167 }
168
169 pub fn new_ec256_token(self, ec_pem: impl AsRef<[u8]>) -> DamlSandboxAuthResult<String> {
173 let encoding_key = &EncodingKey::from_ec_pem(ec_pem.as_ref())?;
174 self.generate_token(Algorithm::ES256, encoding_key)
175 }
176
177 pub fn claims_json(&self) -> DamlSandboxAuthResult<String> {
179 Ok(serde_json::to_string(&(*self).clone().into_token())?)
180 }
181
182 fn generate_token(self, algorithm: Algorithm, encoding_key: &EncodingKey) -> DamlSandboxAuthResult<String> {
183 Ok(jsonwebtoken::encode(&Header::new(algorithm), &self.into_token(), encoding_key)?)
184 }
185
186 fn into_token(self) -> DamlSandboxAuthToken {
187 DamlSandboxAuthToken {
188 details: DamlSandboxAuthDetails {
189 ledger_id: self.ledger_id,
190 participant_id: self.participant_id,
191 application_id: self.application_id,
192 admin: self.admin,
193 act_as: self.act_as,
194 read_as: self.read_as,
195 },
196 exp: self.expiry,
197 }
198 }
199}
200
201pub type DamlSandboxAuthResult<T> = Result<T, DamlSandboxAuthError>;
203
204#[derive(Error, Debug)]
206pub enum DamlSandboxAuthError {
207 #[error("failed to create JSON Web Token: {0}")]
208 JsonWebTokenError(#[from] jsonwebtoken::errors::Error),
209 #[error("failed to serialize JSON Web Token: {0}")]
210 JsonSerializeError(#[from] serde_json::error::Error),
211 #[error("unsupported algorithm")]
212 UnsupportedAlgorithm,
213}
214
215#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
217pub struct DamlSandboxAuthToken {
218 #[serde(rename = "https://daml.com/ledger-api")]
219 details: DamlSandboxAuthDetails,
220 exp: i64,
221}
222
223impl DamlSandboxAuthToken {
224 pub fn parse_jwt(token: &str, key: impl AsRef<[u8]>) -> DamlSandboxAuthResult<Self> {
226 let algorithm = jsonwebtoken::decode_header(token)?.alg;
227 let decoding_key = match algorithm {
228 Algorithm::ES256 => DecodingKey::from_ec_pem(key.as_ref())?,
229 Algorithm::RS256 => DecodingKey::from_rsa_pem(key.as_ref())?,
230 Algorithm::HS256 => DecodingKey::from_secret(key.as_ref()),
231 _ => return Err(DamlSandboxAuthError::UnsupportedAlgorithm),
232 };
233 Ok(jsonwebtoken::decode::<Self>(token, &decoding_key, &Validation::new(algorithm))?.claims)
234 }
235
236 pub fn parse_jwt_no_validation(token: &str) -> DamlSandboxAuthResult<Self> {
238 let algorithm = jsonwebtoken::decode_header(token)?.alg;
239 let mut validation = Validation::new(algorithm);
240 validation.insecure_disable_signature_validation();
241 Ok(jsonwebtoken::decode::<Self>(token, &DecodingKey::from_secret(&[]), &validation)?.claims)
242 }
243
244 pub fn expiry(&self) -> i64 {
246 self.exp
247 }
248
249 pub fn ledger_id(&self) -> Option<&str> {
251 self.details.ledger_id.as_deref()
252 }
253
254 pub fn participant_id(&self) -> Option<&str> {
256 self.details.participant_id.as_deref()
257 }
258
259 pub fn application_id(&self) -> Option<&str> {
261 self.details.application_id.as_deref()
262 }
263
264 pub fn admin(&self) -> bool {
266 self.details.admin
267 }
268
269 pub fn act_as(&self) -> &[String] {
271 self.details.act_as.as_slice()
272 }
273
274 pub fn read_as(&self) -> &[String] {
276 self.details.read_as.as_slice()
277 }
278
279 pub fn parties(&self) -> impl Iterator<Item = &str> {
281 self.details.read_as.iter().chain(self.details.act_as.iter()).map(AsRef::as_ref).unique()
282 }
283
284 pub fn single_party(&self) -> Option<&str> {
286 match (self.details.act_as.as_slice(), self.details.read_as.as_slice()) {
287 ([p], []) | ([], [p]) => Some(p),
288 ([p1], [p2]) if p1 == p2 => Some(p1),
289 _ => None,
290 }
291 }
292}
293
294#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
295#[serde(rename_all = "camelCase")]
296struct DamlSandboxAuthDetails {
297 ledger_id: Option<String>,
298 participant_id: Option<String>,
299 application_id: Option<String>,
300 admin: bool,
301 act_as: Vec<String>,
302 read_as: Vec<String>,
303}
304
305#[cfg(test)]
306mod tests {
307 use super::{DamlSandboxAuthDetails, DamlSandboxAuthResult, DamlSandboxAuthToken, DamlSandboxTokenBuilder};
308 use jsonwebtoken::{encode, EncodingKey, Header};
309
310 #[test]
311 fn test_serialise() {
312 let token = DamlSandboxAuthToken {
313 details: DamlSandboxAuthDetails {
314 ledger_id: Some("test-sandbox".to_owned()),
315 participant_id: None,
316 application_id: None,
317 admin: true,
318 act_as: vec!["Alice".to_owned(), "Bob".to_owned()],
319 read_as: vec!["Alice".to_owned(), "Bob".to_owned()],
320 },
321 exp: 1_804_287_349,
322 };
323 let serialized = serde_json::to_string(&token).unwrap();
324 assert_eq!(
325 r#"{"https://daml.com/ledger-api":{"ledgerId":"test-sandbox","participantId":null,"applicationId":null,"admin":true,"actAs":["Alice","Bob"],"readAs":["Alice","Bob"]},"exp":1804287349}"#,
326 serialized
327 );
328 }
329
330 #[test]
331 fn test_encode_with_secret() {
332 let token = DamlSandboxAuthToken {
333 details: DamlSandboxAuthDetails {
334 ledger_id: Some("sandbox".to_owned()),
335 participant_id: None,
336 application_id: None,
337 admin: true,
338 act_as: vec!["Alice".to_owned(), "Bob".to_owned()],
339 read_as: vec!["Alice".to_owned(), "Bob".to_owned()],
340 },
341 exp: 1_804_287_349,
342 };
343 let token_str =
344 encode(&Header::default(), &token, &EncodingKey::from_secret("testsecret".as_ref())).expect("token");
345 assert_eq!(
346 r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJodHRwczovL2RhbWwuY29tL2xlZGdlci1hcGkiOnsibGVkZ2VySWQiOiJzYW5kYm94IiwicGFydGljaXBhbnRJZCI6bnVsbCwiYXBwbGljYXRpb25JZCI6bnVsbCwiYWRtaW4iOnRydWUsImFjdEFzIjpbIkFsaWNlIiwiQm9iIl0sInJlYWRBcyI6WyJBbGljZSIsIkJvYiJdfSwiZXhwIjoxODA0Mjg3MzQ5fQ.Y5hlJvK7h_9rancE_iO_3tGKWl8xsFVNLPJw9iNBreY"#,
347 token_str
348 );
349 }
350
351 #[test]
352 fn test_builder_with_secret() -> DamlSandboxAuthResult<()> {
353 let token_str = DamlSandboxTokenBuilder::new_with_expiry(1_804_287_349)
354 .ledger_id("sandbox")
355 .admin(true)
356 .act_as(vec!["Alice".to_owned(), "Bob".to_owned()])
357 .read_as(vec!["Alice".to_owned(), "Bob".to_owned()])
358 .new_hs256_unsafe_token("testsecret")?;
359 assert_eq!(
360 r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJodHRwczovL2RhbWwuY29tL2xlZGdlci1hcGkiOnsibGVkZ2VySWQiOiJzYW5kYm94IiwicGFydGljaXBhbnRJZCI6bnVsbCwiYXBwbGljYXRpb25JZCI6bnVsbCwiYWRtaW4iOnRydWUsImFjdEFzIjpbIkFsaWNlIiwiQm9iIl0sInJlYWRBcyI6WyJBbGljZSIsIkJvYiJdfSwiZXhwIjoxODA0Mjg3MzQ5fQ.Y5hlJvK7h_9rancE_iO_3tGKWl8xsFVNLPJw9iNBreY"#,
361 token_str
362 );
363 Ok(())
364 }
365
366 #[test]
367 fn test_decode_no_validation() -> DamlSandboxAuthResult<()> {
368 let jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJodHRwczovL2RhbWwuY29tL2xlZGdlci1hcGkiOnsibGVkZ2VySWQiOiJ3YWxsY2xvY2stdW5zZWN1cmVkLXNhbmRib3giLCJwYXJ0aWNpcGFudElkIjoiQWxpY2UiLCJhcHBsaWNhdGlvbklkIjoiZGVtbyIsImFkbWluIjpmYWxzZSwiYWN0QXMiOlsiQWxpY2UiXSwicmVhZEFzIjpbXX0sImV4cCI6MTgwNDI4NzM0OX0.dlJ0dxeOwEYfAmuuKngRNsibci-w0TSdn1NZRmFjHT9aoW8wsAeuYuLXjtx7e6oQaT-m_rlJqgDdmfTXHhE_t9LkngtpgcG8g0h7sCEq7O-SYGiB1B1jzTX2ZO0QHp6Xdes7QkVnyMn2vwaDv8KWAurchGOJUwDVpgU7k2JKpnFh1ui-AMf0rmP7yu7rSZchD-NTg_1_-RL0rgbwzmWJWL81n2zz213yQW5w_dqhitueFeluyppuZgzNQfni8jtdZF32trHwocg8C6zI9DdqmJSl-TsykQPV8z5wLSOSKCCFwnecEZ0QvZSxEWycNAQvNJTAMiKFcagiYGEeIDc4yQ";
369 let decoded = DamlSandboxAuthToken::parse_jwt_no_validation(jwt_token)?;
370 let token = DamlSandboxAuthToken {
371 details: DamlSandboxAuthDetails {
372 ledger_id: Some("wallclock-unsecured-sandbox".to_owned()),
373 participant_id: Some("Alice".to_owned()),
374 application_id: Some("demo".to_owned()),
375 admin: false,
376 act_as: vec!["Alice".to_owned()],
377 read_as: vec![],
378 },
379 exp: 1_804_287_349,
380 };
381 assert_eq!(decoded, token);
382 Ok(())
383 }
384
385 #[test]
386 fn test_decode() -> DamlSandboxAuthResult<()> {
387 let jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJodHRwczovL2RhbWwuY29tL2xlZGdlci1hcGkiOnsibGVkZ2VySWQiOiJzYW5kYm94IiwicGFydGljaXBhbnRJZCI6bnVsbCwiYXBwbGljYXRpb25JZCI6bnVsbCwiYWRtaW4iOnRydWUsImFjdEFzIjpbIkFsaWNlIiwiQm9iIl0sInJlYWRBcyI6WyJBbGljZSIsIkJvYiJdfSwiZXhwIjoxODA0Mjg3MzQ5fQ.Y5hlJvK7h_9rancE_iO_3tGKWl8xsFVNLPJw9iNBreY";
388 let decoded = DamlSandboxAuthToken::parse_jwt(jwt_token, "testsecret")?;
389 let token = DamlSandboxAuthToken {
390 details: DamlSandboxAuthDetails {
391 ledger_id: Some("sandbox".to_owned()),
392 participant_id: None,
393 application_id: None,
394 admin: true,
395 act_as: vec!["Alice".to_owned(), "Bob".to_owned()],
396 read_as: vec!["Alice".to_owned(), "Bob".to_owned()],
397 },
398 exp: 1_804_287_349,
399 };
400 assert_eq!(decoded, token);
401 Ok(())
402 }
403
404 #[test]
405 fn test_parties() -> DamlSandboxAuthResult<()> {
406 let jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJodHRwczovL2RhbWwuY29tL2xlZGdlci1hcGkiOnsibGVkZ2VySWQiOiJzYW5kYm94LXN0YXRpYyIsInBhcnRpY2lwYW50SWQiOm51bGwsImFwcGxpY2F0aW9uSWQiOm51bGwsImFkbWluIjpmYWxzZSwiYWN0QXMiOlsiQWxpY2UiLCJCb2IiXSwicmVhZEFzIjpbXX0sImV4cCI6MTgwNDI4NzM0OX0.EnjK8is1g0I8BGVu1ZPgSSFRW0WKEGcwdIBLiPPQmo_xcMngu_KzOKADezRJap6B_10IMwRn95b9A3vpBT_E8fZQ95BTMbL8yaODrSjus6feLuKxPhZMy0UgPZjReuPu2x1BsjNWZvl5UXGNz8NMs21X7Uh4fEk5ehdLqctiTzsrjUjVCz-KJSjsJafU-F0VnJJgvb3A2QQprfDg5L7_-yv7HsEZxJov-nJ29ycsYfPfQ1JlwetNoBgCPA2C3QZLusvHhGGJPuot2cw1JG43VxpOTYc9slqSWuC5gZhGDAOEEsslb0LeQU_JjLh4JjFT4iROEyj9ARdqD7tCxm0h2A";
407 let token = DamlSandboxAuthToken::parse_jwt_no_validation(jwt_token)?;
408 assert_eq!(token.parties().collect::<Vec<_>>(), vec!["Alice", "Bob"]);
409 Ok(())
410 }
411}