Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explainer and implementation guidance for Client Hints #58

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions docs/docs/advanced_use_cases/client_hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
sidebar_position: 2
---

# Client Hints

[Client hints](https://www.w3.org/TR/webauthn-3/#enum-hints) are in addition to the Level 3 version of the WebAuthn specification that aims to improve UX flows to aid in the adoption of passkeys. Hints provide a relying party a mechanism to suggest to a client application the type of authenticator that should be used to complete a WebAuthn ceremony.

The ability to suggest a specific type of authenticator can be useful for a variety of situations that can include:

- An enterprise relying party suggesting to the client that it's expecting for a security key to be used
- Prompts in a consumer application that provides different user flows for platform authenticators and security keys
- The ability to hide the QR code presented in a cross-platform defined requests when only a security key is expected

## Client hints vs authenticator attachment

Earlier in this guide, we introduced the concept of [authenticator attachment](https://yubicolabs.github.io/passkey-workshop/docs/relying-party/reg-flow#:~:text=discouraged-,authenticatorAttachment,-defines%20the%20AuthenticatorAttachment). Authenticator attachment has historically been the mechanism to enforce the use of a specific type of authenticator against two choices:

- **Platform**, which indicates the built in authenticator on a device such as Windows Hello, and Touch ID
- **Cross-platform**, which could be any roaming device such as a security key, or a phone acting as a roaming authenticator through the hybrid (QR code) flow

The primary difference between hints and authenticator attachment is in the enforcement of the selection on the property. Hints will immediately show the WebAuthn prompt defined by the hint, but will allow the user to make another selection of their choice. Authenticator attachment always enforces the use of the authenticator that was defined by the relying party (so if platform was selected, then security keys are no longer an option for the user).

Use the outline below to understand how the two properties may interact as they continue to coexist in the WebAuthn specification.

- Hints may contradict what's defined in the authenticator attachment, when this occurs the hint setting should be given precedence.
- Authenticator attachment should not be deprecated from a relying party, as it should be used as a compliment to hints for clients (browsers, OS, and platforms) that have not adopted the use of hints

## Example

Below is a demonstration of how hints work in a client application. The video denotes different (but not all) permutations for hints, and how the client reacts to the input. An in-depth explanation on the different hints enumerations is included in the next section.

\*\*TODO - Add a video once the jws changes are complete

## Types of hints

The WebAuthn specification notes a few different options that can be expressed to the client from the relying party. Hints are expressed in the attestation and assertion requests as the hintsproperty, which is an array list of the enumerations listed below.

The enumerations are ordered in the following decreasing preference: `security-key, client-device, hybrid`. This means that if two hints are contradictory, then the option with the higher preference will be presented over the other.

Meaning that if a hints property is set with a value of `["hybrid", "security-key"]`, then the user will be presented with a modal to register or authenticate with a security key.

### None

The option of no hint can be denoted by not including the hints property, or by passing a hints property with an empty array. This indicates to the client application that your relying party has no preference on the authenticator used, and will show a standard modal presenting all of the standard WebAuthn options of the client.

### security-key

`security-key` denotes to the client application that it's expecting the user to leverage a security key for registration or authentication. This is helpful in enterprise, and other high assurance scenarios as it helps to guide the use of device-bound passkeys found on security keys.

When using this option you should set the `authenticatorAttachment` property to `cross-platform`, This will help ensure compatibility with clients that have not adopted the use of hints in their WebAuthn implementation.

### client-device

`client-device` denotes to the client application that it's expecting the user to leverage a platform authenticator. This will likely result in the creation of copyable passkeys.

When using this option you should set the `authenticatorAttachment` property to `platform`, This will help ensure compatibility with clients that have not adopted the use of hints in their WebAuthn implementation.

### hybrid

`hybrid` denotes to the client application that it's expecting the use of a roaming, platform authenticator that will leverage the hybrid (QR code) flow. The user will be presented with a QR code to scan with their smartphone to leverage a passkey accessible by the device.

When using this option you should set the `authenticatorAttachment` property to `cross-platform`, This will help ensure compatibility with clients that have not adopted the use of hints in their WebAuthn implementation.

## Platform support

Before attempting to implement hints from your relying party, ensure that your chosen platform (browser, OS, client) has support for Client Hints. You can use the [device support matrix on passkeys.dev](https://passkeys.dev/device-support/) to see if your platform is supported.

## Implementation guidance

These code snippets will continue to build off of the guidance provided in the earlier sections of this guide. Please note that client hints are only available in version 2.6+ of the java-webauthn-server library.

** Here is where we will add the new snippet of the java-webauthn-server
** Also link to the new section that are scattered through the docs (mentioned in my outline above)
26 changes: 20 additions & 6 deletions docs/docs/relying-party/auth-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,28 @@ Below is the request body of the `/assertion/options` method

```json
{
"userName": "[email protected]"
"userName": "[email protected]",
"hints": ["security-key", "client-device", "hybrid"]
}
```

In this case, we are only signaling to the relying party the user which we are trying to authenticate. This method is implemented in a way that will support **BOTH** discoverable and non-discoverable credential flows. For the sake of this workshop, our focus will be on the discoverable credential flow, but we will outline how the non-discoverable credential flow is handled.
`hints` defines the [PublicKeyCredentialHints](https://w3c.github.io/webauthn/#enum-hints) option, or the ability for a relying party to suggest to a client the type of authenticator that can be used. More information on this new feature can be found later in this guide in the section [Advanced concepts > Client Hints](https://yubicolabs.github.io/passkey-workshop/docs/advanced_use_cases/client_hints).

- security-key
- client-device
- hybrid
- Empty | none | exclude property

`userName` will signal to the relying party which user is trying to authenticate. This method is implemented in a way that will support **BOTH** discoverable and non-discoverable credential flows. For the sake of this workshop, our focus will be on the discoverable credential flow, but we will outline how the non-discoverable credential flow is handled.

##### Discoverable credential flow

As you may recall from the fundamentals section, a WebAuthn credential must be discoverable in order to be a passkey. A discoverable credential refers to the ability for a relying party to attempt to utilize a credential on an authenticator without the user providing a user handle. This means that in our API we will signal to the RP that we wish to use a discoverable credential flow by passing in an empty username.

```json
{
"userName": ""
"userName": "",
...
}
```

Expand All @@ -52,7 +61,8 @@ In order to trigger the use of a non-discoverable credential flow, you will incl

```json
{
"userName": "[email protected]"
"userName": "[email protected]",
...
}
```

Expand All @@ -71,7 +81,8 @@ Below is the response body of the `/attestation/options` method for a discoverab
"challenge": "NGc3jpB4Q-VnOmbhFBnDAczlYPT4soKA7xviGeJmDhc",
"timeout": 180000,
"rpId": "localhost",
"userVerification": "preferred"
"userVerification": "preferred",
"hints": ["security-key", "client-device", "hybrid"]
}
}
```
Expand All @@ -93,7 +104,8 @@ Below is the response body of the `/attestation/options` method for a non-discov
"type": "public-key"
}
],
"userVerification": "preferred"
"userVerification": "preferred",
"hints": ["security-key", "client-device", "hybrid"]
}
}
```
Expand All @@ -114,6 +126,8 @@ With that said, non-discoverable credentials cannot be leveraged in the discover

### Implementation

TODO - Add hints section when available

Below is a sample implementation of the /attestation/options method. Note that `request` should correlate to the request body mentioned in the previous section, and `response` should correlate to the request response mentioned in the previous section.

```java
Expand Down
21 changes: 18 additions & 3 deletions docs/docs/relying-party/reg-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ Below is the request body of the `/attestation/options` method
"authenticatorAttachment": "cross-platform",
"userVerification": "preferred"
},
"attestation": "direct"
"attestation": "direct",
"hints": ["security-key", "client-device", "hybrid"]
}
```

Expand Down Expand Up @@ -81,16 +82,27 @@ Keep in mind that some devices, like security keys, may have a limit on the numb

::::tip Best to be permissive
Err on the side of caution when using this setting. For most use cases you should not set this setting, and instead allow your users to use any authenticator that they want to use. Allow for both the convenience of platform authenticators, and the high degree of assurance of security keys for the users who want it.

Leveraging the `hints` property (covered below) will allow you to guide the user to a specific authenticator type without restricting their options.
::::

::::danger cross-platform will always present multiple options
If a developer declares the preference for cross-platform, the user will always see options for both security keys and the hybrid flow (QR code). As of now there is not an implementation that allows a developer to hide one option, both will always be presented
If a developer declares the preference for cross-platform, the user will always see options for both security keys and the hybrid flow (QR code). As of now there is not an implementation that allows a developer to hide one option, both will always be presented.

To prevent the multiple options issue with `cross-platform` you may attempt to use the `hints` property listed in the next section.
::::

- platform
- cross-platform
- Empty | none | exclude property (default)

`hints` defines the [PublicKeyCredentialHints](https://w3c.github.io/webauthn/#enum-hints) option, or the ability for a relying party to suggest to a client the type of authenticator that can be used. More information on this new feature can be found later in this guide in the section [Advanced concepts > Client Hints](https://yubicolabs.github.io/passkey-workshop/docs/advanced_use_cases/client_hints).

- security-key
- client-device
- hybrid
- Empty | none | exclude property

#### Response

Below is the response body of the `/attestation/options` method
Expand Down Expand Up @@ -127,7 +139,8 @@ Below is the response body of the `/attestation/options` method
"authenticatorAttachment": "cross-platform",
"userVerification": "preferred"
},
"attestation": "direct"
"attestation": "direct",
"hints": ["security-key", "client-device", "hybrid"]
}
}
```
Expand All @@ -146,6 +159,8 @@ The `challenge` should be randomly generated. This random number should be based

### Implementation

TODO - Add hints section when available

Below is a sample implementation of the /attestation/options method. Note that `request` should correlate to the request body mentioned in the previous section, and `response` should correlate to the request response mentioned in the previous section.

```java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,16 @@

