1use std::fmt;
2
3use sofie_core::{Timestamp, Uuid};
4
5use crate::{Signed, SigningKeys};
6
7const LIFETIME_SECONDS: i64 = 60;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct Acting {
12 pub area: String,
13 pub principal: Uuid,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum TokenRefused {
18 Malformed,
19 Signature,
20 Expired,
21}
22
23impl fmt::Display for TokenRefused {
24 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25 formatter.write_str(match self {
26 Self::Malformed => "the principal token is malformed",
27 Self::Signature => "the principal token's signature does not verify",
28 Self::Expired => "the principal token has expired",
29 })
30 }
31}
32
33impl std::error::Error for TokenRefused {}
34
35impl SigningKeys {
36 #[must_use]
38 pub fn principal_token(&self, target: &str, acting: Option<&Acting>, now: Timestamp) -> String {
39 let expires = now.as_second() + LIFETIME_SECONDS;
40 let payload = match acting {
41 Some(acting) => format!("{expires}:{}:{}", acting.area, acting.principal),
42 None => format!("{expires}::"),
43 };
44 let signature = self.sign(Signed::PrincipalToken, signed(target, &payload).as_bytes());
45 format!("{payload}.{signature}")
46 }
47
48 pub fn verify_principal_token(
49 &self,
50 token: &str,
51 target: &str,
52 now: Timestamp,
53 ) -> std::result::Result<Option<Acting>, TokenRefused> {
54 let (payload, signature) = token.rsplit_once('.').ok_or(TokenRefused::Malformed)?;
55 if !self.verifies(
56 Signed::PrincipalToken,
57 signed(target, payload).as_bytes(),
58 signature,
59 ) {
60 return Err(TokenRefused::Signature);
61 }
62 let mut parts = payload.splitn(3, ':');
63 let expires: i64 = parts
64 .next()
65 .and_then(|expires| expires.parse().ok())
66 .ok_or(TokenRefused::Malformed)?;
67 let now = now.as_second();
69 if now >= expires || expires > now + 2 * LIFETIME_SECONDS {
70 return Err(TokenRefused::Expired);
71 }
72 match (parts.next(), parts.next()) {
73 (Some(""), Some("")) => Ok(None),
74 (Some(area), Some(principal)) => Ok(Some(Acting {
75 area: area.to_owned(),
76 principal: principal.parse().map_err(|_| TokenRefused::Malformed)?,
77 })),
78 _ => Err(TokenRefused::Malformed),
79 }
80 }
81}
82
83fn signed(target: &str, payload: &str) -> String {
84 format!("{target}\n{payload}")
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::session::tests::keys;
91
92 fn at(second: i64) -> Timestamp {
93 Timestamp::from_second(second).unwrap()
94 }
95
96 fn customer() -> Acting {
97 Acting {
98 area: "customer".to_owned(),
99 principal: Uuid::now_v7().unwrap(),
100 }
101 }
102
103 #[test]
104 fn a_token_carries_its_principal_to_its_target_until_it_expires() {
105 let acting = customer();
106 let token = keys(1).principal_token("/_sofie/contracts/a/B/c", Some(&acting), at(1000));
107 let verify = |target: &str, keys: SigningKeys, now: i64| {
108 keys.verify_principal_token(&token, target, at(now))
109 };
110
111 assert_eq!(
112 verify("/_sofie/contracts/a/B/c", keys(1), 1059),
113 Ok(Some(acting))
114 );
115 assert_eq!(
116 verify("/_sofie/contracts/a/B/d", keys(1), 1001),
117 Err(TokenRefused::Signature)
118 );
119 assert_eq!(
120 verify("/_sofie/contracts/a/B/c", keys(2), 1001),
121 Err(TokenRefused::Signature)
122 );
123 assert_eq!(
124 verify("/_sofie/contracts/a/B/c", keys(1), 1060),
125 Err(TokenRefused::Expired)
126 );
127 assert_eq!(
128 verify("/_sofie/contracts/a/B/c", keys(1), 800),
129 Err(TokenRefused::Expired)
130 );
131 }
132
133 #[test]
134 fn an_app_s_own_token_acts_for_nobody_and_a_forgery_is_refused() {
135 let token = keys(1).principal_token("/_sofie/feed", None, at(0));
136
137 assert_eq!(
138 keys(1).verify_principal_token(&token, "/_sofie/feed", at(1)),
139 Ok(None)
140 );
141 assert_eq!(
142 keys(1).verify_principal_token("no-dot", "/_sofie/feed", at(1)),
143 Err(TokenRefused::Malformed)
144 );
145 let link = keys(1).sign(Signed::Link, b"/_sofie/feed");
146 assert_eq!(
147 keys(1).verify_principal_token(&format!("x.{link}"), "/_sofie/feed", at(1)),
148 Err(TokenRefused::Signature)
149 );
150 }
151
152 #[test]
153 fn a_token_is_its_plain_payload_and_a_signature() {
154 let acting = customer();
155
156 let token = keys(1).principal_token("/_sofie/feed", Some(&acting), at(1000));
157
158 let prefix = format!("1060:customer:{}.", acting.principal);
159 assert!(token.starts_with(&prefix), "{token}");
160 }
161}