How to configure Spring MVC HttpSecurity using Java config for login page - spring

I am building a Spring MVC application using Java config rather than xml, with Eclipse, Maven and Spring webmvc ver 4.2.4 and Spring security 4.0.3. I have it running on Tomcat 7.
There are multiple jsp's that I can navigate from one to another, so the #RequestMappings are correct(they are listed in the configure( ) method below). I have logging set up with log4j with everything possible logged, so I can see my configurations and controllers are being called. During startup, the log file shows the mappings being set:
...RequestMappingHandlerMapping - Mapped "{[/login],methods=[GET]}"...
...RequestMappingHandlerMapping - Mapped "{[/login],methods=[POST]}" ...
My problem is the login screen does not POST to the correct method in the LoginController class when it is getting submitted, it keeps going to the "init" method which is annotated for the GET request.
Here is SecurityConfig:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/","/register","/about","/home","/demo").permitAll()
//.loginProcessingUrl("/login")
.antMatchers(HttpMethod.POST,"/login").permitAll()
.anyRequest().authenticated()
.and().formLogin().permitAll().loginPage("/login")
.and().logout().logoutUrl("/logout").invalidateHttpSession(true).logoutSuccessUrl("/home");
}
}
When //.loginProcessingUrl("/login") is uncommented, the autogenerated Spring login form appears and I can log in! So it works with the default form but not my form.
LoginController.java looks like this:
#Controller
public class LoginController {
private Logger logger = Logger.getLogger(LoginController.class);
#RequestMapping(value = "/login", method = RequestMethod.GET)
public String init(Locale locale, Model model) {
logger.info("LoginController login INIT!");
return "login";
}
#RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(#ModelAttribute LoginDTO loginObject, Locale locale, Model model) {
logger.info("LoginController login POST!");
return "home";
}
}
When the Spring default login page is submitted, it doesn't map to my LoginController. When my login.jsp is submitted, the request goes the init( ) method, not the login( ) method mapped to POST.
A snippet of my custom login.jsp I want to use instead of the default jsp:
<form:form action="${loginProcessingUrl}" method="post">
<p>
<label for="username">Username</label>
<input type="text" id="userId" name="userId"/>
</p>
<p>
<label for="password">Password</label>
<input type="password" id="password" name="password"/>
</p>
<div>
<button type="submit">Log in</button>
</div>
</form:form>
The framework is adding the CSRF token on the login page, which I can see on the browser, so that seems to be working but I'm not sure if it matters.
<input type="hidden" name="_csrf" value="ac81daad-f2e4-4357-a7a8-b7b10a67036f">
I am just learning Spring, and have been searching everywhere for some in depth explanation about how Spring Security works and is configured, especially the http object with all the chained methods. If anyone can direct me to a good reference for the latest Spring Security, I would appreciate it, or let me know what I need in my code.

From my understanding, you should remove the #RequestMapping(value = "/login", method = RequestMethod.POST) from your controller and change the security configuration as follows:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/register", "/about", "/home", "/demo").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/home")
.invalidateHttpSession(true);
}
}
Spring will automatically handles your POST request with the Spring security filter and redirects you to your login form if necessary. Please make sure your login field names are correctly named "username" and "password":
<form:form action="${loginProcessingUrl}" method="post">
<p>
<label for="username">Username</label>
<input type="text" id="username" name="username"/>
</p>
<p>
<label for="password">Password</label>
<input type="password" id="password" name="password"/>
</p>
<div>
<button type="submit">Log in</button>
</div>
</form:form>
See http://docs.spring.io/spring-security/site/docs/current/reference/html/jc.html#jc-form for more information.

Related

How to register multiple UserDetailsService on a authenticationManagerBuilder

