@ -1,11 +1,13 @@
import $ from 'jquery' ;
import { encode , decode } from 'uint8-to-base64' ;
import { hideElem , showElem } from '../utils/dom.js' ;
import { encodeURLEncodedBase64 , decodeURLEncodedBase64 } from '../utils.js' ;
import { showElem , hideElem } from '../utils/dom.js' ;
const { appSubUrl , csrfToken } = window . config ;
export function initUserAuthWebAuthn ( ) {
if ( $ ( '.user.signin.webauthn-prompt' ) . length === 0 ) {
export async function initUserAuthWebAuthn ( ) {
hideElem ( '#webauthn-error' ) ;
const elPrompt = document . querySelector ( '.user.signin.webauthn-prompt' ) ;
if ( ! elPrompt ) {
return ;
}
@ -13,49 +15,52 @@ export function initUserAuthWebAuthn() {
return ;
}
$ . getJSON ( ` ${ appSubUrl } /user/webauthn/assertion ` , { } )
. done ( ( makeAssertionOptions ) => {
makeAssertionOptions . publicKey . challenge = decodeURLEncodedBase64 ( makeAssertionOptions . publicKey . challenge ) ;
for ( let i = 0 ; i < makeAssertionOptions . publicKey . allowCredentials . length ; i ++ ) {
makeAssertionOptions . publicKey . allowCredentials [ i ] . id = decodeURLEncodedBase64 ( makeAssertionOptions . publicKey . allowCredentials [ i ] . id ) ;
}
navigator . credentials . get ( {
publicKey : makeAssertionOptions . publicKey
} )
. then ( ( credential ) => {
verifyAssertion ( credential ) ;
} ) . catch ( ( err ) => {
// 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 ;
}
webAuthnError ( 'general' , err . message ) ;
} ) ;
} ) . fail ( ( ) => {
webAuthnError ( 'unknown' ) ;
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 ) ;
}
const credential = await navigator . credentials . get ( {
publicKey : options . publicKey
} ) ;
try {
await verifyAssertion ( credential ) ;
} catch ( err ) {
if ( ! options . publicKey . extensions ? . appid ) {
webAuthnError ( 'general' , err . message ) ;
return ;
}
delete options . publicKey . extensions . appid ;
const credential = await navigator . credentials . get ( {
publicKey : options . publicKey
} ) ;
try {
await verifyAssertion ( credential ) ;
} catch ( err ) {
webAuthnError ( 'general' , err . message ) ;
}
}
}
function verifyAssertion ( assertedCredential ) {
async 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 ( {
const res = await fetch ( ` ${ appSubUrl } /user/webauthn/assertion ` , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json; charset=utf-8'
} ,
body : JSON . stringify ( {
id : assertedCredential . id ,
rawId : encodeURLEncodedBase64 ( rawId ) ,
type : assertedCredential . type ,
@ -67,50 +72,31 @@ function verifyAssertion(assertedCredential) {
userHandle : encodeURLEncodedBase64 ( 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 URLEncoded base64 string.
function encodeURLEncodedBase64 ( value ) {
return encode ( value )
. replace ( /\+/g , '-' )
. replace ( /\//g , '_' )
. replace ( /=/g , '' ) ;
}
if ( res . status === 500 ) {
webAuthnError ( 'unknown' ) ;
return ;
} else if ( res . status !== 200 ) {
webAuthnError ( 'unable-to-process' ) ;
return ;
}
const reply = await res . json ( ) ;
// Dccode a URLEncoded base64 to an ArrayBuffer string.
function decodeURLEncodedBase64 ( value ) {
return decode ( value
. replace ( /_/g , '/' )
. replace ( /-/g , '+' ) ) ;
window . location . href = reply ? . redirect ? ? ` ${ appSubUrl } / ` ;
}
function webauthnRegistered ( newCredential ) {
async 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 ( {
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 ( {
id : newCredential . id ,
rawId : encodeURLEncodedBase64 ( rawId ) ,
type : newCredential . type ,
@ -119,48 +105,47 @@ function webauthnRegistered(newCredential) {
clientDataJSON : encodeURLEncodedBase64 ( clientDataJSON ) ,
} ,
} ) ,
dataType : 'json' ,
contentType : 'application/json; charset=utf-8' ,
} ) . then ( ( ) => {
window . location . reload ( ) ;
} ) . fail ( ( xhr ) => {
if ( xhr . status === 409 ) {
webAuthnError ( 'duplicated' ) ;
return ;
}
webAuthnError ( 'unknown' ) ;
} ) ;
if ( res . status === 409 ) {
webAuthnError ( 'duplicated' ) ;
return ;
} else if ( res . status !== 201 ) {
webAuthnError ( 'unknown' ) ;
return ;
}
window . location . reload ( ) ;
}
function webAuthnError ( errorType , message ) {
hideElem ( $ ( '#webauthn-error [data-webauthn-error-msg]' ) ) ;
const $errorGeneral = $ ( ` #webauthn-error [data-webauthn-error-msg=general] ` ) ;
const elErrorMsg = document . getElementById ( ` webauthn-error-msg ` ) ;
if ( errorType === 'general' ) {
showElem ( $errorGeneral ) ;
$errorGeneral . text ( message || 'unknown error' ) ;
elErrorMsg . textContent = message || 'unknown error' ;
} else {
const $errorTyped = $ ( ` #webauthn-error [data-webauthn-error-msg= ${ errorType } ] ` ) ;
if ( $errorTyped. length ) {
showElem( $errorTyped ) ;
const elTypedError = document . querySelector ( ` #webauthn-error [data-webauthn-error-msg= ${ errorType } ] ` ) ;
if ( elTypedError ) {
elErrorMsg. textContent = ` ${ elTypedError . textContent } ${ message ? ` ${ message } ` : '' } ` ;
} else {
showElem ( $errorGeneral ) ;
$errorGeneral . text ( ` unknown error type: ${ errorType } ` ) ;
elErrorMsg . textContent = ` unknown error type: ${ errorType } ${ message ? ` ${ message } ` : '' } ` ;
}
}
$ ( '#webauthn-error' ) . modal ( 'show' ) ;
showElem ( '#webauthn-error' ) ;
}
function detectWebAuthnSupport ( ) {
if ( ! window . isSecureContext ) {
$ ( '#register-button' ) . prop ( 'disabled' , true ) ;
$ ( '#login-button' ) . prop ( 'disabled' , true ) ;
document . getElementById ( 'register-button' ) . disabled = true ;
document . getElementById ( 'login-button' ) . disabled = true ;
webAuthnError ( 'insecure' ) ;
return false ;
}
if ( typeof window . PublicKeyCredential !== 'function' ) {
$ ( '#register-button' ) . prop ( 'disabled' , true ) ;
$ ( '#login-button' ) . prop ( 'disabled' , true ) ;
document . getElementById ( 'register-button' ) . disabled = true ;
document . getElementById ( 'login-button' ) . disabled = true ;
webAuthnError ( 'browser' ) ;
return false ;
}
@ -169,12 +154,14 @@ function detectWebAuthnSupport() {
}
export function initUserAuthWebAuthnRegister ( ) {
if ( $ ( '#register-webauthn' ) . length === 0 ) {
const elRegister = document . getElementById ( 'register-webauthn' ) ;
if ( ! elRegister ) {
return ;
}
$ ( '#webauthn-error' ) . modal ( { allowMultiple : false } ) ;
$ ( '#register-webauthn' ) . on ( 'click' , ( e ) => {
hideElem ( '#webauthn-error' ) ;
elRegister . addEventListener ( 'click' , ( e ) => {
e . preventDefault ( ) ;
if ( ! detectWebAuthnSupport ( ) ) {
return ;
@ -183,40 +170,48 @@ export function initUserAuthWebAuthnRegister() {
} ) ;
}
function webAuthnRegisterRequest ( ) {
if ( $ ( '#nickname' ) . val ( ) === '' ) {
webAuthnError ( 'empty' ) ;
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' ) ;
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 = decodeURLEncodedBase64 ( makeCredentialOptions . publicKey . challenge ) ;
makeCredentialOptions . publicKey . user . id = decodeURLEncodedBase64 ( makeCredentialOptions . publicKey . user . id ) ;
if ( makeCredentialOptions . publicKey . excludeCredentials ) {
for ( let i = 0 ; i < makeCredentialOptions . publicKey . excludeCredentials . length ; i ++ ) {
makeCredentialOptions . publicKey . excludeCredentials [ i ] . id = decodeURLEncodedBase64 ( makeCredentialOptions . publicKey . excludeCredentials [ i ] . id ) ;
}
}
navigator . credentials . create ( {
publicKey : makeCredentialOptions . publicKey
} ) . then ( webauthnRegistered )
. catch ( ( err ) => {
if ( ! err ) {
webAuthnError ( 'unknown' ) ;
return ;
}
webAuthnError ( 'general' , err . message ) ;
} ) ;
} ) . fail ( ( xhr ) => {
if ( xhr . status === 409 ) {
webAuthnError ( 'duplicated' ) ;
return ;
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 ) ;
}
webAuthnError ( 'unknown' ) ;
} ) ;
}
let credential ;
try {
credential = await navigator . credentials . create ( {
publicKey : options . publicKey
} ) ;
} catch ( err ) {
webAuthnError ( 'unknown' , err ) ;
return ;
}
webauthnRegistered ( credential ) ;
}