I upgraded from Spring Security 6.3 to 6.4. I was using Yubico's WebAuthn, and I am in the process of trying to switch to Spring Security's WebAuthn. However, Spring Security WebAuthn is missing support for registration by an anonymous user. It is a blocker for switching.
Expected Behavior
WebAuthn L1 (2019) and L2 (2021) specifications support registration of a credential by an anonymous user. If the user doesn't exist, then registration is supposed to create the account before associating the credential to it.
Current Behavior
Visiting /webauthn/registration and /webauthn/registration/options fails due to the implementation looking at request.getRemoteUser(), and returning an error if found to be null.
Context
WebAuthn L2 Specification => https://www.w3.org/TR/2021/REC-webauthn-2-20210408
Subsection 1.3.1. Registration specifically says Or the user may be in the process of creating a new account. It is the last sentence from this excerpt.
The user visits example.com, which serves up a script.
At this point, the user may already be logged in using a legacy username and password, or additional authenticator, or other means acceptable to the [Relying Party](https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#relying-party).
Or the user may be in the process of creating a new account.
Example 1
Yubico's demo website https://webauthn.io/ shows how registration by anonymous user is supposed to work. Note, as the user, can choose between two WebAuthn registration types (Non-Resident vs Resident) under Advanced Settings via this setting.
Discoverable Credential:
1. Discouraged (Client wants Non-Resident/Non-Discoverable)
2. Preferred (Client wants Resident/Discoverable, but fallback to Non-Resident/Non-Discoverable is OK)
3. Required (Client wants Resident/Discoverable)
Passkeys is an alias for Resident/Discoverable added in the L2 spec, but the spec is backwards compatible with Non-Resident/Non-Discoverable.
Example 2
Yubico offers a Java WebAuthn Server. It comes with a demo you can run yourself and debug. It supports credential registration by an anonymous user too. - https://github.com/Yubico/java-webauthn-server
Example 3
I used Yubico's WebAuthn Server with Spring Security 6.3 in my own project. - Backend: Spring Boot 3.3 + Spring Security 6.3 + Yubico Java WebAuthn Server - Frontend: https://github.com/justincranford/springs/blob/dev/springs-server-webauthn/src/main/resources/static/index.html
It is a new project, only WebAuthn registration and authentication are supported, and there are no other "legacy" authentication methods. Anonymous registration works. In this screenshot, you can see I used Google Chrome. Chrome's Developer Tools supports WebAuthn virtual authenticators for testing, and you can see I registered multiple Non-Resident and Resident credentials.
Comment From: rwinch
Response
You are correct that WebAuthnRegistrationFilter the user to be authenticated. As you are aware, this is critical to validate that the passkey is only associated to its owner.
When a new user is created we can validate that the passkey is only associated to its owner by ensuring that the user does not exist yet and creating the account at the same time the passkey is associated. The problem for Spring Security, is that it is unlikely to know how to perform the validation in this case. Most real applications customize how a user is created (e.g. where it is persisted) and the properties on the account. For this reason, WebAuthnRegistrationFilter does not (and I expect it will never) support registering a passkey for an unauthenticated user.
If you have ideas on how to make this possible, I'm open to hearing it.
How to Register passkeys for an unauthenticated user
The good news is that there are at least two ways you can register a passkey for an unauthenticated user. The first way is probably the easiest, but the second one is preferred due to some downsides to the first option.
Split Registration into Multiple Steps
You can split registration into multiple steps. This method is probably the easiest, but it has some drawbacks that I'll mention at the end.
First you create the user and then mark the user as authenticated. You can mark the user as authenticated using something like this:
// You could also create a custom Authentication like NewAccountAuthentication
Authentication authentication = new TestingAuthenticationToken(newUsernameFromRegistration, null, "ROLE_USER");
SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
On the next page you would register as you normally would with the build in WebAuthnRegistrationFilter.
The downside to this approach is that if a user performs the first step and then the HttpSession expires, then the user account is lost if you are only supporting passkeys for authentication.
Create Your Own Controller
The second option is a little more work, but it is more robust. In this option, you would create your own endpoints (controllers). This is similar to the Yubico demo which implements its own endpoints. As far as I know creating custom endpoints (and a CredentialRepository) is required in all cases when using the Yubico library.
The first endpoint will be an endpoint to create the options. For this, you can copy/modify the code in the PublicKeyCredentialCreationOptionsFilter. This will be simplified a bit if you implement it in a Controller since the conversions can be done automatically by Spring MVC.
The second endpoint will be your registration endpoint. It first performs the validation of the user and then creates the user and registers the passkey. You are able to reuse the logic in the WebAuthnRegistrationFilter. This is simplified a bit by the fact that Spring MVC can convert the request into your object types automatically for you.
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: justincranford
If I understand correctly, the concern is about preventing hijacking the UserEntity.name? 1. If UserEntity.name doesn't exist, anonymous user can register both UserEntity and Credential. 2. If UserEntity.name exists, anonymous user can't register a Credential; that would hijack the username.
Comment From: justincranford
When a registration request is submitted, I think of it as registering two things: 1. Register UserEntity (i.e. id, name, displayName) - if UserEntity.name doesn't exist 2. Register Credential - if Credential doesn't exist
I think WebAuthnRegistrationFilter only registers Credential. Potentially, it could be updated to register both? Or, add a new WebAuthnUserEntityRegistrationFilter that executes before WebAuthnRegistrationFilter?
You said:
Most real applications customize how a user is created
I think I understand. They can add custom filters, AuthenticationProviders, etc. There is no way for Spring Security to know where to look.
To fix that, give Developers a hook in the UserEntity registration flow. 1. UserEntity check: a) If request is authenticated, and authentication principal == UserEntity.name, then proceed to Credential. b) If request is unauthenticated, reject the request.
If Developers can override 1b: 1. They can check if UserEntity.name is not claimed in all of their custom user repositories 2. And, they can choose to insert UserEntity.name into their custom user repositories, with defaults they choose.