A few weeks ago I had a bug with bitwarden where every passkey wanted to load from the macbook instead of bitwarden. I ended up being locked out of a few accounts that didn't have OTPs as a fallback. Mostly inconsequential stuff like Twitter.
I love passkeys, but they're still kinda hard to use. There's several sites that wont let you enroll multiple ones and it's easy for systems to step on each other like the aforementioned experience.
The problem is fallback. All my banking apps have SMS OTP fallbacks and that's no better than having only SMS OTP. If you're building these systems make sure you have good fallbacks. What matters in design is not so much how well it works when things go right but how well it works when things go wrong. With security you really cannot ignore edge cases
If I'm being honest, I regret every passkey I ever made. With my old flow, I knew when to use my Yubikey, when to use my OTP, and when to use SMS 2FA. With the new flow, these things say "use your passkey" and I don't know where in god's name I did this. If I did this on my iPhone in a WebUI that popped up when I followed a link to buy something, then it's never going to be on Chrome or Bitwarden.
I've decided to stop adding new ones. I'll just OTP 2FA. It's simple, reliable, and I can keep it in Bitwarden safely.
It's all a bit of a mess right now, but with some fiddling in settings you should be able to get your passkeys in one place (probably Bitwarden) and access them everywhere.
Safari on iOS can store and use passkeys from any app that implements the right system API, including the default Apple Passwords but also Bitwarden and Chrome.
For desktop, you can either use a browser extension provided by some password managers (such as Bitwarden), or if you're on a Mac, Safari and Chrome can access passkeys from other apps similarly to on iOS (but not as many providers support this API on Mac as on iOS, and in particular Bitwarden doesn't, so you'd have to use the extension for that).
I can't blame you. I know the passkey UX on Windows was absolutely horrible (and probably still is). However I can't say that I relate. I use 1Password and I don't think I've literally ever been asked to use the native UI. It always goes straight to 1Password. I'm not sure why we have different experiences. (I use a mac, an iphone, and a google pixel)
The scariest thing is the casual mention of the Digital Credentials API[1]. Forget passkeys, when you need government issued credentials to surf the net, the good times are over.
There are plenty of websites and services already where you need to prove your identity to use them. The digital credentials API is an attempt to standardise that which is already legally required in the US, the UK, Australia, and the EU, except without having to upload a picture of your ID to a shady third party website.
It's likely that the websites need your actual government issued credentials are not your twitters and your hacker news, but government websites that actually need to link the web user to the citizen. As an example my country has a portal that you use as a citizen to book appointments to government institutions, keeps you updated about the status of your requests, allows you to securely upload scans for additional documents that your request might need, etc.
One thing I ran into recently when I played around with passkeys is the problem of orphaned keys. Basically if I log into a website using the passkey and then go to my account settings and remove that passkey then log out I have a problem. Now I can’t sign in but when I go to recover my account iOS/macOS will refuse to create a new passkey because one already exists for this website. So I have to go to my passwords list and manually remove it. I believe I was correctly using the JS API for signaling orphaned keys but the OS still wouldn’t remove it so it was a situation of having to educate the user to remove the orphaned key manually (and hoping the user doesn’t get confused and remove the wrong key). You also apparently can’t create more than one passkey for the same username and the same website. So if I initially create an account from my MacBook and the passkey gets listed as “MacBook”, I then go to log in from my iPhone and it still uses the “MacBook” passkey because of iCloud sync. But this is confusing because I cannot have an iPhone key.
Overall it’s not terrible but I think these edge cases are going to keep biting people and need to be addressed in some way. And yes I understand that I could use a Yubikey or Bitwarden or some such but the point was that I wanted to see how this flow works for “normal” users who just use the iCloud Keychain and the experience leaves something to be desired.
> So if I initially create an account from my MacBook and the passkey gets listed as “MacBook”, I then go to log in from my iPhone and it still uses the “MacBook” passkey because of iCloud sync. But this is confusing because I cannot have an iPhone key.
Now try using a Windows or Linux computer...
This is why I strongly prefer to not use OSX passkeys. How the fuck am I supposed to login on my nix machines if you only allow me to enroll one passkey?!
Love these "lessons learned" posts, keep the coming!
My only feedback is about the Quickstart of passkeybot, "feed this example into a good LLM with these instructions". I undeerstand the idea, but I was a bit shocked that the first time I see these sort of instructions is for an auth framework.
I am in the middle of writing a passkey-driven server dashboard app (native SwiftUI app, with a small server component).
In the future, I would like to use passkeys as much as possible, but they do present a bit more friction to users than Sign in with Apple. When I was initially learning them I wrote this up: https://littlegreenviper.com/series/passkeys/
The passkey spec authors think websites should be able to ban clients which allow users to manage their own data[1,2]. It makes me really hesitant to adopt passkeys if my client could get banned because it's open source and lets me control my client how I want to. It appears to be more useful for vendor lock-in than anything else[3]. A shame, since it could've been a cool tech if they had built it to be resilient to this kind of abuse, but it's clear they think vendor lock-in is actually a core feature of the protocol.
The point of passkeys is that they're unexportable. Software implementations like Bitwarden/KeepassXC/etc. making them exportable go right against the point of the protocols.
I personally think the ability to export+import passkeys is a good thing from a backup point of view, but he's not wrong in suggesting that companies actually using the high security features of passkeys will eventually block software implementations like these.
This isn't about vendor lock-in. Nobody is asking for KeepassXC to remove passkey support. This is about security software implementing an API and not fulfilling the expectations that come with such an implementation. To quote the comment you linked:
> That's fine. Let determined people do that, but don't make it easy for a user to be tricked into handing over all of their credentials in clear text.
So if a client ever goes rogue someday (either intentionally or has been compromised) and starts shipping off private material to a malicious third party, you think relying parties shouldn’t have the option not to trust that client anymore?
> generateKey is a JS API that allows you to create new key pairs, where the private key cannot be extracted similar to passkeys.
Is that "cannot be extracted" from JS only, or is this an actual device-locked, TPM/SEP-bound key like passkeys?
If it is, it seems kind of like the buried lede to me that there is a browser API that lets any website built its own completely unstandardized quasi-passkey system and lock the key to the current device.
Yes, where practical. Though recognize that by their very nature web apps aren't part of the trust network. The browser and security stack can make a key for them to use, but it's not possible to be sure that the user of that key is not subject to attack at the backend (or even front end, really the best you can do there is XSS protection, which is hardly at the standard of "crytographically secure").
And likewise you as the app vendor can know the key was generated, and that it works, but you can't[1] know that it's actually locked to a device or that it's non-exportable. You could be running in a virtualized environment that logged everything.
Basically it's not really that useful. Which is sort of true for security hardware in general. It's great for the stuff the device vendors have wired up (which amounts to "secured boot", "identifying specific known devices" and "validating human user biometrics on a secured device"), but not really extensible in the way you'd want it to be.
[1] Within the bounds of this particular API, anyway. There may be some form of vendor signing you can use to e.g. verify that it was done on iOS or ChromeOS or some other fully-secured platform. I honestly don't know.
In oauth2: when I /1 associate a random uuidv4 for each new flow with my user (server side), /2 stick that uuid into the state parameter, and then /3 look up my user with this on callback-endpoint execution. Isn't PKCE in that case redundant?
Oauth's PKCE verifies the continuity of the flow as it is essentially a saga(multi-step process). For example you can initiate oauth access grant request multiple times with the same data, but PKCE ensures that each of those initiations can be individually identified. Do not confuse PKCE with state field, which is for XSS and has no obfuscation.
Just to be clear, the PKCE secret can be the same for each initiation, but in the end its goal is to ensure that the first request matches with the last one. And yes, there is "plain" PKCE method but that is just for testing. SHA256 is the default one used to obfuscate the secret.
I think one point of PKCE is that the oauth token is never sent to the client (it is exchanged on the backchannel), so it theoretically is more protected.
Of course if you trust the client (no bad browser extensions, updated browser) and have good TLS settings and no MITM risk and make sure the your IDs are single-use then it seems like that should be fine.
PKCE protects the auth token from interception by making it so that only your code that started the flow can redeem it by proving they have the secret code_verifier on the redeem_token() call.
The code_challenge == sha256(code_verifier). You will share the code_challenge at the start of the flow.
I also think these are very similar. The main difference in my view is that the state parameter is checked by the client, while PKCE is checked by the server.
I run an authentication server and requiring PKCE allows me to make sure that XSS protection is handled for all clients.
How to add passkeybot support to your site, according to their official guide:
start
(1) Copy / paste example_http_server into your LLM of choice (use a paid/good model).
(2) Prompt: Implement the HTTP handlers here for my project,..
Um, no? How about you give me real instructions on how to do it? I’m not going to delegate a security-critical task to an LLM. And since I need to review it carefully myself anyway, I might as well write it all by hand, right? Like, the whole premise is I just need to implement a couple of webhooks.
It's absolutely hilarious that someone would think that this passes for API docs nowdays. Still it's good to know what to avoid on the very first glance.
Yes, that is true, I was assuming that any LLM code was going to be checked by the developer. Step 7 in the guide is "review your code and ensure the important logic commented in the example server is still present".
The LLM is only for converting the JS based example code into your language X and HTTP framework Y (instead of giving example code for every combination of X and Y).
The standard implementation is in a single file `http_server.ts`, which is around 200 lines of well commented code, with important logic commented (around 5 lines). The example code can be run locally with a few commands.
The repo also contains a sequence diagram [1], a description of the HTTP handlers needed [2], and a live demo [3] where you can see the request/responses.
Thanks for your feedback I have made this clearer in the readme.
> But much of the recommendations say to use the counter as a heuristic rather than evidence of a cloned authenticator because there are many legitimate reasons the counter can be wrong.
I'm curious on why there would be any legitimate reason for that. Security wise it should not happen, it's just some implementations being crappy or some bad practice like reusing same passkey with different devices ?
Why? Passwords can be remembered and entered on other devices for recovery. The plethora of passkeys out there cannot.
A bit the same why although I love the keychain in macOS, it also makes me uncomfortable. Lose your phone and laptop in a theft or fire and you are locked out from your Apple account. Goodbye online presence.
And I wish passkeys could cover all the use cases of passwords, yet here we are. Passwords are simple and well understood. Passkeys have all sorts of sharp edges that you won't discover until you're hurt by them.
The authenticator GUI only shows “sign in to your_domain.com”. It never allows a more general “sign this content for your_domain.com”. E.g. “sign this transaction request to move £50 to Bob's account”.
That is why you should ship a pristine HTML+CSS+JS environment that can use subtle web crypto. YOU show what is being signed. And then the device can sign its hash using the secure enclave.
And you CAN do attestation even on consumer devices, by using the Device or AppAttest framework (I think that’s what it’s called). I did it myself in our app. It does show up 100% of the time but when it does it’s useful.
PS: being the web3 / blockchain geek that I am, I will tell you stuff that triggers anticryptobros on HN.
The Web3 ecosystem already has a standard called EIP712 for signing structured data. If you want to stick to standards, use that!
The secure enclaves all use P-256 (sometimes called R-256) while Bitcoin / Web3 uses K-256 (the Koeblitz curve, they distrust the NIST curve or whatever).
So that means you’re going to have to use Abstract Accounts and the new precompiled smart contracts to verify P256 signatures, which only Binance Smart Chain and a handful of other chains have deployed. Luckily BSC is the most widely used chain by volume and has plenty of money sloshing around so you can build your trustless programs there. If you want to be totally trustless — LET THE SMART CONTRACTS GENERATE THE CHALLENGE TO BE SIGNED BY THE AUTHENTICATOR. Then have it sign the temporary k256 public key (from the keypair) to use, as long as your session is open you can then add use your private key to sign transactions. As usual, do this for small amounts per day, transactions that move larger amounts should still require use of multisig keys etc.)
Regarding PKCE, the way I remember it is that OAuth2 was deliberately designed to eliminate as much actual cryptography as possible, relying instead on same-origin and TLS security; PKCE is one of the few things that introduces an actual cryptography primitive.
godelski|2 months ago
I love passkeys, but they're still kinda hard to use. There's several sites that wont let you enroll multiple ones and it's easy for systems to step on each other like the aforementioned experience.
The problem is fallback. All my banking apps have SMS OTP fallbacks and that's no better than having only SMS OTP. If you're building these systems make sure you have good fallbacks. What matters in design is not so much how well it works when things go right but how well it works when things go wrong. With security you really cannot ignore edge cases
xandrius|2 months ago
awesome_dude|2 months ago
The easier it is to do things, like use another channel, the harder it is to keep secure.
The easier it is to keep secure, the harder it is to use.
xboxnolifes|2 months ago
In terms of security, yes. But not in terms of convenience.
arjie|2 months ago
I've decided to stop adding new ones. I'll just OTP 2FA. It's simple, reliable, and I can keep it in Bitwarden safely.
comex|2 months ago
Safari on iOS can store and use passkeys from any app that implements the right system API, including the default Apple Passwords but also Bitwarden and Chrome.
For desktop, you can either use a browser extension provided by some password managers (such as Bitwarden), or if you're on a Mac, Safari and Chrome can access passkeys from other apps similarly to on iOS (but not as many providers support this API on Mac as on iOS, and in particular Bitwarden doesn't, so you'd have to use the extension for that).
ChadNauseam|2 months ago
quantummagic|2 months ago
[1] https://developer.chrome.com/blog/digital-credentials-api-sh...
jeroenhd|2 months ago
mariusor|2 months ago
IgorPartola|2 months ago
Overall it’s not terrible but I think these edge cases are going to keep biting people and need to be addressed in some way. And yes I understand that I could use a Yubikey or Bitwarden or some such but the point was that I wanted to see how this flow works for “normal” users who just use the iCloud Keychain and the experience leaves something to be desired.
godelski|2 months ago
This is why I strongly prefer to not use OSX passkeys. How the fuck am I supposed to login on my nix machines if you only allow me to enroll one passkey?!
bobbylarrybobby|2 months ago
BoppreH|2 months ago
My only feedback is about the Quickstart of passkeybot, "feed this example into a good LLM with these instructions". I undeerstand the idea, but I was a bit shocked that the first time I see these sort of instructions is for an auth framework.
ChrisMarshallNY|2 months ago
I am in the middle of writing a passkey-driven server dashboard app (native SwiftUI app, with a small server component).
In the future, I would like to use passkeys as much as possible, but they do present a bit more friction to users than Sign in with Apple. When I was initially learning them I wrote this up: https://littlegreenviper.com/series/passkeys/
coldpie|2 months ago
[1] Spec author quote: "To be very honest here, you risk having KeePassXC blocked by relying parties." https://github.com/keepassxreboot/keepassxc/issues/10407#iss...
[2] https://www.smokingonabike.com/2025/01/04/passkey-marketing-...
[3] https://fy.blackhats.net.au/blog/2024-04-26-passkeys-a-shatt...
jeroenhd|2 months ago
I personally think the ability to export+import passkeys is a good thing from a backup point of view, but he's not wrong in suggesting that companies actually using the high security features of passkeys will eventually block software implementations like these.
This isn't about vendor lock-in. Nobody is asking for KeepassXC to remove passkey support. This is about security software implementing an API and not fulfilling the expectations that come with such an implementation. To quote the comment you linked:
> That's fine. Let determined people do that, but don't make it easy for a user to be tricked into handing over all of their credentials in clear text.
otterley|2 months ago
yawaramin|2 months ago
xg15|2 months ago
Is that "cannot be extracted" from JS only, or is this an actual device-locked, TPM/SEP-bound key like passkeys?
If it is, it seems kind of like the buried lede to me that there is a browser API that lets any website built its own completely unstandardized quasi-passkey system and lock the key to the current device.
ajross|2 months ago
And likewise you as the app vendor can know the key was generated, and that it works, but you can't[1] know that it's actually locked to a device or that it's non-exportable. You could be running in a virtualized environment that logged everything.
Basically it's not really that useful. Which is sort of true for security hardware in general. It's great for the stuff the device vendors have wired up (which amounts to "secured boot", "identifying specific known devices" and "validating human user biometrics on a secured device"), but not really extensible in the way you'd want it to be.
[1] Within the bounds of this particular API, anyway. There may be some form of vendor signing you can use to e.g. verify that it was done on iOS or ChromeOS or some other fully-secured platform. I honestly don't know.
smallnix|2 months ago
gethly|2 months ago
Just to be clear, the PKCE secret can be the same for each initiation, but in the end its goal is to ensure that the first request matches with the last one. And yes, there is "plain" PKCE method but that is just for testing. SHA256 is the default one used to obfuscate the secret.
SahAssar|2 months ago
Of course if you trust the client (no bad browser extensions, updated browser) and have good TLS settings and no MITM risk and make sure the your IDs are single-use then it seems like that should be fine.
emadda|2 months ago
The code_challenge == sha256(code_verifier). You will share the code_challenge at the start of the flow.
ximm|2 months ago
I run an authentication server and requiring PKCE allows me to make sure that XSS protection is handled for all clients.
esseph|2 months ago
loloquwowndueo|2 months ago
start
(1) Copy / paste example_http_server into your LLM of choice (use a paid/good model). (2) Prompt: Implement the HTTP handlers here for my project,..
Um, no? How about you give me real instructions on how to do it? I’m not going to delegate a security-critical task to an LLM. And since I need to review it carefully myself anyway, I might as well write it all by hand, right? Like, the whole premise is I just need to implement a couple of webhooks.
gear54rus|2 months ago
emadda|2 months ago
The LLM is only for converting the JS based example code into your language X and HTTP framework Y (instead of giving example code for every combination of X and Y).
The standard implementation is in a single file `http_server.ts`, which is around 200 lines of well commented code, with important logic commented (around 5 lines). The example code can be run locally with a few commands.
The repo also contains a sequence diagram [1], a description of the HTTP handlers needed [2], and a live demo [3] where you can see the request/responses.
Thanks for your feedback I have made this clearer in the readme.
- [1] https://github.com/emadda/passkeybot/tree/master?tab=readme-...
- [2] https://github.com/emadda/passkeybot/tree/master?tab=readme-...
- [3] https://demo.enzom.dev/
PunchyHamster|2 months ago
I'm curious on why there would be any legitimate reason for that. Security wise it should not happen, it's just some implementations being crappy or some bad practice like reusing same passkey with different devices ?
js2|2 months ago
https://pocket-id.org/
wewewedxfgdf|2 months ago
WolfeReader|2 months ago
Example: this article.
boombapoom|2 months ago
spockz|2 months ago
A bit the same why although I love the keychain in macOS, it also makes me uncomfortable. Lose your phone and laptop in a theft or fire and you are locked out from your Apple account. Goodbye online presence.
AlotOfReading|2 months ago
unknown|2 months ago
[deleted]
EGreg|2 months ago
That is why you should ship a pristine HTML+CSS+JS environment that can use subtle web crypto. YOU show what is being signed. And then the device can sign its hash using the secure enclave.
And you CAN do attestation even on consumer devices, by using the Device or AppAttest framework (I think that’s what it’s called). I did it myself in our app. It does show up 100% of the time but when it does it’s useful.
PS: being the web3 / blockchain geek that I am, I will tell you stuff that triggers anticryptobros on HN.
The Web3 ecosystem already has a standard called EIP712 for signing structured data. If you want to stick to standards, use that!
The secure enclaves all use P-256 (sometimes called R-256) while Bitcoin / Web3 uses K-256 (the Koeblitz curve, they distrust the NIST curve or whatever).
So that means you’re going to have to use Abstract Accounts and the new precompiled smart contracts to verify P256 signatures, which only Binance Smart Chain and a handful of other chains have deployed. Luckily BSC is the most widely used chain by volume and has plenty of money sloshing around so you can build your trustless programs there. If you want to be totally trustless — LET THE SMART CONTRACTS GENERATE THE CHALLENGE TO BE SIGNED BY THE AUTHENTICATOR. Then have it sign the temporary k256 public key (from the keypair) to use, as long as your session is open you can then add use your private key to sign transactions. As usual, do this for small amounts per day, transactions that move larger amounts should still require use of multisig keys etc.)
QGQBGdeZREunxLe|2 months ago
emadda|2 months ago
tptacek|2 months ago