Skip to content

Commit

Permalink
FS#22640 - asit-asso#255 - Implémenter la double authentification
Browse files Browse the repository at this point in the history
  • Loading branch information
arxit-ygr committed Mar 19, 2024
1 parent d739a82 commit 63f3251
Show file tree
Hide file tree
Showing 36 changed files with 2,593 additions and 56 deletions.
16 changes: 13 additions & 3 deletions extract/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<version>2.7.17</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

Expand Down Expand Up @@ -204,19 +204,29 @@
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.12.1</version>
<version>4.14.1</version>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.4.1</version>
<version>5.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.aerogear</groupId>
<artifactId>aerogear-otp-java</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.j256.two-factor-auth</groupId>
<artifactId>two-factor-auth</artifactId>
<version>1.3</version>
</dependency>
</dependencies>

<profiles>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import ch.asit_asso.extract.domain.RecoveryCode;
import org.apache.commons.lang3.StringUtils;
import ch.asit_asso.extract.domain.User;
import ch.asit_asso.extract.domain.User.Profile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;


Expand Down Expand Up @@ -62,6 +64,14 @@ public class ApplicationUser implements UserDetails {
*/
private final List<GrantedAuthority> rolesList;

private final boolean twoFactorForced;

private final User.TwoFactorStatus twoFactorStatus;

private final String twoFactorActiveToken;

private final String twoFactorStandbyToken;

/**
* The number that uniquely identifies this user in the application.
*/
Expand All @@ -73,6 +83,9 @@ public class ApplicationUser implements UserDetails {
private final String userName;


//private final Collection<RecoveryCode> recoveryCodes;



/**
* Creates a new instance of an application user based on a user data object.
Expand All @@ -91,6 +104,11 @@ public ApplicationUser(final User domainUser) {
this.userName = domainUser.getLogin();
this.passwordHash = domainUser.getPassword();
this.isActive = domainUser.isActive();
this.twoFactorForced = domainUser.isTwoFactorForced();
this.twoFactorStatus = domainUser.getTwoFactorStatus();
this.twoFactorActiveToken = domainUser.getTwoFactorToken();
this.twoFactorStandbyToken = domainUser.getTwoFactorStandbyToken();
//this.recoveryCodes = domainUser.getTwoFactorRecoveryCodesCollection();
this.rolesList = this.buildRolesList(domainUser);
}

Expand Down Expand Up @@ -154,6 +172,23 @@ public final String getUsername() {



public final boolean isTwoFactorForced() { return this.twoFactorForced; }



public final User.TwoFactorStatus getTwoFactorStatus() { return this.twoFactorStatus; }



public String getTwoFactorActiveToken() { return this.twoFactorActiveToken; }



//public Collection<RecoveryCode> getTwoFactorRecoveryCodes() { return this.recoveryCodes; }


public String getTwoFactorStandbyToken() { return twoFactorStandbyToken; }

/**
* Checks if this user has been granted a given permission.
*
Expand Down Expand Up @@ -246,11 +281,16 @@ private List<GrantedAuthority> buildRolesList(final User domainUser) {
list.add(new ApplicationUserRole(userProfile));
}

if (domainUser.getTwoFactorStatus() == User.TwoFactorStatus.ACTIVE) {
list.add(new SimpleGrantedAuthority("CAN_AUTHENTICATE_2FA"));
} else if (domainUser.isTwoFactorForced() || domainUser.getTwoFactorStatus() == User.TwoFactorStatus.STANDBY) {
list.add(new SimpleGrantedAuthority("CAN_REGISTER_2FA"));
}

if (list.isEmpty()) {
this.logger.warn("No profile found for user {}.", userLogin);
}

return list;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,115 @@
package ch.asit_asso.extract.authentication;

import java.io.IOException;
import java.util.Optional;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import ch.asit_asso.extract.authentication.twofactor.TwoFactorAuthenticationHandler;
import ch.asit_asso.extract.authentication.twofactor.TwoFactorRememberMe;
import ch.asit_asso.extract.domain.User;
import ch.asit_asso.extract.persistence.RememberMeTokenRepository;
import ch.asit_asso.extract.persistence.UsersRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.DefaultSavedRequest;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;

import org.springframework.stereotype.Component;


/**
* Interceptor that carries actions after a successful authentication to the Extract application.
*
* @author Yves Grasset
*/
@Component
public class ExtractAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

/**
* The writer to the application logs.
*/
private final Logger logger = LoggerFactory.getLogger(ExtractAuthenticationSuccessHandler.class);

private final PasswordEncoder passwordEncoder;

private final RememberMeTokenRepository rememberMeRepository;

private final UsersRepository usersRepository;



public ExtractAuthenticationSuccessHandler(PasswordEncoder encoder, RememberMeTokenRepository tokenRepository,
UsersRepository usersRepository) {
this.passwordEncoder = encoder;
this.rememberMeRepository = tokenRepository;
this.usersRepository = usersRepository;
}


/**
* {@inheritDoc}
*/
@Override
public final void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response,
final Authentication authentication) throws ServletException, IOException {
final Authentication authentication) throws ServletException, IOException {
this.logger.debug("Successful username / login authentication.");
ApplicationUser user = (ApplicationUser) authentication.getPrincipal();
Integer userId = user.getUserId();
this.logger.debug("Looking for Extract user with ID {}.", userId);
this.logger.debug("Users repository is {}", this.usersRepository);
Optional<User> domainUserResult = this.usersRepository.findById(userId);

if (domainUserResult.isEmpty()) {
this.logger.error("Could not find domain user with ID {}, even though user / password succeeded. This is not normal.", userId);
throw new IllegalStateException(String.format("No user found in repository with ID %d", userId));
}

User domainUser = domainUserResult.get();
String userName = domainUser.getLogin();
this.logger.debug("Found user {}.", userName);
TwoFactorRememberMe rememberMeUser = new TwoFactorRememberMe(domainUser, this.rememberMeRepository,
this.passwordEncoder);
rememberMeUser.cleanUp();

switch (user.getTwoFactorStatus()) {

case INACTIVE -> {
this.logger.debug("2FA for user {} is inactive. Processing to Extract.", userName);
this.goToApplication(request, response, authentication);
}

case STANDBY -> {
this.logger.debug("2FA for user {} is in standby. Processing to the 2FA registration page.", userName);
request.getSession().setAttribute("2faStep", "REGISTER");
new SimpleUrlAuthenticationSuccessHandler("/2fa/register")
.onAuthenticationSuccess(request, response, authentication);
}

case ACTIVE -> {
this.logger.debug("2FA for user {} is active.", userName);

if (rememberMeUser.hasValidToken(request)) {
this.logger.debug("The 2FA token found in the cookie is valid. Bypassing 2FA authentication and processing to Extract.");
this.goToApplication(request, response, authentication);
break;
}

this.logger.debug("No valid remember-me cookie found for the user. Processing to the 2FA authentication page.");
new TwoFactorAuthenticationHandler("/2fa/authenticate")
.onAuthenticationSuccess(request, response, authentication);
}

default ->
throw new IllegalStateException(String.format("Invalid 2FA status: %s", user.getTwoFactorStatus()));
}
}

private void goToApplication(final HttpServletRequest request, final HttpServletResponse response,
final Authentication authentication) throws IOException, ServletException {
DefaultSavedRequest savedRequest
= (DefaultSavedRequest) new HttpSessionRequestCache().getRequest(request, response);

Expand All @@ -62,11 +139,10 @@ public final void onAuthenticationSuccess(final HttpServletRequest request, fina

if (request.getContextPath().equals(savedRequest.getRequestURI())) {
this.logger.debug("The saved request is the application base URL with no trailing slash. Redirecting to"
+ " the home page.");
+ " the home page.");
this.getRedirectStrategy().sendRedirect(request, response, this.getDefaultTargetUrl());
}

super.onAuthenticationSuccess(request, response, authentication);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package ch.asit_asso.extract.authentication.twofactor;

import javax.validation.constraints.NotNull;
import ch.asit_asso.extract.domain.User;
import com.j256.twofactorauth.TimeBasedOneTimePasswordUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.encrypt.BytesEncryptor;

public class TwoFactorApplication {

private final Logger logger = LoggerFactory.getLogger(TwoFactorApplication.class);

private final BytesEncryptor encryptor;

private final TwoFactorService service;

private final User user;

private enum TokenType {
ACTIVE,
STANDBY
}


public TwoFactorApplication(@NotNull User user, @NotNull BytesEncryptor bytesEncryptor,
@NotNull TwoFactorService twoFactorService) {
this.encryptor = bytesEncryptor;
this.service = twoFactorService;
this.user = user;
}



public boolean authenticate(@NotNull String code) {

return this.service.check(this.getToken(TokenType.ACTIVE), code);
}



public User.TwoFactorStatus cancelEnabling() {
this.user.setTwoFactorStandbyToken(null);
User.TwoFactorStatus newStatus;

if (this.user.getTwoFactorToken() == null) {
this.logger.debug("2FA registration canceled. No active 2FA token for the user so 2FA status returned to INACTIVE.");
newStatus = User.TwoFactorStatus.INACTIVE;

} else {
this.logger.debug("2FA registration canceled. There is an active 2FA token for the user so 2FA status returned to ACTIVE.");
newStatus = User.TwoFactorStatus.ACTIVE;
}

this.user.setTwoFactorStatus(newStatus);
return newStatus;
}


public void disable() {
this.user.setTwoFactorToken(null);
this.user.setTwoFactorStandbyToken(null);
}



public void enable() {
String standbyToken = TimeBasedOneTimePasswordUtil.generateBase32Secret();
String encryptedStandbyToken = new String(Hex.encode(encryptor.encrypt(standbyToken.getBytes())));
this.user.setTwoFactorStandbyToken(encryptedStandbyToken);
}



public String getQrCodeUrl() {

return TimeBasedOneTimePasswordUtil.qrImageUrl(String.format("Extract:%s", this.user.getLogin()),
this.getToken(TokenType.STANDBY));
}



public String getStandbyToken() {

return this.getToken(TokenType.STANDBY);
}



public boolean validateRegistration(@NotNull String code) {

if (!this.service.check(this.getToken(TokenType.STANDBY), code)) {
return false;
}

this.user.setTwoFactorToken(this.user.getTwoFactorStandbyToken());
this.user.setTwoFactorStandbyToken(null);
this.user.setTwoFactorStatus(User.TwoFactorStatus.ACTIVE);

return true;
}



private String getToken(TokenType secretType) {
this.logger.debug("Getting {} token for user {}", secretType.name(), user.getLogin());
String encryptedToken = (secretType == TokenType.STANDBY) ? this.user.getTwoFactorStandbyToken()
: this.user.getTwoFactorToken();
byte[] bytes = Hex.decode(encryptedToken);
return new String(this.encryptor.decrypt(bytes));
}
}
Loading

0 comments on commit 63f3251

Please sign in to comment.