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 { } else {
for (AdditionalAuthenticationProvider<?> provider : providers) { for (AdditionalAuthenticationProvider<?> provider : providers) {
if (provider.supports(auth.getClass())) { if (provider.supports(auth.getClass())) {
auth = provider.authenticate(auth); return provider.authenticate(auth);
return this.secondFactorCheck(auth);
} }
} }
} }
+1 -1
View File
@@ -23,7 +23,7 @@
<unit-api.version>2.2</unit-api.version> <unit-api.version>2.2</unit-api.version>
<commons-csv.version>1.14.1</commons-csv.version> <commons-csv.version>1.14.1</commons-csv.version>
<dnsjava.version>3.6.3</dnsjava.version> <dnsjava.version>3.6.3</dnsjava.version>
<revision>4.0.0</revision> <revision>4.0.1</revision>
</properties> </properties>
<parent> <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() { public String createLoginRequest() {
try { try {
StartAssertionOptions.StartAssertionOptionsBuilder optionsBuilder = StartAssertionOptions.builder() 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) { public Optional<Long> validateLoginRequest(String assertionJson) {
try { try {
JsonNode assertionNode = objectMapper.readTree(assertionJson); JsonNode assertionNode = objectMapper.readTree(assertionJson);
PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> pkc = PublicKeyCredential PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> pkc = PublicKeyCredential
.parseAssertionResponseJson(assertionJson); .parseAssertionResponseJson(assertionJson);
WebAuthnCredential credential = credentialRepository.findByCredentialId(pkc.getId().getBase64Url());
ByteArray credentialId = pkc.getId(); if (credential == null || !credential.isEnabled() || credential.getUsage() != WebAuthnUsage.LOGIN
WebAuthnCredential credential = credentialRepository.findByCredentialId(credentialId.getBase64Url()); && credential.getUsage() != WebAuthnUsage.LOGIN_2FA) {
if (credential == null || !credential.isEnabled() || credential.getUsage() != WebAuthnUsage.LOGIN) {
return Optional.empty(); return Optional.empty();
} }
@@ -337,12 +324,9 @@ public class WebAuthnManager implements UserDataProvider {
PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> pkc = PublicKeyCredential PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> pkc = PublicKeyCredential
.parseAssertionResponseJson(assertionJson); .parseAssertionResponseJson(assertionJson);
WebAuthnCredential credential = credentialRepository.findByCredentialId(pkc.getId().getBase64Url());
ByteArray credentialId = pkc.getId(); if (credential == null || !credential.isEnabled() || (credential.getUsage() != usage)) {
WebAuthnCredential credential = credentialRepository.findByCredentialId(credentialId.getBase64Url());
if (credential == null || !credential.isEnabled()
|| (credential.getUsage() != usage)) {
return false; return false;
} }
@@ -382,6 +366,16 @@ public class WebAuthnManager implements UserDataProvider {
return false; 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) { public ByteArray getUserHandle(Long userId) {
WebAuthnUserHandle webAuthnUserHandle = webAuthnUserHandleRepository.findByTarget(userId); WebAuthnUserHandle webAuthnUserHandle = webAuthnUserHandleRepository.findByTarget(userId);
@@ -1,5 +1,5 @@
package de.bstly.we.webauthn.model; package de.bstly.we.webauthn.model;
public enum WebAuthnUsage { 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.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import de.bstly.we.businesslogic.UserManager; import de.bstly.we.businesslogic.UserManager;
import de.bstly.we.model.User; import de.bstly.we.model.User;
import de.bstly.we.security.LocalUserDetailsService; import de.bstly.we.security.LocalUserDetailsService;
import de.bstly.we.security.businesslogic.AdditionalAuthenticationProvider; 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.businesslogic.WebAuthnManager;
import de.bstly.we.webauthn.model.WebAuthnCredential;
import de.bstly.we.webauthn.model.WebAuthnUsage;
@Component @Component
public class WebAuthnAuthenticationProvider implements AdditionalAuthenticationProvider<WebAuthnAuthenticationToken> { public class WebAuthnAuthenticationProvider implements AdditionalAuthenticationProvider<WebAuthnAuthenticationToken> {
@@ -26,6 +31,8 @@ public class WebAuthnAuthenticationProvider implements AdditionalAuthenticationP
private LocalUserDetailsService userDetailsService; private LocalUserDetailsService userDetailsService;
@Autowired @Autowired
private WebAuthnManager webAuthnManager; private WebAuthnManager webAuthnManager;
@Autowired
private SecondFactorProviderManager secondFactorProviderManager;
@Override @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
@@ -48,6 +55,26 @@ public class WebAuthnAuthenticationProvider implements AdditionalAuthenticationP
UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername()); UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());
List<GrantedAuthority> authorities = List.copyOf(userDetails.getAuthorities()); 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); return new UsernamePasswordAuthenticationToken(userDetails, assertionJson, authorities);
} }