2
0

webauthn-core.test.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  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 "./bootstrap.js";
  18. import { expect } from "chai";
  19. import { assert, fake, match, stub } from "sinon";
  20. import http from "../lib/http.js";
  21. import webauthn from "../lib/webauthn-core.js";
  22. import base64url from "../lib/base64url.js";
  23. describe("webauthn-core", () => {
  24. beforeEach(() => {
  25. global.window = {
  26. btoa: (str) => Buffer.from(str, "binary").toString("base64"),
  27. atob: (b64) => Buffer.from(b64, "base64").toString("binary"),
  28. };
  29. });
  30. afterEach(() => {
  31. delete global.window;
  32. });
  33. describe("isConditionalMediationAvailable", () => {
  34. afterEach(() => {
  35. delete global.window.PublicKeyCredential;
  36. });
  37. it("is available", async () => {
  38. global.window = {
  39. PublicKeyCredential: {
  40. isConditionalMediationAvailable: fake.resolves(true),
  41. },
  42. };
  43. const result = await webauthn.isConditionalMediationAvailable();
  44. expect(result).to.be.true;
  45. });
  46. describe("is not available", async () => {
  47. it("PublicKeyCredential does not exist", async () => {
  48. global.window = {};
  49. const result = await webauthn.isConditionalMediationAvailable();
  50. expect(result).to.be.false;
  51. });
  52. it("PublicKeyCredential.isConditionalMediationAvailable undefined", async () => {
  53. global.window = {
  54. PublicKeyCredential: {},
  55. };
  56. const result = await webauthn.isConditionalMediationAvailable();
  57. expect(result).to.be.false;
  58. });
  59. it("PublicKeyCredential.isConditionalMediationAvailable false", async () => {
  60. global.window = {
  61. PublicKeyCredential: {
  62. isConditionalMediationAvailable: fake.resolves(false),
  63. },
  64. };
  65. const result = await webauthn.isConditionalMediationAvailable();
  66. expect(result).to.be.false;
  67. });
  68. });
  69. });
  70. describe("authenticate", () => {
  71. let httpPostStub;
  72. const contextPath = "/some/path";
  73. const credentialsGetOptions = {
  74. challenge: "nRbOrtNKTfJ1JaxfUDKs8j3B-JFqyGQw8DO4u6eV3JA",
  75. timeout: 300000,
  76. rpId: "localhost",
  77. allowCredentials: [],
  78. userVerification: "preferred",
  79. extensions: {},
  80. };
  81. // This is kind of a self-fulfilling prophecy type of test: we produce array buffers by calling
  82. // base64url.decode ; they will then be re-encoded to the same string in the production code.
  83. // The ArrayBuffer API is not super friendly.
  84. beforeEach(() => {
  85. httpPostStub = stub(http, "post");
  86. httpPostStub.withArgs(contextPath + "/webauthn/authenticate/options", match.any).resolves({
  87. ok: true,
  88. status: 200,
  89. json: fake.resolves(credentialsGetOptions),
  90. });
  91. httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
  92. ok: true,
  93. status: 200,
  94. json: fake.resolves({
  95. authenticated: true,
  96. redirectUrl: "/success",
  97. }),
  98. });
  99. const validAuthenticatorResponse = {
  100. id: "UgghgP5QKozwsSUK1twCj8mpgZs",
  101. rawId: base64url.decode("UgghgP5QKozwsSUK1twCj8mpgZs"),
  102. response: {
  103. authenticatorData: base64url.decode("y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA"),
  104. clientDataJSON: base64url.decode(
  105. "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiUTdlR0NkNUw2cG9fa01meWNIQnBWRlR5dmd3RklCV0QxZWg5OUktRFhnWSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9",
  106. ),
  107. signature: base64url.decode(
  108. "MEUCIGT9PAWfU3lMicOXFMpHGcl033dY-sNSJvehlXvvoivyAiEA_D_yOsChERlXX2rFcK6Qx5BaAbx5qdU2hgYDVN6W770",
  109. ),
  110. userHandle: base64url.decode("tyRDnKxdj7uWOT5jrchXu54lo6nf3bWOUvMQnGOXk7g"),
  111. },
  112. getClientExtensionResults: () => ({}),
  113. authenticatorAttachment: "platform",
  114. type: "public-key",
  115. };
  116. global.navigator = {
  117. credentials: {
  118. get: fake.resolves(validAuthenticatorResponse),
  119. },
  120. };
  121. });
  122. afterEach(() => {
  123. http.post.restore();
  124. delete global.navigator;
  125. });
  126. it("succeeds", async () => {
  127. const redirectUrl = await webauthn.authenticate({ "x-custom": "some-value" }, contextPath, false);
  128. expect(redirectUrl).to.equal("/success");
  129. assert.calledWith(
  130. httpPostStub.lastCall,
  131. `${contextPath}/login/webauthn`,
  132. { "x-custom": "some-value" },
  133. {
  134. id: "UgghgP5QKozwsSUK1twCj8mpgZs",
  135. rawId: "UgghgP5QKozwsSUK1twCj8mpgZs",
  136. credType: "public-key",
  137. response: {
  138. authenticatorData: "y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA",
  139. clientDataJSON:
  140. "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiUTdlR0NkNUw2cG9fa01meWNIQnBWRlR5dmd3RklCV0QxZWg5OUktRFhnWSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9",
  141. signature:
  142. "MEUCIGT9PAWfU3lMicOXFMpHGcl033dY-sNSJvehlXvvoivyAiEA_D_yOsChERlXX2rFcK6Qx5BaAbx5qdU2hgYDVN6W770",
  143. userHandle: "tyRDnKxdj7uWOT5jrchXu54lo6nf3bWOUvMQnGOXk7g",
  144. },
  145. clientExtensionResults: {},
  146. authenticatorAttachment: "platform",
  147. },
  148. );
  149. });
  150. it("calls the authenticator with the correct options", async () => {
  151. await webauthn.authenticate({}, contextPath, false);
  152. assert.calledOnceWithMatch(global.navigator.credentials.get, {
  153. publicKey: {
  154. challenge: base64url.decode("nRbOrtNKTfJ1JaxfUDKs8j3B-JFqyGQw8DO4u6eV3JA"),
  155. timeout: 300000,
  156. rpId: "localhost",
  157. allowCredentials: [],
  158. userVerification: "preferred",
  159. extensions: {},
  160. },
  161. signal: match.any,
  162. });
  163. });
  164. describe("authentication failures", () => {
  165. it("when authentication options call", async () => {
  166. httpPostStub
  167. .withArgs(`${contextPath}/webauthn/authenticate/options`, match.any)
  168. .rejects(new Error("Connection refused"));
  169. try {
  170. await webauthn.authenticate({}, contextPath, false);
  171. } catch (err) {
  172. expect(err).to.be.an("error");
  173. expect(err.message).to.equal(
  174. "Authentication failed. Could not fetch authentication options: Connection refused",
  175. );
  176. return;
  177. }
  178. expect.fail("authenticate should throw");
  179. });
  180. it("when authentication options call returns does not return HTTP 200 OK", async () => {
  181. httpPostStub.withArgs(`${contextPath}/webauthn/authenticate/options`, match.any).resolves({
  182. ok: false,
  183. status: 400,
  184. });
  185. try {
  186. await webauthn.authenticate({}, contextPath, false);
  187. } catch (err) {
  188. expect(err).to.be.an("error");
  189. expect(err.message).to.equal("Authentication failed. Could not fetch authentication options: HTTP 400");
  190. return;
  191. }
  192. expect.fail("authenticate should throw");
  193. });
  194. it("when authentication options are not valid json", async () => {
  195. httpPostStub.withArgs(`${contextPath}/webauthn/authenticate/options`, match.any).resolves({
  196. ok: true,
  197. status: 200,
  198. json: fake.rejects(new Error("Not valid JSON")),
  199. });
  200. try {
  201. await webauthn.authenticate({}, contextPath, false);
  202. } catch (err) {
  203. expect(err).to.be.an("error");
  204. expect(err.message).to.equal("Authentication failed. Could not fetch authentication options: Not valid JSON");
  205. return;
  206. }
  207. expect.fail("authenticate should throw");
  208. });
  209. it("when navigator.credentials.get fails", async () => {
  210. global.navigator.credentials.get = fake.rejects(new Error("Operation was aborted"));
  211. try {
  212. await webauthn.authenticate({}, contextPath, false);
  213. } catch (err) {
  214. expect(err).to.be.an("error");
  215. expect(err.message).to.equal(
  216. "Authentication failed. Call to navigator.credentials.get failed: Operation was aborted",
  217. );
  218. return;
  219. }
  220. expect.fail("authenticate should throw");
  221. });
  222. it("when authentication call fails", async () => {
  223. httpPostStub
  224. .withArgs(`${contextPath}/login/webauthn`, match.any, match.any)
  225. .rejects(new Error("Connection refused"));
  226. try {
  227. await webauthn.authenticate({}, contextPath, false);
  228. } catch (err) {
  229. expect(err).to.be.an("error");
  230. expect(err.message).to.equal(
  231. "Authentication failed. Could not process the authentication request: Connection refused",
  232. );
  233. return;
  234. }
  235. expect.fail("authenticate should throw");
  236. });
  237. it("when authentication call does not return HTTP 200 OK", async () => {
  238. httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
  239. ok: false,
  240. status: 400,
  241. });
  242. try {
  243. await webauthn.authenticate({}, contextPath, false);
  244. } catch (err) {
  245. expect(err).to.be.an("error");
  246. expect(err.message).to.equal("Authentication failed. Could not process the authentication request: HTTP 400");
  247. return;
  248. }
  249. expect.fail("authenticate should throw");
  250. });
  251. it("when authentication call does not return JSON", async () => {
  252. httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
  253. ok: true,
  254. status: 200,
  255. json: fake.rejects(new Error("Not valid JSON")),
  256. });
  257. try {
  258. await webauthn.authenticate({}, contextPath, false);
  259. } catch (err) {
  260. expect(err).to.be.an("error");
  261. expect(err.message).to.equal(
  262. "Authentication failed. Could not process the authentication request: Not valid JSON",
  263. );
  264. return;
  265. }
  266. expect.fail("authenticate should throw");
  267. });
  268. it("when authentication call returns null", async () => {
  269. httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
  270. ok: true,
  271. status: 200,
  272. json: fake.resolves(null),
  273. });
  274. try {
  275. await webauthn.authenticate({}, contextPath, false);
  276. } catch (err) {
  277. expect(err).to.be.an("error");
  278. expect(err.message).to.equal(
  279. 'Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: null',
  280. );
  281. return;
  282. }
  283. expect.fail("authenticate should throw");
  284. });
  285. it('when authentication call returns {"authenticated":false}', async () => {
  286. httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
  287. ok: true,
  288. status: 200,
  289. json: fake.resolves({
  290. authenticated: false,
  291. }),
  292. });
  293. try {
  294. await webauthn.authenticate({}, contextPath, false);
  295. } catch (err) {
  296. expect(err).to.be.an("error");
  297. expect(err.message).to.equal(
  298. 'Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: {"authenticated":false}',
  299. );
  300. return;
  301. }
  302. expect.fail("authenticate should throw");
  303. });
  304. it("when authentication call returns no redirectUrl", async () => {
  305. httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
  306. ok: true,
  307. status: 200,
  308. json: fake.resolves({
  309. authenticated: true,
  310. }),
  311. });
  312. try {
  313. await webauthn.authenticate({}, contextPath, false);
  314. } catch (err) {
  315. expect(err).to.be.an("error");
  316. expect(err.message).to.equal(
  317. 'Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: {"authenticated":true}',
  318. );
  319. return;
  320. }
  321. expect.fail("authenticate should throw");
  322. });
  323. });
  324. });
  325. describe("register", () => {
  326. let httpPostStub;
  327. const contextPath = "/some/path";
  328. beforeEach(() => {
  329. const credentialsCreateOptions = {
  330. rp: {
  331. name: "Spring Security Relying Party",
  332. id: "example.localhost",
  333. },
  334. user: {
  335. name: "user",
  336. id: "eatPy60xmXG_58JrIiIBa5wq8Y76c7MD6mnY5vW8yP8",
  337. displayName: "user",
  338. },
  339. challenge: "s0hBOfkSaVLXdsbyD8jii6t2IjUd-eiTP1Cmeuo1qUo",
  340. pubKeyCredParams: [
  341. {
  342. type: "public-key",
  343. alg: -8,
  344. },
  345. {
  346. type: "public-key",
  347. alg: -7,
  348. },
  349. {
  350. type: "public-key",
  351. alg: -257,
  352. },
  353. ],
  354. timeout: 300000,
  355. excludeCredentials: [
  356. {
  357. id: "nOsjw8eaaqSwVdTBBYE1FqfGdHs",
  358. type: "public-key",
  359. transports: [],
  360. },
  361. ],
  362. authenticatorSelection: {
  363. residentKey: "required",
  364. userVerification: "preferred",
  365. },
  366. attestation: "direct",
  367. extensions: { credProps: true },
  368. };
  369. const validAuthenticatorResponse = {
  370. authenticatorAttachment: "platform",
  371. id: "9wAuex_025BgEQrs7fOypo5SGBA",
  372. rawId: base64url.decode("9wAuex_025BgEQrs7fOypo5SGBA"),
  373. response: {
  374. attestationObject: base64url.decode(
  375. "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAAPv8MAcVTk7MjAtuAgVX170AFPcALnsf9NuQYBEK7O3zsqaOUhgQpQECAyYgASFYIMB9pM2BeSeEG83fAKFVSLKIfvDBBVoyGgMoiGxE-6WgIlggazAojM5sduQy2M7rz1do55nVaNLGXh8k4xBHz-Oy91E",
  376. ),
  377. getAuthenticatorData: () =>
  378. base64url.decode(
  379. "y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAAPv8MAcVTk7MjAtuAgVX170AFPcALnsf9NuQYBEK7O3zsqaOUhgQpQECAyYgASFYIMB9pM2BeSeEG83fAKFVSLKIfvDBBVoyGgMoiGxE-6WgIlggazAojM5sduQy2M7rz1do55nVaNLGXh8k4xBHz-Oy91E",
  380. ),
  381. clientDataJSON: base64url.decode(
  382. "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiUVdwd3lUcXJpYVlqbVdnOWFvZ0FxUlRKNVFYMFBGV2JWR2xNeGNsVjZhcyIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9",
  383. ),
  384. getPublicKey: () =>
  385. base64url.decode(
  386. "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwH2kzYF5J4Qbzd8AoVVIsoh-8MEFWjIaAyiIbET7paBrMCiMzmx25DLYzuvPV2jnmdVo0sZeHyTjEEfP47L3UQ",
  387. ),
  388. getPublicKeyAlgorithm: () => -7,
  389. getTransports: () => ["internal"],
  390. },
  391. type: "public-key",
  392. getClientExtensionResults: () => ({}),
  393. };
  394. global.navigator = {
  395. credentials: {
  396. create: fake.resolves(validAuthenticatorResponse),
  397. },
  398. };
  399. httpPostStub = stub(http, "post");
  400. httpPostStub.withArgs(contextPath + "/webauthn/register/options", match.any).resolves({
  401. ok: true,
  402. status: 200,
  403. json: fake.resolves(credentialsCreateOptions),
  404. });
  405. httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
  406. ok: true,
  407. json: fake.resolves({
  408. success: true,
  409. }),
  410. });
  411. });
  412. afterEach(() => {
  413. httpPostStub.restore();
  414. delete global.navigator;
  415. });
  416. it("succeeds", async () => {
  417. const contextPath = "/some/path";
  418. const headers = { _csrf: "csrf-value" };
  419. await webauthn.register(headers, contextPath, "my passkey");
  420. assert.calledWithExactly(
  421. httpPostStub.lastCall,
  422. `${contextPath}/webauthn/register`,
  423. headers,
  424. match({
  425. publicKey: {
  426. credential: {
  427. id: "9wAuex_025BgEQrs7fOypo5SGBA",
  428. rawId: "9wAuex_025BgEQrs7fOypo5SGBA",
  429. response: {
  430. attestationObject:
  431. "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAAPv8MAcVTk7MjAtuAgVX170AFPcALnsf9NuQYBEK7O3zsqaOUhgQpQECAyYgASFYIMB9pM2BeSeEG83fAKFVSLKIfvDBBVoyGgMoiGxE-6WgIlggazAojM5sduQy2M7rz1do55nVaNLGXh8k4xBHz-Oy91E",
  432. clientDataJSON:
  433. "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiUVdwd3lUcXJpYVlqbVdnOWFvZ0FxUlRKNVFYMFBGV2JWR2xNeGNsVjZhcyIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9",
  434. transports: ["internal"],
  435. },
  436. type: "public-key",
  437. clientExtensionResults: {},
  438. authenticatorAttachment: "platform",
  439. },
  440. label: "my passkey",
  441. },
  442. }),
  443. );
  444. });
  445. it("calls the authenticator with the correct options", async () => {
  446. await webauthn.register({}, contextPath, "my passkey");
  447. assert.calledOnceWithExactly(
  448. global.navigator.credentials.create,
  449. match({
  450. publicKey: {
  451. rp: {
  452. name: "Spring Security Relying Party",
  453. id: "example.localhost",
  454. },
  455. user: {
  456. name: "user",
  457. id: base64url.decode("eatPy60xmXG_58JrIiIBa5wq8Y76c7MD6mnY5vW8yP8"),
  458. displayName: "user",
  459. },
  460. challenge: base64url.decode("s0hBOfkSaVLXdsbyD8jii6t2IjUd-eiTP1Cmeuo1qUo"),
  461. pubKeyCredParams: [
  462. {
  463. type: "public-key",
  464. alg: -8,
  465. },
  466. {
  467. type: "public-key",
  468. alg: -7,
  469. },
  470. {
  471. type: "public-key",
  472. alg: -257,
  473. },
  474. ],
  475. timeout: 300000,
  476. excludeCredentials: [
  477. {
  478. id: base64url.decode("nOsjw8eaaqSwVdTBBYE1FqfGdHs"),
  479. type: "public-key",
  480. transports: [],
  481. },
  482. ],
  483. authenticatorSelection: {
  484. residentKey: "required",
  485. userVerification: "preferred",
  486. },
  487. attestation: "direct",
  488. extensions: { credProps: true },
  489. },
  490. signal: match.any,
  491. }),
  492. );
  493. });
  494. describe("registration failures", () => {
  495. it("when label is missing", async () => {
  496. try {
  497. await webauthn.register({}, "/", "");
  498. } catch (err) {
  499. expect(err).to.be.an("error");
  500. expect(err.message).to.equal("Error: Passkey Label is required");
  501. return;
  502. }
  503. expect.fail("register should throw");
  504. });
  505. it("when cannot get the registration options", async () => {
  506. httpPostStub.withArgs(match.any, match.any).rejects(new Error("Server threw an error"));
  507. try {
  508. await webauthn.register({}, "/", "my passkey");
  509. } catch (err) {
  510. expect(err).to.be.an("error");
  511. expect(err.message).to.equal(
  512. "Registration failed. Could not fetch registration options: Server threw an error",
  513. );
  514. return;
  515. }
  516. expect.fail("register should throw");
  517. });
  518. it("when registration options call does not return HTTP 200 OK", async () => {
  519. httpPostStub.withArgs(match.any, match.any).resolves({
  520. ok: false,
  521. status: 400,
  522. });
  523. try {
  524. await webauthn.register({}, "/", "my passkey");
  525. } catch (err) {
  526. expect(err).to.be.an("error");
  527. expect(err.message).to.equal(
  528. "Registration failed. Could not fetch registration options: Server responded with HTTP 400",
  529. );
  530. return;
  531. }
  532. expect.fail("register should throw");
  533. });
  534. it("when registration options are not valid JSON", async () => {
  535. httpPostStub.withArgs(match.any, match.any).resolves({
  536. ok: true,
  537. status: 200,
  538. json: fake.rejects(new Error("Not a JSON response")),
  539. });
  540. try {
  541. await webauthn.register({}, "/", "my passkey");
  542. } catch (err) {
  543. expect(err).to.be.an("error");
  544. expect(err.message).to.equal(
  545. "Registration failed. Could not fetch registration options: Not a JSON response",
  546. );
  547. return;
  548. }
  549. expect.fail("register should throw");
  550. });
  551. it("when navigator.credentials.create fails", async () => {
  552. global.navigator = {
  553. credentials: {
  554. create: fake.rejects(new Error("authenticator threw an error")),
  555. },
  556. };
  557. try {
  558. await webauthn.register({}, contextPath, "my passkey");
  559. } catch (err) {
  560. expect(err).to.be.an("error");
  561. expect(err.message).to.equal(
  562. "Registration failed. Call to navigator.credentials.create failed: authenticator threw an error",
  563. );
  564. expect(err.cause).to.deep.equal(new Error("authenticator threw an error"));
  565. return;
  566. }
  567. expect.fail("register should throw");
  568. });
  569. it("when registration call fails", async () => {
  570. httpPostStub
  571. .withArgs(`${contextPath}/webauthn/register`, match.any, match.any)
  572. .rejects(new Error("Connection refused"));
  573. try {
  574. await webauthn.register({}, contextPath, "my passkey");
  575. } catch (err) {
  576. expect(err).to.be.an("error");
  577. expect(err.message).to.equal(
  578. "Registration failed. Could not process the registration request: Connection refused",
  579. );
  580. expect(err.cause).to.deep.equal(new Error("Connection refused"));
  581. return;
  582. }
  583. expect.fail("register should throw");
  584. });
  585. it("when registration call does not return HTTP 200 OK", async () => {
  586. httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
  587. ok: false,
  588. status: 400,
  589. });
  590. try {
  591. await webauthn.register({}, contextPath, "my passkey");
  592. } catch (err) {
  593. expect(err).to.be.an("error");
  594. expect(err.message).to.equal("Registration failed. Could not process the registration request: HTTP 400");
  595. return;
  596. }
  597. expect.fail("register should throw");
  598. });
  599. it("when registration call does not return JSON", async () => {
  600. httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
  601. ok: true,
  602. status: 200,
  603. json: fake.rejects(new Error("Not valid JSON")),
  604. });
  605. try {
  606. await webauthn.register({}, contextPath, "my passkey");
  607. } catch (err) {
  608. expect(err).to.be.an("error");
  609. expect(err.message).to.equal(
  610. "Registration failed. Could not process the registration request: Not valid JSON",
  611. );
  612. expect(err.cause).to.deep.equal(new Error("Not valid JSON"));
  613. return;
  614. }
  615. expect.fail("register should throw");
  616. });
  617. it("when registration call returns null", async () => {
  618. httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
  619. ok: true,
  620. status: 200,
  621. json: fake.resolves(null),
  622. });
  623. try {
  624. await webauthn.register({}, contextPath, "my passkey");
  625. } catch (err) {
  626. expect(err).to.be.an("error");
  627. expect(err.message).to.equal("Registration failed. Server responded with: null");
  628. return;
  629. }
  630. expect.fail("register should throw");
  631. });
  632. it('when registration call returns {"success":false}', async () => {
  633. httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
  634. ok: true,
  635. status: 200,
  636. json: fake.resolves({ success: false }),
  637. });
  638. try {
  639. await webauthn.register({}, contextPath, "my passkey");
  640. } catch (err) {
  641. expect(err).to.be.an("error");
  642. expect(err.message).to.equal('Registration failed. Server responded with: {"success":false}');
  643. return;
  644. }
  645. expect.fail("register should throw");
  646. });
  647. });
  648. });
  649. });