Skip to content

Commit

Permalink
mega refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
dagnelies committed Nov 21, 2022
1 parent 3a12351 commit 95a47f1
Show file tree
Hide file tree
Showing 19 changed files with 768 additions and 220 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/node_modules
/.vscode
/dist/esm
/dist/types
/dist/esm
108 changes: 99 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,32 @@ Browser:
</script>
```

The `webauthn` module is basically a "bundle" composed of the following modules:

- `client`: used for invoking webauthn in the browser
- `server`: used for verifying responses in the server
- `parsers`: used to parse part or all of the encoded data without verifications
- `utils`: various encoding, decoding, challenge generator and other utils

It 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.


Utilities
---------

```js
webauthn.isAvailable()
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.

---

```js
await webauthn.isLocalAuthenticator()
await client.isLocalAuthenticator()
```

This promise returns `true` or `false` depending on whether the device itself can act as authenticator. Otherwise, an "extern" authenticator like a smartphone or usb security key can be used. This information is mainly used for information messages and user guidance.
Expand All @@ -55,10 +68,36 @@ This promise returns `true` or `false` depending on whether the device itself ca
Registration
------------

### Overview

The registration process occurs in four steps:

1. The browser requests a challenge from the server
2. The browser triggers `client.register(...)` and sends the result to the server
3. The server parses and verifies the payload
4. The server stores the credential key of this device for the user account

Note 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.


### 1. Requesting challenge

