|
| 1 | +#[cfg(test)] |
| 2 | +mod tests; |
| 3 | + |
1 | 4 | use { |
2 | | - crate::domain::{AuthSubject, DecodedClientId}, |
| 5 | + crate::domain::{AuthSubject, ClientId, ClientIdDecodingError, DecodedClientId}, |
3 | 6 | chrono::{DateTime, Utc}, |
4 | 7 | ed25519_dalek::{ed25519::signature::Signature, Keypair, Signer}, |
5 | 8 | serde::{Deserialize, Serialize}, |
@@ -114,12 +117,28 @@ pub struct JwtClaims<'a> { |
114 | 117 | } |
115 | 118 |
|
116 | 119 | impl<'a> JwtClaims<'a> { |
117 | | - pub fn is_valid(&self, aud: &HashSet<String>, time_leeway: impl Into<Option<i64>>) -> bool { |
| 120 | + pub fn validate( |
| 121 | + &self, |
| 122 | + aud: &HashSet<String>, |
| 123 | + time_leeway: impl Into<Option<i64>>, |
| 124 | + ) -> Result<(), JwtVerificationError> { |
118 | 125 | let time_leeway = time_leeway |
119 | 126 | .into() |
120 | 127 | .unwrap_or(JWT_VALIDATION_TIME_LEEWAY_SECS); |
121 | 128 | let now = Utc::now().timestamp(); |
122 | | - (now + time_leeway) >= self.iat && (now - time_leeway) <= self.exp && aud.contains(self.aud) |
| 129 | + |
| 130 | + if now - time_leeway > self.exp { |
| 131 | + return Err(JwtVerificationError::Expired); |
| 132 | + } |
| 133 | + |
| 134 | + if now + time_leeway < self.iat { |
| 135 | + return Err(JwtVerificationError::NotYetValid); |
| 136 | + } |
| 137 | + |
| 138 | + if !aud.contains(self.aud) { |
| 139 | + return Err(JwtVerificationError::InvalidAudience); |
| 140 | + } |
| 141 | + Ok(()) |
123 | 142 | } |
124 | 143 | } |
125 | 144 |
|
@@ -170,3 +189,125 @@ pub fn encode_auth_token( |
170 | 189 |
|
171 | 190 | Ok(SerializedAuthToken(format!("{message}.{signature}"))) |
172 | 191 | } |
| 192 | + |
| 193 | +#[derive(Debug, thiserror::Error)] |
| 194 | +pub enum JwtVerificationError { |
| 195 | + #[error("Invalid format")] |
| 196 | + Format, |
| 197 | + |
| 198 | + #[error("Invalid encoding")] |
| 199 | + Encoding, |
| 200 | + |
| 201 | + #[error("Invalid JWT signing algorithm")] |
| 202 | + Header, |
| 203 | + |
| 204 | + #[error("JWT Token is expired")] |
| 205 | + Expired, |
| 206 | + |
| 207 | + #[error("JWT Token is not yet valid")] |
| 208 | + NotYetValid, |
| 209 | + |
| 210 | + #[error("Invalid audience")] |
| 211 | + InvalidAudience, |
| 212 | + |
| 213 | + #[error("Invalid signature")] |
| 214 | + Signature, |
| 215 | + |
| 216 | + #[error("Invalid JSON")] |
| 217 | + Serialization, |
| 218 | + |
| 219 | + #[error("Invalid issuer DID prefix")] |
| 220 | + IssuerPrefix, |
| 221 | + |
| 222 | + #[error("Invalid issuer DID method")] |
| 223 | + IssuerMethod, |
| 224 | + |
| 225 | + #[error("Invalid issuer format")] |
| 226 | + IssuerFormat, |
| 227 | + |
| 228 | + #[error(transparent)] |
| 229 | + PubKey(#[from] ClientIdDecodingError), |
| 230 | +} |
| 231 | + |
| 232 | +#[derive(Debug)] |
| 233 | +pub struct Jwt(pub String); |
| 234 | + |
| 235 | +impl Jwt { |
| 236 | + pub fn decode(&self, aud: &HashSet<String>) -> Result<ClientId, JwtVerificationError> { |
| 237 | + let mut parts = self.0.splitn(3, JWT_DELIMITER); |
| 238 | + |
| 239 | + let (Some(header), Some(claims)) = (parts.next(), parts.next()) else { |
| 240 | + return Err(JwtVerificationError::Format); |
| 241 | + }; |
| 242 | + |
| 243 | + let decoder = &data_encoding::BASE64URL_NOPAD; |
| 244 | + |
| 245 | + let header_len = decoder |
| 246 | + .decode_len(header.len()) |
| 247 | + .map_err(|_| JwtVerificationError::Encoding)?; |
| 248 | + let claims_len = decoder |
| 249 | + .decode_len(claims.len()) |
| 250 | + .map_err(|_| JwtVerificationError::Encoding)?; |
| 251 | + |
| 252 | + let mut output = vec![0u8; header_len.max(claims_len)]; |
| 253 | + |
| 254 | + // Decode header. |
| 255 | + data_encoding::BASE64URL_NOPAD |
| 256 | + .decode_mut(header.as_bytes(), &mut output[..header_len]) |
| 257 | + .map_err(|_| JwtVerificationError::Encoding)?; |
| 258 | + |
| 259 | + { |
| 260 | + let header = serde_json::from_slice::<JwtHeader>(&output[..header_len]) |
| 261 | + .map_err(|_| JwtVerificationError::Serialization)?; |
| 262 | + |
| 263 | + if !header.is_valid() { |
| 264 | + return Err(JwtVerificationError::Header); |
| 265 | + } |
| 266 | + } |
| 267 | + |
| 268 | + // Decode claims. |
| 269 | + data_encoding::BASE64URL_NOPAD |
| 270 | + .decode_mut(claims.as_bytes(), &mut output[..claims_len]) |
| 271 | + .map_err(|_| JwtVerificationError::Encoding)?; |
| 272 | + |
| 273 | + let claims = serde_json::from_slice::<JwtClaims>(&output[..claims_len]) |
| 274 | + .map_err(|_| JwtVerificationError::Serialization)?; |
| 275 | + |
| 276 | + // Basic token validation: `iat`, `exp` and `aud`. |
| 277 | + claims.validate(aud, None)?; |
| 278 | + |
| 279 | + let did_key = claims |
| 280 | + .iss |
| 281 | + .strip_prefix(DID_PREFIX) |
| 282 | + .ok_or(JwtVerificationError::IssuerPrefix)? |
| 283 | + .strip_prefix(DID_DELIMITER) |
| 284 | + .ok_or(JwtVerificationError::IssuerFormat)? |
| 285 | + .strip_prefix(DID_METHOD) |
| 286 | + .ok_or(JwtVerificationError::IssuerMethod)? |
| 287 | + .strip_prefix(DID_DELIMITER) |
| 288 | + .ok_or(JwtVerificationError::IssuerFormat)?; |
| 289 | + |
| 290 | + let pub_key = did_key.parse::<DecodedClientId>()?; |
| 291 | + |
| 292 | + let mut parts = self.0.rsplitn(2, JWT_DELIMITER); |
| 293 | + |
| 294 | + let (Some(signature), Some(message)) = (parts.next(), parts.next()) else { |
| 295 | + return Err(JwtVerificationError::Format); |
| 296 | + }; |
| 297 | + |
| 298 | + let key = jsonwebtoken::DecodingKey::from_ed_der(pub_key.as_ref()); |
| 299 | + |
| 300 | + // Finally, verify signature. |
| 301 | + let sig_result = jsonwebtoken::crypto::verify( |
| 302 | + signature, |
| 303 | + message.as_bytes(), |
| 304 | + &key, |
| 305 | + jsonwebtoken::Algorithm::EdDSA, |
| 306 | + ); |
| 307 | + |
| 308 | + match sig_result { |
| 309 | + Ok(true) => Ok(pub_key.into()), |
| 310 | + _ => Err(JwtVerificationError::Signature), |
| 311 | + } |
| 312 | + } |
| 313 | +} |
0 commit comments