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