1use std::{
2 collections::BTreeMap,
3 time::{Duration, SystemTime, UNIX_EPOCH},
4};
5
6use base64::{
7 Engine as _,
8 engine::general_purpose::{STANDARD, URL_SAFE},
9};
10use hmac::{Hmac, KeyInit, Mac};
11use o_sfu_protocol::wire::{UserId, UserPermissions};
12use o_sfu_rfc::jwt::{ALGORITHM_HS256, JwtHeader, TYPE_JWT, URL_SAFE_NO_PAD};
13pub use o_sfu_rfc::jwt::{NumericDate, RegisteredJwtClaims};
14use serde::{Deserialize, Serialize, de::DeserializeOwned};
15use sha2::Sha256;
16use thiserror::Error;
17
18type HmacSha256 = Hmac<Sha256>;
19
20pub(super) struct AuthProof(());
22
23pub const MAX_JWT_TOKEN_BYTES: usize = 16 * 1024;
24
25#[derive(Debug, Clone, PartialEq, Eq, Error)]
26pub enum AuthenticationError {
27 #[error("invalid JWT format")]
28 InvalidJwtFormat,
29 #[error("JWT token exceeds maximum byte length")]
30 TokenTooLarge { actual: usize, limit: usize },
31 #[error("invalid base64 encoding")]
32 InvalidBase64Encoding,
33 #[error("invalid JSON payload")]
34 InvalidJsonPayload,
35 #[error("unsupported JWT algorithm: {0}")]
36 UnsupportedAlgorithm(String),
37 #[error("invalid JWT signature")]
38 InvalidSignature,
39 #[error("token expired")]
40 TokenExpired,
41 #[error("token not valid yet")]
42 TokenNotYetValid,
43 #[error("token issued in the future")]
44 TokenIssuedInFuture,
45}
46
47const MAX_IAT_FUTURE_SKEW: Duration = Duration::from_mins(1);
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct HttpRoomClaims {
56 #[serde(flatten)]
57 pub registered: RegisteredJwtClaims,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub key: Option<String>,
60 #[serde(rename = "keySeed", skip_serializing_if = "Option::is_none")]
61 pub key_seed: Option<String>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct HttpDisconnectClaims {
66 #[serde(flatten)]
67 pub registered: RegisteredJwtClaims,
68 #[serde(rename = "userIdsByRoom", alias = "sessionIdsByChannel")]
69 pub user_ids_by_room: BTreeMap<String, Vec<UserId>>,
70}
71
72impl HttpDisconnectClaims {
73 pub fn normalize_runtime_user_ids(&mut self) {
74 for user_ids in self.user_ids_by_room.values_mut() {
75 for user_id in user_ids {
76 *user_id = user_id.runtime_normalized();
77 }
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct WebSocketConnectClaims {
84 #[serde(flatten)]
85 pub registered: RegisteredJwtClaims,
86 #[serde(rename = "room_id", alias = "sfu_channel_uuid")]
87 pub room_id: String,
88 #[serde(rename = "user_id", alias = "session_id")]
89 pub user_id: UserId,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub label: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub permissions: Option<UserPermissions>,
94}
95
96impl WebSocketConnectClaims {
97 pub fn normalize_runtime_user_id(&mut self) {
98 self.user_id = self.user_id.runtime_normalized();
99 }
100}
101
102#[must_use]
103pub(crate) fn duration_since_epoch() -> Duration {
104 SystemTime::now()
105 .duration_since(UNIX_EPOCH)
106 .unwrap_or(Duration::ZERO)
107}
108
109pub fn sign<T>(claims: &T, key_b64: &str) -> Result<String, AuthenticationError>
113where
114 T: Serialize,
115{
116 let key = decode_key(key_b64)?;
117 let header = JwtHeader {
118 alg: ALGORITHM_HS256.to_owned(),
119 typ: Some(TYPE_JWT.to_owned()),
120 };
121 let header_json =
122 serde_json::to_vec(&header).map_err(|_error| AuthenticationError::InvalidJsonPayload)?;
123 let claims_json =
124 serde_json::to_vec(claims).map_err(|_error| AuthenticationError::InvalidJsonPayload)?;
125 let header_b64 = URL_SAFE_NO_PAD.encode(header_json);
126 let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json);
127 let signed_data = format!("{header_b64}.{claims_b64}");
128 let signature = sign_hs256(signed_data.as_bytes(), &key)?;
129 let signature_b64 = URL_SAFE_NO_PAD.encode(signature);
130 Ok(format!("{signed_data}.{signature_b64}"))
131}
132
133pub fn verify<T>(token: &str, key_b64: &str) -> Result<T, AuthenticationError>
141where
142 T: DeserializeOwned,
143{
144 validate_token_length(token)?;
145 let key = decode_key(key_b64)?;
146 let (header_b64, claims_b64, signature_b64) = split_token(token)?;
147 let header_bytes = decode_jwt_segment(header_b64)?;
148 let header: JwtHeader = serde_json::from_slice(&header_bytes)
149 .map_err(|_error| AuthenticationError::InvalidJsonPayload)?;
150 if header.alg != ALGORITHM_HS256 {
151 return Err(AuthenticationError::UnsupportedAlgorithm(header.alg));
152 }
153 let actual_signature = decode_jwt_segment(signature_b64)?;
154 verify_hs256(
155 format!("{header_b64}.{claims_b64}").as_bytes(),
156 &key,
157 &actual_signature,
158 )?;
159 let claims_bytes = decode_jwt_segment(claims_b64)?;
160 let registered_claims: RegisteredJwtClaims = serde_json::from_slice(&claims_bytes)
161 .map_err(|_error| AuthenticationError::InvalidJsonPayload)?;
162 validate_registered_claims(®istered_claims)?;
163 serde_json::from_slice(&claims_bytes).map_err(|_error| AuthenticationError::InvalidJsonPayload)
164}
165
166pub(super) fn verify_with_proof<T: DeserializeOwned>(
168 token: &str,
169 key_b64: &str,
170) -> Result<(T, AuthProof), AuthenticationError> {
171 verify(token, key_b64).map(|claims| (claims, AuthProof(())))
172}
173
174pub(crate) fn decode_unverified_claims<T>(token: &str) -> Result<T, AuthenticationError>
179where
180 T: DeserializeOwned,
181{
182 validate_token_length(token)?;
183 let (_header_b64, claims_b64, _signature_b64) = split_token(token)?;
184 let claims_bytes = decode_jwt_segment(claims_b64)?;
185 serde_json::from_slice(&claims_bytes).map_err(|_error| AuthenticationError::InvalidJsonPayload)
186}
187
188fn validate_token_length(token: &str) -> Result<(), AuthenticationError> {
189 if token.len() > MAX_JWT_TOKEN_BYTES {
190 return Err(AuthenticationError::TokenTooLarge {
191 actual: token.len(),
192 limit: MAX_JWT_TOKEN_BYTES,
193 });
194 }
195 Ok(())
196}
197
198fn validate_registered_claims(claims: &RegisteredJwtClaims) -> Result<(), AuthenticationError> {
199 validate_registered_claims_at(claims, duration_since_epoch())
200}
201
202fn validate_registered_claims_at(
203 claims: &RegisteredJwtClaims,
204 now: Duration,
205) -> Result<(), AuthenticationError> {
206 let iat_limit = NumericDate::from(now.saturating_add(MAX_IAT_FUTURE_SKEW));
207 let now = NumericDate::from(now);
208 if claims.exp.is_some_and(|exp| exp <= now) {
209 return Err(AuthenticationError::TokenExpired);
210 }
211 if claims.nbf.is_some_and(|nbf| nbf > now) {
212 return Err(AuthenticationError::TokenNotYetValid);
213 }
214 if claims.iat.is_some_and(|iat| iat > iat_limit) {
215 return Err(AuthenticationError::TokenIssuedInFuture);
216 }
217 Ok(())
218}
219
220fn sign_hs256(data: &[u8], key: &[u8]) -> Result<Vec<u8>, AuthenticationError> {
221 let mut mac = HmacSha256::new_from_slice(key)
222 .map_err(|_error| AuthenticationError::InvalidBase64Encoding)?;
223 mac.update(data);
224 Ok(mac.finalize().into_bytes().to_vec())
225}
226
227fn verify_hs256(data: &[u8], key: &[u8], signature: &[u8]) -> Result<(), AuthenticationError> {
228 let mut mac = HmacSha256::new_from_slice(key)
229 .map_err(|_error| AuthenticationError::InvalidBase64Encoding)?;
230 mac.update(data);
231 mac.verify_slice(signature)
232 .map_err(|_error| AuthenticationError::InvalidSignature)
233}
234
235fn split_token(token: &str) -> Result<(&str, &str, &str), AuthenticationError> {
236 let mut parts = token.split('.');
237 let (Some(header), Some(claims), Some(signature), None) =
238 (parts.next(), parts.next(), parts.next(), parts.next())
239 else {
240 return Err(AuthenticationError::InvalidJwtFormat);
241 };
242 if header.is_empty() || claims.is_empty() || signature.is_empty() {
243 return Err(AuthenticationError::InvalidJwtFormat);
244 }
245 Ok((header, claims, signature))
246}
247
248pub(crate) fn decode_key(input: &str) -> Result<Vec<u8>, AuthenticationError> {
249 let padded = pad_base64(input);
250 URL_SAFE
251 .decode(padded.as_bytes())
252 .or_else(|_error| STANDARD.decode(padded.as_bytes()))
253 .map_err(|_error| AuthenticationError::InvalidBase64Encoding)
254}
255
256fn decode_jwt_segment(input: &str) -> Result<Vec<u8>, AuthenticationError> {
257 URL_SAFE_NO_PAD
258 .decode(input.as_bytes())
259 .map_err(|_error| AuthenticationError::InvalidBase64Encoding)
260}
261
262fn pad_base64(input: &str) -> String {
263 let remainder = input.len() % 4;
264 if remainder == 0 {
265 return input.to_owned();
266 }
267 format!("{input}{}", "=".repeat(4 - remainder))
268}
269
270pub(crate) fn derive_key_from_seed(key: &str, seed: &str) -> Result<String, AuthenticationError> {
271 let key_bytes = decode_key(key)?;
272 let seed_bytes = decode_key(seed)?;
273 let derived_key = sign_hs256(&seed_bytes, &key_bytes)?;
274 Ok(STANDARD.encode(derived_key))
275}
276
277#[cfg(test)]
278#[path = "TESTS/auth.rs"]
279mod tests;