2024-10-17 21:40:51 -05:00
/ *
* Copyright 2002 - 2024 the original author or authors .
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* https : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
"use strict" ;
import base64url from "./base64url.js" ;
import http from "./http.js" ;
import abortController from "./abort-controller.js" ;
async function isConditionalMediationAvailable ( ) {
return ! ! (
window . PublicKeyCredential &&
window . PublicKeyCredential . isConditionalMediationAvailable &&
( await window . PublicKeyCredential . isConditionalMediationAvailable ( ) )
) ;
}
async function authenticate ( headers , contextPath , useConditionalMediation ) {
let options ;
try {
const optionsResponse = await http . post ( ` ${ contextPath } /webauthn/authenticate/options ` , headers ) ;
if ( ! optionsResponse . ok ) {
throw new Error ( ` HTTP ${ optionsResponse . status } ` ) ;
}
options = await optionsResponse . json ( ) ;
} catch ( err ) {
throw new Error ( ` Authentication failed. Could not fetch authentication options: ${ err . message } ` , { cause : err } ) ;
}
// FIXME: Use https://www.w3.org/TR/webauthn-3/#sctn-parseRequestOptionsFromJSON
2025-01-17 15:07:24 +01:00
const decodedAllowCredentials = ! options . allowCredentials
? [ ]
: options . allowCredentials . map ( ( cred ) => ( {
... cred ,
id : base64url . decode ( cred . id ) ,
} ) ) ;
2024-10-17 21:40:51 -05:00
const decodedOptions = {
... options ,
2025-01-17 15:07:24 +01:00
allowCredentials : decodedAllowCredentials ,
2024-10-17 21:40:51 -05:00
challenge : base64url . decode ( options . challenge ) ,
} ;
// Invoke the WebAuthn get() method.
const credentialOptions = {
publicKey : decodedOptions ,
signal : abortController . newSignal ( ) ,
} ;
if ( useConditionalMediation ) {
// Request a conditional UI
credentialOptions . mediation = "conditional" ;
}
let cred ;
try {
cred = await navigator . credentials . get ( credentialOptions ) ;
} catch ( err ) {
throw new Error ( ` Authentication failed. Call to navigator.credentials.get failed: ${ err . message } ` , { cause : err } ) ;
}
const { response , type : credType } = cred ;
let userHandle ;
if ( response . userHandle ) {
userHandle = base64url . encode ( response . userHandle ) ;
}
const body = {
id : cred . id ,
rawId : base64url . encode ( cred . rawId ) ,
response : {
authenticatorData : base64url . encode ( response . authenticatorData ) ,
clientDataJSON : base64url . encode ( response . clientDataJSON ) ,
signature : base64url . encode ( response . signature ) ,
userHandle ,
} ,
credType ,
clientExtensionResults : cred . getClientExtensionResults ( ) ,
authenticatorAttachment : cred . authenticatorAttachment ,
} ;
let authenticationResponse ;
try {
const authenticationCallResponse = await http . post ( ` ${ contextPath } /login/webauthn ` , headers , body ) ;
if ( ! authenticationCallResponse . ok ) {
throw new Error ( ` HTTP ${ authenticationCallResponse . status } ` ) ;
}
authenticationResponse = await authenticationCallResponse . json ( ) ;
// if (authenticationResponse && authenticationResponse.authenticated) {
} catch ( err ) {
throw new Error ( ` Authentication failed. Could not process the authentication request: ${ err . message } ` , {
cause : err ,
} ) ;
}
if ( ! ( authenticationResponse && authenticationResponse . authenticated && authenticationResponse . redirectUrl ) ) {
throw new Error (
` Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: ${ JSON . stringify ( authenticationResponse ) } ` ,
) ;
}
return authenticationResponse . redirectUrl ;
}
async function register ( headers , contextPath , label ) {
if ( ! label ) {
throw new Error ( "Error: Passkey Label is required" ) ;
}
let options ;
try {
const optionsResponse = await http . post ( ` ${ contextPath } /webauthn/register/options ` , headers ) ;
if ( ! optionsResponse . ok ) {
throw new Error ( ` Server responded with HTTP ${ optionsResponse . status } ` ) ;
}
options = await optionsResponse . json ( ) ;
} catch ( e ) {
throw new Error ( ` Registration failed. Could not fetch registration options: ${ e . message } ` , { cause : e } ) ;
}
// FIXME: Use https://www.w3.org/TR/webauthn-3/#sctn-parseCreationOptionsFromJSON
const decodedExcludeCredentials = ! options . excludeCredentials
? [ ]
: options . excludeCredentials . map ( ( cred ) => ( {
... cred ,
id : base64url . decode ( cred . id ) ,
} ) ) ;
const decodedOptions = {
... options ,
user : {
... options . user ,
id : base64url . decode ( options . user . id ) ,
} ,
challenge : base64url . decode ( options . challenge ) ,
excludeCredentials : decodedExcludeCredentials ,
} ;
let credentialsContainer ;
try {
credentialsContainer = await navigator . credentials . create ( {
publicKey : decodedOptions ,
signal : abortController . newSignal ( ) ,
} ) ;
} catch ( e ) {
throw new Error ( ` Registration failed. Call to navigator.credentials.create failed: ${ e . message } ` , { cause : e } ) ;
}
// FIXME: Let response be credential.response. If response is not an instance of AuthenticatorAttestationResponse, abort the ceremony with a user-visible error. https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
const { response } = credentialsContainer ;
const credential = {
id : credentialsContainer . id ,
rawId : base64url . encode ( credentialsContainer . rawId ) ,
response : {
attestationObject : base64url . encode ( response . attestationObject ) ,
clientDataJSON : base64url . encode ( response . clientDataJSON ) ,
transports : response . getTransports ? response . getTransports ( ) : [ ] ,
} ,
type : credentialsContainer . type ,
clientExtensionResults : credentialsContainer . getClientExtensionResults ( ) ,
authenticatorAttachment : credentialsContainer . authenticatorAttachment ,
} ;
const registrationRequest = {
publicKey : {
credential : credential ,
label : label ,
} ,
} ;
let verificationJSON ;
try {
const verificationResp = await http . post ( ` ${ contextPath } /webauthn/register ` , headers , registrationRequest ) ;
if ( ! verificationResp . ok ) {
throw new Error ( ` HTTP ${ verificationResp . status } ` ) ;
}
verificationJSON = await verificationResp . json ( ) ;
} catch ( e ) {
throw new Error ( ` Registration failed. Could not process the registration request: ${ e . message } ` , { cause : e } ) ;
}
if ( ! ( verificationJSON && verificationJSON . success ) ) {
throw new Error ( ` Registration failed. Server responded with: ${ JSON . stringify ( verificationJSON ) } ` ) ;
}
}
export default {
authenticate ,
register ,
isConditionalMediationAvailable ,
} ;