async function registerPasskey(authenticatorAttachment) {
try {

/** Create a string array for hints based on the authenticatorAttachment parameter */

let hinstSelection;
if(authenticatorAttachment === "cross-platform") {
hinstSelection = ["security-key"];
} else if(authenticatorAttachment === "platform") {
hinstSelection = ["client-device"];
} else {
hinstSelection = [];
}
const request = {
"method": "POST",
"headers": {
Expand All @@ -242,7 +251,8 @@
authenticatorAttachment: authenticatorAttachment,
userVerification: "required"
},
attestation: "direct"
attestation: "direct",
hints: hinstSelection
})
}

Expand Down
14 changes: 7 additions & 7 deletions examples/relyingParties/java-spring/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<!-- Bean Validation API support -->
<dependency>
<groupId>org.springframework.boot</groupId>
Expand All @@ -80,18 +80,18 @@
<dependency>
<groupId>com.yubico</groupId>
<artifactId>webauthn-server-core</artifactId>
<version>2.1.0</version>
<version>2.5.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.yubico</groupId>
<artifactId>webauthn-server-attestation</artifactId>
<version>2.1.0</version>
<version>2.5.3</version>
</dependency>
<dependency>
<groupId>com.yubico</groupId>
<artifactId>yubico-util</artifactId>
<version>2.2.0</version>
<version>2.5.3</version>
<scope>compile</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.yubicolabs.passkey_rp.models.api;

