WebAuthn Client Registration: How to access user credentials and send them to server for user authentification?

411 Views Asked by At

I want to implement WebAuthn using Java/Vaadin for the client side.

My goal is to reach that point that any users can register/enroll a WebAuthn in the context of 2FA. The created token should be saved on a privacyIDEA server for later use. It also handles the authentification of the users.

What I have done so far:

  • Creating the web application with Vaadin (Java framework)
  • Implementing methods for the token management in Java (still unfinished)
  • Using a JavaScript client plugin to faciliate authentification against privacyIDEA server using WebAuthn tokens

Code snippet from JavaScript client-side library:

window.registerWebAuthn = function (challenge, rpName, rpId, userId, userName, displayName) {
            const publicKeyCredentialCreationOptions = {
                challenge: new Uint8Array(challenge),
                 rp: {
                    name: rpName,
                    id  : rpId
            },
                user: {
                    id: new Uint8Array(userId),
                    name: userName,
                    displayName: displayName
                }
            };
            
            navigator
                .credentials
                .create({publicKey: publicKeyCredentialCreationOptions})
                .then(function (newCredentialInfo) {
                    console.log(newCredentialInfo);
              }).catch(function (err) {
                 console.error(err);
              });

   };

The JavaScript function registerWebAuthn takes several parameters and utilizes the WebAuthn API to create a new public key credential. The then block handles the successful creation of the credential, and it logs the newCredentialInfo to the console.

It is expected to start the WebAuthn registration process by pressing the button Create token. To do this, I call the Js function registerWebAuthn() inside Vaadins executeJs() function.

Vaadin/Java code:

@JsModule("./src/pi-webauthn.js")
public class MFAPageView {

    private Button buttonPrimary0 = new Button();

    public MFAPageView() {

        buttonPrimary0.setText("Create token");

        layoutColumn2.add(buttonPrimary0);
        buttonPrimary0.addClickListener(e -> {
            UI.getCurrent().getPage().executeJs("registerWebAuthn()");
    }
}

How do I access the WebAuthn credentials in newCredentialInfo and send them to privacyIDEA server to perform user authentification?

Do you have any idea?

Update

The communication between Mozilla Firefox and my Yubikey works fine. The user credentials are being created and can be displayed in the browser console.

Output:

PublicKeyCredential { rawId: ArrayBuffer, response: AuthenticatorAttestationResponse, id: "nV8W7CnC2ZZbNtajZ_gTPWrmuzH52rcHChbZ4qPSfg0CjPMI8SpYNZ8-dCO6TyTzOZavLrRtmw6r0N_9Oem-yw", type: "public-key" }
    id: "nV8W7CnC2ZZbNtajZ_gTPWrmuzH52rcHChbZ4qPSfg0CjPMI8SpYNZ8-dCO6TyTzOZavLrRtmw6r0N_9Oem-yw"
    rawId: ArrayBuffer { byteLength: 64 }
    ...
    response: AuthenticatorAttestationResponse { attestationObject: ArrayBuffer, clientDataJSON: ArrayBuffer }
    ...
    type: "public-key"
    <prototype>: PublicKeyCredentialPrototype { getClientExtensionResults: getClientExtensionResults(), rawId: Getter, response: Getter, … }
    ...

I need these credentials to be transferred to privacyIDEA to finish the registration of the WebAuthn token. Before I can do that, the credentials need to be transferred to the Java/Vaadin client.

For this purpose I edited the registerWebAuthn function slightly:

window.registerWebAuthn = function () {
            const publicKeyCredentialCreationOptions = {
                challenge: new Uint8Array(26),
                 rp: {
                    name: "Example",
                    id  : "localhost"
            },
                user: {
                    id: new Uint8Array(26),
                    name: "[email protected]",
                    displayName: "Foo Bar"
                }
            };
            
            navigator
                .credentials
                .create({publicKey: publicKeyCredentialCreationOptions})
                .then(function (newCredentialInfo) {
                    console.log(newCredentialInfo);
                    return newCredentialInfo;
              }).catch(function (err) {
                 console.error(err);
              });
   };

The essentiell part for me is coming now. I have tried to return newCredentialInfo in the Java console. For that I changed the Vaadin/Java code:

@JsModule("./src/pi-webauthn.js")
public class MFAPageView {

    private Button buttonPrimary0 = new Button();

    public MFAPageView() {

        buttonPrimary0.setText("Create token");

        layoutColumn2.add(buttonPrimary0);
        buttonPrimary0.addClickListener(e -> {
            UI.getCurrent().getPage().executeJs("return registerWebAuthn()")
                    .then(newCredentialInfo -> {
                        System.out.println(newCredentialInfo);
                        System.out.println(new Gson().toJson(newCredentialInfo));
                    });
        });

Unfortunately I get an empty string as a result.

elemental.json.impl.JreJsonNull@5f7e6f19
{}

What is wrong with my code?

3

There are 3 best solutions below

5
On

You can encode newCredentialInfo with something like this:

const encodedCredential = {
  attestationObject: btoa(String.fromCharCode(...new Uint8Array(newCredentialInfo.response.attestationObject))),
  clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(newCredentialInfo.response.clientDataJSON))),
  transports: (newCredentialInfo.response.getTransports && credential.response.getTransports()) || [],
  // TODO encode all you need, etc clientExtensionJSON: (newCredentialInfo.response.getClientExtensionResults && newCredentialInfo.response.getClientExtensionResults()) || {}
};

Yeah, probably clean that up a bit, but to illustrate the point.

Then you can return encodedCredential from registerWebAuthn() and handle it in a then() of your executeJS() call. Something like:

executeJS(...).then(encodedCredential -> {
                    if (encodedCredential.getType() == JsonType.OBJECT) {
                        JsonObject obj = (JsonObject) encodedCredential;
                        String attestationObjectBase64 = obj.getString("attestationObject");
                        byte[] attestationObject = Base64.getDecoder().decode(attestationObjectBase64);

Again, you might really want to clean up this code.

HTH even though it's not exactly production ready.

0
On

Hi I am the developer who implemented the webauthn support in privacyIDEA. I'm afraid none of the code you posted is sensible, so I'll just give you some pointers to the documentation.

pi-webauthn.js is a thin wrapper around navigator.credential.create() and navigator.credentials.get(), to make those methods more comfortable to use with privacyIDEA. If you wanna use window.credentials.create() directly, you don't need it, but in any case, you'll have to actually supply the function with a valid challenge coming from the server, which you currently don't. How to do this is outlined in great detail in the documentation

Sending data to the server works exactly the same as with any other object, and has nothing to do with webauthn or privacyIDEA. If you don't know how to do that, I suggest reading the documentation for Vaadin, before diving into more advanced stuff.

When using privacyIDEA, your server doesn't need to do much at all, except proxy the requests to privacyIDEA and check the response to see if authentication was successful. It's a very simple API and it is documented in great detail in the documentation. There is also an example app that shows the entire flow from start to finish. The request is sent to the server here, which is proxied on the server side here.

I suggest you try to understand how a web-browser and a web-server work, what the difference between client-side and server-side code is, and how they interact. Your code samples are not even close to working for a whole myriad of reasons and nobody will write your code for you.

1
On

How do I access the WebAuthn credentials in newCredentialInfo and send them to privacyIDEA server to perform user authentification?

The toJSON method is not yet widely supported, I'm afraid. So you need to grab the fields from the object and serialise them yourself in most cases.