Spring security with thymeleaf and ajax call - spring-boot

I use spring boot with spring-security
#Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers(
"/",
"/email",
"/starter**",
"/forgetpassword**",
"/resetpassword**",
"/register**",
"/register/**",
"/css/**",
"/js/**",
"/img/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").permitAll()
.successHandler(customAuthenticationSuccessHandler)
.and()
.logout();
}
I want to do ajax call to save information
#PostMapping("/book")
public ResponseEntity generateBook(#RequestBody Book book){
}
I tried this but i get a 403
$.ajax({
url : 'http://localhost:8080/book',
type : 'post',
contentType: 'application/json',
dataType: "json",
headers:{
'_csrf' : '[[${_csrf.token}]]',
'_csrf_header' : '[[${_csrf.headerName}]]'
},
data : '....',
success : function(response) {
debugger;
...
}
});
I enabled spring security log with that
logging.level.org.springframework.security=DEBUG
Edit
I get this
Invalid CSRF token found for http://localhost:8080/book
Responding with 403 status code
I just don't understand why?
the value of the token is generated by the server
Edit 2
Scenario is
User log to the system, he arrive on page with a button, When He click generate book start.
Generate book fail, if user try to change its password, i see the same csrf token value than the one created by
[[${_csrf.token}]]

Add these lines to head tag:
<meta id="_csrf" name="_csrf" th:content="${_csrf.token}"/>
<meta id="_csrf_header" name="_csrf_header" th:content="${_csrf.headerName}"/>
And in the AJAX request:
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
//Add beforeSend to the ajax call, like:
$.ajax({
type: ‘POST’,
url: url,
beforeSend: function(request) {
request.setRequestHeader(header, token);
},
data: data
});

Related

Why are Cross-Domain Cookies not carried in the new page's ajax request?

There are two pages in my front-end demo, login.html and home.html.
When I click the "login" button in the login.html, a Ajax request which carries the username and password will be sent to the back-end. If I log in successfully, the page will jump to home.html.
<body>
<button id="login-btn">login</button>
<script>
$('#login-btn').click(function() {
$.ajax({
method: 'POST',
url: 'http://localhost:8080/api/login',
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify({ 'username': 'hover', 'password': 'hover' }),
success: function(data, status, xhr) {
alert(data.success);
// jump to home
window.location.href = 'home.html';
},
error: function(xhr, error_type, exception) {
console.log(error_type);
}
});
});
</script>
</body>
There is a "request" button in home.html. When I click the "request" button, a Ajax request which carries a "greeting" will be sent to the back-end. If I have logged in, I will receive the echo response, otherwise I will receive a "not yet logged in" message.
To tell the back-end "I have logged in!", I set withCredentials to true to make the request carry the cookie which contains JSESSIONID.
<body>
<button id="request-btn">request</button>
<script>
$('#request-btn').click(function () {
$.ajax({
method: 'POST',
url: 'http://localhost:8080/api/echo',
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify({ 'greeting': 'hello' }),
crossDomain: true,
xhrFields: {
withCredentials: true
},
success: function (data, status, xhr) {
console.log(data);
},
error: function (xhr, error_type, exception) {
console.log(error_type);
}
});
});
</script>
</body>
I use SpringMVC to bulid my back-end and I have configured CORS and LoginInterceptor.
#Configuration
#EnableWebMvc
#ComponentScan(basePackages = "org.example")
public class AppConfig implements WebMvcConfigurer {
//CORS
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://127.0.0.1:5500")
.allowedMethods("GET", "POST", "OPTIONS")
.allowedHeaders("content-type", "origin")
.allowCredentials(true).maxAge(1800);
}
// Register the LoginInterceptor
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**").excludePathPatterns("/login");
}
}
LoginInterceptor will intercept any requests other than Preflight OPTIONS and login requests.
public class LoginInterceptor implements HandlerInterceptor {
#Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws IOException {
// Let OPTIONS pass directly
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
// Check whether the client has already logged in.
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("username") != null) {
return true;
}
else {
response.setContentType("application/json; charset=utf-8");
// {"message": "not yet logged in"}
response.getWriter().write("{\"message\": \"not yet logged in\"}");
}
return false;
}
}
In ApiController, the login method prints the username and password, creates a session and stores the username, and returns a "success" message.
The echo method returns the greeting message from home.html.
#RestController
public class ApiController {
#PostMapping(value = "/login", produces = "application/json; charset=utf-8")
public String login(#RequestBody Map<String, String> param, HttpServletRequest request) {
String username = param.get("username");
String password = param.get("password");
System.out.println("username: " + username);
System.out.println("password: " + password);
// create a session and store the username
request.getSession().setAttribute("username", username);
// {"success": true}
return "{\"success\": true}";
}
#PostMapping(value = "/echo", produces = "application/json; charset=utf-8")
public String echo(#RequestBody Map<String, String> param) {
String greeting = param.get("greeting");
System.out.println(greeting);
// {"greeting": "hello"}
return "{\"greeting\": " + "\"" + greeting + "\"}";
}
}
Because a session is created in the login method, the header of the response to the login request will have Set-Cookie field set to JSESSIONID, which is what I want to carry in the home.html's request.
login request images:
preflight OPTIONS
POST
But in reality, when I later jump to home.html and send greeting requests, JSESSIONID is not carried. Thus I receive the "not yet logged in" message. I wonder why this happens?
images:
preflight OPTIONS
POST which does not carry a cookie
If I set that in "login" Ajax request as well, the problem will be solved.
Just like the codes:
<body>
<button id="login-btn">login</button>
<script>
$('#login-btn').click(function() {
$.ajax({
method: 'POST',
url: 'http://localhost:8080/api/login',
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify({ 'username': 'hover', 'password': 'hover' }),
xhrFields: {
withCredentials: true
},
success: function(data, status, xhr) {
alert(data.success);
// jump to home
window.location.href = 'home.html';
},
error: function(xhr, error_type, exception) {
console.log(error_type);
}
});
});
</script>
</body>
On the face of it, the problem is solved.
Actually, I only want to make the "greeting" Ajax request carry the cookie, but now I have to set withCredentials to true in "login" Ajax request as well.
So what's going on behind the scenes?
The XMLHttpRequest.withCredentials property is a Boolean that indicates whether or not cross-site Access-Control requests should be made using credentials such as cookies, authorization headers or TLS client certificates. Setting withCredentials has no effect on same-site requests.
In addition, this flag is also used to indicate when cookies are to be ignored in the response. The default is false. XMLHttpRequest from a different domain cannot set cookie values for their own domain unless withCredentials is set to true before making the request. The third-party cookies obtained by setting withCredentials to true will still honor same-origin policy and hence can not be accessed by the requesting script through document.cookie or from response headers.
In addition, this flag is also used to indicate when cookies are to be ignored in the response.

