-
Notifications
You must be signed in to change notification settings - Fork 145
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
Passing opaque user data down into CredentialRepository
#289
Comments
Hm, these are good questions. Let's think about this for a bit. I suspect your needs could also be served by adding an optional First, some background. As of now, the The most important of these uses is the latter: setting So if we ignore Now if we again include Now, about your use case as the real-world example in question: you say that you indeed have no unique human-readable identifier. I presume this means there is no way for your users to first enter a user identifier into a form and then get prompted with the WebAuthn request as the second step - either you use passkey ("username-less") authentication exclusively, or you have some other, external way to discover a unique (but non-human-readable) user identifier. If those presumptions are correct, it seems to me like at least one of these is always true in your case:
So how about adding a method like this to /**
* Get the credential IDs of all credentials registered to the user with the given user handle.
*
* <p>The <i>user handle</i> is defined by the <code>
* {@link UserIdentity}.{@link UserIdentity.UserIdentityBuilder#id(ByteArray) id(ByteArray)}
* </code> setting in {@link StartRegistrationOptions}.
*
* <p>After a successful registration ceremony, the {@link RegistrationResult#getKeyId()} method
* returns a value suitable for inclusion in this set.
*
* @param userHandle the user handle of the user whose credential IDs to look up.
* @return a {@link Set} containing one {@link PublicKeyCredentialDescriptor} for each credential
* registered to the user with the given user handle.
*/
Set<PublicKeyCredentialDescriptor> getCredentialIdsForUserHandle(ByteArray userHandle); If so, I'll have to think a bit about how exactly to implement it. The simple approach is to give it a Another option is to make RelyingParty rp = RelyingParty.builder()
.identity(/* ... */)
.credentialRepository(new CredentialRepository() { /* ... */ })
// ...other settings
.build(); or with a new, stripped-down RelyingPartyWithoutUsernames rp = RelyingParty.builder()
.identity(/* ... */)
.credentialRepository(new CredentialRepositoryV2() {
Set<PublicKeyCredentialDescriptor> getCredentialIdsForUserHandle(ByteArray userHandle) { /* ... */ }
Optional<RegisteredCredential> lookup(ByteArray credentialId, ByteArray userHandle) { /* ... */ }
boolean credentialIdExists(ByteArray credentialId) { /* ... */ }
})
// ...other settings
.build();
rp.startAssertion(
StartAssertionOptionsV2.builder() // Note: new variant of StartAssertionOptions
.username("foo") // Compile error: method "username(String)" does not exist
.build()
); or with the new interface but also with support for usernames: RelyingParty rp = RelyingParty.builder()
.identity(/* ... */)
.credentialRepository(
new CredentialRepositoryV2() {
Set<PublicKeyCredentialDescriptor> getCredentialIdsForUserHandle(ByteArray userHandle) { /* ... */ }
Optional<RegisteredCredential> lookup(ByteArray credentialId, ByteArray userHandle) { /* ... */ }
boolean credentialIdExists(ByteArray credentialId) { /* ... */ }
},
new UsernameRepository() {
Optional<ByteArray> getUserHandleForUsername(String username) { /* ... */ }
Optional<String> getUsernameForUserHandle(ByteArray userHandle) { /* ... */ }
})
// ...other settings
.build();
rp.startAssertion(
StartAssertionOptions.builder()
.username("foo") // OK, method exists
.build()
); Then perhaps a future major version could drop the older variant in favor of the new ones. I think I like this idea, as it allows the implementer to pick the features they need while keeping most of the complexity encapsulated within And yes, this would certainly relate to #274 as well. This "type state" kind of design would also open some opportunities for type safe passthrough of auxiliary credential data as discussed in #274 (comment) and attestation data which tangentially relates to #279. With more than one use for that kind of design, I'm more inclined to extend the library in that direction. Sorry for the wall of text... 😅 What do you think? |
For our use case, we are exclusively concerned with The "internal username" suggestion I gave would cover our very narrow use case and leave the interface untouched, but if you are already open to extending the interface, I would offer the following suggestion instead: Extend To cover the |
In particular, the |
Continuing from #293 (comment):
Would it make sense to add this as an optional property in RelyingParty rp = /* ... */;
AssertionRequest request = /* ... */;
PublicKeyCredential</* ... */> response = /* ... */;
Set<RegisteredCredential> credentials = /* Credentials already looked up... */;
AssertionResult result =
rp.finishAssertion(
FinishAssertionOptions.builder()
.request(request)
.response(response)
.preloadCredentials(credentials)
.build()); Would that help your use case? (In addition to the |
Adding this kind of field to PS: For our specific use case, we wouldn't actually need this information in |
Interesting, my intuition says the opposite! 🙂 It also occurred to me that the ...actually, come to think of it: that's actually already supported, in a way. The library as a whole is designed with no internal mutable state, and one major reason for that is that this means Would that be an acceptable solution for your use case? (Again, apart from the redesign to better support username-less application.) If so, new library features might not actually be necessary. |
Hmm, yes, we could potentially make that work. Even though I understand the library guarantees you describe, it still doesn't feel like a clean solution; it feels like something that'd confuse a maintenance developer stumbling across my code in a few years' time. |
Hm, yeah, I can understand that. I've been considering back and forth whether to go ahead and add that method to override Anyway, see also my latest comment in #274 (comment) . The new, experimental |
Thanks for your patience and sorry for the delay - the experimental |
Sibling issue to #274, I suppose...
Is there a way to pass opaque data about the user to the
CredentialRepository
(in particular, thegetCredentialIdsForUsername
implementation) without that data being exposed to the user?For context, we have an internal unique user identifier which is not human readable. We do not have unique human-readable usernames. We set
.name()
to a non-unique self-descriptor for the user. It is impossible for us to map this self-descriptor back to a unique user for purposes ofgetCredentialIdsForUsername
.From an implementation point of view, I suppose this is complicated by
getCredentialIdsForUsername
receiving the username (in particular, the same.name
that is included in thePublicKeyCredentialCreationOptions
that's sent to the client) instead of the fullUser
object. Changing this would break backwards compatibility.One backwards-compatible way of solving this would be to provide an
@JsonIgnore Optional<String> internalUsername
field (and associated builder method) onUserIdentity
. InRelyingParty::startRegistration
, you could then passinternalUsername
togetCredentialIdsForUsername
if it is set, and otherwise fall back toname
.The text was updated successfully, but these errors were encountered: