How to register multiple UserDetailsService on a authenticationManagerBuilder - spring

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());
}

Related

Custom login with Spring Security and OAuth2

I am trying to customize login behavior in my Spring Boot app.
The security configuration of my app is as follows:
#EnableWebSecurity
#Import(SecurityProblemSupport.class)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final CustomAuth2UserService customAuth2UserService;
public SecurityConfiguration(CustomAuth2UserService customAuth2UserService) {
this.customAuth2UserService = customAuth2UserService;
}
#Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/app/**/*.{js,html}")
.antMatchers("/bundle.js")
.antMatchers("/slds-icons/**")
.antMatchers("/assets/**")
.antMatchers("/i18n/**")
.antMatchers("/content/**")
.antMatchers("/swagger-ui/**")
.antMatchers("/swagger-resources")
.antMatchers("/v2/api-docs")
.antMatchers("/api/redirectToHome")
.antMatchers("/test/**");
}
public void configure(HttpSecurity http) throws Exception {
RequestMatcher csrfRequestMatcher = new RequestMatcher() {
private RegexRequestMatcher requestMatcher =
new RegexRequestMatcher("/api/", null);
#Override
public boolean matches(HttpServletRequest request) {
return requestMatcher.matches(request);
}
};
http.csrf()
.requireCsrfProtectionMatcher(csrfRequestMatcher)
.and()
.authorizeRequests()
.antMatchers("/oauth2login").permitAll()
.antMatchers("/manage/**").permitAll()
.antMatchers("/entry-point").permitAll()
.antMatchers("/oauth2/*").permitAll()
.antMatchers("/api/auth-info").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/prometheus").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.anyRequest().authenticated()
.and()
.oauth2Login().loginPage("/oauth2login")
.defaultSuccessUri("/")
.userInfoEndpoint()
.userService(customOAuth2UserService);
http.cors().disable();
}
}
The custom OAuth2 user service is as follows:
#Component
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private UserRepository userRepository;
private RoleUserEmailMapRepository roleUserEmailMapRepository;
...
#Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
DefaultOAuth2User oAuth2User = (DefaultOAuth2User) super.loadUser(userRequest);
Collection<GrantedAuthority> authorities = new HashSet<>(oAuth2User.getAuthorities());
Map<String, Object> attributes = oAuth2User.getAttributes();
...
}
}
The OAuth controller is as follows:
#Controller
public class OAuthController {
private final Logger log = LoggerFactory.getLogger(OAuthController.class);
#GetMapping("/oauth2login")
public String signIn(Model model) {
log.info("Sign in!!");
model.addAttribute("email",
"");
return "first-page";
}
}
first-page.html is as follows:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Enter email</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="container my-5">
<div class="row">
<div class="col-md-6">
<form th:action="#{/redirect-to-auth-provider}" method="post">
<div class="row">
<div class="form-group col-md-6">
<label for="email" class="col-form-label">Email id</label>
<input type="text" th:value="${email}" class="form-control" id="email" placeholder="Email">
</div>
</div>
<div class="row">
<div class="col-md-6 mt-5">
<input type="submit" class="btn btn-primary" value="Submit">
</div>
</div>
</form>
</div>
</div>
</div>
</body>
</html>
OAuth client config properties are as follows:
spring.security.oauth2.client.registration.abc-auth.client-id=123456
spring.security.oauth2.client.registration.abc-auth.client-name=Auth Server
spring.security.oauth2.client.registration.abc-auth.scope=api
spring.security.oauth2.client.registration.abc-auth.provider=abc-auth
spring.security.oauth2.client.registration.abc-auth.client-authentication-method=basic
spring.security.oauth2.client.registration.abc-auth.authorization-grant-type=authorization_code
myapp.oauth2.path=https://abc-auth.com/services/oauth2/
spring.security.oauth2.client.provider.abc-auth.token-uri=${myapp.oauth2.path}token
spring.security.oauth2.client.provider.abc-auth.authorization-uri=${myapp.oauth2.path}authorize
spring.security.oauth2.client.provider.abc-auth.user-info-uri=${myapp.oauth2.path}userinfo
spring.security.oauth2.client.provider.abc-auth.user-name-attribute=name
The Auth provider controller is as follows:
#Controller
public class AuthProviderController {
private final Logger log = LoggerFactory.getLogger(AuthProviderController.class);
#Autowired
private OAuth2ClientProperties properties;
Map<String, String> oauth2AuthenticationUrls
= new HashMap<>();
#RequestMapping(value = "/redirect-to-auth-provider", method = RequestMethod.POST)
public ModelANdView entryPoint(String email) {
log.info("EMAIL RECEIVED AS PART OF REQUEST PARAM = " + email);
List<ClientRegistration> clientRegistrations = new ArrayList<>(
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
String authorizationRequestBaseUri
= "oauth2/authorization/";
String tenantName = email.split("#")[1];
log.info("DERIVED TENANT NAME = " + tenantName);
clientRegistrations.forEach(registration -> //FOR TESTING, HARD-CODED THE VALUES
oauth2AuthenticationUrls.put("abc.com",
authorizationRequestBaseUri + "abc-auth"));
return new ModelAndView("redirect:" + oauth2AuthenticationUrls.get("salesforce.com"));
}
}
Now while I am browsing the end-point of my app: https://localhost:4060(existing end-point of my app), it's successfully redirecting to https://localhost:4060/oauth2login, where I am able to enter email id.
On submit of email id, in the browser network tab, I am seeing: /redirect-to-auth-provider end-point getting invoked, and the following three sequences of calls are happening as expected:
1. https://localhost:4060/oauth2/authorization/abc-auth
2. https://abc-auth.com/services/oauth2/authorize?response_type=code&client_id=123456&scope=api&state=qKPaLcuCp5vtXPlZDQMFKA7Qe4wLApFKtaPB9HOIN0M=&redirect_uri=https://localhost:4060/oauth2
3. https://localhost:4060/oauth2?code=abcdef&state=qKPaLcuCp5vtXPlZDQMFKA7Qe4wLApFKtaPB9HOIN0M=
But, at the end of this call chain, after successful authentication, it's again redirecting to the same page: https://localhost:4060/oauth2login. I want the behavior as, the user gets authenticated properly and after successful authentication, the user should no longer get redirected to https://localhost:4060/oauth2login.
To my surprise, it's working perfectly fine, when I am NOT customizing the login behavior. Then post-authentication, the user is redirected to the home page i.e. https://localhost:4060, and able to properly use the app.
I am not able to figure out what I am missing here.
As I mentioned in an earlier comment, I recommend focusing on a minimal example of your specific problem, instead of a complex application. It helps to learn those basics in small pieces, and it helps us help you more easily if you have a minimal example.
The concept in play here is customizing the redirect_uri. You'll want to thoroughly review the entire section of the docs on OAuth2 Login, specifically the section on the Redirection Endpoint.
It's difficult to tell, but it looks as though you have somehow customized the redirect_uri for the abc-auth client. In order to make this work, you'll need to instruct the OAuth2LoginAuthenticationFilter to use your customized redirect_uri, as in the following example:
#EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.redirectionEndpoint(redirection -> redirection
.baseUri("/oauth2/*")
...
)
);
}
}
Note, the actual endpoint for abc-auth will be /oauth2/abc-auth, not /oauth2. It is essential that you have a unique redirect_uri for each oauth client, so you are not vulnerable to attacks such as mix-up (see our SpringOne 2021 presentation for more on this).
As the docs state, you'll also need a property to set your client registration to match:
spring.security.oauth2.client.registration.abc-auth.redirect-uri={baseUrl}/oauth2/{registrationId}

