authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-05 09:09:40-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-05 15:36:02-07:00
logc3d894fed29df8d1a864de6a5bd1d0fda57882f5
tree082baddbd76ecb82cb690e2a289cdba37808c1b3
parentb62d003aab62b3dafb0dd9b662a2996a526b38ba
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

Accept Astheno signing keys without key IDs and simplify guest buttons

Verify the unique compatible P-256 key when the signed header omits kid. Keep signature and claim checks and refuse ambiguous keys. Show the providers as two existing-theme buttons. Assisted-by: gpt-6

4 files changed, 70 insertions(+), 17 deletions(-)

dashboard/src/guest.rs+55-14
...@@ -148,7 +148,10 @@ fn signed_claims(...@@ -148,7 +148,10 @@ fn signed_claims(
148 let header: Value =148 let header: Value =
149 serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[0]).map_err(|_| reject())?)149 serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[0]).map_err(|_| reject())?)
150 .map_err(|_| reject())?;150 .map_err(|_| reject())?;
151 if header["alg"] != "ES256"151 if header
152 .get("kid")
153 .is_some_and(|id| id.as_str().is_none_or(str::is_empty))
154 || header["alg"] != "ES256"
152 || header.get("crit").is_some()155 || header.get("crit").is_some()
153 || header.get("jku").is_some()156 || header.get("jku").is_some()
154 || header.get("jwk").is_some()157 || header.get("jwk").is_some()
...@@ -158,8 +161,9 @@ fn signed_claims(...@@ -158,8 +161,9 @@ fn signed_claims(
158 let matching: Vec<_> = array(&keys["keys"])161 let matching: Vec<_> = array(&keys["keys"])
159 .iter()162 .iter()
160 .filter(|key| {163 .filter(|key| {
161 key["kid"].is_string()164 header
162 && key["kid"] == header["kid"]165 .get("kid")
166 .is_none_or(|id| key.get("kid") == Some(id))
163 && key["kty"] == "EC"167 && key["kty"] == "EC"
164 && key["crv"] == "P-256"168 && key["crv"] == "P-256"
165 && key.get("alg").is_none_or(|v| v == "ES256")169 && key.get("alg").is_none_or(|v| v == "ES256")
...@@ -544,17 +548,23 @@ async fn handle(app: &App, request: Request) -> Result<Response> {...@@ -544,17 +548,23 @@ async fn handle(app: &App, request: Request) -> Result<Response> {
544 ],548 ],
545 )549 )
546 .into_response()),550 .into_response()),
547 Err(_) => Ok((551 Err(error) => {
548 StatusCode::FOUND,552 eprintln!(
549 [553 "guest callback {provider}: {}: {}",
550 (554 error.status, error.message
551 "location",555 );
552 format!("/sign-in?next={}&guest_error=1", encoded(&next)),556 Ok((
553 ),557 StatusCode::FOUND,
554 ("set-cookie", auth::set_cookie(COOKIE, "", 0)),558 [
555 ],559 (
556 )560 "location",
557 .into_response()),561 format!("/sign-in?next={}&guest_error=1", encoded(&next)),
562 ),
563 ("set-cookie", auth::set_cookie(COOKIE, "", 0)),
564 ],
565 )
566 .into_response())
567 }
558 }568 }
559}569}
560570
...@@ -585,6 +595,37 @@ mod tests {...@@ -585,6 +595,37 @@ mod tests {
585 let claims =595 let claims =
586 signed_claims(token, &vector["keys"], "fixture", "fixture-nonce", true).unwrap();596 signed_claims(token, &vector["keys"], "fixture", "fixture-nonce", true).unwrap();
587 assert_eq!(claims["sub"], "external-123");597 assert_eq!(claims["sub"], "external-123");
598 let unnamed = &vector["withoutKid"];
599 let unnamed_token = string(&unnamed["token"]);
600 assert_eq!(
601 signed_claims(
602 unnamed_token,
603 &unnamed["keys"],
604 "fixture",
605 "fixture-nonce",
606 true
607 )
608 .unwrap()["sub"],
609 "external-123"
610 );
611 let mut ambiguous = unnamed["keys"].clone();
612 ambiguous["keys"]
613 .as_array_mut()
614 .unwrap()
615 .push(unnamed["keys"]["keys"][0].clone());
616 assert!(
617 signed_claims(unnamed_token, &ambiguous, "fixture", "fixture-nonce", true).is_err()
618 );
619 assert!(
620 signed_claims(
621 unnamed_token,
622 &vector["keys"],
623 "fixture",
624 "fixture-nonce",
625 true
626 )
627 .is_err()
628 );
588 for (name, token) in vector["invalid"].as_object().unwrap() {629 for (name, token) in vector["invalid"].as_object().unwrap() {
589 assert!(630 assert!(
590 signed_claims(631 signed_claims(
dashboard/tests/guest-jwt.json+13
...@@ -22,5 +22,18 @@...@@ -22,5 +22,18 @@
22 "subject": "eyJhbGciOiJFUzI1NiIsImtpZCI6InRlc3Qta2V5In0.eyJpc3MiOiJodHRwczovL2lkZW50aXR5LmFzdGhlbm8uc29mdHdhcmUiLCJzdWIiOiIiLCJhdWQiOiJmaXh0dXJlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjQxMDI0NDQ4MDAsIm5vbmNlIjoiZml4dHVyZS1ub25jZSJ9.LvYyzvg0HAzhAWKo76lWyPktuoxSdmXySfXjiIWea40O0fdqGa-Du73lHzTLUJ3BDS5yWd5gF3doHbYrhbL4TA",22 "subject": "eyJhbGciOiJFUzI1NiIsImtpZCI6InRlc3Qta2V5In0.eyJpc3MiOiJodHRwczovL2lkZW50aXR5LmFzdGhlbm8uc29mdHdhcmUiLCJzdWIiOiIiLCJhdWQiOiJmaXh0dXJlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjQxMDI0NDQ4MDAsIm5vbmNlIjoiZml4dHVyZS1ub25jZSJ9.LvYyzvg0HAzhAWKo76lWyPktuoxSdmXySfXjiIWea40O0fdqGa-Du73lHzTLUJ3BDS5yWd5gF3doHbYrhbL4TA",
23 "authorizedParty": "eyJhbGciOiJFUzI1NiIsImtpZCI6InRlc3Qta2V5In0.eyJpc3MiOiJodHRwczovL2lkZW50aXR5LmFzdGhlbm8uc29mdHdhcmUiLCJzdWIiOiJleHRlcm5hbC0xMjMiLCJhdWQiOiJmaXh0dXJlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjQxMDI0NDQ4MDAsIm5vbmNlIjoiZml4dHVyZS1ub25jZSIsImF6cCI6Im90aGVyIn0.W8C8sSVCpLn32JAZ6VKoJyUV9Ew_-eBsatAtynsnzJpj8l28sKv_bo8lGPq5Ef3HTxiqEGtgdWRR1wO92k_-Tw",23 "authorizedParty": "eyJhbGciOiJFUzI1NiIsImtpZCI6InRlc3Qta2V5In0.eyJpc3MiOiJodHRwczovL2lkZW50aXR5LmFzdGhlbm8uc29mdHdhcmUiLCJzdWIiOiJleHRlcm5hbC0xMjMiLCJhdWQiOiJmaXh0dXJlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjQxMDI0NDQ4MDAsIm5vbmNlIjoiZml4dHVyZS1ub25jZSIsImF6cCI6Im90aGVyIn0.W8C8sSVCpLn32JAZ6VKoJyUV9Ew_-eBsatAtynsnzJpj8l28sKv_bo8lGPq5Ef3HTxiqEGtgdWRR1wO92k_-Tw",
24 "multipleAudience": "eyJhbGciOiJFUzI1NiIsImtpZCI6InRlc3Qta2V5In0.eyJpc3MiOiJodHRwczovL2lkZW50aXR5LmFzdGhlbm8uc29mdHdhcmUiLCJzdWIiOiJleHRlcm5hbC0xMjMiLCJhdWQiOlsiZml4dHVyZSIsIm90aGVyIl0sImlhdCI6MTcwMDAwMDAwMCwiZXhwIjo0MTAyNDQ0ODAwLCJub25jZSI6ImZpeHR1cmUtbm9uY2UifQ.D3HM97jMyMnqatXGcjDRopQIYe-ZKjzMuvVNt5dkqFW-tqXKXJlNXGd1hfpCc8XIdK4pL_iRaYwIbnMnVfZoQA"24 "multipleAudience": "eyJhbGciOiJFUzI1NiIsImtpZCI6InRlc3Qta2V5In0.eyJpc3MiOiJodHRwczovL2lkZW50aXR5LmFzdGhlbm8uc29mdHdhcmUiLCJzdWIiOiJleHRlcm5hbC0xMjMiLCJhdWQiOlsiZml4dHVyZSIsIm90aGVyIl0sImlhdCI6MTcwMDAwMDAwMCwiZXhwIjo0MTAyNDQ0ODAwLCJub25jZSI6ImZpeHR1cmUtbm9uY2UifQ.D3HM97jMyMnqatXGcjDRopQIYe-ZKjzMuvVNt5dkqFW-tqXKXJlNXGd1hfpCc8XIdK4pL_iRaYwIbnMnVfZoQA"
25 },
26 "withoutKid": {
27 "token": "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJodHRwczovL2lkZW50aXR5LmFzdGhlbm8uc29mdHdhcmUiLCJzdWIiOiJleHRlcm5hbC0xMjMiLCJhdWQiOiJmaXh0dXJlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjQxMDI0NDQ4MDAsIm5vbmNlIjoiZml4dHVyZS1ub25jZSJ9.EOWeGwhkb5C0H0844bjW3hfieiTusaMAnGsrXjR9xZsVA59uy-zggNrWBQWf6v_eyb2aWZDNwMDcVo240GPyGQ",
28 "keys": {
29 "keys": [
30 {
31 "kty": "EC",
32 "crv": "P-256",
33 "x": "CGn5dXeweSkJttck59RKlf4xg3en2nz1wepSXpOywvU",
34 "y": "poKWPsMzUkytFzQmVtH7ps0sajMsZrCuYbeKAoOCw60"
35 }
36 ]
37 }
25 }38 }
26}39}
dashboard/web/pages/SignIn.css+1-1
...@@ -9,5 +9,5 @@ body { font: 16px "Name Sans", sans-serif; }...@@ -9,5 +9,5 @@ body { font: 16px "Name Sans", sans-serif; }
9.pf-v5-c-login__main .checkbox label:has(:focus-visible) { outline: 2px solid var(--primary); }9.pf-v5-c-login__main .checkbox label:has(:focus-visible) { outline: 2px solid var(--primary); }
10.setup-intro { margin-bottom: 1rem; text-align: center; }10.setup-intro { margin-bottom: 1rem; text-align: center; }
11.guest-providers { margin-top: 1.5rem; }11.guest-providers { margin-top: 1.5rem; }
12.guest-providers p { text-align: center; }12.guest-providers a { display: flex; align-items: center; justify-content: center; text-decoration: none; }
13.guest-providers a + a { margin-top: .75rem; }13.guest-providers a + a { margin-top: .75rem; }
dashboard/web/pages/SignIn.tsx+1-2
...@@ -110,8 +110,7 @@ export function SignIn() {...@@ -110,8 +110,7 @@ export function SignIn() {
110 <Show when={!setup}><a id="authenticateWebAuthnButton" href="#" class="pf-v5-c-button pf-m-secondary pf-m-block" aria-disabled={busy() || !status()} onClick={(event) => { event.preventDefault(); void passkey(); }}>sign in with passkey</a></Show>110 <Show when={!setup}><a id="authenticateWebAuthnButton" href="#" class="pf-v5-c-button pf-m-secondary pf-m-block" aria-disabled={busy() || !status()} onClick={(event) => { event.preventDefault(); void passkey(); }}>sign in with passkey</a></Show>
111 <Show when={!setup && status()?.providers?.length}>111 <Show when={!setup && status()?.providers?.length}>
112 <div id="kc-social-providers" class="guest-providers">112 <div id="kc-social-providers" class="guest-providers">
113 <p>sign in as a Shale guest</p>113 <For each={status()?.providers}>{(provider) => <a rel="external" class="pf-v5-c-button pf-m-primary pf-m-block" href={`/auth/guest/start/${provider.id}?next=${encodeURIComponent(params.get("next") ?? "")}`}>continue with {provider.name}</a>}</For>
114 <For each={status()?.providers}>{(provider) => <a rel="external" class="pf-v5-c-button pf-m-secondary pf-m-block" href={`/auth/guest/start/${provider.id}?next=${encodeURIComponent(params.get("next") ?? "")}`}>continue with {provider.name}</a>}</For>
115 </div>114 </div>
116 </Show>115 </Show>
117 </Show>116 </Show>