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.
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({
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})
}
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,
});
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']);
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.
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;
}
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');
}
});
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)
}
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.