Choice between form login and OAuth2 login with Spring Security

I want to implement a simple app that enables users to log in with a local account or to register a new account or to login with OAuth2 - e.g. facebook. For the users which chose Facebook I would like to automatically create a local account and log them in with that account.
As far as I understand Spring Social is dead (it would be really helpful if this is mentioned on the home page of the project, because it would save efforts for people like me who invested in learning spring social).
The other thing that I understand is that "OAuth2 and OIDC are now first-class citizens in the Spring Boot and Spring Security ecosystems." Seems that the right way to go is to use Spring Security 5 with its first-class support of OAuth2!
So... let's go. My application.yaml:
spring:
security:
oauth2:
client:
registration:
facebook:
client-id: senko
client-secret: topsecret
The security configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
// for the ant pattern matcher syntax, please check:
// https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/AntPathMatcher.html
http
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login/authenticate")
.failureUrl("/login?param.error=bad_credentials")
.successForwardUrl("/home")
.and()
.logout()
.logoutUrl("/logout")
.deleteCookies("JSESSIONID").
and()
.authorizeRequests()
.antMatchers("/login**").permitAll()
.antMatchers("/**").authenticated().
and().
oauth2Login().
loginPage("/login");
}
#Override
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(
new BCryptPasswordEncoder());
}
My user service uses local storage backed by MySQL. The login form in short (thymeleaf):
Login:
<form id="signin" th:action="#{/login/authenticate}" method="post">
<input id="login" name="username" type="text" size="25"></input>
<input id="password" name="password" type="password" size="25"></input>
<button type="submit">Login In</button>
</form>
Or...:
<a th:href="#{/oauth2/authorization/facebook}">Sign in with Facebook</a>
So far I'm able to login with local account. I'm also able to login with Facebook. What I miss here is the part where I should create a local user account after the successful Facebook login. What is the correct way to implement that? I'm totally clueless. What I've tried so far is to search in google and to read the code of OAuth2LoginAuthenticationProvider. Any help will be appreciated.
UPDATE: I'm exploring if implementing an AuthenticationSuccessHandler is a proper option...
You can register a custom bean (OAuth2UserService) that will automatically replace the default configuration. Actually the example below just delegates to an implementation from the default configuration but allows to extend / add additional logic (in this example to process a user).
#Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
DefaultOAuth2UserService service = new DefaultOAuth2UserService();
return request -> {
OAuth2User user = service.loadUser(request);
System.out.println("User attributes: " + user.getAttributes()); // can be converted and saved
return user;
};
}

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 login fail wrong redirect and no error message

