mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-11-04 08:39:05 +00:00 
			
		
		
		
	
		
			
	
	
		
			280 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			280 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 
								 | 
							
								/*
							 | 
						||
| 
								 | 
							
								 * Copyright 2002-2024 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 "./bootstrap.js";
							 | 
						||
| 
								 | 
							
								import { expect, util, Assertion } from "chai";
							 | 
						||
| 
								 | 
							
								import { setupRegistration } from "../lib/webauthn-registration.js";
							 | 
						||
| 
								 | 
							
								import webauthn from "../lib/webauthn-core.js";
							 | 
						||
| 
								 | 
							
								import { assert, fake, match, stub } from "sinon";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								describe("webauthn-registration", () => {
							 | 
						||
| 
								 | 
							
								  before(() => {
							 | 
						||
| 
								 | 
							
								    Assertion.addProperty("visible", function () {
							 | 
						||
| 
								 | 
							
								      const obj = util.flag(this, "object");
							 | 
						||
| 
								 | 
							
								      new Assertion(obj).to.have.nested.property("style.display", "block");
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								    Assertion.addProperty("hidden", function () {
							 | 
						||
| 
								 | 
							
								      const obj = util.flag(this, "object");
							 | 
						||
| 
								 | 
							
								      new Assertion(obj).to.have.nested.property("style.display", "none");
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  describe("bootstrap", () => {
							 | 
						||
| 
								 | 
							
								    let registerStub;
							 | 
						||
| 
								 | 
							
								    let registerButton;
							 | 
						||
| 
								 | 
							
								    let labelField;
							 | 
						||
| 
								 | 
							
								    let errorPopup;
							 | 
						||
| 
								 | 
							
								    let successPopup;
							 | 
						||
| 
								 | 
							
								    let deleteForms;
							 | 
						||
| 
								 | 
							
								    let ui;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    beforeEach(() => {
							 | 
						||
| 
								 | 
							
								      registerStub = stub(webauthn, "register").resolves(undefined);
							 | 
						||
| 
								 | 
							
								      errorPopup = {
							 | 
						||
| 
								 | 
							
								        style: {
							 | 
						||
| 
								 | 
							
								          display: undefined,
							 | 
						||
| 
								 | 
							
								        },
							 | 
						||
| 
								 | 
							
								        textContent: undefined,
							 | 
						||
| 
								 | 
							
								      };
							 | 
						||
| 
								 | 
							
								      successPopup = {
							 | 
						||
| 
								 | 
							
								        style: {
							 | 
						||
| 
								 | 
							
								          display: undefined,
							 | 
						||
| 
								 | 
							
								        },
							 | 
						||
| 
								 | 
							
								        textContent: undefined,
							 | 
						||
| 
								 | 
							
								      };
							 | 
						||
| 
								 | 
							
								      registerButton = {
							 | 
						||
| 
								 | 
							
								        addEventListener: fake(),
							 | 
						||
| 
								 | 
							
								      };
							 | 
						||
| 
								 | 
							
								      labelField = {
							 | 
						||
| 
								 | 
							
								        value: undefined,
							 | 
						||
| 
								 | 
							
								      };
							 | 
						||
| 
								 | 
							
								      deleteForms = [];
							 | 
						||
| 
								 | 
							
								      ui = {
							 | 
						||
| 
								 | 
							
								        getSuccess: function () {
							 | 
						||
| 
								 | 
							
								          return successPopup;
							 | 
						||
| 
								 | 
							
								        },
							 | 
						||
| 
								 | 
							
								        getError: function () {
							 | 
						||
| 
								 | 
							
								          return errorPopup;
							 | 
						||
| 
								 | 
							
								        },
							 | 
						||
| 
								 | 
							
								        getRegisterButton: function () {
							 | 
						||
| 
								 | 
							
								          return registerButton;
							 | 
						||
| 
								 | 
							
								        },
							 | 
						||
| 
								 | 
							
								        getLabelInput: function () {
							 | 
						||
| 
								 | 
							
								          return labelField;
							 | 
						||
| 
								 | 
							
								        },
							 | 
						||
| 
								 | 
							
								        getDeleteForms: function () {
							 | 
						||
| 
								 | 
							
								          return deleteForms;
							 | 
						||
| 
								 | 
							
								        },
							 | 
						||
| 
								 | 
							
								      };
							 | 
						||
| 
								 | 
							
								      global.window = {
							 | 
						||
| 
								 | 
							
								        location: {
							 | 
						||
| 
								 | 
							
								          href: {},
							 | 
						||
| 
								 | 
							
								        },
							 | 
						||
| 
								 | 
							
								      };
							 | 
						||
| 
								 | 
							
								      global.console = {
							 | 
						||
| 
								 | 
							
								        error: stub(),
							 | 
						||
| 
								 | 
							
								      };
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    afterEach(() => {
							 | 
						||
| 
								 | 
							
								      registerStub.restore();
							 | 
						||
| 
								 | 
							
								      delete global.window;
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    describe("when webauthn is not supported", () => {
							 | 
						||
| 
								 | 
							
								      beforeEach(() => {
							 | 
						||
| 
								 | 
							
								        delete global.window.PublicKeyCredential;
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      it("does not set up a click event listener", async () => {
							 | 
						||
| 
								 | 
							
								        await setupRegistration({}, "/", ui);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        assert.notCalled(registerButton.addEventListener);
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      it("shows an error popup", async () => {
							 | 
						||
| 
								 | 
							
								        await setupRegistration({}, "/", ui);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        expect(errorPopup).to.be.visible;
							 | 
						||
| 
								 | 
							
								        expect(errorPopup.textContent).to.equal("WebAuthn is not supported");
							 | 
						||
| 
								 | 
							
								        expect(successPopup).to.be.hidden;
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    describe("when webauthn is supported", () => {
							 | 
						||
| 
								 | 
							
								      beforeEach(() => {
							 | 
						||
| 
								 | 
							
								        global.window.PublicKeyCredential = fake();
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      it("hides the popups", async () => {
							 | 
						||
| 
								 | 
							
								        await setupRegistration({}, "/", ui);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        expect(successPopup).to.be.hidden;
							 | 
						||
| 
								 | 
							
								        expect(errorPopup).to.be.hidden;
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      it("sets up a click event listener on the register button", async () => {
							 | 
						||
| 
								 | 
							
								        await setupRegistration({}, "/some/path", ui);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        assert.calledOnceWithMatch(registerButton.addEventListener, "click", match.typeOf("function"));
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      describe(`when the query string contains "success"`, () => {
							 | 
						||
| 
								 | 
							
								        beforeEach(() => {
							 | 
						||
| 
								 | 
							
								          global.window.location.search = "?success&continue=true";
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        it("shows the success popup", async () => {
							 | 
						||
| 
								 | 
							
								          await setupRegistration({}, "/", ui);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          expect(successPopup).to.be.visible;
							 | 
						||
| 
								 | 
							
								          expect(errorPopup).to.be.hidden;
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      describe("when the register button is clicked", () => {
							 | 
						||
| 
								 | 
							
								        const headers = { "x-header": "value" };
							 | 
						||
| 
								 | 
							
								        const contextPath = "/some/path";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        beforeEach(async () => {
							 | 
						||
| 
								 | 
							
								          await setupRegistration(headers, contextPath, ui);
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        it("hides all the popups", async () => {
							 | 
						||
| 
								 | 
							
								          successPopup.textContent = "dummy-content";
							 | 
						||
| 
								 | 
							
								          successPopup.style.display = "block";
							 | 
						||
| 
								 | 
							
								          errorPopup.textContent = "dummy-content";
							 | 
						||
| 
								 | 
							
								          errorPopup.style.display = "block";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          await registerButton.addEventListener.firstCall.lastArg();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          expect(successPopup).to.be.hidden;
							 | 
						||
| 
								 | 
							
								          expect(errorPopup).to.be.hidden;
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        it("calls register", async () => {
							 | 
						||
| 
								 | 
							
								          labelField.value = "passkey name";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          await registerButton.addEventListener.firstCall.lastArg();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          assert.calledOnceWithExactly(registerStub, headers, contextPath, labelField.value);
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        it("navigates to success page", async () => {
							 | 
						||
| 
								 | 
							
								          labelField.value = "passkey name";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          await registerButton.addEventListener.firstCall.lastArg();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          expect(global.window.location.href).to.equal(`${contextPath}/webauthn/register?success`);
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        it("handles errors", async () => {
							 | 
						||
| 
								 | 
							
								          registerStub.rejects(new Error("The registration failed"));
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          await registerButton.addEventListener.firstCall.lastArg();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          expect(errorPopup.textContent).to.equal("The registration failed");
							 | 
						||
| 
								 | 
							
								          expect(errorPopup).to.be.visible;
							 | 
						||
| 
								 | 
							
								          expect(successPopup).to.be.hidden;
							 | 
						||
| 
								 | 
							
								          assert.calledOnceWithMatch(
							 | 
						||
| 
								 | 
							
								            global.console.error,
							 | 
						||
| 
								 | 
							
								            match.instanceOf(Error).and(match.has("message", "The registration failed")),
							 | 
						||
| 
								 | 
							
								          );
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      describe("delete", () => {
							 | 
						||
| 
								 | 
							
								        beforeEach(() => {
							 | 
						||
| 
								 | 
							
								          global.fetch = fake.resolves({ ok: true });
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        afterEach(() => {
							 | 
						||
| 
								 | 
							
								          delete global.fetch;
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        it("no errors when no forms", async () => {
							 | 
						||
| 
								 | 
							
								          await setupRegistration({}, "/some/path", ui);
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        it("sets up forms for fetch", async () => {
							 | 
						||
| 
								 | 
							
								          const deleteFormOne = {
							 | 
						||
| 
								 | 
							
								            addEventListener: fake(),
							 | 
						||
| 
								 | 
							
								          };
							 | 
						||
| 
								 | 
							
								          const deleteFormTwo = {
							 | 
						||
| 
								 | 
							
								            addEventListener: fake(),
							 | 
						||
| 
								 | 
							
								          };
							 | 
						||
| 
								 | 
							
								          deleteForms = [deleteFormOne, deleteFormTwo];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          await setupRegistration({}, "", ui);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          assert.calledOnceWithMatch(deleteFormOne.addEventListener, "submit", match.typeOf("function"));
							 | 
						||
| 
								 | 
							
								          assert.calledOnceWithMatch(deleteFormTwo.addEventListener, "submit", match.typeOf("function"));
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        describe("when the delete button is clicked", () => {
							 | 
						||
| 
								 | 
							
								          it("calls POST to the form action", async () => {
							 | 
						||
| 
								 | 
							
								            const contextPath = "/some/path";
							 | 
						||
| 
								 | 
							
								            const deleteForm = {
							 | 
						||
| 
								 | 
							
								              addEventListener: fake(),
							 | 
						||
| 
								 | 
							
								              action: `${contextPath}/webauthn/1234`,
							 | 
						||
| 
								 | 
							
								            };
							 | 
						||
| 
								 | 
							
								            deleteForms = [deleteForm];
							 | 
						||
| 
								 | 
							
								            const headers = {
							 | 
						||
| 
								 | 
							
								              "X-CSRF-TOKEN": "token",
							 | 
						||
| 
								 | 
							
								            };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            await setupRegistration(headers, contextPath, ui);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            const clickEvent = {
							 | 
						||
| 
								 | 
							
								              preventDefault: fake(),
							 | 
						||
| 
								 | 
							
								            };
							 | 
						||
| 
								 | 
							
								            await deleteForm.addEventListener.firstCall.lastArg(clickEvent);
							 | 
						||
| 
								 | 
							
								            assert.calledOnce(clickEvent.preventDefault);
							 | 
						||
| 
								 | 
							
								            assert.calledOnceWithExactly(global.fetch, `/some/path/webauthn/1234`, {
							 | 
						||
| 
								 | 
							
								              method: "DELETE",
							 | 
						||
| 
								 | 
							
								              headers: {
							 | 
						||
| 
								 | 
							
								                "Content-Type": "application/json",
							 | 
						||
| 
								 | 
							
								                ...headers,
							 | 
						||
| 
								 | 
							
								              },
							 | 
						||
| 
								 | 
							
								            });
							 | 
						||
| 
								 | 
							
								            expect(global.window.location.href).to.equal(`/some/path/webauthn/register?success`);
							 | 
						||
| 
								 | 
							
								          });
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        it("handles errors", async () => {
							 | 
						||
| 
								 | 
							
								          global.fetch = fake.rejects("Server threw an error");
							 | 
						||
| 
								 | 
							
								          global.window.location.href = "/initial/location";
							 | 
						||
| 
								 | 
							
								          const deleteForm = {
							 | 
						||
| 
								 | 
							
								            addEventListener: fake(),
							 | 
						||
| 
								 | 
							
								          };
							 | 
						||
| 
								 | 
							
								          deleteForms = [deleteForm];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          await setupRegistration({}, "", ui);
							 | 
						||
| 
								 | 
							
								          const clickEvent = { preventDefault: fake() };
							 | 
						||
| 
								 | 
							
								          await deleteForm.addEventListener.firstCall.lastArg(clickEvent);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          expect(errorPopup).to.be.visible;
							 | 
						||
| 
								 | 
							
								          expect(errorPopup.textContent).to.equal("Server threw an error");
							 | 
						||
| 
								 | 
							
								          // URL does not change
							 | 
						||
| 
								 | 
							
								          expect(global.window.location.href).to.equal("/initial/location");
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								});
							 |