2022-01-29 00:00:11 +03:00
import $ from 'jquery' ;
2022-01-14 18:03:31 +03:00
import { encode , decode } from 'uint8-to-base64' ;
const { appSubUrl , csrfToken } = window . config ;
export function initUserAuthWebAuthn ( ) {
if ( $ ( '.user.signin.webauthn-prompt' ) . length === 0 ) {
return ;
}
if ( ! detectWebAuthnSupport ( ) ) {
return ;
}
$ . getJSON ( ` ${ appSubUrl } /user/webauthn/assertion ` , { } )
. done ( ( makeAssertionOptions ) => {
makeAssertionOptions . publicKey . challenge = decode ( makeAssertionOptions . publicKey . challenge ) ;
for ( let i = 0 ; i < makeAssertionOptions . publicKey . allowCredentials . length ; i ++ ) {
makeAssertionOptions . publicKey . allowCredentials [ i ] . id = decode ( makeAssertionOptions . publicKey . allowCredentials [ i ] . id ) ;
}
navigator . credentials . get ( {
publicKey : makeAssertionOptions . publicKey
} )
. then ( ( credential ) => {
verifyAssertion ( credential ) ;
} ) . catch ( ( err ) => {
2022-02-09 10:37:58 +03:00
// Try again... without the appid
if ( makeAssertionOptions . publicKey . extensions && makeAssertionOptions . publicKey . extensions . appid ) {
delete makeAssertionOptions . publicKey . extensions [ 'appid' ] ;
navigator . credentials . get ( {
publicKey : makeAssertionOptions . publicKey
} )
. then ( ( credential ) => {
verifyAssertion ( credential ) ;
} ) . catch ( ( err ) => {
webAuthnError ( 'general' , err . message ) ;
} ) ;
return ;
}
2022-01-15 19:52:56 +03:00
webAuthnError ( 'general' , err . message ) ;
2022-01-14 18:03:31 +03:00
} ) ;
} ) . fail ( ( ) => {
webAuthnError ( 'unknown' ) ;
} ) ;
}
function verifyAssertion ( assertedCredential ) {
// Move data into Arrays incase it is super long
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 ) ;
$ . ajax ( {
url : ` ${ appSubUrl } /user/webauthn/assertion ` ,
type : 'POST' ,
data : JSON . stringify ( {
id : assertedCredential . id ,
rawId : bufferEncode ( rawId ) ,
type : assertedCredential . type ,
clientExtensionResults : assertedCredential . getClientExtensionResults ( ) ,
response : {
authenticatorData : bufferEncode ( authData ) ,
clientDataJSON : bufferEncode ( clientDataJSON ) ,
signature : bufferEncode ( sig ) ,
userHandle : bufferEncode ( userHandle ) ,
} ,
} ) ,
contentType : 'application/json; charset=utf-8' ,
dataType : 'json' ,
success : ( resp ) => {
if ( resp && resp [ 'redirect' ] ) {
window . location . href = resp [ 'redirect' ] ;
} else {
window . location . href = '/' ;
}
} ,
error : ( xhr ) => {
if ( xhr . status === 500 ) {
webAuthnError ( 'unknown' ) ;
return ;
}
webAuthnError ( 'unable-to-process' ) ;
}
} ) ;
}
// Encode an ArrayBuffer into a base64 string.
function bufferEncode ( value ) {
return encode ( value )
. replace ( /\+/g , '-' )
. replace ( /\//g , '_' )
. replace ( /=/g , '' ) ;
}
function webauthnRegistered ( newCredential ) {
const attestationObject = new Uint8Array ( newCredential . response . attestationObject ) ;
const clientDataJSON = new Uint8Array ( newCredential . response . clientDataJSON ) ;
const rawId = new Uint8Array ( newCredential . rawId ) ;
return $ . ajax ( {
url : ` ${ appSubUrl } /user/settings/security/webauthn/register ` ,
type : 'POST' ,
headers : { 'X-Csrf-Token' : csrfToken } ,
data : JSON . stringify ( {
id : newCredential . id ,
rawId : bufferEncode ( rawId ) ,
type : newCredential . type ,
response : {
attestationObject : bufferEncode ( attestationObject ) ,
clientDataJSON : bufferEncode ( clientDataJSON ) ,
} ,
} ) ,
dataType : 'json' ,
contentType : 'application/json; charset=utf-8' ,
} ) . then ( ( ) => {
window . location . reload ( ) ;
} ) . fail ( ( xhr ) => {
if ( xhr . status === 409 ) {
webAuthnError ( 'duplicated' ) ;
return ;
}
webAuthnError ( 'unknown' ) ;
} ) ;
}
function webAuthnError ( errorType , message ) {
$ ( '#webauthn-error [data-webauthn-error-msg]' ) . hide ( ) ;
2022-01-15 19:52:56 +03:00
const $errorGeneral = $ ( ` #webauthn-error [data-webauthn-error-msg=general] ` ) ;
if ( errorType === 'general' ) {
$errorGeneral . show ( ) . text ( message || 'unknown error' ) ;
2022-01-14 18:03:31 +03:00
} else {
2022-01-15 19:52:56 +03:00
const $errorTyped = $ ( ` #webauthn-error [data-webauthn-error-msg= ${ errorType } ] ` ) ;
if ( $errorTyped . length ) {
$errorTyped . show ( ) ;
} else {
$errorGeneral . show ( ) . text ( ` unknown error type: ${ errorType } ` ) ;
}
2022-01-14 18:03:31 +03:00
}
$ ( '#webauthn-error' ) . modal ( 'show' ) ;
}
function detectWebAuthnSupport ( ) {
if ( ! window . isSecureContext ) {
$ ( '#register-button' ) . prop ( 'disabled' , true ) ;
$ ( '#login-button' ) . prop ( 'disabled' , true ) ;
webAuthnError ( 'insecure' ) ;
return false ;
}
if ( typeof window . PublicKeyCredential !== 'function' ) {
$ ( '#register-button' ) . prop ( 'disabled' , true ) ;
$ ( '#login-button' ) . prop ( 'disabled' , true ) ;
webAuthnError ( 'browser' ) ;
return false ;
}
return true ;
}
export function initUserAuthWebAuthnRegister ( ) {
if ( $ ( '#register-webauthn' ) . length === 0 ) {
return ;
}
$ ( '#webauthn-error' ) . modal ( { allowMultiple : false } ) ;
$ ( '#register-webauthn' ) . on ( 'click' , ( e ) => {
e . preventDefault ( ) ;
2022-01-24 22:57:01 +03:00
if ( ! detectWebAuthnSupport ( ) ) {
return ;
}
2022-01-14 18:03:31 +03:00
webAuthnRegisterRequest ( ) ;
} ) ;
}
function webAuthnRegisterRequest ( ) {
if ( $ ( '#nickname' ) . val ( ) === '' ) {
webAuthnError ( 'empty' ) ;
return ;
}
$ . post ( ` ${ appSubUrl } /user/settings/security/webauthn/request_register ` , {
_csrf : csrfToken ,
name : $ ( '#nickname' ) . val ( ) ,
} ) . done ( ( makeCredentialOptions ) => {
$ ( '#nickname' ) . closest ( 'div.field' ) . removeClass ( 'error' ) ;
makeCredentialOptions . publicKey . challenge = decode ( makeCredentialOptions . publicKey . challenge ) ;
makeCredentialOptions . publicKey . user . id = decode ( makeCredentialOptions . publicKey . user . id ) ;
if ( makeCredentialOptions . publicKey . excludeCredentials ) {
for ( let i = 0 ; i < makeCredentialOptions . publicKey . excludeCredentials . length ; i ++ ) {
makeCredentialOptions . publicKey . excludeCredentials [ i ] . id = decode ( makeCredentialOptions . publicKey . excludeCredentials [ i ] . id ) ;
}
}
navigator . credentials . create ( {
publicKey : makeCredentialOptions . publicKey
} ) . then ( webauthnRegistered )
. catch ( ( err ) => {
if ( ! err ) {
webAuthnError ( 'unknown' ) ;
return ;
}
2022-01-15 19:52:56 +03:00
webAuthnError ( 'general' , err . message ) ;
2022-01-14 18:03:31 +03:00
} ) ;
} ) . fail ( ( xhr ) => {
if ( xhr . status === 409 ) {
webAuthnError ( 'duplicated' ) ;
return ;
}
webAuthnError ( 'unknown' ) ;
} ) ;
}