| ... | ... | @@ -158,22 +158,33 @@ fn signed_claims( |
| 158 | 158 | nonce: &str, |
| 159 | 159 | require_nonce: bool, |
| 160 | 160 | ) -> Result<Value> { |
| 161 | | let reject = || { |
| 161 | let reject = |reason: &str| { |
| 162 | eprintln!( |
| 163 | "guest token verification: {} {reason}", |
| 164 | if require_nonce { |
| 165 | "id_token" |
| 166 | } else { |
| 167 | "userinfo" |
| 168 | } |
| 169 | ); |
| 162 | 170 | Error::new( |
| 163 | 171 | 502, |
| 164 | 172 | "The provider couldn't verify your sign-in. Return to Shale and try again.", |
| 165 | 173 | ) |
| 166 | 174 | }; |
| 167 | 175 | if token.len() > 32768 { |
| 168 | | return Err(reject()); |
| 176 | return Err(reject("size")); |
| 169 | 177 | } |
| 170 | 178 | let parts: Vec<_> = token.split('.').collect(); |
| 171 | 179 | if parts.len() != 3 { |
| 172 | | return Err(reject()); |
| 180 | return Err(reject("format")); |
| 173 | 181 | } |
| 174 | | let header: Value = |
| 175 | | serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[0]).map_err(|_| reject())?) |
| 176 | | .map_err(|_| reject())?; |
| 182 | let header: Value = serde_json::from_slice( |
| 183 | &URL_SAFE_NO_PAD |
| 184 | .decode(parts[0]) |
| 185 | .map_err(|_| reject("header_encoding"))?, |
| 186 | ) |
| 187 | .map_err(|_| reject("header_json"))?; |
| 177 | 188 | if header |
| 178 | 189 | .get("kid") |
| 179 | 190 | .is_some_and(|id| id.as_str().is_none_or(str::is_empty)) |
| ... | ... | @@ -182,7 +193,7 @@ fn signed_claims( |
| 182 | 193 | || header.get("jku").is_some() |
| 183 | 194 | || header.get("jwk").is_some() |
| 184 | 195 | { |
| 185 | | return Err(reject()); |
| 196 | return Err(reject("header")); |
| 186 | 197 | } |
| 187 | 198 | let matching: Vec<_> = array(&keys["keys"]) |
| 188 | 199 | .iter() |
| ... | ... | @@ -197,17 +208,17 @@ fn signed_claims( |
| 197 | 208 | }) |
| 198 | 209 | .collect(); |
| 199 | 210 | if matching.len() != 1 { |
| 200 | | return Err(reject()); |
| 211 | return Err(reject("key_selection")); |
| 201 | 212 | } |
| 202 | 213 | let key = matching[0]; |
| 203 | 214 | let x = URL_SAFE_NO_PAD |
| 204 | 215 | .decode(string(&key["x"])) |
| 205 | | .map_err(|_| reject())?; |
| 216 | .map_err(|_| reject("key_x"))?; |
| 206 | 217 | let y = URL_SAFE_NO_PAD |
| 207 | 218 | .decode(string(&key["y"])) |
| 208 | | .map_err(|_| reject())?; |
| 219 | .map_err(|_| reject("key_y"))?; |
| 209 | 220 | if x.len() != 32 || y.len() != 32 { |
| 210 | | return Err(reject()); |
| 221 | return Err(reject("key_coordinates")); |
| 211 | 222 | } |
| 212 | 223 | let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?; |
| 213 | 224 | let mut point = EcPoint::new(&group)?; |
| ... | ... | @@ -218,9 +229,11 @@ fn signed_claims( |
| 218 | 229 | let ec = EcKey::from_public_key(&group, &point)?; |
| 219 | 230 | ec.check_key()?; |
| 220 | 231 | let key = PKey::from_ec_key(ec)?; |
| 221 | | let raw = URL_SAFE_NO_PAD.decode(parts[2]).map_err(|_| reject())?; |
| 232 | let raw = URL_SAFE_NO_PAD |
| 233 | .decode(parts[2]) |
| 234 | .map_err(|_| reject("signature_encoding"))?; |
| 222 | 235 | if raw.len() != 64 { |
| 223 | | return Err(reject()); |
| 236 | return Err(reject("signature_length")); |
| 224 | 237 | } |
| 225 | 238 | let signature = EcdsaSig::from_private_components( |
| 226 | 239 | BigNum::from_slice(&raw[..32])?, |
| ... | ... | @@ -230,30 +243,55 @@ fn signed_claims( |
| 230 | 243 | let mut verify = Verifier::new(MessageDigest::sha256(), &key)?; |
| 231 | 244 | verify.update(format!("{}.{}", parts[0], parts[1]).as_bytes())?; |
| 232 | 245 | if !verify.verify(&signature)? { |
| 233 | | return Err(reject()); |
| 246 | return Err(reject("signature")); |
| 234 | 247 | } |
| 235 | | let claims: Value = |
| 236 | | serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).map_err(|_| reject())?) |
| 237 | | .map_err(|_| reject())?; |
| 248 | let claims: Value = serde_json::from_slice( |
| 249 | &URL_SAFE_NO_PAD |
| 250 | .decode(parts[1]) |
| 251 | .map_err(|_| reject("claims_encoding"))?, |
| 252 | ) |
| 253 | .map_err(|_| reject("claims_json"))?; |
| 238 | 254 | let time = now() as i64; |
| 239 | 255 | let audience = claims["aud"].as_str().is_some_and(|a| a == client) |
| 240 | 256 | || array(&claims["aud"]) |
| 241 | 257 | .iter() |
| 242 | 258 | .any(|a| a.as_str() == Some(client)); |
| 243 | | if claims["iss"] != ASTHENO |
| 244 | | || !audience |
| 245 | | || (claims["aud"].is_array() && array(&claims["aud"]).len() > 1 && claims["azp"] != client) |
| 246 | | || claims.get("azp").is_some_and(|a| a != client) |
| 247 | | || string(&claims["sub"]).is_empty() |
| 248 | | || string(&claims["sub"]).len() > 512 |
| 249 | | || claims["exp"].as_i64().is_none_or(|t| t <= time) |
| 250 | | || claims["iat"].as_i64().is_none_or(|t| t > time + 60) |
| 251 | | || claims |
| 252 | | .get("nbf") |
| 253 | | .is_some_and(|t| t.as_i64().is_none_or(|n| n > time + 60)) |
| 254 | | || (require_nonce && claims["nonce"].as_str() != Some(nonce)) |
| 255 | | { |
| 256 | | return Err(reject()); |
| 259 | for (reason, invalid) in [ |
| 260 | ("issuer", claims["iss"] != ASTHENO), |
| 261 | ("audience", !audience), |
| 262 | ( |
| 263 | "authorized_party", |
| 264 | (claims["aud"].is_array() |
| 265 | && array(&claims["aud"]).len() > 1 |
| 266 | && claims["azp"] != client) |
| 267 | || claims.get("azp").is_some_and(|a| a != client), |
| 268 | ), |
| 269 | ( |
| 270 | "subject", |
| 271 | string(&claims["sub"]).is_empty() || string(&claims["sub"]).len() > 512, |
| 272 | ), |
| 273 | ( |
| 274 | "expiration", |
| 275 | claims["exp"].as_i64().is_none_or(|t| t <= time), |
| 276 | ), |
| 277 | ( |
| 278 | "issued_at", |
| 279 | claims["iat"].as_i64().is_none_or(|t| t > time + 60), |
| 280 | ), |
| 281 | ( |
| 282 | "not_before", |
| 283 | claims |
| 284 | .get("nbf") |
| 285 | .is_some_and(|t| t.as_i64().is_none_or(|n| n > time + 60)), |
| 286 | ), |
| 287 | ( |
| 288 | "nonce", |
| 289 | require_nonce && claims["nonce"].as_str() != Some(nonce), |
| 290 | ), |
| 291 | ] { |
| 292 | if invalid { |
| 293 | return Err(reject(reason)); |
| 294 | } |
| 257 | 295 | } |
| 258 | 296 | Ok(claims) |
| 259 | 297 | } |
| ... | ... | @@ -560,17 +598,8 @@ async fn handle(app: &App, request: Request) -> Result<Response> { |
| 560 | 598 | headers |
| 561 | 599 | }) |
| 562 | 600 | .build()?; |
| 563 | | let (subject, name) = exchange(&http, provider, &client, &secret, code, &callback, &flow) |
| 564 | | .await |
| 565 | | .map_err(|error| { |
| 566 | | eprintln!( |
| 567 | | "guest rejected code: {provider} length {} whitespace {} dots {}", |
| 568 | | code.len(), |
| 569 | | code.bytes().any(|c| c.is_ascii_whitespace()), |
| 570 | | code.bytes().filter(|c| *c == b'.').count() |
| 571 | | ); |
| 572 | | error |
| 573 | | })?; |
| 601 | let (subject, name) = |
| 602 | exchange(&http, provider, &client, &secret, code, &callback, &flow).await?; |
| 574 | 603 | let id = account(auth, provider, &subject, &name)?; |
| 575 | 604 | auth.create_session(&id, "dashboard", headers, None) |
| 576 | 605 | } |
| ... | ... | @@ -578,11 +607,8 @@ async fn handle(app: &App, request: Request) -> Result<Response> { |
| 578 | 607 | match result { |
| 579 | 608 | Ok(session) => Ok(( |
| 580 | 609 | StatusCode::FOUND, |
| 581 | | [ |
| 582 | | ("location", next), |
| 583 | | ("set-cookie", session), |
| 584 | | ("set-cookie", auth::set_cookie(COOKIE, "", 0)), |
| 585 | | ], |
| 610 | [("location", next), ("set-cookie", session)], |
| 611 | axum::response::AppendHeaders([("set-cookie", auth::set_cookie(COOKIE, "", 0))]), |
| 586 | 612 | ) |
| 587 | 613 | .into_response()), |
| 588 | 614 | Err(error) => { |