#PreAuthorize makes validation not working for primitive type - spring-boot

First of all, this is my development environent.
Spring boot: org.springframework.boot:2.5.6
io.spring.dependency-management: 1.0.11.RELEASE
Spring Security: org.springframework.boot:spring-boot-starter-security
org.springframework.boot:spring-boot-starter-webflux
kotlin
org.springframework.boot:spring-boot-starter-validation
When I call this api with invalid value of limit(as 100), validator(#Max) is working successfuly.
Call: GET {{apiEndpoint}}/workspaces/{{workspaceId}}/test?limit=1000
Code
#GetMapping("/test")
#ResponseStatus(HttpStatus.OK)
suspend fun test(
auth: AuthToken,
#PathVariable workspaceId: String,
#RequestParam(name = "limit", defaultValue = "15") #Max(20) limit: Int
): String {
return "OK"
}
Response
HTTP/1.1 500 Internal Server Error
requestId: 32511EB3433F4D1DBEAC56641E6BE1A2
Content-Type: application/json
Content-Length: 213
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1 ; mode=block
Referrer-Policy: no-referrer
{
"requestId": "32511EB3433F4D1DBEAC56641E6BE1A2",
"message": "test.limit: 20 이하여야 합니다",
"sys": {
"id": "UnhandledError",
"code": "50001",
"type": "Error"
},
"details": {
"exception": "ConstraintViolationException"
}
}
But with #PreAutorize annotation, validator is not working.
Code
#GetMapping("/test")
#ResponseStatus(HttpStatus.OK)
#PreAuthorize("hasRole('USER')")
suspend fun test(
auth: AuthToken,
#PathVariable workspaceId: String,
#RequestParam(name = "limit", defaultValue = "15") #Max(20) limit: Int
): String {
return "OK"
}
Response
HTTP/1.1 200 OK
requestId: B0F832B0AACA49DDB2F5774FE3BFB8D4
Content-Type: application/json;charset=utf8
Content-Length: 2
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1 ; mode=block
Referrer-Policy: no-referrer
OK
Why spring-security makes validator not working??
Plus, validator for Object(not primitive type) type is working successfuly with #PreAuthorize.
#PostMapping("/test")
#ResponseStatus(HttpStatus.OK)
#PreAuthorize("hasRole('USER')")
suspend fun test(
auth: AuthToken,
#PathVariable workspaceId: String,
#RequestBody #Valid body: Payload // It works!!
): String {
return "OK"
}
data class Payload(
#field:Max(10)
val age: Int
)
Please help me.
I also tried to use `groups(ValidationGroups)', but it's not works too.

Related

Spring webflux: ServerResponse redirection

This is my related code:
#RestController
public class GicarController {
#PostMapping("/login")
public Mono<ServerResponse> gicar(#RequestHeader("GICAR_ID") String gicarId) {
return ServerResponse.temporaryRedirect(URI.create("/me")).build();
}
}
Issue arises when I'm calling to _/login endpoint:
$ curl -i -X POST localhost:8080/login -H "GICAR_ID: tre"
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: text/event-stream;charset=UTF-8
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1 ; mode=block
Referrer-Policy: no-referrer
curl: (18) transfer closed with outstanding read data remaining
Why am I getting an 200 http code response?
On spring boot logging I'm getting this exception:
022-06-27 13:11:19.931 ERROR 79654 --- [or-http-epoll-2] r.n.http.server.HttpServerOperations : [9750a9d8-1, L:/127.0.0.1:8080 - R:/127.0.0.1:33150] Error finishing response. Closing connection
org.springframework.core.codec.CodecException: Type definition error: [simple type, class org.springframework.web.reactive.function.server.DefaultServerResponseBuilder$WriterFunctionResponse]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.springframework.web.reactive.function.server.DefaultServerResponseBuilder$WriterFunctionResponse and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
Why above exception is reaised?
Any ideas?
According to Spring documentation ServerResponse
Represents a typed server-side HTTP response, as returned by a handler function or filter function.
and it supposed to be used in Functional Endpoints
#Configuration
public class GicarConfiguration {
#Bean
public RouterFunction<ServerResponse> route() {
return RouterFunctions
.route(POST("/login"), this::loginHandler);
}
private Mono<ServerResponse> loginHandler(ServerRequest request) {
var gicarId = request.headers().firstHeader("GICAR_ID");
return ServerResponse.temporaryRedirect(URI.create("/me")).build();
}
}
If you still want to use Annotated Controllers, use ResponseEntity instead
#RestController
public class GicarController {
#PostMapping("/login")
public Mono<ResponseEntity<Void>> gicar() {
return Mono.just(ResponseEntity
.status(HttpStatus.TEMPORARY_REDIRECT)
.header(HttpHeaders.LOCATION, "/me")
.build()
);
}
}

How to properly configure ldap authentication in spring boot? Every time I log in, it redirects to /login after calling /auth

NOTE
Suggested answer didn't work.
I have the following configuration in WebSecurityConfigurerAdapter:
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userSearchBase("somevalue")
.userSearchFilter("somevalue")
.contextSource().url("somevalue").port("somevalue")
.managerDn("somevalue").managerPassword("somevalue");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/*.css", "/*.html", "/*.js", "/*.jpg").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login")
.loginProcessingUrl("/auth")
.permitAll();
}
And this is my configuration in WebMvcConfigurer:
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/*.css", "/*.html", "/*.js", "/*.jpg")
.addResourceLocations("classpath:/static/");
registry.addResourceHandler("/", "/login", "/myapp")
.addResourceLocations("classpath:/static/index.html")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
#Override
protected Resource getResource(String resourcePath, Resource location) {
if (resourcePath.startsWith("/api") || resourcePath.startsWith("/api".substring(1))) {
return null;
}
return location.exists() && location.isReadable() ? location : null;
}
});
}
WHen I click on login button I send the following request in angular:
this.http.post(`localhost:8080/auth`, {username: 'user', password: 'password'})...
But I only got redirected to /login. Here's the network console:
auth 302
login 200
Here's the request (/auth) headers and body:
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Content-Length: 47
Content-Type: application/json
Cookie: JSESSIONID=*****somecookie*****
Host: localhost:8080
Origin: http://localhost:8080
Referer: http://localhost:8080/login
Payload:
{username: "user", password: "password"}
General:
Request URL: http://localhost:8080/auth
Request Method: POST
Status Code: 302
Remote Address: [::1]:8080
Referrer Policy: no-referrer-when-downgrade
Response headers:
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 0
Date: Fri, 01 Mar 2019 10:49:09 GMT
Expires: 0
Location: http://localhost:8080/login
Pragma: no-cache
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
And here's when it redirects to /login:
General:
Request URL: http://localhost:8080/login
Request Method: GET
Status Code: 200
Remote Address: [::1]:8080
Referrer Policy: no-referrer-when-downgrade
Request:
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Cookie: JSESSIONID=*****somecookie*****
Host: localhost:8080
Referer: http://localhost:8080/login
Basically, the response body is the content of my index.html.
And in the console it says:
error: {error: SyntaxError: Unexpected token < in JSON at position 0 at JSON.parse...
headers: e {normalizedNames: Map(0), lazyUpdate: null, lazyInit: ƒ}
message: "Http failure during parsing for http://localhost:8080/login"
name: "HttpErrorResponse"
ok: false
status: 200
statusText: "OK"
url: "http://localhost:8080/login"
Also, is my ldap config correct?
How to properly configure for ldap authentication?
UPDATE
As per recommendation, I changed my code to send the body as http params. The request is now this:
Request Headers:
Content-Type: application/x-www.form-urlencoded;charset=UTF-8
Form Data
username=user&password=password
It still the same issue
The answer to my question is the missing csrf. If I don't disable csrf, spring security seems to look for this, otherwise, my request gets rerouted back to login page. I fixed by adding the following:
http.csrf().disable()
For the Request method 'POST' not supported during /login, in my AuthenticationSuccessHandler.onAuthenticationSuccess(), I put
request.getRequestDispatcher(targetURI).forward(request, response);
Instead, I just set the reponse with a string value:
response.getOutputStream().println(mapper.writeValueAsString(data));

Spring Boot Test with Mockito : #Validated annotation is being ignored during unit tests

I'm using Spring Boot 2.1.1, JUnit 5, Mockito 2.23.4.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>
Here's my controller :
#RestController
#Validated
public class AramaController {
#ResponseStatus(value = HttpStatus.OK)
#GetMapping("/arama")
public List<Arama> arama(#RequestParam #NotEmpty #Size(min = 4, max = 20) String query) {
return aramaService.arama(query);
}
}
This controller works as expected.
curl with no "query" parameter returns Bad Request 400 :
~$ curl http://localhost:8080/arama -v
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /arama HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 400
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Length: 0
< Date: Wed, 12 Dec 2018 21:47:11 GMT
< Connection: close
<
* Closing connection 0
curl with "query=a" as parameter returns Bad Request 400 as well :
~$ curl http://localhost:8080/arama?query=a -v
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /arama?query=a HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 400
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Wed, 12 Dec 2018 21:47:33 GMT
< Connection: close
<
* Closing connection 0
{"message":"Input error","details":["size must be between 4 and 20"]}
This controller and validation works flawlessly when running on a server.
During unit tests the #Validated annotation doesn't seem to have any effect.
Here my test code :
#ExtendWith(MockitoExtension.class)
class AramaControllerTest {
#Mock
private AramaService aramaService;
#InjectMocks
private AramaController aramaController;
private MockMvc mockMvc;
#BeforeEach
private void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(aramaCcontroller)
.setControllerAdvice(new RestResponseEntityExceptionHandler())
.build();
}
#Test
void aramaValidationError() throws Exception {
mockMvc
.perform(
get("/arama").param("query", "a")
)
.andExpect(status().isBadRequest());
verifyNoMoreInteractions(aramaService);
}
}
This test results in failure :
java.lang.AssertionError: Status expected:<400> but was:<200>
Expected :400
Actual :200
Since the #Valid annotations pass my other test cases, and they work without loading the Spring context, is there a way to make the #Validated annotation work as well with Mockito (again, without loading the Spring context) ?
I got the answer elsewhere and wanted to share :
Without starting up the context, you won't have #Validator getting
tested because validator instances are Spring beans. However, #Valid
will work as it is a JSR-303 standard.
As of now, what I can suggest is.
#SpringBootTest
#ExtendWith(SpringExtension.class)
maybe you can try using #WebMvcTest and add SpringExtension
#ExtendWith({SpringExtension.class, MockitoExtension.class})
#WebMvcTest(AramaController.class)
class AramaControllerTest {
#Mock
private AramaService aramaService;
#InjectMocks
private AramaController aramaController;
#Autowired
private MockMvc mockMvc;
#Test
void aramaValidationError() throws Exception {
mockMvc
.perform(
get("/arama").param("query", "a")
)
.andExpect(status().isBadRequest());
verifyNoMoreInteractions(aramaService);
}
}

RequestMapping GET and POST methods handling in Spring REST

I have such code:
#Configuration
#ComponentScan
#EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#RequestMapping(value = "/foo", method = RequestMethod.POST)
String testPost(#RequestParam("param1") String param1, #RequestParam("param2") String param2) {
return param1 + param2;
}
#RequestMapping("/foo")
String testGet(#RequestParam String param1) {
return param1;
}
}
And I execute such curl expressions:
curl --verbose http://localhost:9000/foo?param1=Sergei
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 9000 (#0)
> GET /foo?param1=Sergei HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:9000
> Accept: */*
>
< HTTP/1.1 404 Not Found
* Server Apache-Coyote/1.1 is not blacklisted
< Server: Apache-Coyote/1.1
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 29 Dec 2015 08:55:56 GMT
<
* Connection #0 to host localhost left intact
{"timestamp":1451379356514,"status":404,"error":"Not Found","message":"No message available","path":"/foo"}
and,
curl --verbose --data "param1=value1&param2=value2" http://localhost:9000/foo
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 9000 (#0)
> POST /foo HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:9000
> Accept: */*
> Content-Length: 27
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 27 out of 27 bytes
< HTTP/1.1 405 Method Not Allowed
* Server Apache-Coyote/1.1 is not blacklisted
< Server: Apache-Coyote/1.1
< Allow: HEAD, GET
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 29 Dec 2015 09:01:46 GMT
<
* Connection #0 to host localhost left intact
{"timestamp":1451379706832,"status":405,"error":"Method Not Allowed","exception":"org.springframework.web.HttpRequestMethodNotSupportedException","message":"Request method 'POST' not supported","path":"/foo"}
Help me please get both my methods work.
Add a RestController or Controller stereotype annotation to your Application class like this:
#RestController
#Configuration
#ComponentScan
#EnableAutoConfiguration
public class Application {
...
}
Note: You can use SpringBootApplication meta annotation instead of those three, so you would have:
#RestController
#SpringBootApplication
public class Application {
....
}
You should change the scope level of the two controller methods to public. Right now, they don't have any, so the methods are package local by default.
public String testPost(#RequestParam("param1") String param1, #RequestParam("param2") String param2) {
public String testGet(#RequestParam String param1) {

WebSocket with Spring backend loses connection after a while, onclose is not called

In our spring application most of the controllers are protected with oauth security. Websockets are behind basic. Before accessing websocket logged user asks for username and hashed password for websocket connection. Both are going to be generated, but for now for testing purposes it always returns the same creditentials.
URL for info looks as follows:
https://user:debaee4affbeaba909a184066981d55a#localhost:8000/project-name/chat/info
WebSocket is opened properly. We can send few messages and they go trough broker and are displayed to the users. Here's request info from chrome tools:
Remote Address:127.0.0.1:8000
Request URL:https://benny:debaee4affbeaba909a184066981d55a#localhost:8000/project-name/chat/033/7szz8k_f/xhr_send
Request Method:POST
Status Code:204 No Content
Response Headers:
HTTP/1.1 204 No Content
server: Apache-Coyote/1.1
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
strict-transport-security: max-age=31536000 ; includeSubDomains
x-frame-options: DENY
access-control-allow-origin: https://localhost:8000
access-control-allow-credentials: true
vary: Origin
content-type: text/plain;charset=UTF-8
date: Mon, 15 Jun 2015 08:22:43 GMT
Connection: keep-alive
Request Headers:
POST /project-name/chat/033/7szz8k_f/xhr_send HTTP/1.1
Host: localhost:8000
Connection: keep-alive
Content-Length: 143
Origin: https://localhost:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: https://localhost:8000/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8,pl;q=0.6
Cookie: JSESSIONID=FF967D3DD1247C1D572C15CF8A3D5E8E; i18next=en; language=pl; tmhDynamicLocale.locale=%22pl-pl%22
["SEND\npriority:9\ndestination:/random/chat/1/FUNNY\ncontent-length:49\n\n{\"message\":\"sfsdf\",\"display\":\"The great wizard.\"}\u0000"]
But after a minute or so when sending another request we get 404 response. It doesn't matter if any SEND requests were issued before. We can write 50+ messages in that time span and then we get 404.
Sample 404 request data follows:
Remote Address:127.0.0.1:8000
Request URL:https://hill:debaee4affbeaba909a184066981d55a#localhost:8000/project-name/chat/033/7szz8k_f/xhr_send
Request Method:POST
Status Code:404 Not Found
Response Headers:
HTTP/1.1 404 Not Found
server: Apache-Coyote/1.1
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
strict-transport-security: max-age=31536000 ; includeSubDomains
x-frame-options: DENY
content-length: 0
date: Mon, 15 Jun 2015 08:24:17 GMT
Connection: keep-alive
Request Headers:
POST /project-name/chat/033/7szz8k_f/xhr_send HTTP/1.1
Host: localhost:8000
Connection: keep-alive
Content-Length: 143
Origin: https://localhost:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: https://localhost:8000/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8,pl;q=0.6
Cookie: JSESSIONID=FF967D3DD1247C1D572C15CF8A3D5E8E; i18next=en; language=pl; tmhDynamicLocale.locale=%22pl-pl%22
Request Payload:
["SEND\npriority:9\ndestination:/random/chat/1/FUNNY\ncontent-length:49\n\n{\"message\":\"yhgfh\",\"username\":\"The great wizard.\"}\u0000"]
When setting up stomp we setup function to react onclose:
socket.client = new SockJS(targetUrl);
socket.stomp = Stomp.over(socket.client);
socket.stomp.connect({}, startListener);
socket.stomp.onclose = reconnect;
With reconnect function looking as this(it's in AngularJS):
var reconnect = function() {
$log.debug('Reconnect called');
$timeout(function() {
initialize();
}, this.RECONNECT_TIMEOUT);
};
But the function is never called.
Controller for Chat is pretty simple:
#Controller
public class StageChatController {
#Inject
private SimpMessagingTemplate template;
#Inject
private ChatMessageRepository chatMessageRepository;
#MessageMapping("/chat/{channel}/{type}")
public void sendMessage(#DestinationVariable Long channel, #DestinationVariable ChatType type, ChatMessageDto message) {
ChatMessage chatMessage = new ChatMessage();
chatMessage.setDatestamp(LocalDateTime.now());
chatMessage.setMessage(message.getMessage());
chatMessage.setChannelId(channel);
chatMessage.setChatType(type);
chatMessage.setDisplayName(message.getDisplay());
chatMessage = this.chatMessageRepository.save(chatMessage);
this.template.convertAndSend("/channel/" + project + "/" + type, chatMessage);
}
Security for chat overrides oauth security for chat urls:
#Configuration
#EnableWebSecurity
#Order(2)
static class BasicAccessConfig extends WebSecurityConfigurerAdapter {
#Inject
private OAuth2ClientContextFilter oauth2ClientContextFilter;
#Value("${project.name.chat.token}")
private String chat_token;
#Override
protected void configure(HttpSecurity http) throws Exception {
//#formatter:off
http
.requestMatcher(new AntPathRequestMatcher("/chat/**/*"))
.authorizeRequests().anyRequest().authenticated()
.and()
.httpBasic()
.and()
.anonymous().disable()
.csrf().disable()
.addFilterBefore(this.oauth2ClientContextFilter, SecurityContextPersistenceFilter.class);
;
//#formatter:on
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**");
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("hill").password(this.chat_token).authorities("read_chat");
}
}

Resources