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(
158158 nonce: &str,
159159 require_nonce: bool,
160160) -> 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 );
162170 Error::new(
163171 502,
164172 "The provider couldn't verify your sign-in. Return to Shale and try again.",
165173 )
166174 };
167175 if token.len() > 32768 {
168 return Err(reject());
176 return Err(reject("size"));
169177 }
170178 let parts: Vec<_> = token.split('.').collect();
171179 if parts.len() != 3 {
172 return Err(reject());
180 return Err(reject("format"));
173181 }
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"))?;
177188 if header
178189 .get("kid")
179190 .is_some_and(|id| id.as_str().is_none_or(str::is_empty))
......@@ -182,7 +193,7 @@ fn signed_claims(
182193 || header.get("jku").is_some()
183194 || header.get("jwk").is_some()
184195 {
185 return Err(reject());
196 return Err(reject("header"));
186197 }
187198 let matching: Vec<_> = array(&keys["keys"])
188199 .iter()
......@@ -197,17 +208,17 @@ fn signed_claims(
197208 })
198209 .collect();
199210 if matching.len() != 1 {
200 return Err(reject());
211 return Err(reject("key_selection"));
201212 }
202213 let key = matching[0];
203214 let x = URL_SAFE_NO_PAD
204215 .decode(string(&key["x"]))
205 .map_err(|_| reject())?;
216 .map_err(|_| reject("key_x"))?;
206217 let y = URL_SAFE_NO_PAD
207218 .decode(string(&key["y"]))
208 .map_err(|_| reject())?;
219 .map_err(|_| reject("key_y"))?;
209220 if x.len() != 32 || y.len() != 32 {
210 return Err(reject());
221 return Err(reject("key_coordinates"));
211222 }
212223 let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?;
213224 let mut point = EcPoint::new(&group)?;
......@@ -218,9 +229,11 @@ fn signed_claims(
218229 let ec = EcKey::from_public_key(&group, &point)?;
219230 ec.check_key()?;
220231 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"))?;
222235 if raw.len() != 64 {
223 return Err(reject());
236 return Err(reject("signature_length"));
224237 }
225238 let signature = EcdsaSig::from_private_components(
226239 BigNum::from_slice(&raw[..32])?,
......@@ -230,30 +243,55 @@ fn signed_claims(
230243 let mut verify = Verifier::new(MessageDigest::sha256(), &key)?;
231244 verify.update(format!("{}.{}", parts[0], parts[1]).as_bytes())?;
232245 if !verify.verify(&signature)? {
233 return Err(reject());
246 return Err(reject("signature"));
234247 }
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"))?;
238254 let time = now() as i64;
239255 let audience = claims["aud"].as_str().is_some_and(|a| a == client)
240256 || array(&claims["aud"])
241257 .iter()
242258 .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 }
257295 }
258296 Ok(claims)
259297}
......@@ -560,17 +598,8 @@ async fn handle(app: &App, request: Request) -> Result<Response> {
560598 headers
561599 })
562600 .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?;
574603 let id = account(auth, provider, &subject, &name)?;
575604 auth.create_session(&id, "dashboard", headers, None)
576605 }
......@@ -578,11 +607,8 @@ async fn handle(app: &App, request: Request) -> Result<Response> {
578607 match result {
579608 Ok(session) => Ok((
580609 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))]),
586612 )
587613 .into_response()),
588614 Err(error) => {