authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-05 12:08:27-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-05 15:36:02-07:00
logb299d590ed68d0c8d7bece74fb2c69e5e0f7f287
tree688bac2311f90543c7ac0329ef53bfcfe9e157e6
parent3bf9328875537be8dc3ab53792a4596b0ab4e1bf
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

Preserve the guest session cookie on the provider callback

Append the temporary-cookie deletion without overwriting the new Snowglobe session. Log the exact Astheno verification invariant that failed without logging token values. Remove the temporary authorization-code shape diagnostic. Assisted-by: gpt-6

1 files changed, 73 insertions(+), 47 deletions(-)

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