I made some api with REST Spring. GET request works fine in Postman but when I try to do POST request I receive this error :
{
"timestamp": "2018-09-25T06:39:27.226+0000",
"status": 403,
"error": "Forbidden",
"message": "Forbidden",
"path": "/cidashboard/projects"
}
This is my controller :
#RestController
#RequestMapping(ProjectController.PROJECT_URL)
public class ProjectController {
public static final String PROJECT_URL = "/cidashboard/projects";
private final ProjectService projectService;
public ProjectController(ProjectService projectService) {
this.projectService = projectService;
}
#GetMapping
List<Project> getAllProjects(){
return projectService.findAllProjects();
}
#GetMapping("/{id}")
Project getProjectById(#PathVariable int id) {
return projectService.findProjectById(id);
}
#PostMapping
void addProject(#RequestBody Project newProject) {
projectService.saveProject(newProject);
}
}
Security configuration
initial I wanted to work with ldap, but in my application properties i left only the conection at database....................................................................................................................................................
#EnableGlobalMethodSecurity
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/css/**").permitAll();
// .anyRequest().fullyAuthenticated();
// .and()
// .formLogin().loginPage("/login").permitAll()
// .failureUrl("/login-error");
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.groupSearchBase("ou=groups")
.contextSource(contextSource())
.passwordCompare()
//.passwordEncoder(new LdapShaPasswordEncoder())
.passwordAttribute("userPassword");
}
#Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/resources/static/**"); // #3
}
#Bean
public DefaultSpringSecurityContextSource contextSource() {
return new DefaultSpringSecurityContextSource(Arrays.asList("ldap://localhost:8389/"), "dc=springframework,dc=org");
}
}
Enable spring security with #EnableWebSecurity usage.By default enables csrf support, you have to disable it to prevent Forbidden errors.
#Override
protected void configure(HttpSecurity http) throws Exception {
http //other configure params.
.csrf().disable();
}
PS: 415 unsupported type --> add to your mapping like this annotation for which type of data is sending from Postman.
#PostMapping(consumes = "application/json")
void addProject(#RequestBody Project newProject) {
projectService.saveProject(newProject);
}
In case you want to solve this issue without compromising security, you can send the xsrf-token with your request in postman.
Create a new environment in Postman (e.g. "local").
Create a new variable in this environment (e.g. "xsrf-token")
Go back to your request and make sure the right environment is selected on the top right corner ("local" in this case)
In your POST request, add a header with key "X-XSRF-TOKEN" and value "{{csrf-token}}"
In the "tests" tab, add following code:
var xsrfCookie = pm.cookies.get('XSRF-TOKEN')
pm.environment.set("xsrf-token", xsrfCookie)
The first time you make this request, you will still get a 403, but you'll also receive a cookie with the xsrf-token. The script will copy this token in the environment variable and the next requests you'll make use the appropriate token.
Check the "User-Agent" included in Headers section, If not add the "User-Agent" field
I I was also getting the same error. I found the solution using a different application, not postman {Insomnia REST Client}.
When I went back to postman after wondering, I realized that it is related to permissions in spring security. So after setting the permissions it will work.
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 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.
I have a react app running on a separate port (localhost:3000) that i want to use to authenticate users with, currently i have a proxy setup to my Spring backend (localhost:8080).
Can I somehow manually authenticate instead of http.httpBasic() by sending a POST request to my backend and getting back a session cookie then include the cookie with every request? It would simplify the auth process on iOS side aswell (using this process i could only store the session cookie value in keychain and pass it with every request made to my api)
How would I disable csrf for non-browser requests?
Is there a better approach to this? Diffrent paths for browser and mobile auth?
{
"username": "user",
"password": "12345678"
}
Handle the request in spring controller
#PostMapping(path = "/web")
public String authenticateUser() {
//Receive the auth data here... and perform auth
//Send back session cookie
return "Success?";
}
My WebSecurityConfigurerAdapter.java
#Configuration
#EnableWebSecurity
public class WebsecurityConfig extends WebSecurityConfigurerAdapter {
private final DetailService detailService;
public WebsecurityConfig(DetailService detailService) {
this.detailService = detailService;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(detailService).passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST,"/api/v1/authenticate/new").permitAll()
.antMatchers(HttpMethod.POST,"/api/v1/authenticate/web").permitAll()
.anyRequest().authenticated();
}
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "POST").allowedOrigins("http://localhost:8080");
}
};
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(14);
}
}
You can create an endpoint that takes user's credentials in a request body, perform authentication and then set tokens, and other required parameters in HttpOnly cookies.
After setting cookies, subsequent requests can read access/refresh token from cookies and add it in requests, you can then use custom CheckTokenEndpoint to verify tokens.
In the following example TokenParametersDto is a POJO that has username and password properties.
For issuing token (by verifying credentials) you can delegate call to TokenEndpoint#postAccessToken(....) or use its logic to your own method.
#PostMapping(path = "/oauth/http/token", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> issueToken(#RequestBody final #Valid #NotNull TokenParametersDto tokenParametersDto,
final HttpServletResponse response) {
final OAuth2AccessToken token = tokenService.issueToken(tokenParametersDto);
storeTokenInCookie(token, response);
return new ResponseEntity<>(HttpStatus.OK);
}
private void storeTokenInCookie(final OAuth2AccessToken token, final HttpServletResponse response) {
final Cookie accessToken = new Cookie("access_token", token.getValue());
accessToken.setHttpOnly(true);
accessToken.setSecure(sslEnabled);
accessToken.setPath("/");
accessToken.setMaxAge(cookieExpiration);
final Cookie tokenType = new Cookie("token_type", token.getTokenType());
tokenType.setHttpOnly(true);
tokenType.setSecure(sslEnabled);
tokenType.setPath("/");
tokenType.setMaxAge(cookieExpiration);
// Set Refresh Token and other required cookies.
response.addCookie(accessToken);
response.addCookie(tokenType);
}
Check this answer for disabling CSRF for a specific URL section.
How can I get my custom ResponseEntityExceptionHandler or OAuth2ExceptionRenderer to handle Exceptions raised by Spring security on a pure resource server?
We implemented a
#ControllerAdvice
#RestController
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
so whenever there is an error on the resource server we want it to answer with
{
"message": "...",
"type": "...",
"status": 400
}
The resource server uses the application.properties setting:
security.oauth2.resource.userInfoUri: http://localhost:9999/auth/user
to authenticate and authorize a request against our auth server.
However any spring security error will always bypass our exception handler at
#ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<Map<String, Object>> handleInvalidTokenException(InvalidTokenException e) {
return createErrorResponseAndLog(e, 401);
}
and produce either
{
"timestamp": "2016-12-14T10:40:34.122Z",
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/api/templates/585004226f793042a094d3a9/schema"
}
or
{
"error": "invalid_token",
"error_description": "5d7e4ab5-4a88-4571-b4a4-042bce0a076b"
}
So how do I configure the security exception handling for a resource server? All I ever find are examples on how to customize the Auth Server by implementing a custom OAuth2ExceptionRenderer. But I can't find where to wire this to the resource server's security chain.
Our only configuration/setup is this:
#SpringBootApplication
#Configuration
#ComponentScan(basePackages = {"our.packages"})
#EnableAutoConfiguration
#EnableResourceServer
As noted in previous comments the request is rejected by the security framework before it reaches the MVC layer so #ControllerAdvice is not an option here.
There are 3 interfaces in the Spring Security framework that may be of interest here:
org.springframework.security.web.authentication.AuthenticationSuccessHandler
org.springframework.security.web.authentication.AuthenticationFailureHandler
org.springframework.security.web.access.AccessDeniedHandler
You can create implementations of each of these Interfaces in order to customize the response sent for various events: successful login, failed login, attempt to access protected resource with insufficient permissions.
The following would return a JSON response on unsuccessful login attempt:
#Component
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler
{
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException ex) throws IOException, ServletException
{
response.setStatus(HttpStatus.FORBIDDEN.value());
Map<String, Object> data = new HashMap<>();
data.put("timestamp", new Date());
data.put("status",HttpStatus.FORBIDDEN.value());
data.put("message", "Access Denied");
data.put("path", request.getRequestURL().toString());
OutputStream out = response.getOutputStream();
com.fasterxml.jackson.databind.ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(out, data);
out.flush();
}
}
You also need to register your implementation(s) with the Security framework. In Java config this looks like the below:
#Configuration
#EnableWebSecurity
#ComponentScan("...")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
#Override
public void configure(HttpSecurity http) throws Exception
{
http
.addFilterBefore(corsFilter(), ChannelProcessingFilter.class)
.logout()
.deleteCookies("JESSIONID")
.logoutUrl("/api/logout")
.logoutSuccessHandler(logoutSuccessHandler())
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/api/login")
.failureHandler(authenticationFailureHandler())
.successHandler(authenticationSuccessHandler())
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler());
}
/**
* #return Custom {#link AuthenticationFailureHandler} to send suitable response to REST clients in the event of a
* failed authentication attempt.
*/
#Bean
public AuthenticationFailureHandler authenticationFailureHandler()
{
return new RestAuthenticationFailureHandler();
}
/**
* #return Custom {#link AuthenticationSuccessHandler} to send suitable response to REST clients in the event of a
* successful authentication attempt.
*/
#Bean
public AuthenticationSuccessHandler authenticationSuccessHandler()
{
return new RestAuthenticationSuccessHandler();
}
/**
* #return Custom {#link AccessDeniedHandler} to send suitable response to REST clients in the event of an attempt to
* access resources to which the user has insufficient privileges.
*/
#Bean
public AccessDeniedHandler accessDeniedHandler()
{
return new RestAccessDeniedHandler();
}
}
In case if you're using #EnableResourceServer, you may also find convenient to extend ResourceServerConfigurerAdapter instead of WebSecurityConfigurerAdapter in your #Configuration class. By doing this, you may simply register a custom AuthenticationEntryPoint by overriding configure(ResourceServerSecurityConfigurer resources) and using resources.authenticationEntryPoint(customAuthEntryPoint()) inside the method.
Something like this:
#Configuration
#EnableResourceServer
public class CommonSecurityConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationEntryPoint(customAuthEntryPoint());
}
#Bean
public AuthenticationEntryPoint customAuthEntryPoint(){
return new AuthFailureHandler();
}
}
There's also a nice OAuth2AuthenticationEntryPoint that can be extended (since it's not final) and partially re-used while implementing a custom AuthenticationEntryPoint. In particular, it adds "WWW-Authenticate" headers with error-related details.
You are not able to make use of Spring MVC Exception handler annotations such as #ControllerAdvice because spring security filters kicks in much before Spring MVC.
If you're using token validation URL with config similar to Configuring resource server with RemoteTokenServices in Spring Security Oauth2 which returns HTTP status 401 in case of unauthorized:
#Primary
#Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("https://token-validation-url.com");
tokenService.setTokenName("token");
return tokenService;
}
Implementing custom authenticationEntryPoint as described in other answers (https://stackoverflow.com/a/44372313/5962766) won't work because RemoteTokenService use 400 status and throws unhandled exceptions for other statuses like 401:
public RemoteTokenServices() {
restTemplate = new RestTemplate();
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
#Override
// Ignore 400
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400) {
super.handleError(response);
}
}
});
}
So you need to set custom RestTemplate in RemoteTokenServices config which would handle 401 without throwing exception:
#Primary
#Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("https://token-validation-url.com");
tokenService.setTokenName("token");
RestOperations restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
#Override
// Ignore 400 and 401
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
}
tokenService.setRestTemplate(restTemplate);
return tokenService;
}
And add dependency for HttpComponentsClientHttpRequestFactory:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
OAuth2ExceptionRenderer is for an Authorization Server. The correct answer is likely to handle it like detailed in this post (that is, ignore that it's oauth and treat it like any other spring security authentication mechanism): https://stackoverflow.com/a/26502321/5639571
Of course, this will catch oauth related exceptions (which are thrown before you reach your resource endpoint), but any exceptions happening within your resource endpoint will still require an #ExceptionHandler method.
We can use this security handler to pass the handler to spring mvc #ControllerAdvice
#Component
public class AuthExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
private static final Logger LOG = LoggerFactory.getLogger(AuthExceptionHandler.class);
private final HandlerExceptionResolver resolver;
#Autowired
public AuthExceptionHandler(#Qualifier("handlerExceptionResolver") final HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
LOG.error("Responding with unauthorized error. Message - {}", authException.getMessage());
resolver.resolveException(request, response, null, authException);
}
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
LOG.error("Responding with access denied error. Message - {}", accessDeniedException.getMessage());
resolver.resolveException(request, response, null, accessDeniedException);
}
}
Then define the exception by using #ControllerAdvice so that we can manage the global exception handler in one place..
This is possible. Since the original question is for a REST controller that needs to return a custom JSON response, I will write up a complete answer step by step which worked for me. First and foremost, it seems you cannot handle this with a #ControllerAdvice that extends ControllResponseEntityExceptionHandler. You need a separate handler that extends AccessDeniedHandler. Follow the below steps.
Step 1: Create a custom handler class that extends AccessDeniedHandler
#Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
private static final String JSON_TYPE = "application/json";
#Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
MyErrorList errors = new MyErrorList();
errors.addError(new MyError("", "You do not have permission to access this resource."));
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType(JSON_TYPE);
OutputStream output = response.getOutputStream();
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(output, errors);
output.flush();
}
}
'MyError' above is a simple POJO to represent an error json structure and MyErrorList is another POJO that holds a list of 'MyError's.
Step 2: Inject the Handler created above into the Security configuration
#Autowired
private VOMSAccessDeniedHandler accessDeniedHandler;
Step 3: Register the accessDeniedHandler in your configure method
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
With Step 2 and Step 3, Your SecurityConfiguration should look something like this (Note that I am omitting code that is not relevant to this problem to shorten the length of this answer):
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private MyAccessDeniedHandler accessDeniedHandler;
// Other stuff
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/register").permitAll()
.antMatchers("/authenticate").permitAll()
.antMatchers("/public").permitAll()
.anyRequest().authenticated()
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
Adapting the accepted answer for use with Spring OAuth2ResourceServer for JWT authentication, because without special configuration, it will register its own BearerTokenAuthenticationEntryPoint, and ignore the one we set in .exceptionHandling().authenticationEntryPoint()
Hence, in our WebSecurityConfigurerAdapter we have:
#Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
protected void configure(HttpSecurity http) throws Exception {
http
// ... all the usual stuff ...
// configure OAuth2 (OIDC) JWT and set a custom authentication failure handler
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt().and()
.authenticationEntryPoint(authenticationFailureHandler));
}
where AuthenticationFailureHandler is coded as suggested in earlier answers:
#Component
public class AuthenticationFailureHandler implements AuthenticationEntryPoint {
public AuthenticationFailureHandler() {
}
// Autowire our own CustomExceptionHandler: must be qualified because Spring Boot has others in the classpath
#Autowired
#Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws
IOException {
resolver.resolveException(request, response, null, authException);
}
}
In our CustomExceptionHandler (which is autowired above, but not mentioned by class name explicitly) we add a method for AuthenticationException handling:
#ExceptionHandler(value = {AuthenticationException.class})
protected ResponseEntity<?> handleAuthenticationException(RuntimeException ex, WebRequest request) {
return ... something ... // create custom error response here
}
Spring 3.0 Onwards,You can use #ControllerAdvice (At Class Level) and extends org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler class from CustomGlobalExceptionHandler
#ExceptionHandler({com.test.CustomException1.class,com.test.CustomException2.class})
public final ResponseEntity<CustomErrorMessage> customExceptionHandler(RuntimeException ex){
return new ResponseEntity<CustomErrorMessage>(new CustomErrorMessage(false,ex.getMessage(),404),HttpStatus.BAD_REQUEST);
}
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.