Skip to content

Commit

Permalink
improved docs & tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dagnelies committed Nov 23, 2022
1 parent 900baa6 commit 09332f5
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 44 deletions.
126 changes: 104 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Example call:
```js
import { client } from '@passwordless-id/webauthn'

const registration = client.register("Arnaud", "random-server-challenge", {
const registration = await client.register("Arnaud", "a7c61ef9-dc23-4806-b486-2428938a547e", {
"authenticatorType": "auto",
"userVerification": "required",
"timeout": 60000,
Expand All @@ -120,12 +120,12 @@ The `registration` object looks like this:
{
"username": "Arnaud",
"credential": {
"id": "EfDlefdOHjBOkRLsjtTTsNXf64-4d0Zb9zO_Ivj4eLI",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAER3xWhgyoePGH6iUDhr3ATVugwT6Vq9xg8HluGVHrqAJbUvWxtDlzQV0xe5l_dfzkaNkoPwzrxs_3wA8Jxr9RDA==",
"id": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
"algorithm": "ES256"
},
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAiYcFjK3EuBtuEw3lDcvpYAIBHw5Xn3Th4wTpES7I7U07DV3-uPuHdGW_czvyL4-HiypQECAyYgASFYIEd8VoYMqHjxh-olA4a9wE1boME-lavcYPB5bhlR66gCIlggW1L1sbQ5c0FdMXuZf3X85GjZKD8M68bP98APCca_UQw=",
"clientData": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiNGU4YjFiYWMtMmJiOC00YjIzLWI5YzAtZWY1NTk5MjU5OWY0Iiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ=="
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAiYcFjK3EuBtuEw3lDcvpYAIN_duB4SXSTMv7L51KME_HqF6zjjujSz_EivOatkT8XVpQECAyYgASFYIIMmKkJlAJg5_Se3UecZfh5cgANEdl1ebIEEZ0hl2y7fIlgg8QqxHQ9SFb75Mk5kQ9esvadwtjuD02dDhf2WA9iYE1Q=",
"clientData": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYTdjNjFlZjktZGMyMy00ODA2LWI0ODYtMjQyODkzOGE1NDdlIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ=="
}
```

Expand All @@ -138,8 +138,8 @@ Then simply send this object as JSON to the server.
import { server } from '@passwordless-id/webauthn'

const expected = {
challenge: randomChallengeIssuedInitially
origin: "http:https://localhost:8080",
challenge: "a7c61ef9-dc23-4806-b486-2428938a547e", // whatever was randomly generated by the server
origin: "http:https://localhost:8080",
}
const registrationParsed = await server.verifyRegistration(registration, expected)
```
Expand All @@ -151,13 +151,13 @@ Example result:
{
"username": "Arnaud",
"credential": {
"id": "EfDlefdOHjBOkRLsjtTTsNXf64-4d0Zb9zO_Ivj4eLI",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAER3xWhgyoePGH6iUDhr3ATVugwT6Vq9xg8HluGVHrqAJbUvWxtDlzQV0xe5l_dfzkaNkoPwzrxs_3wA8Jxr9RDA==",
"id": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
"algorithm": "ES256"
},
"client": {
"type": "webauthn.create",
"challenge": "4e8b1bac-2bb8-4b23-b9c0-ef55992599f4",
"challenge": "a7c61ef9-dc23-4806-b486-2428938a547e",
"origin": "http:https://localhost:8080",
"crossOrigin": false
},
Expand Down Expand Up @@ -206,7 +206,7 @@ Example call:
```js
import { client } from 'webauthn'

