1use std::error::Error;
2use std::fmt;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use biscuit::jwa::SignatureAlgorithm;
6use biscuit::{jws, ClaimsSet, RegisteredClaims, JWT};
7use secrecy::{ExposeSecret as _, SecretSlice};
8use serde::{Deserialize, Serialize};
9
10use crate::topic_selector::TopicSelector;
11
12pub const HS256_SECRET_KEY_LEN: usize = 64;
23
24#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
28#[serde(transparent)]
29pub struct PublisherJwt(JWT<MercureJwtClaims, biscuit::Empty>);
30
31#[derive(Clone)]
52pub struct PublisherJwtSecret(SecretSlice<u8>);
53
54#[derive(Debug)]
56#[non_exhaustive]
57pub struct PublisherJwtError {
58 kind: PublisherJwtErrorKind,
59 inner: Box<dyn Error + Send + Sync + 'static>,
60}
61
62#[derive(Debug)]
64#[non_exhaustive]
65pub enum PublisherJwtErrorKind {
66 EncodeAndSign,
68}
69
70#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
74#[serde(transparent)]
75pub struct SubscriberJwt(JWT<MercureJwtClaims, biscuit::Empty>);
76
77#[derive(Clone)]
98pub struct SubscriberJwtSecret(SecretSlice<u8>);
99
100#[derive(Copy, Clone, Debug)]
107pub struct SubscriberJwtMaxAge(std::time::Duration);
108
109#[derive(Debug)]
112#[non_exhaustive]
113pub struct TryFromDurationError {
114 kind: TryFromDurationErrorKind,
115}
116
117#[derive(Debug)]
120#[non_exhaustive]
121pub enum TryFromDurationErrorKind {
122 CookieLifetimeLimitExceeded,
126}
127
128#[derive(Debug)]
130#[non_exhaustive]
131pub struct SubscriberJwtError {
132 kind: SubscriberJwtErrorKind,
133 inner: Box<dyn Error + Send + Sync + 'static>,
134}
135
136#[derive(Debug)]
138#[non_exhaustive]
139pub enum SubscriberJwtErrorKind {
140 EncodeAndSign,
142}
143
144#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
145struct MercureJwtClaims {
146 mercure: MercureClaim,
147}
148
149#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
150struct MercureClaim {
151 #[serde(skip_serializing_if = "Option::is_none")]
157 publish: Option<Vec<TopicSelector>>,
158 #[serde(skip_serializing_if = "Option::is_none")]
164 subscribe: Option<Vec<TopicSelector>>,
165}
166
167impl fmt::Display for PublisherJwt {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 let jwt = self
170 .0
171 .encoded()
172 .expect("`PublisherJwt` should be already encoded");
173 write!(f, "{jwt}")
174 }
175}
176
177impl PublisherJwt {
178 pub fn new(
196 publisher_jwt_secret: &PublisherJwtSecret,
197 topic_selectors: Vec<TopicSelector>,
198 ) -> Result<Self, PublisherJwtError> {
199 let mercure_jwt = JWT::<MercureJwtClaims, biscuit::Empty>::new_decoded(
200 jws::RegisteredHeader {
201 algorithm: SignatureAlgorithm::HS256,
202 ..Default::default()
203 }
204 .into(),
205 ClaimsSet {
206 registered: RegisteredClaims::default(),
207 private: MercureJwtClaims {
208 mercure: MercureClaim {
209 publish: Some(topic_selectors),
210 subscribe: None,
211 },
212 },
213 },
214 );
215 let mercure_jwt = match mercure_jwt.encode(&jws::Secret::Bytes(
216 publisher_jwt_secret.0.expose_secret().to_vec(),
217 )) {
218 Ok(mercure_jwt) => mercure_jwt,
219 Err(biscuit::errors::Error::UnsupportedOperation) => {
220 panic!("`mercure_jwt` should not already be encoded");
221 },
222 Err(err) => {
223 return Err(PublisherJwtError {
224 kind: PublisherJwtErrorKind::EncodeAndSign,
225 inner: err.into(),
226 })?;
227 },
228 };
229
230 Ok(Self(mercure_jwt))
231 }
232}
233
234impl From<Vec<u8>> for PublisherJwtSecret {
235 fn from(vec: Vec<u8>) -> Self {
236 Self(SecretSlice::from(vec))
237 }
238}
239
240impl fmt::Display for PublisherJwtError {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match self.kind {
243 PublisherJwtErrorKind::EncodeAndSign => {
244 let err = self.inner.downcast_ref::<biscuit::errors::Error>().unwrap();
245 write!(f, "failed to encode and sign JWT: {err}")
246 },
247 }
248 }
249}
250
251impl Error for PublisherJwtError {
252 fn source(&self) -> Option<&(dyn Error + 'static)> {
253 match self.kind {
254 PublisherJwtErrorKind::EncodeAndSign => {
255 let err = self.inner.downcast_ref::<biscuit::errors::Error>().unwrap();
256 Some(err)
257 },
258 }
259 }
260}
261
262impl PublisherJwtError {
263 #[must_use]
265 pub const fn kind(&self) -> &PublisherJwtErrorKind {
266 &self.kind
267 }
268}
269
270impl fmt::Display for SubscriberJwt {
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272 let jwt = self
273 .0
274 .encoded()
275 .expect("`SubscriberJwt` should be already encoded");
276 write!(f, "{jwt}")
277 }
278}
279
280impl SubscriberJwt {
281 pub fn new(
315 subscriber_jwt_secret: &SubscriberJwtSecret,
316 subscriber_jwt_max_age: Option<SubscriberJwtMaxAge>,
317 topic_selectors: Vec<TopicSelector>,
318 ) -> Result<Self, SubscriberJwtError> {
319 let mercure_jwt = JWT::<MercureJwtClaims, biscuit::Empty>::new_decoded(
320 jws::RegisteredHeader {
321 algorithm: SignatureAlgorithm::HS256,
322 ..Default::default()
323 }
324 .into(),
325 ClaimsSet {
326 registered: RegisteredClaims {
327 expiry: subscriber_jwt_max_age.map(|subscriber_jwt_max_age| {
328 let expires_at = SystemTime::now()
329 .checked_add(subscriber_jwt_max_age.0)
330 .expect("`expires_at` should fit in `SystemTime`");
331 let timestamp: i64 = expires_at
332 .duration_since(UNIX_EPOCH)
333 .unwrap()
334 .as_secs()
335 .try_into()
336 .expect("`timestamp` should fit in `i64`");
337 timestamp.into()
338 }),
339 ..Default::default()
340 },
341 private: MercureJwtClaims {
342 mercure: MercureClaim {
343 publish: None,
344 subscribe: Some(topic_selectors),
345 },
346 },
347 },
348 );
349 let mercure_jwt = match mercure_jwt.encode(&jws::Secret::Bytes(
350 subscriber_jwt_secret.0.expose_secret().to_vec(),
351 )) {
352 Ok(mercure_jwt) => mercure_jwt,
353 Err(biscuit::errors::Error::UnsupportedOperation) => {
354 panic!("`mercure_jwt` should not already be encoded");
355 },
356 Err(err) => {
357 return Err(SubscriberJwtError {
358 kind: SubscriberJwtErrorKind::EncodeAndSign,
359 inner: err.into(),
360 })?;
361 },
362 };
363
364 Ok(Self(mercure_jwt))
365 }
366}
367
368impl From<Vec<u8>> for SubscriberJwtSecret {
369 fn from(vec: Vec<u8>) -> Self {
370 Self(SecretSlice::from(vec))
371 }
372}
373
374impl TryFrom<std::time::Duration> for SubscriberJwtMaxAge {
375 type Error = TryFromDurationError;
376
377 fn try_from(duration: std::time::Duration) -> Result<Self, Self::Error> {
378 if duration > crate::cookie::MAX_AGE_LIMIT {
379 return Err(Self::Error {
380 kind: TryFromDurationErrorKind::CookieLifetimeLimitExceeded,
381 })?;
382 }
383
384 Ok(Self(duration))
385 }
386}
387
388impl From<SubscriberJwtMaxAge> for std::time::Duration {
389 fn from(subscriber_jwt_max_age: SubscriberJwtMaxAge) -> Self {
390 subscriber_jwt_max_age.0
391 }
392}
393
394impl SubscriberJwtMaxAge {
395 pub const MAX: Self = Self(crate::cookie::MAX_AGE_LIMIT);
396}
397
398impl fmt::Display for TryFromDurationError {
399 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400 match self.kind {
401 TryFromDurationErrorKind::CookieLifetimeLimitExceeded => {
402 const SECONDS_IN_DAYS: u64 = 60 * 60 * 24;
403 const LIMIT_DAYS: u64 = crate::cookie::MAX_AGE_LIMIT.as_secs() / SECONDS_IN_DAYS;
404 write!(f, "max-age must not be more than {LIMIT_DAYS} days")
405 },
406 }
407 }
408}
409
410impl Error for TryFromDurationError {}
411
412impl TryFromDurationError {
413 #[must_use]
415 pub const fn kind(&self) -> &TryFromDurationErrorKind {
416 &self.kind
417 }
418}
419
420impl fmt::Display for SubscriberJwtError {
421 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422 match self.kind {
423 SubscriberJwtErrorKind::EncodeAndSign => {
424 let err = self.inner.downcast_ref::<biscuit::errors::Error>().unwrap();
425 write!(f, "failed to encode and sign JWT: {err}")
426 },
427 }
428 }
429}
430
431impl Error for SubscriberJwtError {
432 fn source(&self) -> Option<&(dyn Error + 'static)> {
433 match self.kind {
434 SubscriberJwtErrorKind::EncodeAndSign => {
435 let err = self.inner.downcast_ref::<biscuit::errors::Error>().unwrap();
436 Some(err)
437 },
438 }
439 }
440}
441
442impl SubscriberJwtError {
443 #[must_use]
445 pub const fn kind(&self) -> &SubscriberJwtErrorKind {
446 &self.kind
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use anyhow::{Context as _, Result};
453
454 use super::*;
455
456 #[test]
457 fn it_creates_publisher_jwt_with_wildcard() -> Result<()> {
458 let publisher_jwt_secret =
459 PublisherJwtSecret::from(b"!ChangeThisMercureHubJWTSecretKey!".to_vec());
460 let publisher_jwt =
461 PublisherJwt::new(&publisher_jwt_secret, vec![TopicSelector::Wildcard])?;
462 let publisher_jwt = publisher_jwt.0.encoded().context("JWT is not encoded")?;
463 assert_eq!(
464 publisher_jwt.to_string(),
465 "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.\
466 a8cjcSRUAcHdnGNMKifA4BK5epRXxQI0UBp2XpNrBdw"
467 );
468 Ok(())
469 }
470
471 #[test]
472 fn it_creates_publisher_jwt_with_uri_template() -> Result<()> {
473 let publisher_jwt_secret =
474 PublisherJwtSecret::from(b"!ChangeThisMercureHubJWTSecretKey!".to_vec());
475 let publisher_jwt =
476 PublisherJwt::new(&publisher_jwt_secret, vec![TopicSelector::UriTemplate(
477 "https://example.com/books/{book_id}".try_into()?,
478 )])?;
479 let publisher_jwt = publisher_jwt.0.encoded().context("JWT is not encoded")?;
480 assert_eq!(
481 publisher_jwt.to_string(),
482 "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\
483 eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9ib29rcy97Ym9va19pZH0iXX19.\
484 eyl-c2BUWrnx6VZNBfKWnTI2t28yO5NcHUgn83womNE"
485 );
486 Ok(())
487 }
488
489 #[test]
490 fn it_creates_subscriber_jwt_with_wildcard() -> Result<()> {
491 let subscriber_jwt_secret =
492 SubscriberJwtSecret::from(b"!ChangeThisMercureHubJWTSecretKey!".to_vec());
493 let subscriber_jwt =
494 SubscriberJwt::new(&subscriber_jwt_secret, None, vec![TopicSelector::Wildcard])?;
495 let subscriber_jwt = subscriber_jwt.0.encoded().context("JWT is not encoded")?;
496 assert_eq!(
497 subscriber_jwt.to_string(),
498 "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIqIl19fQ.\
499 TMzyyYqIldgBLhqpiOR9a_HBk7iiP60Pb4X65ICaouA"
500 );
501 Ok(())
502 }
503
504 #[test]
505 fn it_creates_subscriber_jwt_with_uri_template() -> Result<()> {
506 let subscriber_jwt_secret =
507 SubscriberJwtSecret::from(b"!ChangeThisMercureHubJWTSecretKey!".to_vec());
508 let subscriber_jwt = SubscriberJwt::new(&subscriber_jwt_secret, None, vec![
509 TopicSelector::UriTemplate("https://example.com/users/1/books/{book_id}".try_into()?),
510 ])?;
511 let subscriber_jwt = subscriber_jwt.0.encoded().context("JWT is not encoded")?;
512 assert_eq!(
513 subscriber_jwt.to_string(),
514 "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\
515 eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyJodHRwczovL2V4YW1wbGUuY29tL3VzZXJzLzEvYm9va3Mve2Jvb2tfaWR9Il19fQ.\
516 8ctfXioRle93VxIwoCxikZtTBBSGrL_WtkXrS5wVPDY"
517 );
518 Ok(())
519 }
520}