123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- /*
- * 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,
- };
|