Skip to content

Commit

Permalink
conditional / mediation => autocomplete
Browse files Browse the repository at this point in the history
  • Loading branch information
dagnelies committed Aug 2, 2024
1 parent e104419 commit 18c5051
Show file tree
Hide file tree
Showing 17 changed files with 173 additions and 47 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
---------

- [Basic Demo](https://webauthn.passwordless.id/demos/basic.html)
- [Conditional UI](https://webauthn.passwordless.id/demos/conditional-ui.html)
- [Passkeys autocomplete](https://webauthn.passwordless.id/demos/conditional-ui.html)
- [Testing Playground](https://webauthn.passwordless.id/demos/playground.html)
- [Authenticators list](https://webauthn.passwordless.id/demos/authenticators.html)
- [Docs](https://webauthn.passwordless.id)
Expand Down
2 changes: 1 addition & 1 deletion docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ The following options are available.
| `hints` | `[]` | Which device to use as authenticator, by order of preference. Possible values: `client-device`, `security-key`, `hybrid` (delegate to smartphone).
| `domain` | `window.location.hostname` | By default, the current domain name is used. Also known as "relying party id". You may want to customize it for ...
| `allowedCredentials` | The list of credentials and the transports it supports. Used to skip passkey selection. Either a list of credential ids (discouraged) or list of credential objects with `id` and supported `transports` (recommended).
| `autofill` | `false` | See concepts
| `autocomplete` | `false` | See concepts



Expand Down
13 changes: 7 additions & 6 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,28 +84,29 @@ Moreover, another benefit is that non-discoverable credentials can also be used,
*✅ Works in most platforms and browsers.*


### Using the input autofill feature
### Using the input autocomplete feature

...also known as *Conditional UI*.
...also known as *Conditional mediation*.

![screenshot-small](screenshots/windows-passkeys-autofill.png)
![screenshot-small](screenshots/windows-passkeys-autocomplete.png)

Unlike the previous methods, which invokes the protocol "directly", this one is triggered during page load.
It activates autocomplete of passkey for input fields having the attribute `autocomplete="username webauthn"`.

```js
client.authenticate({
challenge: ...,
conditional: true
autocomplete: true
})
```

Since there is no way to programmatically know if the user has credentials/passkeys already registered for this domain,
it offers an alternative by skipping the "authenticate" button click. Once selected, the promise will return with the authentication result.
Calling the registration or authentication afterwards will abord the previous pending one.

*⚠️ While this feature is present in Chrome and Safari, it is still very experimental and not available on all browsers.*


Therefore, the usage of `await client.isAutocompleteAvailable()` is advised.


🛡️ Security considerations
Expand Down Expand Up @@ -196,7 +197,7 @@ Whether the platform can create or use a Passkey using the local device or a roa



### Autofill with conditional-UI
### autocomplete with "conditional mediation"

- ✅ Chrome
- ✅/❌ Edge: kind of buggy!
Expand Down
26 changes: 14 additions & 12 deletions docs/demos/authenticators.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Passkeys authenticators list</title>
<link rel="stylesheet" href="https://unpkg.com/buefy/dist/buefy.min.css">

<style>
code {
text-wrap: nowrap;
Expand All @@ -16,18 +18,18 @@
vertical-align: middle;
}
</style>
<script defer src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script defer src="https://unpkg.com/[email protected]/dist/buefy.min.js"></script>
<script defer type="module">
import { authenticatorMetadata } from './js/webauthn.min.js'
console.log(authenticatorMetadata)
const app = new Vue({
el: '#app',
data: {
authenticatorMetadata
}})
</script>
<script defer src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script defer src="https://unpkg.com/[email protected]/dist/buefy.min.js"></script>
<script defer type="module">
import { authenticatorMetadata } from './js/webauthn.min.js'
console.log(authenticatorMetadata)

const app = new Vue({
el: '#app',
data: {
authenticatorMetadata
}})
</script>
</head>

<body>
Expand Down
1 change: 1 addition & 0 deletions docs/demos/basic.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Passkeys demo</title>
<link rel="stylesheet" href="https://unpkg.com/buefy/dist/buefy.min.css">
<script defer src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script defer src="https://unpkg.com/[email protected]/dist/buefy.min.js"></script>
Expand Down
26 changes: 17 additions & 9 deletions docs/demos/conditional-ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Passkeys autocomplete / conditional mediation demo</title>
<link rel="stylesheet" href="https://unpkg.com/buefy/dist/buefy.min.css">
<script defer src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script defer src="https://unpkg.com/[email protected]/dist/buefy.min.js"></script>
Expand All @@ -17,19 +18,26 @@
<img src="/img/banner-biometric-auth.svg" style="max-width: 600px;"/>
<p><b-tag>Please note: this is a demo. Nothing is stored server side, only locally.</b-tag></p>
<br/>
<article class="message is-warning has-text-left">
<div class="message-body">
Passkeys autocomplete, also known as "conditional mediation" triggers authentication when selecting a username from the autocomplete.<br>
However, note that it does not work "smoothly" (or at all) on every browser / platform / authenticators. <br>
Therefore, it is recommended to provide a fallback to trigger authentication without <code>autocomplete: true</code> too.
</div>
</article>

<p>
<code>isAutocompleteAvailable()</code> ?
<b-tag type="is-success" v-if="autocompleteAvailable === true">true</b-tag>
<b-tag type="is-danger" v-if="autocompleteAvailable === false">false</b-tag>
</p>

<br/>
<b-input v-model="username" placeholder="Username" autocomplete="username webauthn"></b-input>
<div class="my-3">
<b-button type="is-primary" @click="register()" :disabled="!username">Register</b-button>
<b-button type="is-primary" @click="logout()" :disabled="!registrationParsed && !authenticationParsed">Sign Out</b-button>
</div>
<article class="message is-warning">
<div class="message-body">
Conditional UI triggers authentication when selecting a username from the autocomplete. <br>
However, note that it does not work on every browser / platform / authenticators. <br>
Therefore, it is recommended to provide a fallback to trigger authentication without <code>conditional: true</code> too.
</div>
</article>
</section>

<section v-if="registrationParsed" class="has-text-left">
Expand All @@ -41,8 +49,8 @@
<p>
<b>Synced?</b>
<template v-if="registrationParsed.authenticator">
<b-tag type="is-primary" v-if="registrationParsed.credential.synced === true">Synced / multi-device credential</b-tag>
<b-tag type="is-primary" v-if="registrationParsed.credential.synced === false">Device-bound credential</b-tag>
<b-tag type="is-primary" v-if="registrationParsed.synced === true">Synced / multi-device credential</b-tag>
<b-tag type="is-primary" v-if="registrationParsed.synced === false">Device-bound credential</b-tag>
</template>
</p>
<hr/>
Expand Down
1 change: 1 addition & 0 deletions docs/demos/debugger.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Passkeys debugger / verifier</title>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/buefy.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css">
<link rel="stylesheet" href="theme.css">
Expand Down
9 changes: 7 additions & 2 deletions docs/demos/js/conditional-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ const app = new Vue({
data: {
username: null,
registrationParsed: null,
authenticationParsed: null
authenticationParsed: null,
autocompleteAvailable: null
},
methods: {

Expand Down Expand Up @@ -65,14 +66,18 @@ const app = new Vue({
async authenticate() {
this.clear();

this.autocompleteAvailable = await client.isAutocompleteAvailable()
if(!this.autocompleteAvailable)
return

try {
// 1. Get a challenge from the server
const challenge = window.crypto.randomUUID(); // faking it here of course

// 2. Invoking WebAuthn in the browser
const authentication = await client.authenticate({
challenge,
conditional: true,
autocomplete: true,
debug: true
})

Expand Down
2 changes: 1 addition & 1 deletion docs/demos/js/webauthn.min.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/demos/js/webauthn.min.js.map

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/demos/playground.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Passkeys playground</title>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/buefy.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css">
<link rel="stylesheet" href="theme.css">
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
---------

- [Basic Demo](/demos/basic.html)
- [Conditional UI](/demos/conditional-ui.html)
- [Autocomplete with conditional mediation](/demos/conditional-ui.html)
- [Testing Playground](/demos/playground.html)
- [Authenticators list](/demos/authenticators.html)

Expand Down
2 changes: 1 addition & 1 deletion docs/registration.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ Example result:
"id": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
"algorithm": "ES256",
"synced": true
"transports": ["internal", "hybrid"]
},
"authenticator": {
...
Expand Down
6 changes: 3 additions & 3 deletions docs/ways.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ User enters username and requests allowed credential IDs from server.



Per conditional UI
------------------
Per autocomplete (a.k.a. conditional mediation)
-------------------------------------------

### How?

- Use `discoverable: 'required'` during registration.
- Call authentication with `conditional: true` when input is mounted in DOM.
- Call authentication with `autocomplete: true` when input is mounted in DOM.

### Advantages

Expand Down
20 changes: 14 additions & 6 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function getAuthAttachment(hints?: PublicKeyCredentialHints[]): AuthenticatorAtt


/**
* For conditional UI, the ongoing "authentication" must be aborted when triggering a registration.
* For autocomplete / conditional mediation, the ongoing "authentication" must be aborted when triggering a registration.
* It should also be aborted when triggering authentication another time.
*/
let ongoingAuth: AbortController | null = null;
Expand Down Expand Up @@ -138,6 +138,9 @@ export async function register(options: RegisterOptions): Promise<RegistrationJS
return json
}

export async function isAutocompleteAvailable() {
return PublicKeyCredential.isConditionalMediationAvailable && PublicKeyCredential.isConditionalMediationAvailable();
}

/**
* Signs a challenge using one of the provided credentials IDs in order to authenticate the user.
Expand All @@ -152,6 +155,9 @@ export async function authenticate(options: AuthenticateOptions): Promise<Authen
if (!utils.isBase64url(options.challenge))
throw new Error('Provided challenge is not properly encoded in Base64url')

if (options.autocomplete && !(await isAutocompleteAvailable()))
throw new Error('PAsskeys autocomplete with conditional mediation is not available in this browser.')

let authOptions: WebAuthnGetOptions = {
challenge: utils.parseBase64url(options.challenge),
rpId: options.domain ?? window.location.hostname,
Expand All @@ -163,13 +169,15 @@ export async function authenticate(options: AuthenticateOptions): Promise<Authen

console.debug(authOptions)

if (ongoingAuth != null)
ongoingAuth.abort('Cancel ongoing authentication')
ongoingAuth = new AbortController();

if (options.autocomplete) {
if(ongoingAuth != null)
ongoingAuth.abort('Cancel ongoing authentication')
ongoingAuth = new AbortController();
}

const raw = await navigator.credentials.get({
publicKey: authOptions,
mediation: options.conditional ? 'conditional' : undefined,
mediation: options.autocomplete ? 'conditional' : undefined,
signal: ongoingAuth?.signal
}) as PublicKeyCredential

Expand Down
Loading

0 comments on commit 18c5051

Please sign in to comment.