You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
202 lines
6.7 KiB
202 lines
6.7 KiB
/* |
|
* Copyright 2004-present 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 |
|
const decodedAllowCredentials = !options.allowCredentials |
|
? [] |
|
: options.allowCredentials.map((cred) => ({ |
|
...cred, |
|
id: base64url.decode(cred.id), |
|
})); |
|
|
|
const decodedOptions = { |
|
...options, |
|
allowCredentials: decodedAllowCredentials, |
|
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, |
|
};
|
|
|