I have made a custom exception which should give the message to client when raised. However, that does not seem to be working whenever error code is 401 i.e. UNAUTHORIZED. I tried changing the status code to something else, and message showed up.
Note - I have already set the flag server.error.include-message=always in application.properties
BadCredentialsException.java
#ResponseStatus(value = HttpStatus.UNAUTHORIZED)
public class BadCredentialsException extends RuntimeException{
// Runtime exception just needs this, I guess :/
private static final long serialVersionUID = 1;
public BadCredentialsException(String message){
super(message);
}
}
Here's how I have tried raising the exception.
public ResponseEntity<Boolean> loginUser(String username, String password){
// validating username
User user = myUserRepository.findByUsername(username).
orElseThrow(() -> new ResourceNotFoundException("No username: " + username + " found. Please enter a correct username!"));
// validating password
if(!new BCryptPasswordEncoder().matches(password, user.getPassword())){
throw new BadCredentialsException("Incorrect Password. Please enter correct password to login!");
}
return ResponseEntity.ok(true);
}
Note - message is correctly being displayed in the terminal though. Just not showing up on the client.
UPDATE 1 - I have made this particular endpoint to be accessible by everyone, using permitAll(). In postman, when I select select "no auth" and call this endpoint with expected exception, exception does not give the message unless, I give correct basic auth credentials (any role). I have absolutely no clue why this is happening.
UPDATE 2 - Adding SecurityConfiguration code.
SecurityConfiguration.java
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().
authorizeRequests().
antMatchers(HttpMethod.POST, "/api/v2/user/login/**").permitAll().
antMatchers(HttpMethod.POST, "/api/v2/user/", "/api/v2/user", "/api/v2/user/change-role/**").hasAuthority("ROOT").
antMatchers(HttpMethod.GET, "/api/v2/user/", "/api/v2/user").hasAuthority("ROOT").
antMatchers(HttpMethod.POST, "/api/v1/customers/", "/api/v1/customers").hasAnyAuthority("ADMIN", "ROOT").
antMatchers(HttpMethod.GET, "/api/v1/customers/", "/api/v1/customers").hasAnyAuthority("EMPLOYEE", "ADMIN", "ROOT").
anyRequest().
authenticated().
and().
httpBasic();
}
Actually, there are multiple ways to do that.
First, as #SergVasylchak said in the comments, you can use ControllerAdvice.
So the approach is like below:
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(BadCredentialsException.class)
#ResponseBody
#ResponseStatus(value = HttpStatus.UNAUTHORIZED)
private Message handleMessageAuth(BadCredentialsException e, HttpServletRequest request) {
Message message = new Message();
message.setTimestamp(getTimeStamp());
message.setError(HttpStatus.UNAUTHORIZED.getReasonPhrase());
message.setStatus(HttpStatus.UNAUTHORIZED.value());
message.setMessage(e.getMessage());
message.setPath(String.valueOf(request.getRequestURI()));
return message;
}
}
Message is your custom pojo.
public class Message {
private String timestamp;
private int status;
private String error;
private String message;
private String path;
// getters & setters
}
Another solution is that implement AuthenticationEntryPoint.
What is AuthenticationEntryPoint?
It is an interface implemented by ExceptionTranslationFilter,
basically a filter which is the first point of entry for Spring
Security. It is the entry point to check if a user is authenticated
and logs the person in or throws exception (unauthorized). Usually, the
class can be used like that in simple applications but when using
Spring security in REST, JWT etc one will have to extend it to provide
better Spring Security filter chain management.
AuthenticationEntryPoint is used in Spring Web Security to configure
an application to perform certain actions whenever an unauthenticated
client tries to access private resources.
Look at this.
AuthenticationEntryPoint is used to send an HTTP response that
requests credentials from a client. Sometimes a client will
proactively include credentials such as a username/password to request
a resource. In these cases, Spring Security does not need to provide
an HTTP response that requests credentials from the client since they
are already included. In other cases, a client will make an
unauthenticated request to a resource that they are not authorized to
access. In this case, an implementation of AuthenticationEntryPoint is
used to request credentials from the client. The
AuthenticationEntryPoint implementation might perform a redirect to a
log in page, respond with a WWW-Authenticate header, etc.
For example, if you have a JWT authentication the approach is like below.
#Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
// if you want to return 401 status, this is enough.
// if you want to customize your response you can make it as below.
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().println("{ \"error\": \"" + authException.getMessage() + "\" }");
}
}
Now it's time to config the AuthenticationEntryPoint in SecurityConfig.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationEntryPoint authenticationEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.and()
authorizeRequests().
antMatchers(HttpMethod.POST, "/api/v2/user/login/**").permitAll().
antMatchers(HttpMethod.POST, "/api/v2/user/", "/api/v2/user", "/api/v2/user/change-role/**").hasAuthority("ROOT").
antMatchers(HttpMethod.GET, "/api/v2/user/", "/api/v2/user").hasAuthority("ROOT").
antMatchers(HttpMethod.POST, "/api/v1/customers/", "/api/v1/customers").hasAnyAuthority("ADMIN", "ROOT").
antMatchers(HttpMethod.GET, "/api/v1/customers/", "/api/v1/customers").hasAnyAuthority("EMPLOYEE", "ADMIN", "ROOT").
anyRequest().
authenticated().
and().
httpBasic();
}
}
Related
I am working on a Spring Boot protecting my APIs using Spring Security and JWT tokens and I have the following iusse.
I have an API handling endpoint like this: http://localhost:8019/api/admin/user/54/wallet
I have 2 user types:
ADMIN USER: having ADMIN autority defined into the JWT tokend.
CLIENT USER: having CLIENT autority defined into the JWT tokend.
The previous API must be accessible by both the user types (I know that the /admin/ section in the URI is not the best...it will be refactored in the near future).
Then I have this class extending the Spring Boot WebSecurityConfigurerAdapter class and implementing my security configuration:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
#Qualifier("customUserDetailsService")
private UserDetailsService userDetailsService;
#Autowired
private JwtConfig jwtConfig;
#Autowired
private JwtTokenUtil jwtTokenUtil;
private static final String[] USER_MATCHER = { "/api/user/email/**"};
private static final String[] CLIENT_MATCHER = {
"/api/users/email/*",
//"/api/admin/**",
"/api/admin/user/{userID}/wallet"
};
private static final String[] ADMIN_MATCHER = {
"/api/users/email/**",
"/api/admin/users",
"/api/admin/user/{userID}/wallet",
"/api/admin/**"
};
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
/*
* NOTE:
* Using hasRole expects the authority names to start with 'ROLE_' prefix
* Instead, we use hasAuthority as we can use the names as it is
*/
http.csrf().disable()
.authorizeRequests()
.antMatchers(USER_MATCHER).hasAnyAuthority("USER")
.antMatchers(CLIENT_MATCHER).hasAnyAuthority("CLIENT")
.antMatchers(ADMIN_MATCHER).hasAnyAuthority("ADMIN")
//.antMatchers(CLIENT_MATCHER).hasAnyAuthority("CLIENT")
.antMatchers("/api/users/test").authenticated()
.antMatchers(HttpMethod.POST, jwtConfig.getUri()).permitAll()
.anyRequest().denyAll()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(
new TokenVerificationFilter(authenticationManager(), jwtConfig, jwtTokenUtil),UsernamePasswordAuthenticationFilter.class);
}
/* To allow Pre-flight [OPTIONS] request from browser */
#Override
public void configure(WebSecurity web)
{
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
web.ignoring().antMatchers("/swagger-ui/**",
"/webjars/**",
"/v2/**",
"/swagger-resources/**",
"/swagger-ui.html");
}
#Bean
public BCryptPasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
};
}
The problem is that as it is defined in the previous configuration if I try to call the previous API (http://localhost:8019/api/admin/user/54/wallet) with a token related to an user having the CLIENT autority I am obtaining this error message:
{
"timestamp": "2022-02-13T20:22:10.418+00:00",
"status": 403,
"error": "Forbidden",
"message": "Forbidden",
"path": "/api/admin/user/54/wallet"
}
Viceversa if I try to call the previous API passing a token of an user having the AMDIN authority I obtain the expected API output.
It seems pretty strange to me because into the CLIENT_MATCHER I have defined this rule:
"/api/admin/user/{userID}/wallet"
The strangest thing is that if I remove this line from the ADMIN_MATCHER:
"/api/admin/user/{userID}/wallet",
that become something like:
private static final String[] ADMIN_MATCHER = {
"/api/users/email/**",
"/api/admin/users",
//"/api/admin/user/{userID}/wallet",
"/api/admin/**"
}
Now the behavior is the complete opposite: using a JWT token of an user having the CLIENT authority I retrieve the expected output, but now using a JWT token of an user having the ADMIN authoruty I obtain this error message:
{
"timestamp": "2022-02-13T20:27:47.793+00:00",
"status": 403,
"error": "Forbidden",
"message": "Forbidden",
"path": "/api/admin/user/54/wallet"
}
and this seems pretty strange to me because the ADMIN_MATCHER contaisn this rule:
"/api/admin/**"
that should meaning: access to all the API having an enptoint starting with "/api/admin/" folloed by anything else.
What is wrong in my Spring configuration? What am I missing? How can I fix it in such a way that this API could be accessible using a JWT token having ADMIN or CLIENT authority?
instead of
.antMatchers(ADMIN_MATCHER).hasAnyAuthority("ADMIN")
try
.antMatchers(ADMIN_MATCHER).hasAnyAuthority("ADMIN", "CLIENT")
and removing this from CLIENT_MATCHER
"/api/admin/user/{userID}/wallet"
The problem is the way you have written your rules. The rule that matches is the one that is used so if a request comes in and it finds "/api/user/email/**" it will find the first that matches, the one for USER.
What you should write is .antMatchers("/api/user/email/**").hasAnyAuthority("USER", "CLIENT", "ADMIN") to allow all of them access. That is your security rule, it will not go over all rules it will detect the one that matches first (which also means that ordering is very important for your rules!).
This applies to all your other rules as well BTW.
I'm struggling to implement the jwt authentication while i understand (i think so) the theory behind it.
The whole idea is to send a jwt token every time user wants to access personal data through backend secured endpoints, instead of sending the credentials every time.
My backend is based on this repo. I have edited the code for my needs:
JwtTokenFilter.java
public class JwtTokenFilter extends OncePerRequestFilter {
private JwtTokenProvider jwtTokenProvider;
public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
// Save initial path. Since CustomErrorController always uses /error path
// we can't retrieve the initial path using httpRequest.getRequestURI() method
httpServletRequest.setAttribute("initialPath", httpServletRequest.getRequestURI());
if (httpServletRequest.getHeader("Authorization") == null) {
throw new TokenException("Authorization field is empty.");
}
String token = jwtTokenProvider.resolveToken(httpServletRequest);
if (token == null) {
throw new TokenException("Non Bearer Token");
}
// throws TokenException
jwtTokenProvider.validateToken(token);
// throws NoUserFoundException
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
WebSecurityConfig.java
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtTokenProvider jwtTokenProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
// Disable CSRF (cross site request forgery)
http.csrf().disable();
// No session will be created or used by spring security
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Entry points
// http.authorizeRequests()
// .antMatchers("/signin").permitAll()
// .antMatchers("/signup").permitAll()
// Disallow everything else..
// .antMatchers("/profile").authenticated();
// If a user tries to access a resource without having enough permissions
// http.exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> {
// System.out.println("**************** INSIDE HANDLEEEEEEEEER");
// response.sendError(417);
// });
// IMPORTANT!!!
// DO I NEED THESE???
// http.authorizeRequests().antMatchers("/signin", "signup").permitAll();
// http.authorizeRequests().antMatchers("/signin", "signup").hasAnyRole("ADMIN", "USER");
// http.authorizeRequests().antMatchers("/signin", "signup").hasAnyRole("ADMIN");
// http.authorizeRequests().antMatchers("/profile").authenticated();
http.antMatcher("/profile").apply(new JwtTokenFilterConfigurer(jwtTokenProvider));
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Here are my questions:
See the commented out code on the WebSecurityConfigurerAdapter. First of, it doesn't seem to work. But even if it did, it wouldn't make sense.
We want the jwt filter to be applied only to specific urls. It wouldn't make sense to ask for a token when everyone has the right to sign up or sign in. The last line inside WebSecurityConfigurerAdapter applies the filter only on the /profile endpoint.
The jwt token contains (if it is valid) the username. When extracted, we search for that user on the database. This user object contains its' role. So, in order to have a user's role we MUST pass through the jwt filter in order to validate it and THEN find the saved user.
So why the code doesn't work at all?
Is the commented code really useless or am i missing something? How would it be possible to authenticate the user before even applying the jwt filter?
I have a situation I would need to know the requested payload details when the POST request got 401 Unauthorized error.
I am thinking we will NOT be able to capture the payload when the request has NOT made it to the API endpoint due to Unauthorized error. It will be filtered out before hitting this endpoint.
I am using Springboot 2.1.6
My controller method as below
#PostMapping(value = "/users", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<PayloadResponse> processPayload(#RequestBody String payload) {
logger.info("Received a payload: {}", payload);
}
Are there any ways we can log this payload somehow even on 401 error?
Thanks in advance
You cannot use any of SpringMVC mechanisms to catch and log this kind of error because it happens before going in MVC stack. #ControlerAdvice won't do a thing.
You can extend AuthenticationEntryPoint and config it by
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
}
}
extend it like this
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest req, HttpServletResponse res,
AuthenticationException authException)
throws IOException {
res.setContentType("application/json;charset=UTF-8");
res.setStatus(401);
res.getWriter().write(JsonUtil.getWriteMapper().writeValueAsString(
new ErrorRestResponse(authException,false,""))); //This is my custom error response
// You can put logging code around here
}
}
you can use a #ControllerAdvice to handle all kinds of requests and read their payloads if supplied, and give back the appropriate response.
We have a legacy Spring application (A) (that is not using spring-boot) that handles authentication and writes the session to Redis using spring-session (the data in Redis is stored as XML).
We now want to introduce a new application (B), using spring-boot 2.2.6.RELEASE and spring-session Corn-RC1, that should be useable if a user has signed into (A) with ROLE_ADMIN. I.e. this can be regarded as a very crude way of doing single sign on. A user should never be able to authenticate in B (it'd like to disable authentication if possible), it should only check that an existing user is authenticated in the session repository (redis) and has ROLE_ADMIN. Both A and B will be located under the same domain so cookies will be propagated by the browser. I've tried various different ways of getting this to work, for example:
#Configuration
#EnableWebSecurity
class ServiceBSpringSecurityConfig : WebSecurityConfigurerAdapter() {
#Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
}
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.formLogin()
.and()
.httpBasic().disable()
}
}
but this will show the default login screen:
I've also tried removing this part entirely:
#Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
}
but then it'll generate a default user and password and it does not seem to call the configure method (or the configuration doesn't work regardless).
How can I solve this?
What you need is to disable formLogin and httBasic on Application B and add a filter before spring's authentication filter AnonymousAuthenticationFilter or UsernamePasswordAuthenticationFilter. In the custom filter you will extract the cookie/header/token from the request object and based on that reach out to the redis cache for session details. This filter would then validate the session and create object of type org.springframework.security.core.Authentication and set that in the current SpringSecurityContext.
Below is the sudo code for this;
ServiceBSpringSecurityConfig
#Configuration
#EnableWebSecurity
public class ServiceBSpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(authEntryPoint()).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.httpBasic().disabled().and()
.formLogin().disabled().and()
.authorizeRequests().anyRequest().hasRole("ADMIN")
http.addFilterBefore(authTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
#Bean
public AuthTokenFilter authTokenFilter() {
return new AuthTokenFilter();
}
#Bean
public AuthEntryPoint authEntryPoint() {
return new AuthEntryPoint()
}
}
AuthEntryPoint
public class AuthEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPoint.class);
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// Very generic authEntryPoint which simply returns unauthorized
// Could implement additional functionality of forwarding the Application A login-page
logger.error("Unauthorized error: {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
}
}
AuthTokenFilter
public class AuthTokenFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// extract some sort of token or cookie value from request
token = request.getHeader("Token");
if (token != null) {
// Validate the token by retrieving session from redis cache
// Create org.springframework.security.core.Authentication from the token
Authentication auth = authFactory.getAuthentication(token);
// Set the spring security context with the auth
SecurityContextHolder.getContext().setAuthentication(auth);
} else {
// Do something if token not present at all
}
// Continue to to filter chain
filterChain.doFilter(request, response);
}
}
As mentioned this is sudo code so some adjustment might be required. However the general gist of token based auth remains the same.
The application logs all requested urls. This means, that it's critical not to authenticate using url parameters, because it would cause the situation in which logs are full of pairs (login=abc&password=123). For this reason I've configured spring-security to read parameters from request-body. It's done by adding the following line to the request-header:
'Content-Type': 'application/x-www-form-urlencoded'
The body will be:
{'login':'admin', 'password':'password'}
It's fine, but the QA forces me to disable the possibility of authentication via url paramters. At the moment a POST to the following URL will also authenticate:
https://example.com/foo?login=admin&password=password
Does anyone know a trick to disable this option? With an annotation preferably.
Due to the comment I decided to add some more details to my problem. My spring-security is configured with WebSecurityConfigurerAdapter. I have
http.usernameParameter("login")
.passwordParameter("password")
(...)
This makes Spring searching login data in both - parameters and body. I wish to disable searching those parameters in the url.
This makes Spring searching login data in both - parameters and body. I wish to disable searching those parameters in the url.
I believe this is not possible since this behaviour is not implemented by Spring rather than JavaEE itself.
HttpServletRequest.getParameter doc states:
Returns the value of a request parameter as a String, or null if the parameter does not exist. Request parameters are extra information sent with the request. For HTTP servlets, parameters are contained in the query string or posted form data.
But you can try to alter this with filter that should look something like this:
public class DisableGetAuthFiler extends OncePerRequestFilter {
...
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
filterChain.doFilter(
new HttpServletRequestWrapper(request) {
#Override
public String getParameter(String name) {
if (("login".equals(name) && getQueryString().contains("login"))
|| ("password".equals(name) && getQueryString().contains("password"))) {
return null;
} else {
return super.getParameter(name);
}
}
},
response
);
}
}
EDIT Haim Raman proposed another solution that uses existing filter instead of introducing a new one. Only I would suggest overriding obtainUsername() and obtainPassword() instead of attemptAuthentication().
I would like to suggest an alternative which is based on spring-security rater then a workaround as suggested by chimmi.
This answer provide a solution to the issue suggested by xenteros on bres26 answer as well
Override the exiting UsernamePasswordAuthenticationFilter implementation
public class ImprovedUsernamePasswordAuthenticationFilter
extends UsernamePasswordAuthenticationFilter {
#Override
protected String obtainUsername(HttpServletRequest request) {
final String usernameParameter = getUsernameParameter();
validateQueryParameter(request, usernameParameter);
return super.obtainUsername(request);
}
#Override
protected String obtainPassword(HttpServletRequest request) {
final String passwordParameter = getPasswordParameter();
validateQueryParameter(request, passwordParameter);
return super.obtainPassword(request);
}
private void validateQueryParameter(HttpServletRequest request, String parameter) {
final String queryString = request.getQueryString();
if (!StringUtils.isEmpty(queryString)) {
if (queryString.contains(parameter))
throw new AuthenticationServiceException("Query parameters for login are a prohibit, use message body only!");
}
}
}
You need to replace your own implementation with the existing one (see doc here)
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home","/login").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.permitAll()
.and()
//Replace FORM_LOGIN_FILTER with your own custom implementation
.addFilterAt(improvedUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
.and()
//disable csrf to allow easy testing
.csrf().disable();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
public UsernamePasswordAuthenticationFilter improvedUsernamePasswordAuthenticationFilter() throws Exception {
UsernamePasswordAuthenticationFilter authFilter = new ImprovedUsernamePasswordAuthenticationFilter();
authFilter.setRequiresAuthenticationRequestMatcher(
new AntPathRequestMatcher("/login", "POST")
);
authFilter
.setAuthenticationManager(authenticationManager());
authFilter
.setAuthenticationSuccessHandler(
new SavedRequestAwareAuthenticationSuccessHandler()
);
authFilter
.setAuthenticationFailureHandler(
new SimpleUrlAuthenticationFailureHandler("/login?error")
);
return authFilter;
}
}
Advantages: it’s based on spring security and flexible to changes.
Disadvantage: Unfortunately I found Spring Java Config very hard to set and to read
EDIT: I accepted chimmi comment and overridden obtainUsername and obtainPassword
You can find the source code in github.
To the best of my knowledge and intuition, like jhan had mentioned, the appropriate solution would be to use annotation #RequestMapping(value="/login", method="RequestMethod.POST"). Then, no matter what parameters the user may pass with the URL, both the URL and URI will always default to /login. And that is what the logger will document. Not the username and password pairs, but "http://localhost:8080/login", or whatever your port is.
You can achieve this by modifying the UsernamePasswordAuthenticationFilter's RequestMatcher. For example:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.withObjectPostProcessor(new ObjectPostProcessor<UsernamePasswordAuthenticationFilter>() {
#Override
public <O extends UsernamePasswordAuthenticationFilter> O postProcess(
O filter) {
AntPathRequestMatcher pathMatcher = new AntPathRequestMatcher("/login", "POST");
RequestMatcher noQuery = new RequestMatcher() {
#Override
public boolean matches(HttpServletRequest request) {
return request.getQueryString() == null;
}
};
AndRequestMatcher matcher = new AndRequestMatcher(Arrays.asList(pathMatcher, noQuery));
filter.setRequiresAuthenticationRequestMatcher(matcher);
return filter;
}
})
.and()
...
}
}
NOTE: The requirement below does not prevent a GET request from being issued (and thus leaking the credentials). It is really up to the UI to ensure this doesn't happen.
It's fine, but the QA forces me to disable the possibility of
authentication via url paramters.