2024-10-17 21:40:51 -05:00
/*
2025-07-29 10:52:42 -05:00
* Copyright 2004-present the original author or authors.
2024-10-17 21:40:51 -05:00
*
* 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 ,
};