I have a login page where the user need put the below information VIN number,email, zip code and accessCode which they will get from different application.
So to val
Thanks. I created a custom filter class for authenticating the user based on three parameters - username, password, and account id. I autowired it as a bean in SecurityConfig class:
@Bean
public AccountCredentialsAuthenticationFilter accountCredentialsAuthenticationFilter()
throws Exception {
AccountCredentialsAuthenticationFilter accountCredentialsAuthenticationFilter = new AccountCredentialsAuthenticationFilter();
accountCredentialsAuthenticationFilter
.setAuthenticationManager(authenticationManagerBean());
return accountCredentialsAuthenticationFilter;
}
So, instead of just the traditional username and password fields, I was able to perform authentication using three fields (username, password, and account id) by calling appropriate service methods required for authentication and setting authorities for the logged in user:
public class AccountCredentialsAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
private UserService userService;
@Qualifier("authenticationManager")
protected AuthenticationManager authenticationManager;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
String account = request.getParameter("account");
final String userName = request.getParameter("userName");
final String password = request.getParameter("password");
boolean isFound = userService.checkLogin(userName, password, account);
if (isFound == true) {
boolean selectedAccount = false;
UserDetails userDetails = userService.loadUserByUsername(userName);
User user = (User) userDetails;
Set<Account> accounts = user.getAccounts();
String acctSelect = null;
// user has multiple accounts
for (Account acct : accounts) {
acctSelect = acct.getAccountId().toString();
if (acctSelect.equals(account)) {
// confirm which account user has logged in with
selectedAccount = true;
account = acctSelect;
request.getSession().setAttribute("account", account);
break;
}
}
if (selectedAccount) {
Set<? extends GrantedAuthority> authorities = (HashSet<? extends GrantedAuthority>) userDetails
.getAuthorities();
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, password,
authorities);
token.setDetails(new WebAuthenticationDetails(request));
super.setDetails(request, token);
Authentication auth = this.getAuthenticationManager().authenticate(token);
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(auth);
// Create a new session and add the security context.
HttpSession session = request.getSession(true);
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
return auth;
} else {
SecurityContextHolder.getContext().setAuthentication(null);
request.getSession().setAttribute("SPRING_SECURITY_CONTEXT", null);
throw new UsernameNotFoundException("Please input correct credentials");
}
} else {
SecurityContextHolder.getContext().setAuthentication(null);
request.getSession().setAttribute("SPRING_SECURITY_CONTEXT", null);
throw new UsernameNotFoundException("Please input correct credentials");
}
}
I overrode following methods of UsernamePasswordAuthenticationFilter class for appropriate redirection after authentication & authorization:
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
redirectStrategy.sendRedirect(request, response, "/home");
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
redirectStrategy.sendRedirect(request, response, "/login?error=true");
}
I also modified the configure method in SecurityConfig class to execute the custom filter:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(accountCredentialsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()....rest of the code....}
For custom authentication in Spring Security, the method
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){---- call service methods here ----}
in this filter class (AccountCredentialsAuthenticationFilter) makes the following method in controller class redundant:
@RequestMapping(value = { "/login" }, method = RequestMethod.POST)
public String loginPage(@Valid @ModelAttribute("user") User user, BindingResult result, ModelMap model, HttpServletRequest request){---- call ervice methods here ----}
It is not the responisibility of UserDetailsService to validate the Authentication token. This is what an AuthenticationProvider does.
So first leave your implementation of UserDetailsService the single responsibility of loading all the data of the user from the database by login:
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Autowired
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = null;
try {
user = userRepository.findByUsername(username);
} catch (NotFoundException e) {
throw new UsernameNotFoundException(String.format("No user found for username %s!", username);
}
retrun new UserDetailsImpl(user);
}
}
Than to intercept additional parameters from a login form you need to implement AuthenticationDetailsSource. It may be a good idea to extend WebAuthenticationDetails, but you can have just any object returned by AuthenticationDetailsSource.
@Component
public class WebAuthenticationDetailsSourceImpl implements AuthenticationDetailsSource<HttpServletRequest, MyWebAuthenticationDetails> {
@Override
public MyWebAuthenticationDetails buildDetails(HttpServletRequest context) {
// the constructor of MyWebAuthenticationDetails can retrieve
// all extra parameters given on a login form from the request
// MyWebAuthenticationDetails is your LoginDetails class
return new MyWebAuthenticationDetails(context);
}
}
And to do the validation implement your own AuthenticationProvider by either implementing the interface itself or extending AbstractUserDetailsAuthenticationProvider or DaoAuthenticationProvider:
@Component
public class UserDetailsAuthenticationProviderImpl extends AbstractUserDetailsAuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
MyWebAuthenticationDetails detais = (MyWebAuthenticationDetails) authentication.getDetails();
// verify the authentication details here !!!
// and return proper authentication token (see DaoAuthenticationProvider for example)
}
}
Than you just need to pass your implementations to AuthenticationManager and UsernamePasswordAuthenticationFilter.
<util:list id="authenticationProviders">
<ref bean="userDetailsAuthenticationProviderImpl" />
</util:list>
<!--
This bean MUST have this exact ID to be the default authenticationManager!
This is required prior Spring 3.1, as authentication-manager-ref is not
present in sec:http element before!
-->
<bean id="org.springframework.security.authenticationManager"
name="authenticationManager"
class="org.springframework.security.authentication.ProviderManager"
c:providers-ref="authenticationProviders" />
<bean id="usernamePasswordAuthenticationFilter"
class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter"
p:authenticationManager-ref="authenticationManager"
p:authenticationDetailsSource-ref="webAuthenticationDetailsSourceImpl" />
<sec:http authentication-manager-ref="authenticationManager">
<sec:custom-filter position="FORM_LOGIN_FILTER" ref="usernamePasswordAuthenticationFilter" />
</sec:http>
Hope this helps!
P.S. Consider constructor injection over field injection! It's more testable and states the contract of the class better. See this discussion.
First of all, I would solve your problem differently. I would do a multi step authentication. The first would be a traditional user name / password login, using spring security's default model. The second step would be to show another form which would have to be filled up by the user to provide additional details for authentication, which your application wants to enforce.
Regardless, if you want to continue customizing the spring security model to ask more details on login in in a single step. Follow the steps reference in the previous answer from @Petr. And then to access session attributes in your UserDetailsService class, use the http://static.springsource.org/spring/docs/2.0.8/api/org/springframework/web/context/request/RequestContextHolder.html class provided by Spring.
You can get access to currentRequestAttributes(), which returns a RequestAttributes object. You can query the RequestAttributes object to get the desired attribute from the desired scope.
Note: This is a static method, which means its not going to be friendly to unit test.
You can also downcast RequestAttributes to ServletRequestAttributes if you want to get access to the underlying HttpServletRequest
Hope this helps.
Here is your answer, you need to implement your own filter and override the default one in order to add parameters to the login form.