Home | Send Feedback

Implementing WebAuthn Authentication with Bitwarden Passwordless.dev

Published: 17. January 2025  •  go, angular

In two previous blog posts, I showed you how to implement WebAuthn authentication with Java and with Go.

In this blog post, I will show you another way to implement WebAuthn authentication, this time with a SaaS solution called Bitwarden Passwordless.dev.

Passwordless.dev is a service that provides WebAuthn authentication as a service. It is a simple and secure way to implement WebAuthn authentication without the need to implement the WebAuthn protocol yourself. Note that Passwordless.dev only supports WebAuthn authentication and no other authentication method like Username/Password or OAuth.

Passwordless.dev was bought by Bitwarden in January 2023. Bitwarden is known for its password manager that allows you to store and manage your passwords in a secure way.

At the time of writing (January 2025), Passwordless.dev provides a free tier that allows you to create one project and is perfect for testing and small projects. The free tier includes up to 10,000 users, but you can only create one project, or rather one domain. If you need more projects, you need to upgrade to a paid plan.

Check out the pricing page for more information.

Account

To implement WebAuthn authentication with Passwordless.dev, you need to create an account on Passwordless.dev and create a new project.

The official documentation shows you how to create a new project. With the free tier, you can create one project.

After you have created a project, you will get a public and a private key. The public key will be integrated into the client application and, when it's a publicly available webpage, will be visible to everyone. The private key is used on the server side and must be kept secret. Do not commit the private key to a repository.

Implementation

Passwordless.dev provides a client library for JavaScript that you can use to implement the WebAuthn workflows on the client side. The client library is available on npm, and you can install it with the following command:

npm install @passwordlessdev/passwordless-client

The library is framework-agnostic. For this blog post, I wrote a demo application with Ionic/Angular, but you can use the library with any other framework or even vanilla JavaScript.

Passwordless.dev also needs a backend component that you need to implement. From Passwordless.dev, you get official libraries for ASP .NET Core, Java, Node.js, PHP, Python 2, and Python 3. From the community, you get SDKs for Rust, Go, and Java Spring Boot. Even if your programming language is not listed here, it's not that difficult to implement the backend part if you have a way to send HTTP requests. Passwordless.dev provides an OpenAPI interface that you can use to implement the backend part.

For more information about the backend integration, check out this page: https://docs.passwordless.dev/guide/backend

The backend part of the demo application I wrote for this blog post is written in Go and uses the passwordless-go library. You can install the library with the following command:

go get github.com/AJAYK-01/passwordless-go

Next, we will take a look at some code from the demo application. Similar to when you implement WebAuthn authentication yourself, you need to implement two workflows in your application: registration and authentication.

Registration

In the registration workflow, the user's authenticator creates the private and public key pair and sends the public key to the server. The server stores the public key and the user's ID in a database. In this case, Passwordless.dev stores the public key and the user's ID for you.

Passwordless APIYour BackendWeb BrowserAuthenticatorPasswordless APIYour BackendWeb BrowserAuthenticatorRequest token1Retrieve token2Return token3Return token4Request new key pair5Return public key6Store public key7

Before the authenticator can create the key pair, the client, in this case, the browser, needs to request a token from the backend. In this demo application, that's a simple HTTP request to the backend.

    const createTokenInput: CreateTokenInput = {username};

    this.#httpClient.post<CreateTokenOutput>(`${environment.API_URL}/create-token`, createTokenInput).subscribe({

registration.page.ts

The backend first needs to register the request in the Passwordless.dev service and then, with a call to CreateRegisterToken, create a new token.

func (app *application) createToken(w http.ResponseWriter, r *http.Request) {
  var input CreateTokenInput
  err := request.DecodeJSON(w, r, &input)
  if err != nil {
    response.BadRequest(w, err)
    return
  }

  if input.Username == "" {
    response.BadRequest(w, fmt.Errorf("username is required"))
    return
  }

  params := passwordless.RegisterRequest{
    UserId:   uuid.New().String(),
    Username: input.Username,
  }
  resp, err := app.passwordlessClient.CreateRegisterToken(params)
  if err != nil {
    response.InternalServerError(w, err)
    return
  }

  response.JSON(w, http.StatusOK, CreateTokenOutput{Token: resp.Token})
}

