Skip to content

Commit 42eae04

Browse files
docs(credmgmt): document persistent token usage and add example
1 parent 3c1df1f commit 42eae04

3 files changed

Lines changed: 91 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ _Looking for the D-Bus API proposal?_ Check out [credentialsd][credentialsd].
3333
- 🟢 GetPinToken
3434
- 🟢 GetPinUvAuthTokenUsingPinWithPermissions
3535
- 🟢 GetPinUvAuthTokenUsingUvWithPermissions
36+
- 🟢 Persistent pinUvAuthToken for read-only credential management (pcmr, CTAP 2.2+)
3637
- [Passkey Authentication][passkeys]
3738
- 🟢 Discoverable credentials (resident keys)
3839
- 🟢 Hybrid transport (caBLE v2): QR-initiated transactions
@@ -93,6 +94,7 @@ $ cargo run --example change_pin_hid
9394
$ cargo run --example bio_enrollment_hid
9495
$ cargo run --example authenticator_config_hid
9596
$ cargo run --example cred_management_hid
97+
$ cargo run --example persistent_cred_management_hid
9698
```
9799

98100
## Contributing

libwebauthn/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,7 @@ path = "examples/management/authenticator_config_hid.rs"
176176
[[example]]
177177
name = "cred_management_hid"
178178
path = "examples/management/cred_management_hid.rs"
179+
180+
[[example]]
181+
name = "persistent_cred_management_hid"
182+
path = "examples/management/persistent_cred_management_hid.rs"
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//! Read-only credential management backed by a persistent pinUvAuthToken (pcmr).
2+
//!
3+
//! A persistent token (CTAP 2.2+) lets a credential manager enumerate passkeys without
4+
//! re-prompting for the PIN on every launch or replug: the platform mints the token once
5+
//! and reuses it on later connections, until a PIN change or authenticator reset.
6+
//!
7+
//! The token is a long-lived bearer secret, so a real application MUST supply a durable,
8+
//! secure store (an OS keyring, or encrypted-at-rest with OS access control). This
9+
//! example uses the in-memory [`EphemeralPersistentTokenStore`], which lives only for the
10+
//! lifetime of the process: it shows same-session reuse, but not the across-restart
11+
//! persistence that is the whole point of the feature. Swap in a durable store and the
12+
//! second run (after replug or restart) skips the PIN prompt too.
13+
14+
use std::sync::Arc;
15+
use std::time::Duration;
16+
17+
use libwebauthn::management::CredentialManagement;
18+
use libwebauthn::pin::persistent_token::{EphemeralPersistentTokenStore, PersistentTokenStore};
19+
use libwebauthn::proto::ctap2::Ctap2;
20+
use libwebauthn::transport::hid::list_devices;
21+
use libwebauthn::transport::{Channel as _, Device};
22+
use libwebauthn::webauthn::Error as WebAuthnError;
23+
24+
#[path = "../common/mod.rs"]
25+
mod common;
26+
27+
const TIMEOUT: Duration = Duration::from_secs(10);
28+
29+
#[tokio::main]
30+
pub async fn main() -> Result<(), WebAuthnError> {
31+
common::setup_logging();
32+
33+
// Production callers: replace this with a durable, secure store. See the module docs.
34+
let store: Arc<dyn PersistentTokenStore> = Arc::new(EphemeralPersistentTokenStore::new());
35+
36+
let devices = list_devices().await.unwrap();
37+
println!("Devices found: {:?}", devices);
38+
39+
for device in devices {
40+
// Attach the store to the device; every channel opened from it forwards the store.
41+
let mut device = device.with_persistent_token_store(store.clone());
42+
println!("Selected HID authenticator: {}", &device);
43+
44+
let mut channel = device.channel().await?;
45+
let state_recv = channel.get_ux_update_receiver();
46+
tokio::spawn(common::handle_uv_updates(state_recv));
47+
48+
let info = channel.ctap2_get_info().await?;
49+
if !info.supports_credential_management() {
50+
println!("This authenticator does not support credential management.");
51+
continue;
52+
}
53+
if !info.supports_persistent_credential_management_read_only() {
54+
println!(
55+
"This authenticator does not advertise perCredMgmtRO. Read-only credential \
56+
management will use an ordinary (ephemeral) pinUvAuthToken and prompt for \
57+
the PIN as usual."
58+
);
59+
}
60+
61+
// First pass: with an empty store the platform mints a persistent token, so a PIN
62+
// prompt is expected (a UV-capable authenticator may prompt for a touch instead).
63+
println!("\nFirst enumeration (expect a PIN prompt if nothing is cached yet):");
64+
print_metadata(&mut channel).await?;
65+
66+
// Second pass: the persistent token is recognized in the store and reused, so no
67+
// PIN prompt. With a durable store this also holds across restarts and replugs.
68+
println!("\nSecond enumeration (persistent token reused, no PIN prompt expected):");
69+
print_metadata(&mut channel).await?;
70+
71+
return Ok(());
72+
}
73+
74+
Ok(())
75+
}
76+
77+
async fn print_metadata(channel: &mut impl CredentialManagement) -> Result<(), WebAuthnError> {
78+
let metadata = channel.get_credential_metadata(TIMEOUT).await?;
79+
println!(
80+
"Discoverable credentials: {} (max remaining: {})",
81+
metadata.existing_resident_credentials_count,
82+
metadata.max_possible_remaining_resident_credentials_count,
83+
);
84+
Ok(())
85+
}

0 commit comments

Comments
 (0)