webauthn-core.test.js 25 KB

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