webauthn-core.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. /*
  2. * Copyright 2002-2024 the original author or authors.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * https://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. "use strict";
  17. import base64url from "./base64url.js";
  18. import http from "./http.js";
  19. import abortController from "./abort-controller.js";
  20. async function isConditionalMediationAvailable() {
  21. return !!(
  22. window.PublicKeyCredential &&
  23. window.PublicKeyCredential.isConditionalMediationAvailable &&
  24. (await window.PublicKeyCredential.isConditionalMediationAvailable())
  25. );
  26. }
  27. async function authenticate(headers, contextPath, useConditionalMediation) {
  28. let options;
  29. try {
  30. const optionsResponse = await http.post(`${contextPath}/webauthn/authenticate/options`, headers);
  31. if (!optionsResponse.ok) {
  32. throw new Error(`HTTP ${optionsResponse.status}`);
  33. }
  34. options = await optionsResponse.json();
  35. } catch (err) {
  36. throw new Error(`Authentication failed. Could not fetch authentication options: ${err.message}`, { cause: err });
  37. }
  38. // FIXME: Use https://www.w3.org/TR/webauthn-3/#sctn-parseRequestOptionsFromJSON
  39. const decodedOptions = {
  40. ...options,
  41. challenge: base64url.decode(options.challenge),
  42. };
  43. // Invoke the WebAuthn get() method.
  44. const credentialOptions = {
  45. publicKey: decodedOptions,
  46. signal: abortController.newSignal(),
  47. };
  48. if (useConditionalMediation) {
  49. // Request a conditional UI
  50. credentialOptions.mediation = "conditional";
  51. }
  52. let cred;
  53. try {
  54. cred = await navigator.credentials.get(credentialOptions);
  55. } catch (err) {
  56. throw new Error(`Authentication failed. Call to navigator.credentials.get failed: ${err.message}`, { cause: err });
  57. }
  58. const { response, type: credType } = cred;
  59. let userHandle;
  60. if (response.userHandle) {
  61. userHandle = base64url.encode(response.userHandle);
  62. }
  63. const body = {
  64. id: cred.id,
  65. rawId: base64url.encode(cred.rawId),
  66. response: {
  67. authenticatorData: base64url.encode(response.authenticatorData),
  68. clientDataJSON: base64url.encode(response.clientDataJSON),
  69. signature: base64url.encode(response.signature),
  70. userHandle,
  71. },
  72. credType,
  73. clientExtensionResults: cred.getClientExtensionResults(),
  74. authenticatorAttachment: cred.authenticatorAttachment,
  75. };
  76. let authenticationResponse;
  77. try {
  78. const authenticationCallResponse = await http.post(`${contextPath}/login/webauthn`, headers, body);
  79. if (!authenticationCallResponse.ok) {
  80. throw new Error(`HTTP ${authenticationCallResponse.status}`);
  81. }
  82. authenticationResponse = await authenticationCallResponse.json();
  83. // if (authenticationResponse && authenticationResponse.authenticated) {
  84. } catch (err) {
  85. throw new Error(`Authentication failed. Could not process the authentication request: ${err.message}`, {
  86. cause: err,
  87. });
  88. }
  89. if (!(authenticationResponse && authenticationResponse.authenticated && authenticationResponse.redirectUrl)) {
  90. throw new Error(
  91. `Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: ${JSON.stringify(authenticationResponse)}`,
  92. );
  93. }
  94. return authenticationResponse.redirectUrl;
  95. }
  96. async function register(headers, contextPath, label) {
  97. if (!label) {
  98. throw new Error("Error: Passkey Label is required");
  99. }
  100. let options;
  101. try {
  102. const optionsResponse = await http.post(`${contextPath}/webauthn/register/options`, headers);
  103. if (!optionsResponse.ok) {
  104. throw new Error(`Server responded with HTTP ${optionsResponse.status}`);
  105. }
  106. options = await optionsResponse.json();
  107. } catch (e) {
  108. throw new Error(`Registration failed. Could not fetch registration options: ${e.message}`, { cause: e });
  109. }
  110. // FIXME: Use https://www.w3.org/TR/webauthn-3/#sctn-parseCreationOptionsFromJSON
  111. const decodedExcludeCredentials = !options.excludeCredentials
  112. ? []
  113. : options.excludeCredentials.map((cred) => ({
  114. ...cred,
  115. id: base64url.decode(cred.id),
  116. }));
  117. const decodedOptions = {
  118. ...options,
  119. user: {
  120. ...options.user,
  121. id: base64url.decode(options.user.id),
  122. },
  123. challenge: base64url.decode(options.challenge),
  124. excludeCredentials: decodedExcludeCredentials,
  125. };
  126. let credentialsContainer;
  127. try {
  128. credentialsContainer = await navigator.credentials.create({
  129. publicKey: decodedOptions,
  130. signal: abortController.newSignal(),
  131. });
  132. } catch (e) {
  133. throw new Error(`Registration failed. Call to navigator.credentials.create failed: ${e.message}`, { cause: e });
  134. }
  135. // 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
  136. const { response } = credentialsContainer;
  137. const credential = {
  138. id: credentialsContainer.id,
  139. rawId: base64url.encode(credentialsContainer.rawId),
  140. response: {
  141. attestationObject: base64url.encode(response.attestationObject),
  142. clientDataJSON: base64url.encode(response.clientDataJSON),
  143. transports: response.getTransports ? response.getTransports() : [],
  144. },
  145. type: credentialsContainer.type,
  146. clientExtensionResults: credentialsContainer.getClientExtensionResults(),
  147. authenticatorAttachment: credentialsContainer.authenticatorAttachment,
  148. };
  149. const registrationRequest = {
  150. publicKey: {
  151. credential: credential,
  152. label: label,
  153. },
  154. };
  155. let verificationJSON;
  156. try {
  157. const verificationResp = await http.post(`${contextPath}/webauthn/register`, headers, registrationRequest);
  158. if (!verificationResp.ok) {
  159. throw new Error(`HTTP ${verificationResp.status}`);
  160. }
  161. verificationJSON = await verificationResp.json();
  162. } catch (e) {
  163. throw new Error(`Registration failed. Could not process the registration request: ${e.message}`, { cause: e });
  164. }
  165. if (!(verificationJSON && verificationJSON.success)) {
  166. throw new Error(`Registration failed. Server responded with: ${JSON.stringify(verificationJSON)}`);
  167. }
  168. }
  169. export default {
  170. authenticate,
  171. register,
  172. isConditionalMediationAvailable,
  173. };