1717package org .springframework .security .web .webauthn .registration ;
1818
1919import java .io .IOException ;
20+ import java .util .function .Supplier ;
2021
2122import jakarta .servlet .FilterChain ;
2223import jakarta .servlet .ServletException ;
3536import org .springframework .http .converter .json .JacksonJsonHttpMessageConverter ;
3637import org .springframework .http .server .ServletServerHttpRequest ;
3738import org .springframework .http .server .ServletServerHttpResponse ;
39+ import org .springframework .security .authorization .AuthorizationManager ;
40+ import org .springframework .security .authorization .AuthorizationResult ;
41+ import org .springframework .security .authorization .SingleResultAuthorizationManager ;
42+ import org .springframework .security .core .Authentication ;
43+ import org .springframework .security .core .context .SecurityContextHolder ;
44+ import org .springframework .security .core .context .SecurityContextHolderStrategy ;
3845import org .springframework .security .web .servlet .util .matcher .PathPatternRequestMatcher ;
3946import org .springframework .security .web .util .matcher .RequestMatcher ;
4047import org .springframework .security .web .webauthn .api .Bytes ;
@@ -88,6 +95,9 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
8895
8996 private final UserCredentialRepository userCredentials ;
9097
98+ private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
99+ .getContextHolderStrategy ();
100+
91101 private HttpMessageConverter <Object > converter = new JacksonJsonHttpMessageConverter (
92102 JsonMapper .builder ().addModule (new WebauthnJacksonModule ()).build ());
93103
@@ -99,6 +109,9 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
99109 private RequestMatcher removeCredentialMatcher = PathPatternRequestMatcher .withDefaults ()
100110 .matcher (HttpMethod .DELETE , "/webauthn/register/{id}" );
101111
112+ private AuthorizationManager <Bytes > deleteCredentialAuthorizationManager = SingleResultAuthorizationManager
113+ .denyAll ();
114+
102115 public WebAuthnRegistrationFilter (UserCredentialRepository userCredentials ,
103116 WebAuthnRelyingPartyOperations rpOptions ) {
104117 Assert .notNull (userCredentials , "userCredentials must not be null" );
@@ -133,6 +146,42 @@ public void setRemoveCredentialMatcher(RequestMatcher removeCredentialMatcher) {
133146 this .removeCredentialMatcher = removeCredentialMatcher ;
134147 }
135148
149+ /**
150+ * Sets the {@link AuthorizationManager} used to authorize the delete credential
151+ * operation. The object being authorized is the credential id as {@link Bytes}. By
152+ * default, all delete requests are denied.
153+ *
154+ * <p>
155+ * Per the <a href="https://www.w3.org/TR/webauthn-3/#credential-id">WebAuthn
156+ * specification</a>, a credential id must contain at least 16 bytes with at least 100
157+ * bits of entropy, making it practically unguessable. The specification also advises
158+ * that credential ids should be kept private, as exposing them can leak personally
159+ * identifying information (see
160+ * <a href="https://www.w3.org/TR/webauthn-3/#sctn-credential-id-privacy-leak">§
161+ * 14.6.3 Privacy leak via credential IDs</a>). This {@link AuthorizationManager} is
162+ * therefore intended as defense in depth: even if a credential id were somehow
163+ * exposed, an unauthorized user could not delete another user's credential.
164+ * @param deleteCredentialAuthorizationManager the {@link AuthorizationManager} to use
165+ * @since 6.5.10
166+ */
167+ public void setDeleteCredentialAuthorizationManager (
168+ AuthorizationManager <Bytes > deleteCredentialAuthorizationManager ) {
169+ Assert .notNull (deleteCredentialAuthorizationManager , "deleteCredentialAuthorizationManager cannot be null" );
170+ this .deleteCredentialAuthorizationManager = deleteCredentialAuthorizationManager ;
171+ }
172+
173+ /**
174+ * Sets the {@link SecurityContextHolderStrategy} to use. The default is
175+ * {@link SecurityContextHolder#getContextHolderStrategy()}.
176+ * @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to
177+ * use
178+ * @since 6.5.10
179+ */
180+ public void setSecurityContextHolderStrategy (SecurityContextHolderStrategy securityContextHolderStrategy ) {
181+ Assert .notNull (securityContextHolderStrategy , "securityContextHolderStrategy cannot be null" );
182+ this .securityContextHolderStrategy = securityContextHolderStrategy ;
183+ }
184+
136185 @ Override
137186 protected void doFilterInternal (HttpServletRequest request , HttpServletResponse response , FilterChain filterChain )
138187 throws ServletException , IOException {
@@ -204,7 +253,15 @@ private void registerCredential(HttpServletRequest request, HttpServletResponse
204253
205254 private void removeCredential (HttpServletRequest request , HttpServletResponse response , @ Nullable String id )
206255 throws IOException {
207- this .userCredentials .delete (Bytes .fromBase64 (id ));
256+ Bytes credentialId = Bytes .fromBase64 (id );
257+ Supplier <Authentication > authentication = () -> this .securityContextHolderStrategy .getContext ()
258+ .getAuthentication ();
259+ AuthorizationResult result = this .deleteCredentialAuthorizationManager .authorize (authentication , credentialId );
260+ if (result != null && !result .isGranted ()) {
261+ response .setStatus (HttpStatus .FORBIDDEN .value ());
262+ return ;
263+ }
264+ this .userCredentials .delete (credentialId );
208265 response .setStatus (HttpStatus .NO_CONTENT .value ());
209266 }
210267
0 commit comments