I have two different repository for normal user and admin and separate url endpoints to authenticate. I want the authentication manager to use separate UserDetailsService for the endpoints since both normal and admin users can have same username and password but different repositories.
Example:
if the endpoint hit is user_login then UserDetailsService1 and
if the endpoint hit is admin_login then UserDetailsService2
How can I achieve this?
The HttpSecurity.formLogin DSL only supports a single log in URL because that is what is most common. However, you can do this by explicitly registering a second UsernamePasswordAuthenticationFilter. The documentation has some nice diagrams of how form based log in works.
I created a sample (make sure to use the linked branch). Below is a summary and description of what is happening:
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// allow anyone to access the admin log in page
.mvcMatchers("/admin_login").permitAll()
// require admin access to any admin URLs
.mvcMatchers("/admin/**").hasRole("ADMIN")
// any other URL just requires to be authenticated
.anyRequest().authenticated()
.and()
// configure the user based authentication
.formLogin()
// this means you should serve a log in page for users at GET /user_login
// Spring Security will process POST /user_login as a user log in
.loginPage("/user_login")
// allow anyone to access the /user_login since they aren't authenticated when they see a log in page
.permitAll()
.and()
// formLogin above only supports a single repository because that is what is most common
// fortunately formLogin is just a shortcut for the code below
// here we add the admin login form explicitly
.addFilter(adminAuthenticationFilter());
}
// formLogin for users will pick up a UserDetailsService exposed as a Bean
#Bean
static InMemoryUserDetailsManager userDetailsManager() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("user")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
// create an admin version of UsernamePasswordAuthenticationFilter
private static UsernamePasswordAuthenticationFilter adminAuthenticationFilter() {
// inject the adminAuthenticationProvider so only admins are authenticated with this Filter
UsernamePasswordAuthenticationFilter result = new UsernamePasswordAuthenticationFilter(adminAuthenticationProvider());
// only process POST /admin_login
result.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin_login", "POST"));
// errors should go to /admin_login?error
result.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/admin_login?error"));
return result;
}
// create an AuthenticationManager that is only used by Admin users
private static AuthenticationManager adminAuthenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(adminUsers());
return new ProviderManager(authenticationProvider);
}
// we use the same username as user based to demon that it will work properly
// the difference is that the password is admin and the user will have admin role so it can access URLs in /admin/
static InMemoryUserDetailsManager adminUsers() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("admin")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
}
As mentioned above you are responsible for creating the log in pages and ensuring they post to the correct URLs. The first step is to create a controller that maps the URLs to the views you want to display. Here we use a single Controller for convenience, but you can split this up:
#Controller
public class LoginController {
#GetMapping("/admin_login")
String adminLogin() {
return "admin_login";
}
#GetMapping("/user_login")
String login() {
return "user_login";
}
}
Then you need to have two views. The first view is admin_login.html. In a Boot + Thymeleaf application something like this would be located in src/main/resources/templates/user_login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>User Log In</title>
</head>
<body>
<div class="container">
<h1>User Log In</h1>
<form method="post"
th:action="#{/user_login}">
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<div>
<label for="username">Username</label>
<input type="text"
id="username"
name="username"
placeholder="Username"
required autofocus>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password"
id="password"
name="password"
placeholder="Password"
required>
</div>
<button type="submit">Sign in</button>
</form>
</div>
</body>
</html>
This is all detailed in the link I provided above. The key is that it submits a POST to /user_login with HTTP parameters username and password.
You need a similar view for the admin login that does a POST to /admin_login with HTTP parameters username and password.
You can have something like this
#Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(service1)
.passwordEncoder(passwordEncoder());
authenticationManagerBuilder
.userDetailsService(service2)
.passwordEncoder(passwordEncoder());
}

SpringBoot 2.1.3 Security multiple available login page for first authentication