Redirect from an AJAX Post

I have an AJAX call to my endpoint in my Spring controller. After verifying the info from the POST, my controller makes a redirect decision, whether to forward the request to the next location, or send them back to a login page. The response to the post is correct, it's a 302 with the Location header set correctly. However, when the page makes the redirect call, it makes an OPTIONS call to the URL, then a GET call, which just returns the HTML. Great, I have the HTML, but the page stays on my JSP page and never goes to the external URL. How do I manage this?
Sample Java code:
#RequestMapping(value = "/token/{token_code}", method = {RequestMethod.GET})
public void validateToken(HttpServletRequest servletRequest, HttpServletResponse servletResponse, #PathVariable String token_code) {
//set some servlet request attributes from incoming packet info
if(isTokenValid(token_code)) {
servletRequest.getRequestDispatcher(MyConstants.JSP_DEVICE_INFO).forward(servletRequest, servletResponse);
}
else {
servletRequest.getRequestDispatcher(MyConstants.FAILURE_URL).forward(servletRequest, servletResponse);
}
}
#RequestMapping(value = "/token/tokenRedirect", method = {RequestMethod.POST},headers = "content-type=application/json",consumes = {MediaType.APPLICATION_JSON_VALUE})
public ModelAndView getSession(HttpServletRequest servletRequest,
HttpServletResponse servletResponse,
#RequestBody TokenValidateRequest request)
{
boolean isValid = verifyCollectedInfo(request);
if(isValid) {
servletResponse.setHeader("Location", request.url());
servletResponse.setStatus(302);
}
else {
servletResponse.setHeader("Location", MyConstants.FAILURE_URL);
servletResponse.setStatus(302);
}
}
JSP Ajax call:
$.ajax({
headers: {
'accept': 'application/json',
'content-type': 'application/json'
},
type: "POST",
url: "tokenRedirect",
context:document.body,
contentType:"application/json",
data:JSON.stringify(TokenValidateObject)
});
So when I inspect my network traffic, I see the 302 status is set for the response and the Location header has the URL I want, but it just fetches the HTML for the redirect URL, it doesn't actually switch views
You should try using the front end to redirect instead of the back-end. Ajax is meant to make a asynchronous request which then can be consumed on the front end.
So, instead of redirecting in the back end, just tell it print the redirect location and redirect using the returned data on the front end.
$.ajax({
headers: {
'accept': 'application/json',
'content-type': 'application/json'
},
type: "POST",
url: "tokenRedirect",
context:document.body,
contentType:"application/json",
data:JSON.stringify(TokenValidateObject)
success: function(locationToRedirect){
window.location = locationToRedirect
}
});
Because you're using XHR, the client-side code needs to read the HTTP response and handle the redirect using JavaScript. The browser will only execute the redirect for a PAGE, not an XHR call.
See: Redirecting after Ajax post
How to manage a redirect request after a jQuery Ajax call
Redirect on Ajax Jquery Call

Ajax call to login to spring secrurity failed

I am trying the following ajax call to login to spring
$.ajax({
url: "http://localhost:8080/context/j_spring_security_check",
data: { j_username: 'user_name' , j_password: 'user_pass' },
type: "POST",
beforeSend: function (xhr) {
xhr.setRequestHeader("X-Ajax-call", "true");
},
success: function(result) {
// ..
},
error: function(XMLHttpRequest, textStatus, errorThrown){
$("#ajax_login_error_" ).html("Bad user/password") ;
return false;
}
});
This is doesn't work, because the username and password are not passed to the UsernamePasswordAuthenticationFilter. When I debug the request they are both null. In other cases when I login, I can see my user name and password in this filter.
give exception for path (j_spring_security_check) in security filter as
final String contextName = ((HttpServletRequest) request).getContextPath();
final String requestURI = ((HttpServletRequest) request).getRequestURI();
if (requestURI.equals(contextName+"/j_spring_security_check"){
chain.doFilter(request, response);
}
That means you should give exception for login url
You need to override SimpleUrlAuthenticationSuccessHandler. See similar questions here

How to provide the CSRF Token in single page application (spring security)?

When I try to load part of a page using ajax I got 403 error
Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.
Spring Security FAQ tells us
If an HTTP 403 Forbidden is returned for HTTP POST, but works for HTTP
GET then the issue is most likely related to CSRF. Either provide the
CSRF Token or disable CSRF protection (not recommended).
So, how can a do this?
function getPage(url) {
$.ajax({
type: 'POST',
url: url,
data: {_csrf: "??"},
success: function (data) {
loadPage(url, data);
}
});
}
You can get the token from the cookie which is stored at your client. For that you have to use something like this cookie-service:
https://www.npmjs.com/package/angular2-cookie
write a function like this to get the token:
getCookie(){
return this._cookieService.get("token-name");
}
Finaly add the token to the request header:
doSomething(token){
var json = JSON.stringify({});
var headers = new Headers();
headers.append('Content-Type','application/json');
headers.append('token-name', token);
return this._http.post('http://localhost:8080/doSomething', json, {
headers: headers
}).map(res => res.text()
);
}
This solved the problem for me
Found an answer to my own question here:
http://spring.io/blog/2013/08/21/spring-security-3-2-0-rc1-highlights-csrf-protection/
this - to html
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
this - to js
$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token);
});
});