I'm trying to build a simple login form (JS) and to user Spring security. As far as I understood, when login fails, it should redirect to login page (or is that only for JSP login pages inside bootstrap project?) but it fails do to that.
And query Error string parameter is also empty.
My spring security configuration:
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.defaultSuccessUrl("/", true)
.permitAll()
.and()
.httpBasic()
.and()
.csrf().disable()
.logout()
.permitAll()
.logoutSuccessUrl("/");
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(ImmutableList.of("*"));
configuration.setAllowedMethods(ImmutableList.of("HEAD",
"GET", "POST", "PUT", "DELETE", "PATCH"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(ImmutableList.of("Authorization", "Cache-Control", "Content-Type"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Bean
public UserDetailsService userDetailsService() {
// ensure the passwords are encoded properly
User.UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users.username("user").password("user").roles("USER").build());
manager.createUser(users.username("admin").password("admin").roles("USER","ADMIN").build());
return manager;
}
}
Boot:
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
From JS app I am sending a request to http://localhost:8080/login, I don't think it matters in this case, but I'm using MithrilJS request:
m.request({
method: "POST",
url: "http://localhost:8080/login",
body: {username: login, password: pass}
})
.then((result) => {
UserLogin.loggedIn = true;
})
.catch(error => {
console.log(error);
});
Responses (2 for some reason) I get:
http://localhost:8080/login?error
Request Method: OPTIONS
Response is empty
error string is also empty
http://localhost:8080/login?error
Request Method: GET
error String is empty
And now the funny part, response contains html (note that I don't have this HTML anywhere in my code):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Please sign in</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
</head>
<body>
<div class="container">
<form class="form-signin" method="post" action="/login">
<h2 class="form-signin-heading">Please sign in</h2>
<div class="alert alert-danger" role="alert">Invalid credentials</div> <p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
</p>
<p>
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
</p>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</body></html>
Any ideas where am I failing?
EDIT:
Thank your for the answers, while it did not answer exactly what I had in mind, it did lead me to right direction.
My main problem was that I have 2 separate projects: 1) a spring boot project 2) a JS application. JS application contains form html itself (or JS in this case) since I don't want any front end code to be or come from backend (spring boot project) while all the login logic is in spring boot spring security.
If I disable formLogin (which I have to do, no to use spring login form) I get no /login endpoint.
To summarize, I want to use spring security while bypassing spring login form (this way backend contains login logic, which can be accessed by any form, or that is the idea).
While I'm not quite there yet, I'm getting there.
For anyone that's curious, this is the article that helped: spring security without login form
You are trying to do authentication with ajax, so you can not redirect to any other page dependent on server response, you should do that in you JS(e.g. window.location.href).
Now let's talk about the form login in your case. The UsernamePasswordAuthenticationFilter is enabled based on your configuration.
.formLogin()
.defaultSuccessUrl("/", true)
.permitAll()
This filter will get username and password from the request params.
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
But you are trying to send a json body to the server, so it can not get the right credential. You should change it to a form request.
Next one is about the fail redirect url, now you should know the ajax can not redirect to an other page, the default failureHandler in you configuration will redirect to the login page with error, now you are using ajax, so you just can get the HTML, I think you can just validate the request based on the header(e.g. 401), here is an example.
.formLogin()
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
Here is the code in SimpleUrlAuthenticationFailureHandler
if (defaultFailureUrl == null) {
logger.debug("No failure URL set, sending 401 Unauthorized error");
response.sendError(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase());
}
You can get the result based on the header and the body.
Now I think your should know the defaultSuccessUrl in your configuration will not work as you expect. You need to implement you own AuthenticationSuccessHandler.
The last one is about your form authentication, the form authentication most of it is based on cookie, I think all your requests should contains the cookie to the server after login successfully. Maybe you can research JWT to instead.
The HTML is the default login form.
Why did you define formLogin()?
You must send username and password in the Authorization header not in the body.
From https://mithril.js.org/request.html
m.request({
method: "POST",
url: "http://localhost:8080/login",
user: login,
password: pass
})
.then((result) => {
UserLogin.loggedIn = true;
})
.catch(error => {
console.log(error);
});

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

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.

Resources