2023-06-06 06:29:37 +01:00
import { encodeURLEncodedBase64 , decodeURLEncodedBase64 } from '../utils.js' ;
2023-06-07 19:20:18 +08:00
import { showElem } from '../utils/dom.js' ;
2022-01-14 23:03:31 +08:00
const { appSubUrl , csrfToken } = window . config ;
2023-06-06 06:29:37 +01:00
export async function initUserAuthWebAuthn ( ) {
const elPrompt = document . querySelector ( '.user.signin.webauthn-prompt' ) ;
if ( ! elPrompt ) {
2022-01-14 23:03:31 +08:00
return ;
}
if ( ! detectWebAuthnSupport ( ) ) {
return ;
}
2023-06-06 06:29:37 +01:00
const res = await fetch ( ` ${ appSubUrl } /user/webauthn/assertion ` ) ;
if ( res . status !== 200 ) {
webAuthnError ( 'unknown' ) ;
return ;
}
const options = await res . json ( ) ;
options . publicKey . challenge = decodeURLEncodedBase64 ( options . publicKey . challenge ) ;
for ( const cred of options . publicKey . allowCredentials ) {
cred . id = decodeURLEncodedBase64 ( cred . id ) ;
}
try {
2023-06-07 19:20:18 +08:00
const credential = await navigator . credentials . get ( {
publicKey : options . publicKey
} ) ;
2023-06-06 06:29:37 +01:00
await verifyAssertion ( credential ) ;
} catch ( err ) {
if ( ! options . publicKey . extensions ? . appid ) {
webAuthnError ( 'general' , err . message ) ;
return ;
}
delete options . publicKey . extensions . appid ;
try {
2023-06-07 19:20:18 +08:00
const credential = await navigator . credentials . get ( {
publicKey : options . publicKey
} ) ;
2023-06-06 06:29:37 +01:00
await verifyAssertion ( credential ) ;
} catch ( err ) {
webAuthnError ( 'general' , err . message ) ;
}
}
2022-01-14 23:03:31 +08:00
}
2023-06-06 06:29:37 +01:00
async function verifyAssertion ( assertedCredential ) {
2023-06-07 19:20:18 +08:00
// Move data into Arrays in case it is super long
2022-01-14 23:03:31 +08:00
const authData = new Uint8Array ( assertedCredential . response . authenticatorData ) ;
const clientDataJSON = new Uint8Array ( assertedCredential . response . clientDataJSON ) ;
const rawId = new Uint8Array ( assertedCredential . rawId ) ;
const sig = new Uint8Array ( assertedCredential . response . signature ) ;
const userHandle = new Uint8Array ( assertedCredential . response . userHandle ) ;
2023-06-06 06:29:37 +01:00
const res = await fetch ( ` ${ appSubUrl } /user/webauthn/assertion ` , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json; charset=utf-8'
} ,
body : JSON . stringify ( {
2022-01-14 23:03:31 +08:00
id : assertedCredential . id ,
2023-02-01 07:24:10 +00:00
rawId : encodeURLEncodedBase64 ( rawId ) ,
2022-01-14 23:03:31 +08:00
type : assertedCredential . type ,
clientExtensionResults : assertedCredential . getClientExtensionResults ( ) ,
response : {
2023-02-01 07:24:10 +00:00
authenticatorData : encodeURLEncodedBase64 ( authData ) ,
clientDataJSON : encodeURLEncodedBase64 ( clientDataJSON ) ,
signature : encodeURLEncodedBase64 ( sig ) ,
userHandle : encodeURLEncodedBase64 ( userHandle ) ,
2022-01-14 23:03:31 +08:00
} ,
} ) ,
} ) ;
2023-06-06 06:29:37 +01:00
if ( res . status === 500 ) {
webAuthnError ( 'unknown' ) ;
return ;
} else if ( res . status !== 200 ) {
webAuthnError ( 'unable-to-process' ) ;
return ;
}
const reply = await res . json ( ) ;
2022-01-14 23:03:31 +08:00
2023-06-06 06:29:37 +01:00
window . location . href = reply ? . redirect ? ? ` ${ appSubUrl } / ` ;
2023-02-01 07:24:10 +00:00
}
2023-06-06 06:29:37 +01:00
async function webauthnRegistered ( newCredential ) {
2022-01-14 23:03:31 +08:00
const attestationObject = new Uint8Array ( newCredential . response . attestationObject ) ;
const clientDataJSON = new Uint8Array ( newCredential . response . clientDataJSON ) ;
const rawId = new Uint8Array ( newCredential . rawId ) ;
2023-06-06 06:29:37 +01:00
const res = await fetch ( ` ${ appSubUrl } /user/settings/security/webauthn/register ` , {
method : 'POST' ,
headers : {
'X-Csrf-Token' : csrfToken ,
'Content-Type' : 'application/json; charset=utf-8' ,
} ,
body : JSON . stringify ( {
2022-01-14 23:03:31 +08:00
id : newCredential . id ,
2023-02-01 07:24:10 +00:00
rawId : encodeURLEncodedBase64 ( rawId ) ,
2022-01-14 23:03:31 +08:00
type : newCredential . type ,
response : {
2023-02-01 07:24:10 +00:00
attestationObject : encodeURLEncodedBase64 ( attestationObject ) ,
clientDataJSON : encodeURLEncodedBase64 ( clientDataJSON ) ,
2022-01-14 23:03:31 +08:00
} ,
} ) ,
} ) ;
2023-06-06 06:29:37 +01:00
if ( res . status === 409 ) {
webAuthnError ( 'duplicated' ) ;
return ;
} else if ( res . status !== 201 ) {
webAuthnError ( 'unknown' ) ;
return ;
}
window . location . reload ( ) ;
2022-01-14 23:03:31 +08:00
}
function webAuthnError ( errorType , message ) {
2023-06-06 06:29:37 +01:00
const elErrorMsg = document . getElementById ( ` webauthn-error-msg ` ) ;
2022-01-15 16:52:56 +00:00
if ( errorType === 'general' ) {
2023-06-06 06:29:37 +01:00
elErrorMsg . textContent = message || 'unknown error' ;
2022-01-14 23:03:31 +08:00
} else {
2023-06-06 06:29:37 +01:00
const elTypedError = document . querySelector ( ` #webauthn-error [data-webauthn-error-msg= ${ errorType } ] ` ) ;
if ( elTypedError ) {
elErrorMsg . textContent = ` ${ elTypedError . textContent } ${ message ? ` ${ message } ` : '' } ` ;
2022-01-15 16:52:56 +00:00
} else {
2023-06-06 06:29:37 +01:00
elErrorMsg . textContent = ` unknown error type: ${ errorType } ${ message ? ` ${ message } ` : '' } ` ;
2022-01-15 16:52:56 +00:00
}
2022-01-14 23:03:31 +08:00
}
2023-06-06 06:29:37 +01:00
showElem ( '#webauthn-error' ) ;
2022-01-14 23:03:31 +08:00
}
function detectWebAuthnSupport ( ) {
if ( ! window . isSecureContext ) {
webAuthnError ( 'insecure' ) ;
return false ;
}
if ( typeof window . PublicKeyCredential !== 'function' ) {
webAuthnError ( 'browser' ) ;
return false ;
}
return true ;
}
export function initUserAuthWebAuthnRegister ( ) {
2023-06-06 06:29:37 +01:00
const elRegister = document . getElementById ( 'register-webauthn' ) ;
if ( ! elRegister ) {
2022-01-14 23:03:31 +08:00
return ;
}
2023-06-07 19:20:18 +08:00
if ( ! detectWebAuthnSupport ( ) ) {
elRegister . disabled = true ;
return ;
}
elRegister . addEventListener ( 'click' , async ( e ) => {
2022-01-14 23:03:31 +08:00
e . preventDefault ( ) ;
2023-06-07 19:20:18 +08:00
await webAuthnRegisterRequest ( ) ;
2022-01-14 23:03:31 +08:00
} ) ;
}
2023-06-06 06:29:37 +01:00
async function webAuthnRegisterRequest ( ) {
const elNickname = document . getElementById ( 'nickname' ) ;
const body = new FormData ( ) ;
body . append ( 'name' , elNickname . value ) ;
const res = await fetch ( ` ${ appSubUrl } /user/settings/security/webauthn/request_register ` , {
method : 'POST' ,
headers : {
'X-Csrf-Token' : csrfToken ,
} ,
body ,
} ) ;
if ( res . status === 409 ) {
webAuthnError ( 'duplicated' ) ;
return ;
} else if ( res . status !== 200 ) {
webAuthnError ( 'unknown' ) ;
2022-01-14 23:03:31 +08:00
return ;
}
2023-06-06 06:29:37 +01:00
const options = await res . json ( ) ;
elNickname . closest ( 'div.field' ) . classList . remove ( 'error' ) ;
options . publicKey . challenge = decodeURLEncodedBase64 ( options . publicKey . challenge ) ;
options . publicKey . user . id = decodeURLEncodedBase64 ( options . publicKey . user . id ) ;
if ( options . publicKey . excludeCredentials ) {
for ( const cred of options . publicKey . excludeCredentials ) {
cred . id = decodeURLEncodedBase64 ( cred . id ) ;
2022-01-14 23:03:31 +08:00
}
2023-06-06 06:29:37 +01:00
}
try {
2023-06-07 19:20:18 +08:00
const credential = await navigator . credentials . create ( {
2023-06-06 06:29:37 +01:00
publicKey : options . publicKey
} ) ;
2023-06-07 19:20:18 +08:00
await webauthnRegistered ( credential ) ;
2023-06-06 06:29:37 +01:00
} catch ( err ) {
webAuthnError ( 'unknown' , err ) ;
}
2022-01-14 23:03:31 +08:00
}