webauthn-core.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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 decodedAllowCredentials = !options.allowCredentials
  40. ? []
  41. : options.allowCredentials.map((cred) => ({
  42. ...cred,
  43. id: base64url.decode(cred.id),
  44. }));
  45. const decodedOptions = {
  46. ...options,
  47. allowCredentials: decodedAllowCredentials,
  48. challenge: base64url.decode(options.challenge),
  49. };
  50. // Invoke the WebAuthn get() method.
  51. const credentialOptions = {
  52. publicKey: decodedOptions,
  53. signal: abortController.newSignal(),
  54. };
  55. if (useConditionalMediation) {
  56. // Request a conditional UI
  57. credentialOptions.mediation = "conditional";
  58. }
  59. let cred;
  60. try {
  61. cred = await navigator.credentials.get(credentialOptions);
  62. } catch (err) {
  63. throw new Error(`Authentication failed. Call to navigator.credentials.get failed: ${err.message}`, { cause: err });
  64. }
  65. const { response, type: credType } = cred;
  66. let userHandle;
  67. if (response.userHandle) {
  68. userHandle = base64url.encode(response.userHandle);
  69. }
  70. const body = {
  71. id: cred.id,
  72. rawId: base64url.encode(cred.rawId),
  73. response: {
  74. authenticatorData: base64url.encode(response.authenticatorData),
  75. clientDataJSON: base64url.encode(response.clientDataJSON),
  76. signature: base64url.encode(response.signature),
  77. userHandle,
  78. },
  79. credType,
  80. clientExtensionResults: cred.getClientExtensionResults(),
  81. authenticatorAttachment: cred.authenticatorAttachment,
  82. };
  83. let authenticationResponse;
  84. try {
  85. const authenticationCallResponse = await http.post(`${contextPath}/login/webauthn`, headers, body);
  86. if (!authenticationCallResponse.ok) {
  87. throw new Error(`HTTP ${authenticationCallResponse.status}`);
  88. }
  89. authenticationResponse = await authenticationCallResponse.json();
  90. // if (authenticationResponse && authenticationResponse.authenticated) {
  91. } catch (err) {
  92. throw new Error(`Authentication failed. Could not process the authentication request: ${err.message}`, {
  93. cause: err,
  94. });
  95. }
  96. if (!(authenticationResponse && authenticationResponse.authenticated && authenticationResponse.redirectUrl)) {
  97. throw new Error(
  98. `Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: ${JSON.stringify(authenticationResponse)}`,
  99. );
  100. }
  101. return authenticationResponse.redirectUrl;
  102. }
  103. async function register(headers, contextPath, label) {
  104. if (!label) {
  105. throw new Error("Error: Passkey Label is required");
  106. }
  107. let options;
  108. try {
  109. const optionsResponse = await http.post(`${contextPath}/webauthn/register/options`, headers);
  110. if (!optionsResponse.ok) {
  111. throw new Error(`Server responded with HTTP ${optionsResponse.status}`);
  112. }
  113. options = await optionsResponse.json();
  114. } catch (e) {
  115. throw new Error(`Registration failed. Could not fetch registration options: ${e.message}`, { cause: e });
  116. }
  117. // FIXME: Use https://www.w3.org/TR/webauthn-3/#sctn-parseCreationOptionsFromJSON
  118. const decodedExcludeCredentials = !options.excludeCredentials
  119. ? []
  120. : options.excludeCredentials.map((cred) => ({
  121. ...cred,
  122. id: base64url.decode(cred.id),
  123. }));
  124. const decodedOptions = {
  125. ...options,
  126. user: {
  127. ...options.user,
  128. id: base64url.decode(options.user.id),
  129. },
  130. challenge: base64url.decode(options.challenge),
  131. excludeCredentials: decodedExcludeCredentials,
  132. };
  133. let credentialsContainer;
  134. try {
  135. credentialsContainer = await navigator.credentials.create({
  136. publicKey: decodedOptions,
  137. signal: abortController.newSignal(),
  138. });
  139. } catch (e) {
  140. throw new Error(`Registration failed. Call to navigator.credentials.create failed: ${e.message}`, { cause: e });
  141. }
  142. // 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
  143. const { response } = credentialsContainer;
  144. const credential = {
  145. id: credentialsContainer.id,
  146. rawId: base64url.encode(credentialsContainer.rawId),
  147. response: {
  148. attestationObject: base64url.encode(response.attestationObject),
  149. clientDataJSON: base64url.encode(response.clientDataJSON),
  150. transports: response.getTransports ? response.getTransports() : [],
  151. },
  152. type: credentialsContainer.type,
  153. clientExtensionResults: credentialsContainer.getClientExtensionResults(),
  154. authenticatorAttachment: credentialsContainer.authenticatorAttachment,
  155. };
  156. const registrationRequest = {
  157. publicKey: {
  158. credential: credential,
  159. label: label,
  160. },
  161. };
  162. let verificationJSON;
  163. try {
  164. const verificationResp = await http.post(`${contextPath}/webauthn/register`, headers, registrationRequest);
  165. if (!verificationResp.ok) {
  166. throw new Error(`HTTP ${verificationResp.status}`);
  167. }
  168. verificationJSON = await verificationResp.json();
  169. } catch (e) {
  170. throw new Error(`Registration failed. Could not process the registration request: ${e.message}`, { cause: e });
  171. }
  172. if (!(verificationJSON && verificationJSON.success)) {
  173. throw new Error(`Registration failed. Server responded with: ${JSON.stringify(verificationJSON)}`);
  174. }
  175. }
  176. export default {
  177. authenticate,
  178. register,
  179. isConditionalMediationAvailable,
  180. };