Skip to content

Commit d891f15

Browse files
committed
Implement Multi-Factor Authentication (MFA) feature
1 parent c46ffa4 commit d891f15

14 files changed

+666
-16
lines changed

pom.xml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,24 @@
136136
<artifactId>spring-data-commons</artifactId>
137137
</dependency>
138138

139-
</dependencies>
139+
<!-- TOTP MFA Dependencies -->
140+
<dependency>
141+
<groupId>dev.samstevens.totp</groupId>
142+
<artifactId>totp</artifactId>
143+
<version>1.7.1</version>
144+
</dependency>
145+
<dependency>
146+
<groupId>com.google.zxing</groupId>
147+
<artifactId>core</artifactId>
148+
<version>3.5.1</version>
149+
</dependency>
150+
<dependency>
151+
<groupId>com.google.zxing</groupId>
152+
<artifactId>javase</artifactId>
153+
<version>3.5.1</version>
154+
</dependency>
155+
156+
</dependencies>
140157

141158
<build>
142159
<plugins>

src/main/java/net/codejava/AppController.java

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.beans.factory.annotation.Autowired;
99
import org.springframework.beans.factory.annotation.Value;
1010
import org.springframework.data.domain.Pageable;
11+
import org.springframework.security.core.Authentication;
1112
import org.springframework.security.core.context.SecurityContextHolder;
1213
import org.springframework.stereotype.Controller;
1314
import org.springframework.ui.Model;
@@ -28,7 +29,6 @@
2829
import org.springframework.security.authentication.AuthenticationManager;
2930
import org.springframework.security.authentication.BadCredentialsException;
3031
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
31-
import org.springframework.security.core.Authentication;
3232
import org.springframework.web.bind.annotation.RequestParam;
3333
import org.springframework.data.domain.PageRequest; // Add this import statement
3434
import org.springframework.data.domain.Page; // Add this import statement
@@ -65,6 +65,9 @@ public class AppController {
6565
@Autowired
6666
private AuthenticationManager authenticationManager;
6767

68+
@Autowired
69+
private UserRepository userRepository;
70+
6871
// private static final Logger logger = Logger.getLogger(AppController.class.getName());
6972

7073
@Value("${enableSearchFeature}")
@@ -74,6 +77,18 @@ public boolean getEnableSearchFeature() {
7477
return this.enableSearchFeature;
7578
}
7679

80+
// Add MFA status to all pages that need it
81+
private void addUserInfoToModel(Model model, Principal principal) {
82+
if (principal != null) {
83+
String username = principal.getName();
84+
User user = userRepository.findByUsername(username);
85+
if (user != null) {
86+
model.addAttribute("mfaEnabled", user.isMfaEnabled());
87+
model.addAttribute("username", username);
88+
}
89+
}
90+
}
91+
7792
private String handleSale(Sale sale, HttpSession session, RedirectAttributes redirectAttributes, Runnable action) {
7893
sale.setEditing(true); // set isEditing to true
7994
action.run();
@@ -89,7 +104,7 @@ private String handleSale(Sale sale, HttpSession session, RedirectAttributes red
89104
}
90105

91106
@RequestMapping("/")
92-
public String viewHomePage(Model model , Principal principal, @RequestParam(defaultValue = "0") int page, HttpSession session) {
107+
public String viewHomePage(Model model, Principal principal, @RequestParam(defaultValue = "0") int page, HttpSession session) {
93108
String lastSearchQuery = (String) session.getAttribute("lastSearchQuery");
94109
if (lastSearchQuery != null && !lastSearchQuery.isEmpty()) {
95110
session.setAttribute("lastSearchQuery", null); // set lastSearchQuery to null
@@ -103,37 +118,67 @@ public String viewHomePage(Model model , Principal principal, @RequestParam(defa
103118
model.addAttribute("listSale", salePage.getContent());
104119
model.addAttribute("currentPage", page);
105120
model.addAttribute("totalPages", salePage.getTotalPages());
121+
122+
// Add user info including MFA status
123+
addUserInfoToModel(model, principal);
124+
106125
return "index";
107126
}
108127

109128
@RequestMapping("/new")
110-
public ModelAndView showNewForm() {
129+
public ModelAndView showNewForm(Principal principal) {
111130
ModelAndView mav = new ModelAndView("new_form");
112131
Sale sale = new Sale();
113132
mav.addObject("sale", sale);
114133
mav.addObject("currentDate", LocalDate.now());
115134
mav.addObject("enableSearchFeature", enableSearchFeature);
135+
136+
// Add user info including MFA status
137+
if (principal != null) {
138+
String username = principal.getName();
139+
User user = userRepository.findByUsername(username);
140+
if (user != null) {
141+
mav.addObject("mfaEnabled", user.isMfaEnabled());
142+
mav.addObject("username", username);
143+
}
144+
}
145+
116146
return mav;
117147
}
118148

119149
@RequestMapping("/edit/{serialNumber}")
120-
public ModelAndView showEditForm(@PathVariable(name = "serialNumber") String serialNumber) {
150+
public ModelAndView showEditForm(@PathVariable(name = "serialNumber") String serialNumber, Principal principal) {
121151
ModelAndView mav = new ModelAndView("edit_form");
122152
Sale sale = dao.get(serialNumber);
123153
sale.setEditing(true);
124154
mav.addObject("sale", sale);
125155
mav.addObject("enableSearchFeature", enableSearchFeature);
156+
157+
// Add user info including MFA status
158+
if (principal != null) {
159+
String username = principal.getName();
160+
User user = userRepository.findByUsername(username);
161+
if (user != null) {
162+
mav.addObject("mfaEnabled", user.isMfaEnabled());
163+
mav.addObject("username", username);
164+
}
165+
}
166+
126167
return mav;
127168
}
128169

129170
@RequestMapping("/search")
130-
public String search(@ModelAttribute("q") String query, Model model, HttpSession session) {
171+
public String search(@ModelAttribute("q") String query, Model model, HttpSession session, Principal principal) {
131172
List<Sale> listSale = dao.search(query);
132173
model.addAttribute("listSale", listSale);
133174

134175
boolean enableSearchFeature = true;
135176
model.addAttribute("enableSearchFeature", enableSearchFeature);
136177
session.setAttribute("lastSearchQuery", query); // save the last search query in the session
178+
179+
// Add user info including MFA status
180+
addUserInfoToModel(model, principal);
181+
137182
return "search";
138183
}
139184

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package net.codejava;
2+
3+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
4+
import org.springframework.security.core.GrantedAuthority;
5+
6+
import java.util.Collection;
7+
8+
public class MfaAuthentication extends UsernamePasswordAuthenticationToken {
9+
10+
private boolean mfaAuthenticated;
11+
private User user;
12+
13+
public MfaAuthentication(User user, Object credentials,
14+
Collection<? extends GrantedAuthority> authorities,
15+
boolean mfaAuthenticated) {
16+
super(user.getUsername(), credentials, authorities);
17+
this.user = user;
18+
this.mfaAuthenticated = mfaAuthenticated;
19+
}
20+
21+
public boolean isMfaAuthenticated() {
22+
return mfaAuthenticated;
23+
}
24+
25+
public void setMfaAuthenticated(boolean mfaAuthenticated) {
26+
this.mfaAuthenticated = mfaAuthenticated;
27+
}
28+
29+
public User getUser() {
30+
return user;
31+
}
32+
33+
@Override
34+
public boolean isAuthenticated() {
35+
// Only consider the authentication fully complete if MFA has been verified
36+
// for users who have MFA enabled
37+
if (user.isMfaEnabled()) {
38+
return super.isAuthenticated() && mfaAuthenticated;
39+
}
40+
return super.isAuthenticated();
41+
}
42+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package net.codejava;
2+
3+
import org.springframework.beans.factory.annotation.Autowired;
4+
import org.springframework.security.core.Authentication;
5+
import org.springframework.security.core.context.SecurityContextHolder;
6+
import org.springframework.stereotype.Component;
7+
import org.springframework.web.filter.OncePerRequestFilter;
8+
9+
import javax.servlet.FilterChain;
10+
import javax.servlet.ServletException;
11+
import javax.servlet.http.HttpServletRequest;
12+
import javax.servlet.http.HttpServletResponse;
13+
import javax.servlet.http.HttpSession;
14+
import java.io.IOException;
15+
16+
@Component
17+
public class MfaAuthenticationFilter extends OncePerRequestFilter {
18+
19+
private static final String MFA_VERIFICATION_URL = "/mfa/verify";
20+
private static final String MFA_SETUP_URL = "/mfa/setup";
21+
private static final String LOGIN_URL = "/login";
22+
23+
@Override
24+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
25+
throws ServletException, IOException {
26+
27+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
28+
HttpSession session = request.getSession();
29+
30+
// Skip the filter for login and MFA-related URLs
31+
String requestURI = request.getRequestURI();
32+
if (requestURI.equals(LOGIN_URL) || requestURI.startsWith(MFA_VERIFICATION_URL) ||
33+
requestURI.startsWith(MFA_SETUP_URL) || requestURI.startsWith("/css/") ||
34+
requestURI.startsWith("/js/")) {
35+
filterChain.doFilter(request, response);
36+
return;
37+
}
38+
39+
// Check if the user is authenticated with MFA
40+
if (authentication instanceof MfaAuthentication) {
41+
MfaAuthentication mfaAuthentication = (MfaAuthentication) authentication;
42+
43+
// If MFA is needed but not yet verified
44+
if (mfaAuthentication.getUser().isMfaEnabled() && !mfaAuthentication.isMfaAuthenticated()) {
45+
// Store the requested URL in the session
46+
session.setAttribute("REQUESTED_URL", requestURI);
47+
48+
// Redirect to MFA verification page
49+
response.sendRedirect(MFA_VERIFICATION_URL);
50+
return;
51+
}
52+
}
53+
54+
filterChain.doFilter(request, response);
55+
}
56+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package net.codejava;
2+
3+
import org.springframework.beans.factory.annotation.Autowired;
4+
import org.springframework.security.authentication.AuthenticationProvider;
5+
import org.springframework.security.authentication.BadCredentialsException;
6+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
7+
import org.springframework.security.core.Authentication;
8+
import org.springframework.security.core.AuthenticationException;
9+
import org.springframework.security.core.userdetails.UserDetails;
10+
import org.springframework.security.core.userdetails.UserDetailsService;
11+
import org.springframework.security.crypto.password.PasswordEncoder;
12+
import org.springframework.stereotype.Component;
13+
14+
@Component
15+
public class MfaAuthenticationProvider implements AuthenticationProvider {
16+
17+
@Autowired
18+
private UserRepository userRepository;
19+
20+
@Autowired
21+
private UserDetailsService userDetailsService;
22+
23+
@Autowired
24+
private PasswordEncoder passwordEncoder;
25+
26+
@Autowired
27+
private MfaService mfaService;
28+
29+
@Override
30+
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
31+
// First stage: verify username and password (primary authentication)
32+
String username = authentication.getName();
33+
String password = authentication.getCredentials().toString();
34+
35+
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
36+
37+
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
38+
throw new BadCredentialsException("Invalid username or password");
39+
}
40+
41+
// Find the user entity to check MFA settings
42+
User user = userRepository.findByUsername(username);
43+
44+
// Check if MFA is enabled for the user
45+
if (user != null && user.isMfaEnabled()) {
46+
// For MFA-enabled users, we return a partially authenticated token
47+
// The MfaAuthentication object will be used in the filter chain
48+
return new MfaAuthentication(
49+
user,
50+
authentication.getCredentials(),
51+
userDetails.getAuthorities(),
52+
true // authenticated with password, but pending MFA verification
53+
);
54+
}
55+
56+
// For users without MFA, return a fully authenticated token
57+
return new UsernamePasswordAuthenticationToken(
58+
userDetails,
59+
null, // clear credentials
60+
userDetails.getAuthorities()
61+
);
62+
}
63+
64+
@Override
65+
public boolean supports(Class<?> authentication) {
66+
return authentication.equals(UsernamePasswordAuthenticationToken.class);
67+
}
68+
}

0 commit comments

Comments
 (0)