cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
46.43k stars 3.14k forks source link

Feature Request: WebAuthn/FIDO2 testing via virtual authenticators #6991

Open JamesCullum opened 4 years ago

JamesCullum commented 4 years ago

Current behavior:

Cypress does not support any way to test WebAuthn / FIDO2 flows without mocking the authenticator, which does not allow to test all cryptographic interactions sufficiently. If a valid request is forwarded to the browser, it will be delegated out of the browser and can not be interacted with.

Desired behavior:

Just like Selenium implemented it end of last year, Cypress should offer a way to use the W3C automation API to programatically interact with the browser to manage virtual authenticators that can confirm actions without leaving the browser context.

JamesCullum commented 4 years ago

I was able to implement this using the chrome debugger, using code examples from the Google virtual authenticator extension and @gabbersepp blog post on how to access the low level API in Cypress.

For anyone curious, you can see the result in the script here and the setup as plugin here.

Should we aim to have this documented or put it into the sink? Having a Cypress-native interaction with the chrome driver might be useful for this as well.

jennifer-shehane commented 4 years ago

@JamesCullum Yes, this could potentially work as a example recipe if a good example can be extracted out.

aaujayasena commented 2 years ago

Most of the software access management contains the facility of authentication with security key and biometric identifications. Having this feature with your product will be a great since now traditional username and password authentications are not much popular.

Hope cypress can consider this requirement in high priority.

thiagonzalez commented 2 years ago

2 years have passed since you started this discussion. Is this a reality or still a proposal? We are building a project with just webauthn as the authentication method, so it would be nice to have a way to test it. Let me know what you guys have done :)

JamesCullum commented 2 years ago

As mentioned, above is an example that you can use for it. Only thing missing is Cypress adding it as official example.

thiagonzalez commented 2 years ago

@JamesCullum this may be a dumb question, but I'm kind of lost of what should I host on 127.0.0.1:9222 to receive the CRI requests.

I replicated your sendCRI task as follows:

const CRI = require("chrome-remote-interface");

module.exports = (on, config) => {
  on("task", {
    async sendCRI(args) {
      criClient = await CRI();
      return criClient.send(args.query, args.opts);
    },
  });

  return config;
}

And I'm getting this error:

image

I know you've mentioned the plugin Virtual Authenticators Tab (link) from Google, but how exactly you started this process on port 9222? I read their documentation, but I couldn't find this procedure anywhere.

Thanks for sharing your project with us! 😃

kishore8 commented 2 years ago

Hi @JamesCullum, I keep getting this exception, when trying to write e2e tests for biometric registration on my device.I guess I am unable to register the biometric key because of it. how do we overcome this? TIA! I am trying to do something like this in my code - window.navigator.credentials.create({ publicKey });

Screenshot 2022-05-19 at 7 16 43 PM
hcnode commented 1 year ago

I was working on webauthn related in cypress, I found actually cypress already provides Cypress.automation for Low level access to Chrome Debugger Protocol, so we don't need to use 3rd party library to do this. here are the code to enable, create and remove virtual authenticator:

addVirtualAuthenticator() {
    return Cypress.automation("remote:debugger:protocol", {
      command: "WebAuthn.enable",
      params: {},
    }).then((result) => {
      console.log("WebAuthn.enable", result);
      return Cypress.automation("remote:debugger:protocol", {
        command: "WebAuthn.addVirtualAuthenticator",
        params: {
          options: {
            protocol: "ctap2",
            transport: "internal",
            hasResidentKey: true,
            hasUserVerification: true,
            isUserVerified: true,
          },
        },
      }).then((result) => {
        console.log("WebAuthn.addVirtualAuthenticator", result);
        return result.authenticatorId;
      });
    });
  }
removeVirtualAuthenticator(authenticatorId) {
    Cypress.automation("remote:debugger:protocol", {
      command: "WebAuthn.removeVirtualAuthenticator",
      params: {
        authenticatorId,
      },
    }).then((result) => {
      console.log("WebAuthn.removeVirtualAuthenticator", result);
    });
  }
Andy2003 commented 1 year ago

I wrote a Blogost about how I solved this issue with Cypress, Active Directory and Ping Identity

Brij-M commented 6 months ago

I was working on webauthn related in cypress, I found actually cypress already provides Cypress.automation for Low level access to Chrome Debugger Protocol, so we don't need to use 3rd party library to do this. here are the code to enable, create and remove virtual authenticator:

addVirtualAuthenticator() {
    return Cypress.automation("remote:debugger:protocol", {
      command: "WebAuthn.enable",
      params: {},
    }).then((result) => {
      console.log("WebAuthn.enable", result);
      return Cypress.automation("remote:debugger:protocol", {
        command: "WebAuthn.addVirtualAuthenticator",
        params: {
          options: {
            protocol: "ctap2",
            transport: "internal",
            hasResidentKey: true,
            hasUserVerification: true,
            isUserVerified: true,
          },
        },
      }).then((result) => {
        console.log("WebAuthn.addVirtualAuthenticator", result);
        return result.authenticatorId;
      });
    });
  }