I'm developing a SpringBoot 2.1.3 + Thymeleaf 3 + Tomcat 8 WebApp. I have implemented Spring Security and all works well. Now I have a little problem because I want to realize two form login page, one for backoffice users and other one for all others users.
I don't wanna put both form in the same page, and like to create a page with a simple form for backoffice users and a link that redirect to another page (with another form) for customer users.
I have read some and I have found just a way to Ordering the WebSecurityConfigurerAdapter and creating multiple entry point but this way, I can log in with page with order 1 and then I can go the the other form page. It'isnt what I want to do.
Do you know if there is a way to do this??
Thank you
I found a way and it is pretty simple. I have configured the WebSecurityConfigurerAdapter as follow:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login-procout").permitAll() // login for furnishers
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login-officers") // default login page
.permitAll()
.successHandler(customAuthenticationSuccessHandler())
.and()
.logout()
.permitAll();
http.csrf().disable();
}
Then I create two html page login-officers.html and login-procout.html referred by Controller as follow:
#Controller
public class MyController {
#GetMapping("/login-officers")
public String loginOfficers() {
return "login-officers";
}
#GetMapping("/login-procout")
public String loginProcout() {
return "login-procout";
}
}
And in both page I have default Spring Security Form:
<form action="" th:action="#{/login}" method="post">
<input type="text" name="username" id="username" placeholder="Username"/>
<input type="password" name="password" id="password" placeholder="Password"/>
<input type="submit" id="login" value="Log In" />
</form>
I don't know if is the right way to do but it works.

Spring Security 5 authentication always return 302