auth.go

The client then instantiates a new client with the public key that was provided by Passwordless.dev.

    const passwordlessClient = new Client({
      apiKey: environment.PASSWORDLESS_PUBLIC_KEY,
    });

registration.page.ts

and, after receiving the token from the backend, calls the register method of the client. This method then initiates the creation of the key pair in the authenticator. After successfully creating the key pair, the public key is sent to the Passwordless.dev service.

        const {error} = await passwordlessClient.register(response.token);
        if (error) {
          await this.#messagesService.showErrorToast('Registration failed');
          return;
        }
        await this.#router.navigate(['/login']);

registration.page.ts

This concludes the registration workflow. The public key is now stored in the Passwordless.dev service, and the user can now sign in.

Authentication

The authentication workflow is where the user signs in. The client first requests a random challenge from Passwordless.dev. The challenge is then signed by the authenticator with the private key and sent back to Passwordless.dev. Passwordless.dev then verifies the signature with the public key and returns an authentication token.

The client sends this token to our backend, which then verifies with a call to Passwordless.dev if the token is valid. In this demo application, if the token is valid, a session with a session cookie will be created.

Your BackendPasswordless APIWeb BrowserAuthenticatorYour BackendPasswordless APIWeb BrowserAuthenticatorRequests challenge1Random challenge2Forwards challenge3Authenticator signs challenge4Signed challenge5Return authentication token6Send token to backend7Verify token8Return verified user information9

The whole process of fetching the random challenge, signing the challenge, and sending the signed challenge back to Passwordless.dev is encapsulated in the signinWithDiscoverable method of the Passwordless.dev client.

    const passwordlessClient = new Client({
      apiKey: environment.PASSWORDLESS_PUBLIC_KEY,
    });

    const {token, error} = await passwordlessClient.signinWithDiscoverable();

    if (error) {
      await this.#messagesService.showErrorToast('Login failed');
      return;
    }

authentication.page.ts

The signinWithDiscoverable method returns a token when the authentication was successful. This token can then be used to authenticate the user on the backend. This demo application does this with a call to the /signin endpoint of the backend.

    const signinInput: SigninInput = {
      token,
    };
    this.#httpClient.post<void>(`${environment.API_URL}/signin`, signinInput).subscribe({
      next: () => {
        this.#navCtrl.navigateRoot('/home', {replaceUrl: true});
      },
      error: () => {
        this.#messagesService.showErrorToast('Login failed');
      }
    });

authentication.page.ts

The backend can then validate the token with a call to the VerifySignin method of the Passwordless.dev client. If successful, a session is created. This demo application uses SCS for session management.

func (app *application) signin(w http.ResponseWriter, r *http.Request) {
  var input SigninInput
  err := request.DecodeJSON(w, r, &input)
  if err != nil {
    response.BadRequest(w, err)
    return
  }

  resp, err := app.passwordlessClient.VerifySignin(input.Token)
  if err != nil {
    response.InternalServerError(w, err)
    return
  }

  if !resp.Success {
    if err := app.sessionManager.Destroy(r.Context()); err != nil {
      response.InternalServerError(w, err)
      return
    }
    response.Forbidden(w)
    return
  }
  app.sessionManager.Put(r.Context(), "userID", resp.UserId)

  w.WriteHeader(http.StatusNoContent)
}

auth.go

Conclusion

This blog post showed you how to implement WebAuthn authentication with Passwordless.dev. Unlike a self-implemented WebAuthn authentication, there is less code to write and maintain. However, the downside is that you are dependent on a third-party service. If the service goes down, your application will not work anymore. This is a general architectural decision you have to make when deciding between a self-implemented solution and a SaaS solution.