client.authenticate(["credentialIdBase64encoded"], "random-server-challenge", {
const authentication = await webauthn.authenticate(["3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU"], "56535b13-5d93-4194-a282-f234c1c24500", {
"authenticatorType": "auto",
"userVerification": "required",
"timeout": 60000
Expand All @@ -217,10 +217,10 @@ Example response:

```json
{
"credentialId": "tIbwHucpNqA5KXeb_7_fXHAZYm5yPiYK1KrTgbie3ZQ",
"credentialId": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAQ==",
"clientData": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiTnpGbVlUY3dORGN0TUdVeVpTMDBabVZqTFdFMU5HUXRNR1JpTkdVNU5HUmxObVUxIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ==",
"signature": "aoOiX2zxBCvEzebefHZY8GNudCeERuyly4TiSE5eUDyw3iPOnPBFoj0WniN3nuKwhIw8gmPnGhPTArI0apYxoX2mJQaHtAhMS-AxkTKHR63ysArR0Cpd9XMeJicuOGuY5c_zo_hMq91qioI-Ksr0SUTAMS_lWH2Tebe29iKwcT10l0L7ccueKW3G7U5yYxZq3InAuPA5_aJXHeX2neAvng3CFba8we0eQsD5JKh2otAK6Kgy-nT2EHIsBDtXtACn3Q6GfjFWSaeWPa9vngXKuKbLsnpCQjYvlwHt4PrnkvC5WBzGhEoBCF1L9NcxZbRDw_ksWJFYvJcMNcq9DYhxVg=="
"clientData": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiNTY1MzViMTMtNWQ5My00MTk0LWEyODItZjIzNGMxYzI0NTAwIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0=",
"signature": "MEUCIAqtFVRrn7q9HvJCAsOhE3oKJ-Hb4ISfjABu4lH70MKSAiEA666slmop_oCbmNZdc-QemTv2Rq4g_D7UvIhWT_vVp8M="
}
```

Expand All @@ -232,6 +232,93 @@ Parameters:

### Server side



```js
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:https://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
}

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 registration.
Example result:

```json
{
"credentialId": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"client": {
"type": "webauthn.get",
"challenge": "56535b13-5d93-4194-a282-f234c1c24500",
"origin": "http:https://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="
}
```

Please note that the parsed result is returned for the sake of completeness. It is already verified, including the signature.


Remarks
-------

### The `challenge` is critical

It should be truly random. Otherwise, your whole implementation might become vulnerable.

### There can be multiple credentials per user ID

Unlike traditional authentication, you can have multiple public/private key pairs per user: one per device.

### Authentication does *not* provide `username` out of the box

Only `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.

### Let the platform choose the user

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.


### This library simplifies a few things by using sensible defaults


Unlike the [webauthn protocol](), some defaults are different:

- The `timeout` is one minute by default.
- If the device can act as authenticator itself, it is preferred instead of asking which authenticator type to use.
- The `userVerification` is required by default.
- The protocol "Relying Party ID" is always set to be the origin domain
- The `username` is used for both the protocol level user "name" and "displayName"


Parsing
-------

Expand All @@ -240,12 +327,14 @@ If you want to parse the encoded registration or authentication without verifyin
```js
import { parsers } from 'webauthn'


// TODO
```

Options
-------

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:
Expand Down Expand Up @@ -304,12 +393,5 @@ Please note that `aaguid` and `name` are only available during registration.



Notes
-----

Unlike the [webauthn protocol](), some defaults are different:

- The `timeout` is one minute by default.
- If the device can act as authenticator itself, it is preferred instead of asking which authenticator type to use.
- The `userVerification` is required by default.

18 changes: 14 additions & 4 deletions demos/playground.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ <h2 class="title">Registration</h2>
<pre>{{registration.parsed ?? '...'}}</pre>



<div class="notification is-warning" v-if="registration.parsed">
<p>At this point, you should store the `credential` and associate it with the user account. You will need it later to verify authentication attempts.</p>
</div>

</section>

Expand Down Expand Up @@ -173,17 +175,23 @@ <h2 class="title">Authentication</h2>
algorithm: "{{registration?.parsed?.credential?.algorithm ?? '...'}}"
}

const verified = await server.verifyAuthentication(res, credentialKey, {
const expected = {
challenge: "{{authentication?.challenge ?? '...'}}",
origin: "{{origin}}",
userVerified: {{authentication?.options?.userVerification === 'required'}},
counter: 0
})
}

const verified = await server.verifyAuthentication(res, credentialKey, expected)
</pre>

<p>Resulting into:</p>

<pre>{{authentication.parsed ?? '...'}}</pre>

<div class="notification is-success" v-if="authentication.parsed">
<p>Please note that the parsed result is returned for the sake of completeness. It is already verified, including the signature.</p>
</div>
</section>


Expand All @@ -192,7 +200,9 @@ <h2 class="title">Authentication</h2>
<img src="img/icon_validate.svg"/>
<h2 class="title">Signature validation</h2>
</header>
<p>For completeness purpose, here is how only the signature is verified.</p>
<p>This part is mainly for debugging purposes or validation.</p>
<hr/>


<b-field label="Algorithm" horizontal>
<b-select v-model="verification.algorithm" expanded>
Expand Down
Loading

0 comments on commit 09332f5

Please sign in to comment.