
Security News
/Research
Wallet-Draining npm Package Impersonates Nodemailer to Hijack Crypto Transactions
Malicious npm package impersonates Nodemailer and drains wallets by hijacking crypto transactions across multiple blockchains.
webauthnone
Advanced tools
A small wrapper around the webauthn protocol to make one's life easier.
A greatly simplified and opinionated wrapper to invoke the webauthn protocol more conveniently.
Check out the demos:
Or the tutorial here:
This diagram shows how the webauthn protocol works, slightly simplified.
To learn more about the protocol, check out the webauthn guide at Passwordless.ID.
npm install @passwordless-id/webauthn
import * as webauthn from '@passwordless-id/webauthn'
<script type="module">
import { client } from 'https://unpkg.com/@passwordless-id/webauthn'
</script>
The webauthn
module is basically a "bundle" composed of the following modules:
client
: used for invoking webauthn in the browserserver
: used for verifying responses in the serverparsers
: used to parse part or all of the encoded data without verificationsutils
: various encoding, decoding, challenge generator and other utilsIt was designed that way so that you can import only the module(s) you need. That way, the size of your final js bundle is reduced even further. Importing all is dependency free and < 10kb anyway.
So you might for example import { client } from '@passwordless-id/webauthn'
for browser side stuff and import { server } from '@passwordless-id/webauthn'
for server side stuff.
WebCrypto
is only available as crypto
global starting from node 19!)import { client } from '@passwordless-id/webauthn'
client.isAvailable()
Returns true
or false
depending on whether the Webauthn protocol is available on this platform/browser.
Particularly linux and "exotic" web browsers might not have support yet.
await client.isLocalAuthenticator()
This promise returns true
or false
depending on whether the device itself can act as authenticator. Otherwise, a "roaming" authenticator like a smartphone or usb security key can be used. This information is mainly used for information messages and user guidance.
The registration process occurs in four steps:
client.register(...)
and sends the result to the serverNote that unlike traditionnal authentication, the credential key is attached to the device. Therefore, it might make sense for a single user account to have multiple credential keys.
The challenge is basically a nonce to avoid replay attacks.
const challenge = /* request it from server */
Remember it on the server side during a certain amount of time and "consume" it once used.
Example call:
import { client } from '@passwordless-id/webauthn'
const challenge = "a7c61ef9-dc23-4806-b486-2428938a547e"
const registration = await client.register("Arnaud", challenge, {
"authenticatorType": "auto",
"userVerification": "required",
"timeout": 60000,
"attestation": false,
"debug": false
})
Parameters:
username
: The desired username.challenge
: A server-side randomly generated string.options
: See below.The registration
object looks like this:
{
"username": "Arnaud",
"credential": {
"id": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
"algorithm": "ES256"
},
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAiYcFjK3EuBtuEw3lDcvpYAIN_duB4SXSTMv7L51KME_HqF6zjjujSz_EivOatkT8XVpQECAyYgASFYIIMmKkJlAJg5_Se3UecZfh5cgANEdl1ebIEEZ0hl2y7fIlgg8QqxHQ9SFb75Mk5kQ9esvadwtjuD02dDhf2WA9iYE1Q=",
"clientData": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYTdjNjFlZjktZGMyMy00ODA2LWI0ODYtMjQyODkzOGE1NDdlIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ=="
}
Then simply send this object as JSON to the server.
import { server } from '@passwordless-id/webauthn'
const expected = {
challenge: "a7c61ef9-dc23-4806-b486-2428938a547e", // whatever was randomly generated by the server
origin: "http://localhost:8080",
}
const registrationParsed = await server.verifyRegistration(registration, expected)
Either this operation fails and throws an Error, or the verification is successful and returns the parsed registration. Example result:
{
"username": "Arnaud",
"credential": {
"id": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
"algorithm": "ES256"
},
"authenticator": {
...
"name": "Windows Hello Hardware Authenticator"
},
...
}
NOTE: Currently, the attestation which proves the exact model type of the authenticator is not verified. Do I need attestation?
The credential key is the most important part and should be stored in a database for later since it will be used to verify the authentication signature.
"credential": {
"id": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
"algorithm": "ES256"
},
Please note that unlike traditional systems, a user might have multiple credential keys, one per device.
There are two kinds of authentications possible:
Both have their pros & cons (TODO: article).
The authentication procedure is similar to the procedure and divided in four steps.
client.authenticate(...)
and sends the result to the serverThe challenge is basically a nonce to avoid replay attacks.
const challenge = /* request it from server */
Remember it on the server side during a certain amount of time and "consume" it once used.
Example call:
import { client } from 'webauthn'
const challenge = "56535b13-5d93-4194-a282-f234c1c24500"
const authentication = await client.authenticate(["3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU"], challenge, {
"authenticatorType": "auto",
"userVerification": "required",
"timeout": 60000
})
Example response:
{
"credentialId": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAQ==",
"clientData": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiNTY1MzViMTMtNWQ5My00MTk0LWEyODItZjIzNGMxYzI0NTAwIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0=",
"signature": "MEUCIAqtFVRrn7q9HvJCAsOhE3oKJ-Hb4ISfjABu4lH70MKSAiEA666slmop_oCbmNZdc-QemTv2Rq4g_D7UvIhWT_vVp8M="
}
Parameters:
credentialIds
: The list of credential IDs that can be used for signing.challenge
: A server-side randomly generated string, the base64url encoded version will be signed.options
: See below.import { server } from '@passwordless-id/webauthn'
const credentialKey = { // obtained from database by looking up `authentication.credentialId`
id: "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
publicKey: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
algorithm: "ES256"
} as const
const expected = {
challenge: "56535b13-5d93-4194-a282-f234c1c24500", // whatever was randomly generated by the server.
origin: "http://localhost:8080",
userVerified: true, // should be set if `userVerification` was set to `required` in the authentication options (default)
counter: 0 // for enhanced security, you can store the number of times this authenticator was used and ensure it increases each time
}
Often, it might also be more practical to use functions to verify challenge or origin. This is possible too:
const expected = {
challenge: async (challenge) => { /* async call to DB for example */ return true },
origin: (origin) => listOfAllowedOrigins.includes(origin),
userVerified: true, // no function allowed here
counter: 0 // no function allowed here
}
const authenticationParsed = await server.verifyAuthentication(authentication, credentialKey, expected)
Either this operation fails and throws an Error, or the verification is successful and returns the parsed authentication payload.
Please note that this parsed result authenticationParsed
has no real use. It is solely returned for the sake of completeness. The verifyAuthentication
already verifies the payload, including the signature.
challenge
is criticalIt should be truly random. Otherwise, your whole implementation might become vulnerable.
Unlike traditional authentication, you can have multiple public/private key pairs per user: one per device.
username
out of the boxOnly credentialId
is provided during the authentication.
So either you maintain a mapping credentialId -> username
in your database, or you add the username
in your frontend to backend communication.
You can not specify any credential ids during authentication. In that case, the platform will pop-up a default dialog to let you pick a user and perform authentication. Of course, the look and feel is platform specific.
Unlike the webauthn protocol, some defaults are different:
timeout
is one minute by default.userVerification
is required by default.username
is used for both the protocol level user "name" and "displayName"The following options are available for both register
and authenticate
.
timeout
: Number of milliseconds the user has to respond to the biometric/PIN check. (Default: 60000)userVerification
: Whether to prompt for biometric/PIN check or not. (Default: "required")authenticatorType
: Which device to use as authenticator. Possible values:
'auto'
: if the local device can be used as authenticator it will be preferred. Otherwise it will prompt for a roaming device. (Default)'local'
: use the local device (using TouchID, FaceID, Windows Hello or PIN)'roaming'
: use a roaming device (security key or connected phone)'both'
: prompt the user to choose between local or roaming device. The UI and user interaction in this case is platform specific.attestation
: (Only for registration) If enabled, the device attestation and clientData will be provided as base64 encoded binary data. Note that this is not available on some platforms. (Default: false)debug
: If enabled, parses the "data" objects and provide it in a "debug" properties.If you want to parse the encoded registration, authentication or parts of it without verifying it, it is possible using the parsers
module. This might be helpful when debugging.
import { parsers } from '@passwordless-id/webauthn'
parsers.parseRegistration({
"username": "Arnaud",
"credential": {
"id": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
"algorithm": "ES256"
},
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAiYcFjK3EuBtuEw3lDcvpYAIN_duB4SXSTMv7L51KME_HqF6zjjujSz_EivOatkT8XVpQECAyYgASFYIIMmKkJlAJg5_Se3UecZfh5cgANEdl1ebIEEZ0hl2y7fIlgg8QqxHQ9SFb75Mk5kQ9esvadwtjuD02dDhf2WA9iYE1Q=",
"clientData": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYTdjNjFlZjktZGMyMy00ODA2LWI0ODYtMjQyODkzOGE1NDdlIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ=="
})
{
"username": "Arnaud",
"credential": {
"id": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
"algorithm": "ES256"
},
"client": {
"type": "webauthn.create",
"challenge": "a7c61ef9-dc23-4806-b486-2428938a547e",
"origin": "http://localhost:8080",
"crossOrigin": false
},
"authenticator": {
"rpIdHash": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2M=",
"flags": {
"userPresent": true,
"userVerified": true,
"backupEligibility": false,
"backupState": false,
"attestedData": true,
"extensionsIncluded": false
},
"counter": 0,
"aaguid": "08987058-cadc-4b81-b6e1-30de50dcbe96",
"name": "Windows Hello Hardware Authenticator"
},
"attestation": null
}
import { parsers } from '@passwordless-id/webauthn'
parsers.parseAuthentication({
"credentialId": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAQ==",
"clientData": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiNTY1MzViMTMtNWQ5My00MTk0LWEyODItZjIzNGMxYzI0NTAwIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0=",
"signature": "MEUCIAqtFVRrn7q9HvJCAsOhE3oKJ-Hb4ISfjABu4lH70MKSAiEA666slmop_oCbmNZdc-QemTv2Rq4g_D7UvIhWT_vVp8M="
})
{
"credentialId": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"client": {
"type": "webauthn.get",
"challenge": "56535b13-5d93-4194-a282-f234c1c24500",
"origin": "http://localhost:8080",
"crossOrigin": false,
"other_keys_can_be_added_here": "do not compare clientDataJSON against a template. See https://goo.gl/yabPex"
},
"authenticator": {
"rpIdHash": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2M=",
"flags": {
"userPresent": true,
"userVerified": true,
"backupEligibility": false,
"backupState": false,
"attestedData": false,
"extensionsIncluded": false
},
"counter": 1
},
"signature": "MEUCIAqtFVRrn7q9HvJCAsOhE3oKJ-Hb4ISfjABu4lH70MKSAiEA666slmop_oCbmNZdc-QemTv2Rq4g_D7UvIhWT_vVp8M="
}
clientData
import { parsers } from '@passwordless-id/webauthn'
parsers.parseClient("eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYTdjNjFlZjktZGMyMy00ODA2LWI0ODYtMjQyODkzOGE1NDdlIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ==")
{
"type": "webauthn.create",
"challenge": "a7c61ef9-dc23-4806-b486-2428938a547e",
"origin": "http://localhost:8080",
"crossOrigin": false
}
authenticatorData
import { parsers } from '@passwordless-id/webauthn'
parsers.parseAuthenticator("SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAiYcFjK3EuBtuEw3lDcvpYAIN_duB4SXSTMv7L51KME_HqF6zjjujSz_EivOatkT8XVpQECAyYgASFYIIMmKkJlAJg5_Se3UecZfh5cgANEdl1ebIEEZ0hl2y7fIlgg8QqxHQ9SFb75Mk5kQ9esvadwtjuD02dDhf2WA9iYE1Q=")
{
"rpIdHash": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2M=",
"flags": {
"userPresent": true,
"userVerified": true,
"backupEligibility": false,
"backupState": false,
"attestedData": true,
"extensionsIncluded": false
},
"counter": 0,
"aaguid": "08987058-cadc-4b81-b6e1-30de50dcbe96",
"name": "Windows Hello Hardware Authenticator"
}
Please note that aaguid
and name
are only available during registration.
FAQs
A small wrapper around the webauthn protocol to make one's life easier.
The npm package webauthnone receives a total of 0 weekly downloads. As such, webauthnone popularity was classified as not popular.
We found that webauthnone demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
/Research
Malicious npm package impersonates Nodemailer and drains wallets by hijacking crypto transactions across multiple blockchains.
Security News
This episode explores the hard problem of reachability analysis, from static analysis limits to handling dynamic languages and massive dependency trees.
Security News
/Research
Malicious Nx npm versions stole secrets and wallet info using AI CLI tools; Socket’s AI scanner detected the supply chain attack and flagged the malware.