The challenge is basically a [nonce](https://en.wikipedia.org/wiki/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.

### 2. Trigger registration in browser

Example call:

```js
webauthn.register("Arnaud", "random-server-challenge", {
import { client } from '@passwordless-id/webauthn'

const registration = client.register("Arnaud", "random-server-challenge", {
"authenticatorType": "auto",
"userVerification": "required",
"timeout": 60000,
Expand All @@ -67,7 +106,13 @@ webauthn.register("Arnaud", "random-server-challenge", {
})
```

Example response:
Parameters:

- `username`: The desired username.
- `challenge`: A server-side randomly generated string.
- `options`: See [below](#options).

The `registration` object looks like this:

```json
{
Expand All @@ -83,21 +128,55 @@ Example response:
}
```

Parameters:
Then simply send this object as JSON to the server.

- `username`: The desired username.
- `challenge`: A server-side randomly generated string.
- `options`: See [below](#options).
### 3. Verify it server side


```js
import { server } from '@passwordless-id/webauthn'

const expected = {
challenge: randomChallengeIssuedInitially
origin: "http:https://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:

```json
```

**NOTE:** Currently, the *attestation* which proves the exact model type of the authenticator is not verified. Do I need attestation?


Authentication
--------------

### Overview

There are two kinds of authentications possible:

- by providing a list of allowed credential IDs
- by letting the platform offer a default UI to select the user and its credential

Both have their pros & cons (TODO: article).

The authentication procedure is similar to the procedure and divided in four steps.

1. Request a challenge and possibly a list of allowed credential IDs
2. Authenticate by

### Browser side

Example call:

```js
webauthn.login(["credentialIdBase64encoded"], "random-server-challenge", {
import { client } from 'webauthn'

client.authenticate(["credentialIdBase64encoded"], "random-server-challenge", {
"authenticatorType": "auto",
"userVerification": "required",
"timeout": 60000
Expand All @@ -121,7 +200,18 @@ Parameters:
- `challenge`: A server-side randomly generated string, the base64url encoded version will be signed.
- `options`: See [below](#options).

### Server side

Parsing
-------

If you want to parse the encoded registration or authentication without verifying it, it is possible using the `parsers` module.

```js
import { parsers } from 'webauthn'


```

Options
-------
Expand Down
8 changes: 4 additions & 4 deletions demos/example-cdn.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@


<script type="module">
import * as webauthn from 'https://unpkg.com/@passwordless-id/webauthn'
import { client } from 'https://unpkg.com/@passwordless-id/webauthn'

window.register = async function() {
console.log('Registering...')
let res = webauthn.register('MyUsername', 'random-challenge-base64-encoded')
let res = client.register('MyUsername', 'random-challenge-base64-encoded')
console.log(res)
}

window.login = async function() {
console.log('Login...')
let res = webauthn.login([], 'random-challenge-base64-encoded')
console.log('Authenticating...')
let res = client.authenticate([], 'random-challenge-base64-encoded')
console.log(res)
}

Expand Down
8 changes: 4 additions & 4 deletions demos/example-raw.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@


<script type="module">
import * as webauthn from '../dist/webauthn.min.js'
import { client } from '../dist/webauthn.min.js'

window.register = async function() {
console.log('Registering...')
let res = webauthn.register('MyUsername', 'random-challenge-base64-encoded')
let res = client.register('MyUsername', 'random-challenge-base64-encoded')
console.log(res)
}

window.login = async function() {
console.log('Login...')
let res = webauthn.login([], 'random-challenge-base64-encoded')
console.log('Authenticating...')
let res = client.authenticate([], 'random-challenge-base64-encoded')
console.log(res)
}

Expand Down
84 changes: 42 additions & 42 deletions demos/js/basic.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,49 @@
import * as webauthn from '../../dist/webauthn.min.js'
import { client } from '../../dist/webauthn.min.js'

const app = new Vue({
el: '#app',
data: {
username: null,
isRegistered: false,
isAuthenticated: false,
isExternal: false
const app = new Vue({
el: '#app',
data: {
username: null,
isRegistered: false,
isAuthenticated: false,
isExternal: false
},
methods: {
async checkIsRegistered() {
console.log(this.username + ' => ' + !!window.localStorage.getItem(this.username))
this.isRegistered = !!window.localStorage.getItem(this.username)
},
methods: {
async checkIsRegistered() {
console.log(this.username + ' => ' + !!window.localStorage.getItem(this.username))
this.isRegistered = !!window.localStorage.getItem(this.username)
},
async register() {
let res = await webauthn.register(this.username, window.crypto.randomUUID(),{authType: this.isExternal ? 'extern' : 'auto'})
this.$buefy.toast.open({
message: 'Registered!',
type: 'is-success'
})
async register() {
let res = await client.register(this.username, window.crypto.randomUUID(),{authType: this.isExternal ? 'extern' : 'auto'})
this.$buefy.toast.open({
message: 'Registered!',
type: 'is-success'
})

console.log(res)
console.log(res)

this.isAuthenticated = true;
window.localStorage.setItem(this.username, res.credential.id)
await this.checkIsRegistered()
},
async login() {
let credentialId = window.localStorage.getItem(this.username)
let res = await webauthn.login(credentialId ? [credentialId] : [], window.crypto.randomUUID(), {isExternal: this.isExternal})
console.log(res)
this.isAuthenticated = true;
window.localStorage.setItem(this.username, res.credential.id)
await this.checkIsRegistered()
},
async login() {
let credentialId = window.localStorage.getItem(this.username)
let res = await client.authenticate(credentialId ? [credentialId] : [], window.crypto.randomUUID(), {isExternal: this.isExternal})
console.log(res)

this.isAuthenticated = true;
this.$buefy.toast.open({
message: 'Signed in!',
type: 'is-success'
})
},
async logout() {
this.isAuthenticated = false;
this.$buefy.toast.open({
message: 'Signed out!',
type: 'is-success'
})
this.isAuthenticated = true;
this.$buefy.toast.open({
message: 'Signed in!',
type: 'is-success'
})
},
async logout() {
this.isAuthenticated = false;
this.$buefy.toast.open({
message: 'Signed out!',
type: 'is-success'
})

}
}
})
}
})
Loading

0 comments on commit 95a47f1

Please sign in to comment.