import java.util.Objects;
import java.util.Optional;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;

Expand All @@ -16,6 +18,9 @@ public class AssertionOptionsRequest {
@JsonProperty("userName")
private String userName;

@JsonProperty("hints")
private Optional<String[]> hints = Optional.ofNullable(null);

public AssertionOptionsRequest userName(String userName) {
this.userName = userName;
return this;
Expand All @@ -36,6 +41,16 @@ public void setUserName(String userName) {
this.userName = userName;
}

public void setHints(String[] hints) {
this.hints = Optional.ofNullable(hints);
}

@Schema(name = "hints", example = "[\"security-keys\", \"client-device\"]", required = false)
public Optional<String[]> getHints() {
return hints;
}

// TODO - Currently not checking equality of hints
@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.yubicolabs.passkey_rp.models.api;

import java.util.Objects;
import java.util.Optional;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
Expand All @@ -25,6 +27,9 @@ public class AttestationOptionsRequest {
@JsonProperty("authenticatorSelection")
private AttestationOptionsRequestAuthenticatorSelection authenticatorSelection;

@JsonProperty("hints")
private Optional<String[]> hints = Optional.ofNullable(null);

/**
* Gets or Sets attestation
*/
Expand Down Expand Up @@ -107,6 +112,20 @@ public void setDisplayName(String displayName) {
this.displayName = displayName;
}

public AttestationOptionsRequest hints(Optional<String[]> hints) {
this.hints = hints;
return this;
}

@Schema(name = "hints", example = "[\"security-keys\", \"client-device\"]", required = false)
public Optional<String[]> getHints() {
return this.hints;
}

public void setHints(Optional<String[]> hints) {
this.hints = hints;
}

public AttestationOptionsRequest authenticatorSelection(
AttestationOptionsRequestAuthenticatorSelection authenticatorSelection) {
this.authenticatorSelection = authenticatorSelection;
Expand Down Expand Up @@ -148,6 +167,7 @@ public void setAttestation(AttestationEnum attestation) {
this.attestation = attestation;
}

// TODO - Currently not checking equality of hints
@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.yubico.webauthn.RegisteredCredential;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
Expand Down Expand Up @@ -76,6 +77,8 @@ public Set<RegisteredCredential> lookupAll(ByteArray credentialId) {
@Override
public Boolean addRegistration(CredentialRegistration registration) {
try {
mapper.registerModule(new Jdk8Module());

CredentialRegistrationDBO newItem = CredentialRegistrationDBO.builder()
.userHandle(registration.getUserIdentity().getId().getBase64Url())
.credentialID(registration.getCredential().getCredentialId().getBase64Url())
Expand Down