removeVirtualAuthenticator(authenticatorId) {
    Cypress.automation("remote:debugger:protocol", {
      command: "WebAuthn.removeVirtualAuthenticator",
      params: {
        authenticatorId,
      },
    }).then((result) => {
      console.log("WebAuthn.removeVirtualAuthenticator", result);
    });
  }

Hi, It didn't work for me. WebAuthn.addVirtualAuthenticator didn't return the authenticatorid. result is undefined. is there any additional settings required. However I tried https://webauthn.io to verify, it did open the authenticator pops which doesn't happen if I enable webauthn via UI

tylerccarson commented 4 months ago

I'm having the save problem as @Brij-M, the WebAuthn commands don't seem to register with the chrome debugger protocol. If I run "WebAuthn.enable" manually from the Protocol monitor, I get the following error: { "code": -32601, "message": "'WebAuthn.enable' wasn't found" } or just an empty {}. And the WebAuthn devtool panel doesn't enable the virtual authenticator environment.

Did Chrome possibly pull support for this? Or am I missing something. Basically wondering if this is still working for others @hcnode @JamesCullum

Update: it seems to be working now when using the Cypress.automation API as described. For whatever reason these commands aren't viewable in the monitor, but I am getting the expected responses and able to use the virtual authenticator during a Cypress test.

harrygreen commented 4 months ago

Hey @tylerccarson, would you mind posting an example of how you got these working?

tylerccarson commented 3 months ago

Hey @tylerccarson, would you mind posting an example of how you got these working?

@harrygreen I really didn't add anything novel here except wrapping hcnode's examples in Cypress commands, and fleshing out more functions for adding credentials as well. Chrome's documentation is a good reference for seeing what's possible: CDP Webauthn

Here's one example I added for cy.addCredential() (which relies on WebAuthn.enable and having created a virtual authenticator).

/** https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/#type-Credential */
export type TestWebauthnCredential = {
  credentialId: string
  isResidentCredential: boolean
  rpId?: string // required when adding
  privateKey: string // ECDSA P-256 private key in PKCS#8 format, encoded in base64
  userHandle: string // userId, byte sequence
  signCount: number
}

Cypress.Commands.add('addCredential', (authenticator, credential) => {
  Cypress.log({})
  return cy.wrap(
    new Promise<void>(async (resolve, reject) => {
      if (!authenticator.id) {
        return reject(new Error('addCredential() called with no authenticatorId'))
      }

      // add credential
      await Cypress.automation('remote:debugger:protocol', {
        command: 'WebAuthn.addCredential',
        params: {
          authenticatorId: authenticator.id,
          credential,
        },
      })

      // get credential back
      const getCredentialResult = (await Cypress.automation('remote:debugger:protocol', {
        command: 'WebAuthn.getCredential',
        params: {
          authenticatorId: authenticator.id,
          credentialId: credential.credentialId,
        },
      })) as {credential: TestWebauthnCredential}

      // ensure it was added and resolve
      const wasAdded = !!getCredentialResult.credential
      if (wasAdded) {
        resolve()
      } else {
        reject(new Error('Credential was not added'))
      }
    }),
    {log: false}
  )
})

I found getting the credential after adding to check helpful since like I mentioned, the actual debugger UI doesn't seem to update to reflect the results of these automated commands.

rdubigny commented 3 weeks ago

I am having difficulty getting this to work. Here is my Cypress test:

describe("sign-in with webauthn on untrusted browser", () => {
  before(async () => {
    await Cypress.automation("remote:debugger:protocol", {
      command: "WebAuthn.enable",
    });
  });

  it("should sign-in with webauthn", function () {
    Cypress.automation("remote:debugger:protocol", {
      command: "WebAuthn.addVirtualAuthenticator",
      params: {
        options: {
          protocol: "ctap2",
          transport: "internal",
          hasResidentKey: true,
          hasUserVerification: true,
          isUserVerified: true,
        },
      },
    }).then(({ authenticatorId }) => {
      Cypress.automation("remote:debugger:protocol", {
        command: "WebAuthn.addCredential",
        params: {
          authenticatorId,
          credential: {
            credentialId: "xxx",
            isResidentCredential: true,
            userHandle: "MQ==",
            rpId: "localhost",
            privateKey: "xxx",
            signCount: 0,
          },
        },
      });
    });

    cy.visit(`http://localhost:4001`);
    cy.get("button.moncomptepro-button").click();
    cy.get('[href="/users/sign-in-with-passkey"]')
      .contains("Se connecter avec une clé d’accès")
      .click();

    cy.contains("Se connecter avec une clé d’accès");

    cy.get("#webauthn-btn-begin-authentication").contains("Continuer").click();
  });
});

However, I encountered the following error in the Cypress logs:

The 'publickey-credentials-get' feature is not enabled in this document.
Permissions Policy may be used to delegate Web Authentication capabilities to cross-origin child frames.

The documentation suggests adding an attribute to the Cypress iframe, but I am unsure how to do this. Any help would be gladly appreciated.

Thank you!