add Login+2FA support for webauthn

This commit is contained in:
2025-12-18 21:45:11 +01:00
parent b4b2552e7e
commit 08d213e89a
5 changed files with 45 additions and 25 deletions
@@ -86,8 +86,7 @@ public class LocalAuthenticationProvider extends DaoAuthenticationProvider imple
} else {
for (AdditionalAuthenticationProvider<?> provider : providers) {
if (provider.supports(auth.getClass())) {
auth = provider.authenticate(auth);
return this.secondFactorCheck(auth);
return provider.authenticate(auth);
}
}
}
+1 -1
View File
@@ -23,7 +23,7 @@
<unit-api.version>2.2</unit-api.version>
<commons-csv.version>1.14.1</commons-csv.version>
<dnsjava.version>3.6.3</dnsjava.version>
<revision>4.0.0</revision>
<revision>4.0.1</revision>
</properties>
<parent>
@@ -253,12 +253,6 @@ public class WebAuthnManager implements UserDataProvider {
}
}
/**
* Creates a WebAuthn login request that does not depend on the username.
*
* This avoids username enumeration and allows resolving the user from the
* returned credentialId during the finish step.
*/
public String createLoginRequest() {
try {
StartAssertionOptions.StartAssertionOptionsBuilder optionsBuilder = StartAssertionOptions.builder()
@@ -281,21 +275,14 @@ public class WebAuthnManager implements UserDataProvider {
}
}
/**
* Validates a WebAuthn login assertion where the user is resolved from the
* credentialId contained in the assertion.
*
* @return the authenticated userId if successful, otherwise empty.
*/
public Optional<Long> validateLoginRequest(String assertionJson) {
try {
JsonNode assertionNode = objectMapper.readTree(assertionJson);
PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> pkc = PublicKeyCredential
.parseAssertionResponseJson(assertionJson);
ByteArray credentialId = pkc.getId();
WebAuthnCredential credential = credentialRepository.findByCredentialId(credentialId.getBase64Url());
if (credential == null || !credential.isEnabled() || credential.getUsage() != WebAuthnUsage.LOGIN) {
WebAuthnCredential credential = credentialRepository.findByCredentialId(pkc.getId().getBase64Url());
if (credential == null || !credential.isEnabled() || credential.getUsage() != WebAuthnUsage.LOGIN
&& credential.getUsage() != WebAuthnUsage.LOGIN_2FA) {
return Optional.empty();
}
@@ -337,12 +324,9 @@ public class WebAuthnManager implements UserDataProvider {
PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> pkc = PublicKeyCredential
.parseAssertionResponseJson(assertionJson);
WebAuthnCredential credential = credentialRepository.findByCredentialId(pkc.getId().getBase64Url());
ByteArray credentialId = pkc.getId();
WebAuthnCredential credential = credentialRepository.findByCredentialId(credentialId.getBase64Url());
if (credential == null || !credential.isEnabled()
|| (credential.getUsage() != usage)) {
if (credential == null || !credential.isEnabled() || (credential.getUsage() != usage)) {
return false;
}
@@ -382,6 +366,16 @@ public class WebAuthnManager implements UserDataProvider {
return false;
}
public WebAuthnCredential getCredentialForAssertionJson(String assertionJson) {
try {
PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> pkc = PublicKeyCredential
.parseAssertionResponseJson(assertionJson);
return credentialRepository.findByCredentialId(pkc.getId().getBase64Url());
} catch (Exception e) {
return null;
}
}
public ByteArray getUserHandle(Long userId) {
WebAuthnUserHandle webAuthnUserHandle = webAuthnUserHandleRepository.findByTarget(userId);
@@ -1,5 +1,5 @@
package de.bstly.we.webauthn.model;
public enum WebAuthnUsage {
NONE, TWO_FA, LOGIN
NONE, TWO_FA, LOGIN, LOGIN_2FA
}
@@ -8,14 +8,19 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.stereotype.Component;
import de.bstly.we.businesslogic.UserManager;
import de.bstly.we.model.User;
import de.bstly.we.security.LocalUserDetailsService;
import de.bstly.we.security.businesslogic.AdditionalAuthenticationProvider;
import de.bstly.we.security.businesslogic.SecondFactorProviderManager;
import de.bstly.we.webauthn.businesslogic.WebAuthnManager;
import de.bstly.we.webauthn.model.WebAuthnCredential;
import de.bstly.we.webauthn.model.WebAuthnUsage;
@Component
public class WebAuthnAuthenticationProvider implements AdditionalAuthenticationProvider<WebAuthnAuthenticationToken> {
@@ -26,6 +31,8 @@ public class WebAuthnAuthenticationProvider implements AdditionalAuthenticationP
private LocalUserDetailsService userDetailsService;
@Autowired
private WebAuthnManager webAuthnManager;
@Autowired
private SecondFactorProviderManager secondFactorProviderManager;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
@@ -48,6 +55,26 @@ public class WebAuthnAuthenticationProvider implements AdditionalAuthenticationP
UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());
List<GrantedAuthority> authorities = List.copyOf(userDetails.getAuthorities());
WebAuthnCredential credential = webAuthnManager.getCredentialForAssertionJson(assertionJson);
if (credential == null) {
throw new BadCredentialsException("User not authenticated");
}
if (credential.getUsage() != WebAuthnUsage.LOGIN
&& credential.getUsage() != WebAuthnUsage.LOGIN_2FA) {
throw new BadCredentialsException("User not authenticated");
}
if (credential.getUsage() != WebAuthnUsage.LOGIN_2FA
&& !secondFactorProviderManager.getEnabled(userId).isEmpty()) {
PreAuthenticatedAuthenticationToken newAuth = new PreAuthenticatedAuthenticationToken(userDetails, "",
AuthorityUtils.createAuthorityList("ROLE_PRE_AUTH_USER"));
newAuth.setAuthenticated(false);
return newAuth;
}
return new UsernamePasswordAuthenticationToken(userDetails, assertionJson, authorities);
}