I'm using Spring-Security 5 to secure my web app. I access /login.jsp and fill in username and password, and then click "Log in" to submit the form, and then was redirected to /login.jsp. I see the reponse status code of that http traffic in fiddler is 302.
SecurityConfig class:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private DataSource dataSource;
#Autowired
protected SecurityConfig(DataSource dataSource
) {
this.dataSource = dataSource;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.jsp")
.loginProcessingUrl("/login")
.permitAll();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select name userName, password, enabled from user where name=?")
.authoritiesByUsernameQuery("select name userName 'ROLE_USER' from user where name=?")
;
}
}
login.jsp:
<%# page contentType="text/html;charset=UTF-8" language="java" %>
<%# taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<c:url value="/login" var="loginUrl"/>
<form action="${loginUrl}" method="post"> 1
<c:if test="${param.error != null}"> 2
<p>
Invalid username and password.
</p>
</c:if>
<c:if test="${param.logout != null}"> 3
<p>
You have been logged out.
</p>
</c:if>
<p>
<label for="username">Username</label>
<input type="text" id="username" name="username"/> 4
</p>
<p>
<label for="password">Password</label>
<input type="password" id="password" name="password"/> 5
</p>
<button type="submit" class="btn">Log in</button>
</form>
</body>
</html>
This is because spring default authentication success handler looks for a url to redirect.
What one can do is use a custom AuthenticationSuccessHandler
i have used below and no redirects are happening.
public class AppAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler{
protected void handle(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
}
}
Then define the bean and give it in the configure method for security
#Bean
public AuthenticationSuccessHandler appAuthenticationSuccessHandler(){
return new AppAuthenticationSuccessHandler();
}
Configure method
http
.authorizeRequests()
.antMatchers("/login*")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.successHandler(appAuthenticationSuccessHandler());
I had this problem until I turned csrf-check off by including .csrf().disable() in configure (HttpSecurity) method.
If you don't have it off then provide csrf token as hidden form field.
... though I see that you have it off disabled
the "loginPage url" same of the "form action"
show me code
java config:
http.formLogin().loginPage("/login.html")
html
<form action="/login.html" method="post">
you just need write controller for "/login.html", by http GET method, Leave the rest to “spring”
docs: https://docs.spring.io/spring-security/site/docs/5.3.6.RELEASE/reference/html5/#servlet-authentication-form
the UsernamePasswordAuthenticationFilter match /login.html by http POST method
My English is not good, Hope I can help you
I don't known if this issue is always active but if this can help someone...
What's works for me was to replace
.formLogin()
by
.httpBasic();
in my WebSecurityConfigurerAdapter class.
So my security config looks like this :
protected void configure(final HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login", "/actuator/**", "/clients/refresh", "/oauth/token/revokeById/**", "/tokens/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic();
}
Use successHandler to set the referer true. This does the trick for me. Else I am also getting 302.
In securityConfig need to add the below code.
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login*")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.successHandler(new RefererRedirectionAuthenticationSuccessHandler ());
}
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
public class RefererRedirectionAuthenticationSuccessHandler extends
SimpleUrlAuthenticationSuccessHandler {
public RefererRedirectionAuthenticationSuccessHandler() {
super();
setUseReferer(true);
}
}
}
Check the below link:
http://www.baeldung.com/spring-security-redirect-login

Spring Security 4 and JSF 2 integration

Is there a way to integrate Spring Security 4 (Mainly for managing user access levels and which views they can access) and JSF 2?
I found this neat thing which allows you to mix both Spring Boot, and JSF 2 with PrimeFaces 5. Great stuff. I want to see if you can kick it up another level.
Normally you would configure Spring Security for Spring MVC like so:
WebSecurityConfig.java
#Configuration
#EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("Zyst").password("password").roles("USER");
}
}
And then those would as far as I know, do correct me if I'm mistaken, look in your MvcConfig to see what it actually means by "/home" and the like:
MvcConfig.java
#Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
However, I've been googling for a few hours and cannot really find a conclusive answer how to configure Spring Security for JSF. Can you implement your front end using JSF and then make that managed by Spring Security, so, for example Links, ie: localhost:8080/home instead of localhost:8080/home.xhtml are properly managed and served? And so that user levels defined in WebSecurityConfig.java can only access pages relevant to themselves.
From what I've (briefly) investigated it might not be possible due to Faces and Mvc being different technologies that don't particularly play well together. However, if possible I'd like to make sure of whether it's possible or not.
And if it IS possible, can you provide either a working example, or a link to somewhere that goes more in depth? I did google quite a bit but it's 100% possible I ended up missing something.
Any and all answers are greatly appreciated.
There's no problem in using Spring Boot, Spring Security, JSF and Spring Core all together, in the end, JSF views are resolved as urls and that's what you work in Spring Security with. That's an example for the configuration in my own application, which I've pruned a bit to minimize the code amount. The code is self-explanatory:
#Configuration
#EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// Have to disable it for POST methods:
// http://stackoverflow.com/a/20608149/1199132
http.csrf().disable();
// Logout and redirection:
// http://stackoverflow.com/a/24987207/1199132
http.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.invalidateHttpSession(true)
.logoutSuccessUrl(
"/login.xhtml");
http.authorizeRequests()
// Some filters enabling url regex:
// http://stackoverflow.com/a/8911284/1199132
.regexMatchers(
"\\A/page1.xhtml\\?param1=true\\Z",
"\\A/page2.xhtml.*")
.permitAll()
//Permit access for all to error and denied views
.antMatchers("/500.xhtml", "/denied.xhtml")
.permitAll()
// Only access with admin role
.antMatchers("/config/**")
.hasRole("ADMIN")
//Permit access only for some roles
.antMatchers("/page3.xhtml")
.hasAnyRole("ADMIN", "MANAGEMENT")
//If user doesn't have permission, forward him to login page
.and()
.formLogin()
.loginPage("/login.xhtml")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/main.xhtml")
.and().exceptionHandling().accessDeniedPage("/denied.xhtml");
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
//Configure roles and passwords as in-memory authentication
auth.inMemoryAuthentication()
.withUser("administrator")
.password("pass")
.roles("ADMIN");
auth.inMemoryAuthentication()
.withUser("manager")
.password("pass")
.roles("MANAGEMENT");
}
}
Of course, this code works with *.xhtml suffixed urls, as they're served by the JSF Servlet. If you want to avoid this suffix, you should use a url rewriting tool as Prettyfaces. But that's another story that has already been widely discussed in StackOverflow.
Also, remember to target your login form to the configured login processing url to let Spring Security handle the authentication and redirection to your main page. What I usually do is to use a non-JSF form and apply the Primefaces styles on it:
<form id="login_form" action="#{request.contextPath}/login" method="post">
<p>
<label for="j_username" class="login-form-tag">User</label> <input
type="text" id="username" name="username" class="ui-corner-all"
required="required" />
</p>
<p>
<label for="j_password" class="login-form-tag">Password</label>
<input type="password" id="password" name="password"
class="ui-corner-all" required="required" />
</p>
<p>
<button type="submit"
class="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only">
<span class="ui-button-text">Login</span>
</button>
</p>
</form>
See also:
Spring and JSF integration
Spring Boot JSF Integration

How to configure Spring 4.0 with spring boot and spring security openId

I'm trying to get a Spring 4.0 boot application up and running with Spring Security OpenId. I'm using the standard way to bootstrap a Spring boot app:
#Configuration
#ComponentScan("x.y.z")
#EnableAutoConfiguration
#Import({SecurityConfig.class})
public class ServiceRegistryStart extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(ServiceRegistryStart.class, args);
}
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
application.sources(getClass());
return application;
}
}
The SecurityConfig.class looks like this (Influenced by the "openid-jc sample project in Spring security):
#Configuration
#EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/resources/**").permitAll()
.anyRequest().authenticated()
.and()
.openidLogin()
.loginPage("/login.html")
.permitAll()
.authenticationUserDetailsService(new CustomUserDetailsService())
.attributeExchange("https://www.google.com/.*")
.attribute("email")
.type("http://axschema.org/contact/email")
.required(true)
.and()
.attribute("firstname")
.type("http://axschema.org/namePerson/first")
.required(true)
.and()
.attribute("lastname")
.type("http://axschema.org/namePerson/last")
.required(true)
.and()
.and()
.attributeExchange(".*yahoo.com.*")
.attribute("email")
.type("http://axschema.org/contact/email")
.required(true)
.and()
.attribute("fullname")
.type("http://axschema.org/namePerson")
.required(true)
.and()
.and()
.attributeExchange(".*myopenid.com.*")
.attribute("email")
.type("http://schema.openid.net/contact/email")
.required(true)
.and()
.attribute("fullname")
.type("http://schema.openid.net/namePerson")
.required(true);
}
#Bean(name = "myAuthenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
class CustomUserDetailsService implements AuthenticationUserDetailsService<OpenIDAuthenticationToken> {
#Override
public UserDetails loadUserDetails(OpenIDAuthenticationToken token) throws UsernameNotFoundException {
return new User(token.getName(), "", AuthorityUtils.createAuthorityList("ROLE_USER"));
}
}
}
The login page looks like this:
<form id="googleLoginForm" action="/j_spring_openid_security_check" method="post">
<h1>Login</h1>
<input name="openid_identifier" type="hidden" value="https://www.google.com/accounts/o8/id"/>
<input name="openid.ns.pape" type="hidden" value="http://specs.openid.net/extensions/pape/1.0"/>
<input name="openid.pape.max_auth_age" type="hidden" value="0"/>
<p>
<input name="submit" value="Login using Google" type="submit"/>
</p>
</form>
The problem is that the "/j_spring_openid_security_check" doesn't seem to exist. I think the problem is that I ought to extend from AbstractSecurityWebApplicationInitializer when using Spring Security but for boot I should use SpringBootServletInitializer. What's the best way to combine the two? The javadoc of SpringBootServletInitializer says that it registers a filter automatically when Spring Security is detected but it doesn't seem to work in this case.
I actually managed to solve this. First off all I used Spring Boot to start an embedded container so I didn't need any WebApplicationInitializers. Secondly the post URL in the login page should point to "/login/openid" and thirdly I had to disable cross-site request forgery prevention in the security configuration using:
http.csrf().disable(). ..
in the configure method in the SecurityConfig class.

Resources