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
@@ -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);
}