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.

Blame · server/web/static/auth.js

Line attribution on main · 67 line(s)

c717d4eb
Keenan Nemetz
13d ago
1 // Passkey ceremonies for the browser: sign-in, registration, an enrollment
2 // invite, and adding a passkey to an existing account.
3 //
4 // This is the one script the CSP allows to touch credential APIs, and the only
5 // script that posts JSON rather than HTML. The server returns the challenge and
6 // options; this file converts the base64url fields to and from bytes, calls
7 // navigator.credentials, and posts the result back. It never decides who the
8 // account is: the server resolves that from the assertion.
9 (function(){
10 const csrf = (document.querySelector('meta[name="csrf"]') || {}).content || '';
11 function b64(s){s=s.replace(/-/g,'+').replace(/_/g,'/');while(s.length%4)s+='=';const bin=atob(s);const out=new Uint8Array(bin.length);for(let i=0;i<bin.length;i++)out[i]=bin.charCodeAt(i);return out}
12 function enc(buf){const b=new Uint8Array(buf);let s='';for(let i=0;i<b.length;i++)s+=String.fromCharCode(b[i]);return btoa(s).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'')}
13 function prepCreate(o){o.publicKey.challenge=b64(o.publicKey.challenge);o.publicKey.user.id=b64(o.publicKey.user.id);(o.publicKey.excludeCredentials||[]).forEach(c=>c.id=b64(c.id));return o.publicKey}
14 function prepGet(o){o.publicKey.challenge=b64(o.publicKey.challenge);(o.publicKey.allowCredentials||[]).forEach(c=>c.id=b64(c.id));return o.publicKey}
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 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 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))}
ad79b7ab
Keenan Nemetz
2d ago
18 async function signInWithPasskey(form,username,mediation){
ed03b3d1
Keenan Nemetz
2h ago
19 const b=await post('/-/auth/login/begin',username?{username:username}:{});
ad79b7ab
Keenan Nemetz
2d ago
20 const opts={publicKey:prepGet(b)};
21 if(mediation)opts.mediation=mediation;
22 const c=await navigator.credentials.get(opts);
ed03b3d1
Keenan Nemetz
2h ago
23 await post('/-/auth/login/finish',resp(c));
ad79b7ab
Keenan Nemetz
2d ago
24 location.href=form.dataset.next||'/';
25 }
c717d4eb
Keenan Nemetz
13d ago
26 document.querySelectorAll('form[data-auth]').forEach(function(form){
27 form.addEventListener('submit',async function(ev){
28 ev.preventDefault();const btn=form.querySelector('button[type=submit]');if(btn)btn.disabled=true;
29 try{
30 if(form.dataset.auth==='login'){
ad79b7ab
Keenan Nemetz
2d ago
31 await signInWithPasskey(form,form.username.value.trim());
c717d4eb
Keenan Nemetz
13d ago
32 }else if(form.dataset.auth==='invite'){
edf9db6e
Keenan Nemetz
12d ago
33 // The token is in the fragment, which the browser never sends to the
34 // server in a request line or a Referer. It is read here and posted.
35 const token=location.hash.replace(/^#/,'');
36 if(!token)throw new Error('This enrollment link is incomplete. Ask an instance admin for a new one.');
ed03b3d1
Keenan Nemetz
2h ago
37 const b=await post('/-/auth/invite/begin',{token:token});
c717d4eb
Keenan Nemetz
13d ago
38 const c=await navigator.credentials.create({publicKey:prepCreate(b)});
ed03b3d1
Keenan Nemetz
2h ago
39 await post('/-/auth/invite/finish',resp(c));
c717d4eb
Keenan Nemetz
13d ago
40 location.href='/';
41 }else if(form.dataset.auth==='register'){
ed03b3d1
Keenan Nemetz
2h ago
42 const b=await post('/-/auth/register/begin',{username:form.username.value.trim()});
c717d4eb
Keenan Nemetz
13d ago
43 const c=await navigator.credentials.create({publicKey:prepCreate(b)});
ed03b3d1
Keenan Nemetz
2h ago
44 await post('/-/auth/register/finish',resp(c));
c717d4eb
Keenan Nemetz
13d ago
45 location.href='/';
46 }else if(form.dataset.auth==='passkey'){
ed03b3d1
Keenan Nemetz
2h ago
47 const b=await post('/-/account/passkeys/begin',{label:form.label.value.trim()},true);
c717d4eb
Keenan Nemetz
13d ago
48 const c=await navigator.credentials.create({publicKey:prepCreate(b)});
ed03b3d1
Keenan Nemetz
2h ago
49 await post('/-/account/passkeys/finish',resp(c),true);
c717d4eb
Keenan Nemetz
13d ago
50 location.reload();
51 }
52 }catch(err){fail(form,err);if(btn)btn.disabled=false}
53 })
54 })
ad79b7ab
Keenan Nemetz
2d ago
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 }
c717d4eb
Keenan Nemetz
13d ago
67 })();