Skip to content

Commit cdaacb7

Browse files
docs(credmgmt): document persistent token usage and add example
1 parent ada22af commit cdaacb7

3 files changed

Lines changed: 92 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
@@ -98,6 +99,7 @@ $ cargo run --example change_pin_hid
9899
$ cargo run --example bio_enrollment_hid
99100
$ cargo run --example authenticator_config_hid
100101
$ cargo run --example cred_management_hid
102+
$ cargo run --example persistent_cred_management_hid
101103
```
102104

103105
## Contributing

libwebauthn/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,7 @@ path = "examples/management/authenticator_config_hid.rs"
201201
[[example]]
202202
name = "cred_management_hid"
203203
path = "examples/management/cred_management_hid.rs"
204+
205+
[[example]]
206+
name = "persistent_cred_management_hid"
207+
path = "examples/management/persistent_cred_management_hid.rs"
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 store it with confidentiality equivalent
8+
//! to other credential secrets (an OS keyring, or encrypted-at-rest with OS access
9+
//! control). This example uses the in-memory [`MemoryPersistentTokenStore`], which keeps
10+
//! records for the lifetime of the process and so demonstrates same-session reuse.
11+
12+
use std::sync::Arc;
13+
use std::time::Duration;
14+
15+
use libwebauthn::management::CredentialManagement;
16+
use libwebauthn::pin::persistent_token::{MemoryPersistentTokenStore, PersistentTokenStore};
17+
use libwebauthn::proto::ctap2::Ctap2;
18+
use libwebauthn::transport::hid::list_devices;
19+
use libwebauthn::transport::{Channel as _, ChannelSettings, Device};
20+
use libwebauthn::webauthn::Error as WebAuthnError;
21+
22+
#[path = "../common/mod.rs"]
23+
mod common;
24+
25+
const TIMEOUT: Duration = Duration::from_secs(10);
26+
27+
#[tokio::main]
28+
pub async fn main() -> Result<(), WebAuthnError> {
29+
common::setup_logging();
30+
31+
// In production, use a securely stored implementation. See the module docs.
32+
let store: Arc<dyn PersistentTokenStore> = Arc::new(MemoryPersistentTokenStore::new());
33+
34+
let devices = list_devices().await.unwrap();
35+
println!("Devices found: {:?}", devices);
36+
37+
for mut device in devices {
38+
println!("Selected HID authenticator: {}", &device);
39+
40+
// Pass the store via ChannelSettings; the channel reuses or mints a persistent
41+
// token through it. The same settings apply to any transport.
42+
let settings = ChannelSettings {
43+
persistent_token_store: Some(store.clone()),
44+
};
45+
let mut channel = device.channel(settings).await?;
46+
let state_recv = channel.get_ux_update_receiver();
47+
tokio::spawn(common::handle_uv_updates(state_recv));
48+
49+
let info = channel.ctap2_get_info().await?;
50+
if !info.supports_credential_management() {
51+
println!("This authenticator does not support credential management.");
52+
continue;
53+
}
54+
if !info.supports_persistent_credential_management_read_only() {
55+
println!(
56+
"This authenticator does not advertise perCredMgmtRO. Read-only credential \
57+
management will use an ordinary (ephemeral) pinUvAuthToken and prompt for \
58+
the PIN as usual."
59+
);
60+
}
61+
62+
// First pass: with an empty store the platform mints a persistent token, so a PIN
63+
// prompt is expected (a UV-capable authenticator may prompt for a touch instead).
64+
println!("\nFirst enumeration (expect a PIN prompt if nothing is cached yet):");
65+
print_metadata(&mut channel).await?;
66+
67+
// Second pass: the persistent token is recognized in the store and reused, so no
68+
// PIN prompt. With a durable store this also holds across restarts and replugs.
69+
println!("\nSecond enumeration (persistent token reused, no PIN prompt expected):");
70+
print_metadata(&mut channel).await?;
71+
72+
return Ok(());
73+
}
74+
75+
Ok(())
76+
}
77+
78+
async fn print_metadata(channel: &mut impl CredentialManagement) -> Result<(), WebAuthnError> {
79+
let metadata = channel.get_credential_metadata(TIMEOUT).await?;
80+
println!(
81+
"Discoverable credentials: {} (max remaining: {})",
82+
metadata.existing_resident_credentials_count,
83+
metadata.max_possible_remaining_resident_credentials_count,
84+
);
85+
Ok(())
86+
}

0 commit comments

Comments
 (0)