Spring MVC controller throwing 415 error media unsupported for JSON request

I have looked at all the other answers for this problem and tried most of the solutions but nothing seems to work. I am using Dojo.xhrPOST(xhrArgs) which obviously comes after the xhrArgs definition.
my xhrArgs:
xhrArgs =
{
headers:
{
'Accept': 'application/json',
'Content-Type': 'application/json'
},
'url': thisUrl,
'postData':requestString,
'dataType' : 'json',
'userId': userId,
'measurementSystem': measurementSystem,
'systemId': openedSystemId,
'handleAs': 'text',
'load': function(data)
{
// Replace newlines with nice HTML tags.
data = data.replace(/\n/g, "<br/>");
dojo.byId(target).innerHTML = data;
},
'error': function(error)
{
dojo.byId(target).innerHTML = error;
}
};
and my controller method signature and annotations are as follows
#RequestMapping(value="/saveSystemConditions", method= RequestMethod.POST, headers = {"content-type=application/json"})
public String saveSystemConditions(HttpServletRequest request, HttpServletResponse response, #Valid #RequestBody Load load, BindingResult result)
and my requestString as shown in the xhrArgs is
"{'systemID':'76', 'system.systemType':'1', 'unitsOfMeasure':'english', 'loadID':'63', 'dispersionInstallationLocation':'Duct+or+AHU', 'humidificationSystemType':'1', 'totalAirVolume':'1200.0', 'desiredDryBulb':'70.0', 'desiredAirMoistureType':'2', 'desiredAirMoisture':'55.0', 'outsideAirConditionsType':'1', 'outsideAirIntakeRateMeasuredAs':'0', 'loadCountry':'United+States', 'outsideAirVolumeMeasuredIn':'0', 'loadState':'Minnesota', 'outsideAirIntakeRate':'25.0', 'loadCity':'Minneapolis', 'elevationFeet':'837.0', 'outsideDryBulb':'-6.8', 'outsideAirMoisture':'57.3', 'userEnteredLoad':'7.43'}"
I get
415 (Unsupported Media Type)
Any suggestions would be much appreciated.
David
It was a problem with the explicit declaration of spring mvc mapping handlers along with <mvc:annotation-conig />.
Adding following lines to your application context, It will fix the issue.
<mvc:default-servlet-handler/>
<mvc:annotation-driven />

Resources