diff --git a/dashboard/src/guest.rs b/dashboard/src/guest.rs index 3a4afe262d54be21d7e096c0a116d3d6ea9ee884..ac670598fc8c088c5a27f8d9cba5b8ff5d84baa7 100644 --- a/dashboard/src/guest.rs +++ b/dashboard/src/guest.rs @@ -158,22 +158,33 @@ fn signed_claims( nonce: &str, require_nonce: bool, ) -> Result { - let reject = || { + let reject = |reason: &str| { + eprintln!( + "guest token verification: {} {reason}", + if require_nonce { + "id_token" + } else { + "userinfo" + } + ); Error::new( 502, "The provider couldn't verify your sign-in. Return to Shale and try again.", ) }; if token.len() > 32768 { - return Err(reject()); + return Err(reject("size")); } let parts: Vec<_> = token.split('.').collect(); if parts.len() != 3 { - return Err(reject()); + return Err(reject("format")); } - let header: Value = - serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[0]).map_err(|_| reject())?) - .map_err(|_| reject())?; + let header: Value = serde_json::from_slice( + &URL_SAFE_NO_PAD + .decode(parts[0]) + .map_err(|_| reject("header_encoding"))?, + ) + .map_err(|_| reject("header_json"))?; if header .get("kid") .is_some_and(|id| id.as_str().is_none_or(str::is_empty)) @@ -182,7 +193,7 @@ fn signed_claims( || header.get("jku").is_some() || header.get("jwk").is_some() { - return Err(reject()); + return Err(reject("header")); } let matching: Vec<_> = array(&keys["keys"]) .iter() @@ -197,17 +208,17 @@ fn signed_claims( }) .collect(); if matching.len() != 1 { - return Err(reject()); + return Err(reject("key_selection")); } let key = matching[0]; let x = URL_SAFE_NO_PAD .decode(string(&key["x"])) - .map_err(|_| reject())?; + .map_err(|_| reject("key_x"))?; let y = URL_SAFE_NO_PAD .decode(string(&key["y"])) - .map_err(|_| reject())?; + .map_err(|_| reject("key_y"))?; if x.len() != 32 || y.len() != 32 { - return Err(reject()); + return Err(reject("key_coordinates")); } let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?; let mut point = EcPoint::new(&group)?; @@ -218,9 +229,11 @@ fn signed_claims( let ec = EcKey::from_public_key(&group, &point)?; ec.check_key()?; let key = PKey::from_ec_key(ec)?; - let raw = URL_SAFE_NO_PAD.decode(parts[2]).map_err(|_| reject())?; + let raw = URL_SAFE_NO_PAD + .decode(parts[2]) + .map_err(|_| reject("signature_encoding"))?; if raw.len() != 64 { - return Err(reject()); + return Err(reject("signature_length")); } let signature = EcdsaSig::from_private_components( BigNum::from_slice(&raw[..32])?, @@ -230,30 +243,55 @@ fn signed_claims( let mut verify = Verifier::new(MessageDigest::sha256(), &key)?; verify.update(format!("{}.{}", parts[0], parts[1]).as_bytes())?; if !verify.verify(&signature)? { - return Err(reject()); + return Err(reject("signature")); } - let claims: Value = - serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).map_err(|_| reject())?) - .map_err(|_| reject())?; + let claims: Value = serde_json::from_slice( + &URL_SAFE_NO_PAD + .decode(parts[1]) + .map_err(|_| reject("claims_encoding"))?, + ) + .map_err(|_| reject("claims_json"))?; let time = now() as i64; let audience = claims["aud"].as_str().is_some_and(|a| a == client) || array(&claims["aud"]) .iter() .any(|a| a.as_str() == Some(client)); - if claims["iss"] != ASTHENO - || !audience - || (claims["aud"].is_array() && array(&claims["aud"]).len() > 1 && claims["azp"] != client) - || claims.get("azp").is_some_and(|a| a != client) - || string(&claims["sub"]).is_empty() - || string(&claims["sub"]).len() > 512 - || claims["exp"].as_i64().is_none_or(|t| t <= time) - || claims["iat"].as_i64().is_none_or(|t| t > time + 60) - || claims - .get("nbf") - .is_some_and(|t| t.as_i64().is_none_or(|n| n > time + 60)) - || (require_nonce && claims["nonce"].as_str() != Some(nonce)) - { - return Err(reject()); + for (reason, invalid) in [ + ("issuer", claims["iss"] != ASTHENO), + ("audience", !audience), + ( + "authorized_party", + (claims["aud"].is_array() + && array(&claims["aud"]).len() > 1 + && claims["azp"] != client) + || claims.get("azp").is_some_and(|a| a != client), + ), + ( + "subject", + string(&claims["sub"]).is_empty() || string(&claims["sub"]).len() > 512, + ), + ( + "expiration", + claims["exp"].as_i64().is_none_or(|t| t <= time), + ), + ( + "issued_at", + claims["iat"].as_i64().is_none_or(|t| t > time + 60), + ), + ( + "not_before", + claims + .get("nbf") + .is_some_and(|t| t.as_i64().is_none_or(|n| n > time + 60)), + ), + ( + "nonce", + require_nonce && claims["nonce"].as_str() != Some(nonce), + ), + ] { + if invalid { + return Err(reject(reason)); + } } Ok(claims) } @@ -560,17 +598,8 @@ async fn handle(app: &App, request: Request) -> Result { headers }) .build()?; - let (subject, name) = exchange(&http, provider, &client, &secret, code, &callback, &flow) - .await - .map_err(|error| { - eprintln!( - "guest rejected code: {provider} length {} whitespace {} dots {}", - code.len(), - code.bytes().any(|c| c.is_ascii_whitespace()), - code.bytes().filter(|c| *c == b'.').count() - ); - error - })?; + let (subject, name) = + exchange(&http, provider, &client, &secret, code, &callback, &flow).await?; let id = account(auth, provider, &subject, &name)?; auth.create_session(&id, "dashboard", headers, None) } @@ -578,11 +607,8 @@ async fn handle(app: &App, request: Request) -> Result { match result { Ok(session) => Ok(( StatusCode::FOUND, - [ - ("location", next), - ("set-cookie", session), - ("set-cookie", auth::set_cookie(COOKIE, "", 0)), - ], + [("location", next), ("set-cookie", session)], + axum::response::AppendHeaders([("set-cookie", auth::set_cookie(COOKIE, "", 0))]), ) .into_response()), Err(error) => {