Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Offer passkeys through conditional UI and require discoverable credentials

ad79b7abded893d0771eaf80de555a1236f17474 · Keenan Nemetz · 3d ago · parent 25343521

Offer passkeys through conditional UI and require discoverable credentials

Start a usernameless ceremony when the login page advertises conditional
mediation, so the browser can offer a saved passkey in the account field. The
assertion resolves the account from the credential id and user handle;
username-first sign-in stays for browsers without autofill and for passkeys
created before discoverable credentials were required.

Register each new passkey with residentKey required, and record the autofill
flow and fallback in the account documentation.

Refs #213 (01M6SAK9STFGJ7PECY7ET8277N).

server/web/static/auth.js +21−4

@@ -15,15 +15,20 @@
15 15 function resp(c){const r=c.response;const out={id:c.id,rawId:enc(c.rawId),type:c.type,clientExtensionResults:c.getClientExtensionResults()};if(r.attestationObject){out.response={clientDataJSON:enc(r.clientDataJSON),attestationObject:enc(r.attestationObject)};if(r.getTransports)out.response.transports=r.getTransports()}else{out.response={clientDataJSON:enc(r.clientDataJSON),authenticatorData:enc(r.authenticatorData),signature:enc(r.signature)};if(r.userHandle)out.response.userHandle=enc(r.userHandle)}if(c.authenticatorAttachment)out.authenticatorAttachment=c.authenticatorAttachment;return out}
16 16 async function post(url,body,withCSRF){const h={'Content-Type':'application/json'};if(withCSRF)h['X-Vole-CSRF']=csrf;const r=await fetch(url,{method:'POST',headers:h,body:JSON.stringify(body),credentials:'same-origin'});let d={};try{d=await r.json()}catch(e){}if(!r.ok)throw new Error(d.error||r.statusText);return d}
17 17 function fail(form,err){const e=form.querySelector('[data-auth-error]')||document.querySelector('[data-auth-error]');if(e)e.textContent=err.message||String(err);else alert(err.message||String(err))}
18 async function signInWithPasskey(form,username,mediation){
19 const b=await post('/auth/login/begin',username?{username:username}:{});
20 const opts={publicKey:prepGet(b)};
21 if(mediation)opts.mediation=mediation;
22 const c=await navigator.credentials.get(opts);
23 await post('/auth/login/finish',resp(c));
24 location.href=form.dataset.next||'/';
25 }
18 26 document.querySelectorAll('form[data-auth]').forEach(function(form){
19 27 form.addEventListener('submit',async function(ev){
20 28 ev.preventDefault();const btn=form.querySelector('button[type=submit]');if(btn)btn.disabled=true;
21 29 try{
22 30 if(form.dataset.auth==='login'){
23 const b=await post('/auth/login/begin',{username:form.username.value.trim()});
24 const c=await navigator.credentials.get({publicKey:prepGet(b)});
25 await post('/auth/login/finish',resp(c));
26 location.href=form.dataset.next||'/';
31 await signInWithPasskey(form,form.username.value.trim());
27 32 }else if(form.dataset.auth==='invite'){
28 33 // The token is in the fragment, which the browser never sends to the
29 34 // server in a request line or a Referer. It is read here and posted.
@@ -47,4 +52,16 @@
47 52 }catch(err){fail(form,err);if(btn)btn.disabled=false}
48 53 })
49 54 })
55
56 // Conditional UI puts a saved passkey in the account field's autofill menu.
57 // The request starts only where the browser advertises support; the form below
58 // remains the fallback for browsers without it and for a non-discoverable
59 // passkey. A cancelled or empty autofill is not an error to show on page load.
60 const loginForm=document.querySelector('form[data-auth="login"]');
61 if(loginForm&&window.PublicKeyCredential&&typeof PublicKeyCredential.isConditionalMediationAvailable==='function'){
62 PublicKeyCredential.isConditionalMediationAvailable().then(function(ok){
63 if(!ok)return;
64 signInWithPasskey(loginForm,'','conditional').catch(function(){});
65 }).catch(function(){});
66 }
50 67 })();