| author | |
| committer | |
| log | c3d894fed29df8d1a864de6a5bd1d0fda57882f5 |
| tree | 082baddbd76ecb82cb690e2a289cdba37808c1b3 |
| parent | b62d003aab62b3dafb0dd9b662a2996a526b38ba |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
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-64 files changed, 70 insertions(+), 17 deletions(-)
dashboard/src/guest.rs+55-14| ... | ... | @@ -148,7 +148,10 @@ fn signed_claims( |
| 148 | 148 | let header: Value = |
| 149 | 149 | serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[0]).map_err(|_| reject())?) |
| 150 | 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 | 155 | || header.get("crit").is_some() |
| 153 | 156 | || header.get("jku").is_some() |
| 154 | 157 | || header.get("jwk").is_some() |
| ... | ... | @@ -158,8 +161,9 @@ fn signed_claims( |
| 158 | 161 | let matching: Vec<_> = array(&keys["keys"]) |
| 159 | 162 | .iter() |
| 160 | 163 | .filter(|key| { |
| 161 | key["kid"].is_string() | |
| 162 | && key["kid"] == header["kid"] | |
| 164 | header | |
| 165 | .get("kid") | |
| 166 | .is_none_or(|id| key.get("kid") == Some(id)) | |
| 163 | 167 | && key["kty"] == "EC" |
| 164 | 168 | && key["crv"] == "P-256" |
| 165 | 169 | && key.get("alg").is_none_or(|v| v == "ES256") |
| ... | ... | @@ -544,17 +548,23 @@ async fn handle(app: &App, request: Request) -> Result<Response> { |
| 544 | 548 | ], |
| 545 | 549 | ) |
| 546 | 550 | .into_response()), |
| 547 | Err(_) => Ok(( | |
| 548 | StatusCode::FOUND, | |
| 549 | [ | |
| 550 | ( | |
| 551 | "location", | |
| 552 | format!("/sign-in?next={}&guest_error=1", encoded(&next)), | |
| 553 | ), | |
| 554 | ("set-cookie", auth::set_cookie(COOKIE, "", 0)), | |
| 555 | ], | |
| 556 | ) | |
| 557 | .into_response()), | |
| 551 | Err(error) => { | |
| 552 | eprintln!( | |
| 553 | "guest callback {provider}: {}: {}", | |
| 554 | error.status, error.message | |
| 555 | ); | |
| 556 | Ok(( | |
| 557 | StatusCode::FOUND, | |
| 558 | [ | |
| 559 | ( | |
| 560 | "location", | |
| 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 | } |
| 560 | 570 | |
| ... | ... | @@ -585,6 +595,37 @@ mod tests { |
| 585 | 595 | let claims = |
| 586 | 596 | signed_claims(token, &vector["keys"], "fixture", "fixture-nonce", true).unwrap(); |
| 587 | 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 | 629 | for (name, token) in vector["invalid"].as_object().unwrap() { |
| 589 | 630 | assert!( |
| 590 | 631 | signed_claims( |
dashboard/tests/guest-jwt.json+13| ... | ... | @@ -22,5 +22,18 @@ |
| 22 | 22 | "subject": "eyJhbGciOiJFUzI1NiIsImtpZCI6InRlc3Qta2V5In0.eyJpc3MiOiJodHRwczovL2lkZW50aXR5LmFzdGhlbm8uc29mdHdhcmUiLCJzdWIiOiIiLCJhdWQiOiJmaXh0dXJlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjQxMDI0NDQ4MDAsIm5vbmNlIjoiZml4dHVyZS1ub25jZSJ9.LvYyzvg0HAzhAWKo76lWyPktuoxSdmXySfXjiIWea40O0fdqGa-Du73lHzTLUJ3BDS5yWd5gF3doHbYrhbL4TA", |
| 23 | 23 | "authorizedParty": "eyJhbGciOiJFUzI1NiIsImtpZCI6InRlc3Qta2V5In0.eyJpc3MiOiJodHRwczovL2lkZW50aXR5LmFzdGhlbm8uc29mdHdhcmUiLCJzdWIiOiJleHRlcm5hbC0xMjMiLCJhdWQiOiJmaXh0dXJlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjQxMDI0NDQ4MDAsIm5vbmNlIjoiZml4dHVyZS1ub25jZSIsImF6cCI6Im90aGVyIn0.W8C8sSVCpLn32JAZ6VKoJyUV9Ew_-eBsatAtynsnzJpj8l28sKv_bo8lGPq5Ef3HTxiqEGtgdWRR1wO92k_-Tw", |
| 24 | 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 | 9 | .pf-v5-c-login__main .checkbox label:has(:focus-visible) { outline: 2px solid var(--primary); } |
| 10 | 10 | .setup-intro { margin-bottom: 1rem; text-align: center; } |
| 11 | 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 | 13 | .guest-providers a + a { margin-top: .75rem; } |
dashboard/web/pages/SignIn.tsx+1-2| ... | ... | @@ -110,8 +110,7 @@ export function SignIn() { |
| 110 | 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 | 111 | <Show when={!setup && status()?.providers?.length}> |
| 112 | 112 | <div id="kc-social-providers" class="guest-providers"> |
| 113 | <p>sign in as a Shale guest</p> | |
| 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> | |
| 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> | |
| 115 | 114 | </div> |
| 116 | 115 | </Show> |
| 117 | 116 | </Show> |