Spring Security: How configure correctly CSRF for Ajax security control filter - spring

I am working with Spring Security
The app has enabled the following:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/resources/css/**", "/resources/images/**", "/resources/jquery/**", "/resources/js/**").permitAll()
... more URLs to intercept
.antMatchers("/notification**").hasAuthority("ROLE_ADMIN")
.antMatchers("/ajax/notification**").hasAuthority("ROLE_ADMIN")
.anyRequest().authenticated()
.and()
.csrf()
...
Observe CSRF is applied. For a form the following is used:
<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
method="post">
<input type="submit"
value="Log out" />
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}"/>
</form>
It according Form Submissions
The app works fine how is expected, it intercepts any URL prior to load/render a jsp page and ask for the login control how is expected, if the user has logged the control about authorization is applied how is expected too. Therefore all is working with CSRF how is suggested.
Until here all is Ok.
I added ajax through jQuery and because it works with an URL, therefore I need apply the CSRF control for that URL too.
That's why above appears:
.antMatchers("/ajax/notification**").hasAuthority("ROLE_ADMIN")
Now, I want get the expected HTTP error code and message when CSRF is applied to the ajax URL "/ajax/notification" and the CSRF headers were not send.
The js ajax code is:
$.getJSON(
"/projectname-01/ajax/notification",
function(data, textStatus, req) {
console.log("data: " + data);
console.log("textStatus: " + textStatus);
console.log("req: " + req)
console.log("data: " + data);
console.log(data.content + " " + data.date);
$("#single").empty();
$("#single").append(data.content + " " + data.date);
$("#multiple").append(data.content + " " + data.date)
.append("<br/>");
}
)
Note: The URL needs to be /projectname-01/ajax/notification, the projectname-01 is mandatory, if I remove that, I get the 404. It does not work even if ./ajax/notification is used (observe the dot).
Problem: The code works fine, I mean, the Ajax call happens to the server without any problem, I am expecting some error, it because the code is not using the CRSF requeriments for ajax.
It according from:
Ajax and JSON Requests
The csrfMetaTags Tag
Thus, such as either:
<head>
<meta name="_csrf" content="${_csrf.token}"/>
<!-- default header name is X-CSRF-TOKEN -->
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<!-- ... -->
</head>
or
<sec:csrfMetaTags />
and of course the js code working with:
_csrf_parameter, _csrf_header etc.
Then the expected HTTP error code and message is need it for testing and production purposes
Thus, what is missing? or what is the problem?
Headers: how was requested below the HTTP headers:
From Opera
General
Request URL:http://localhost:8080/security-01/ajax/notification
Request Method:GET
Status Code:200
Remote Address:[::1]:8080
Referrer Policy:no-referrer-when-downgrade
Response Headers
HTTP/1.1 200
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: Sat, 23 Sep 2017 00:14:20 GMT
Request Headers
GET /security-01/ajax/notification HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36 OPR/47.0.2631.80
Referer: http://localhost:8080/security-01/tiles/notification/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.8
Cookie: JSESSIONID=9F2007DA3C8C683D26B8D17D85563140; jenkins-timestamper-offset=18000000
From Firefox
Request Headers
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost:8080/security-01/tiles/notification/
X-Requested-With: XMLHttpRequest
Cookie: JSESSIONID=CF344E56DD03C4019DCA334CD38B73EC
Connection: keep-alive
Response Header
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: Sat, 23 Sep 2017 00:17:06 GMT
Clarification
According with Spring Security Reference Documentation we apply CSRF for a JSP page (submit form) and for Ajax, both work around with a URL. For the former I have already configured and works fine. The latter is the problem.
When I use jQuery's $.getJSON call without send the special CSRF data/headers values (<sec:csrfMetaTags />, meta[name='_csrf_parameter'] , etc), I mean from: 30.6 The csrfMetaTags Tag appears the following:
// using JQuery to send an x-www-form-urlencoded request
var data = {};
data[csrfParameter] = csrfToken;
data["name"] = "John";
...
$.ajax({
url: "http://www.example.org/do/something",
type: "POST",
data: data,
...
});
// using JQuery to send a non-x-www-form-urlencoded request
var headers = {};
headers[csrfHeader] = csrfToken;
$.ajax({
url: "http://www.example.org/do/something",
type: "POST",
headers: headers,
...
});
From above it sends the CRSF information either from data or headers. But in my code I am not sending that. Therefore I expect some special control and error reported through Spring Security
Thus with my current ajax code, through development in runtime (Tomcat running with the app), the call to the server happens and data is returned. Thus Spring Security did not intercept and throw an error due the absence of these CRSF data/headers.
Therefore thinking now for development and testing, if I create #Test methods where should fail because the CSRF data/headers values were not send. The #Test methods are going to fail because the call to the server happens without any security control.

Your HTTP GET request produces no error, because the CSRF token is only required for requests that update state, see Spring Security Reference:
18.2 Synchronizer Token Pattern
[...]
We can relax the expectations to only require the token for each HTTP request that updates state. This can be safely done since the same origin policy ensures the evil site cannot read the response. Additionally, we do not want to include the random token in HTTP GET as this can cause the tokens to be leaked.
and CsrfFilter:
Applies CSRF protection using a synchronizer token pattern. Developers are required to ensure that CsrfFilter is invoked for any request that allows state to change. Typically this just means that they should ensure their web application follows proper REST semantics (i.e. do not change state with the HTTP methods GET, HEAD, TRACE, OPTIONS).
To get a CSRF token you have to use HTTP POST, PUT or DELETE request.

Related

Username and password have been treated as anonymous in Spring-security-oauth2 password mode

I'm using Spring Boot and Spring Security OAuth2 to issue tokens to the front-end.
Postman
When I use postman to test, everything works fine.
.
Browser
But when I sent a same request on browser using vue.js and axios, it didn't work as expected. The status code was 401.
Gerneral:
Request URL: http://localhost:8080/oauth/token
Request Method: POST
Status Code: 401
Remote Address: [::1]:8080
Referrer Policy: no-referrer-when-downgrade
Response Headers:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://localhost:8081
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
Date: Sun, 17 Mar 2019 02:20:54 GMT
Expires: 0
Pragma: no-cache
Transfer-Encoding: chunked
Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers
WWW-Authenticate: Basic realm="oauth2/client"
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Request Headers:
Provisional headers are shown
Accept: application/json, text/plain, */*
Content-Type: application/x-www-form-urlencoded
Origin: http://localhost:8081
Referer: http://localhost:8081/login
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36
Form Data:
{"grant_type":"password","scope":"all","username":"admin","password":"888","client_id":"wellcell","client_secret":"wellcell"}:
Difference in server console log
I made a picture of side-by-side:
On the left side is the server console log of postman request.
And server console log of browser request is on the right.
After "ClientCredentialsTokenEndpointFilter", postman request went to "DaoAuthenticationProvider" to be authenticated.
But the browser request went to "BasicAuthencationFilter" and the "username" and "password" was ignored and an anonymous user was returned. Then, access is denied with an anonymous user.
Anybody had this kind of problem before?
I think problem with Content-Type: application/x-www-form-urlencoded. If you send json, you need to use Content-Type: application/json.
Simple use of axios with post of json:
axios.post("http://localhost:8080/oauth/token", {
"grant_type": "password",
"scope": "all",
"username": "admin",
"password": "888",
"client_id": "wellcell",
"client_secret": "wellcell"
}).then((response) => {
console.log(response.data);
});

rxjs5 Observable.ajax ignores explicitly set HTTP headers

I'm getting my feet wet with redux-observable and OAuth2 authentication. I'm stuck at the point where I have to POST adding Authorization header to my HTTP request. The header is has not been added. Instead, I see any custom-set header names as values of Access-Control-Request-Headers, and that's it.
This is a redux-observable 'epic':
const epicAuth = function(action$){
return action$.ofType(DO_AUTHENTICATE)
.mergeMap(
action => Rx.Observable.ajax( authRequest(action.username, action.password))
.map( response => renewTokens(response))
.catch(error => Rx.Observable.of({
type: AJAX_ERROR,
payload: error,
error: true,
}))
)
}
This is my request object:
const authRequest = function(username, password){
return {
url: TOKEN_PROVIDER + '?grant_type=password&username=' + username + '&password=' + password,
method: 'POST',
responseType: 'json',
crossDomain: true,
withCredentials: true,
headers: {
'Authorization': 'Basic <base64-encoded-user#password>',
}
}
}
The HTTP headers captured:
http://localhost:8082/api/oauth/token?grant_type=password&username=xxx&password=yyy
OPTIONS /api/oauth/token?grant_type=password&username=xxx&password=yyy HTTP/1.1
Host: localhost:8082
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization
Origin: http://localhost:3000
DNT: 1
Connection: keep-alive
HTTP/1.1 401
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
WWW-Authenticate: Basic realm="MY_REALM/client"
Content-Type: text/html;charset=utf-8
Content-Language: en
Content-Length: 1098
Date: Wed, 01 Nov 2017 17:57:38 GMT
It all ends up with 401 response, since the Authorization header was not sent. I have tested the Oauth2 endpoint manually with Postman tool, and all went well: I've got a valid access token, could renew it, etc. CORS is enabled on server side.
What am I missing here?
The client code is working correctly.
You've captured the OPTIONS cors request, which is asking the server if it is OK to POST the Authorization header (see the Access-Control-Request-Headers: authorization).
Make sure that you've configured CORS correctly on your server. It shouldn't be trying to authenticate OPTIONS calls. It should instead be sending a proper response which tells the browser if it is allowed to make the POST call.

Expect: 100-Continue header with XmlHTTPRequest

How do I force XmlHttpRequest to add Expect: 100-continue header? How can I make use of this feature in desktop browsers world?
var xmlhttp = new XMLHttpRequest();
var dataToSend = new FormData();
dataToSend.append('some', 'data');
dataToSend.append('token', 'secret-token');
xmlhttp.open("POST", "/post", true);
xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xmlhttp.setRequestHeader("Expect", "100-continue");
xmlhttp.setRequestHeader("Custom-Header", "This is custom data");
xmlhttp.send(dataToSend);
Here is the TCP Dump output piece
POST /post HTTP/1.1
Host: 127.0.0.1:3000
Connection: keep-alive
Content-Length: 243
Origin: http://127.0.0.1:3000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36
Custom-Header: This is custom data
Content-type: application/x-www-form-urlencoded
Accept: */*
Referer: http://127.0.0.1:3000/
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Cookie: connect.sid=s%3AAKgYIit0sVHMcI7na85UR_Na.o7uSDBEidLEyQ3cTaGyXlMrPiF3vADrwpnCrkCrJBD0
------WebKitFormBoundary9agFn5mlxh7SUBf7
Content-Disposition: form-data; name="some"
data
------WebKitFormBoundary9agFn5mlxh7SUBf7
Content-Disposition: form-data; name="token"
secret-token
------WebKitFormBoundary9agFn5mlxh7SUBf7--
You cannot force the XMLHttpRequest.setRequestHeader() method to add the Expect header for security reasons, as you can read in the W3C XMLHttpRequest specification:
The setRequestHeader(header, value) method must run these steps:
If the state is not OPENED, throw an "InvalidStateError" exception and terminate these steps.
If the send() flag is set, throw an "InvalidStateError" exception and terminate these steps.
If header does not match the field-name production, throw a "SyntaxError" exception and terminate these steps.
If value does not match the field-value production, throw a "SyntaxError" exception and terminate these steps (note: The empty string is legal and represents the empty header value).
Terminate these steps if header is a case-insensitive match for one of the following headers:
Accept-Charset
Accept-Encoding
Access-Control-Request-Headers
Access-Control-Request-Method
Connection Content-Length
Cookie
Cookie2
Blockquote
Date
DNT
Expect
Host
Keep-Alive Origin
Referer
TE
Trailer
Transfer-Encoding
Upgrade
User-Agent
Via
...or if the start of header is a case-insensitive match for Proxy- or Sec- (including when header is just Proxy- or Sec-).
The above headers are controlled by the user agent to let it control those aspects of transport. This guarantees data integrity to some extent. Header names starting with Sec- are not allowed to be set to allow new headers to be minted that are guaranteed not to come from XMLHttpRequest.
As a further reference:
Webkit Tests "set-dangerous-headers.html"
Some browsers (Chrome, for example) will also display an error in their "JavaScript Console":

Redirect as response to Ajax request ends up returning empty

We are using Primefaces 3M4 and one of our pages has a p:dataTable which uses ajax calls for events:
<p:ajax event="rowSelect" update=":newsForm:newsDlg" oncomplete="newsDlg.show();"/>
When the session times out the page gets redirected to /login.xhtml which works fine for non-ajax actions (menu items, etc) but when I select a row in the datatable after the session has expired the page doesn't change to the login page and in Firebug I see the following:
Under dashboard.xhtml Headers section of Firebug
Response Headers
Server Apache-Coyote/1.1
X-Powered-By JSF/2.0
Location http://localhost:8080/RetailerPortal/faces/login.xhtml
Content-Length 0
Date Fri, 11 Nov 2011 18:32:42 GMT
Request Headers
Host localhost:8080
User-Agent Mozilla/5.0 (Windows NT 6.1; WOW64; rv:8.0) Gecko/20100101 Firefox/8.0
Accept application/xml, text/xml, */*; q=0.01
Accept-Language en-us,en;q=0.5
Accept-Encoding gzip, deflate
Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection keep-alive
Content-Type application/x-www-form-urlencoded; charset=UTF-8
Faces-Request partial/ajax
X-Requested-With XMLHttpRequest
Referer http://localhost:8080/RetailerPortal/faces/dashboard.xhtml
Content-Length 389
Cookie csfcfc=_30Xsr; JSESSIONID=fg1bV1sZkzKIgNtkH0bz0N0f; JSESSIONID=C65BF4EED70299ABFE4B73614118295E
Under dashboard.xhtml Response
<?xml version='1.0' encoding='ISO-8859-1'?>
<partial-response><changes><update id="javax.faces.ViewState"><![CDATA[-3728406524126180805:2441995557020829808]]></update></changes></partial-response>
Under dashbaoard.xhtml Post
Parametersapplication/x-www-form-urlencoded
javax.faces.ViewState 7521050094575005695:7928145831130537413
javax.faces.behavior.even... rowSelect
javax.faces.partial.ajax true
javax.faces.partial.event rowSelect
javax.faces.partial.execu... newsForm:newsTable
javax.faces.partial.rende... newsForm:newsDlg
javax.faces.source newsForm:newsTable
newsForm newsForm
newsForm:newsTable_instan... 3
newsForm:newsTable_select... 3
Source
newsForm=newsForm&newsForm%3AnewsTable_selection=3&javax.faces.ViewState=7521050094575005695%3A7928145831130537413&javax.faces.partial.ajax=true&javax.faces.source=newsForm:newsTable&javax.faces.partial.execute=newsForm:newsTable&javax.faces.partial.render=newsForm:newsDlg&javax.faces.behavior.event=rowSelect&javax.faces.partial.event=rowSelect&newsForm:newsTable_instantSelectedRowKey=3
Under login.xhtml's headers
Response Headers
Server Apache-Coyote/1.1
X-Powered-By JSF/2.0
Cache-Control no-cache
Set-Cookie JSESSIONID=MdhyizD+8IkuFvLZD+6jWlUz; Path=/RetailerPortal
Content-Type text/xml;charset=UTF-8
Content-Length 196
Date Fri, 11 Nov 2011 18:32:42 GMT
Request Headers
Host localhost:8080
User-Agent Mozilla/5.0 (Windows NT 6.1; WOW64; rv:8.0) Gecko/20100101 Firefox/8.0
Accept application/xml, text/xml, */*; q=0.01
Accept-Language en-us,en;q=0.5
Accept-Encoding gzip, deflate
Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection keep-alive
Referer http://localhost:8080/RetailerPortal/faces/dashboard.xhtml
X-Requested-With XMLHttpRequest
Faces-Request partial/ajax
Content-Type application/x-www-form-urlencoded
Cookie csfcfc=_30Xsr; JSESSIONID=fg1bV1sZkzKIgNtkH0bz0N0f; JSESSIONID=C65BF4EED70299ABFE4B73614118295E
Under login.xhtml's XML section
XML Parsing Error: no element found Location: moz-nullprincipal:{6ccf85cf-5c69-438c-a9bb-e66423a36a48} Line Number 1, Column 1:
^
Response code
HttpServletResponse servResponse = (HttpServletResponse) response;
servResponse.sendRedirect("login.xhtml");
servResponse.setHeader("Access-Control-Allow-Origin", "*");
Just a guess--
If you're trying to redirect from an ajax response, you can't do that 301/302 style-- you have to send a message back to the browser and have the browser redirect via javascript.
Probably the non-ajax ones are working because they're using 301/302s.
I found the answer to this question in this blog
with the relevant code for at the bottom of the blog post in the doRedirect method.

Authorize with JsonReult action jQuery ajax returned status code 200 when not authorized

I have JsonResult action which required authentication and special role
[Authorize(Roles = "User")]
public virtual JsonResult Cancel()
{
//...
}
But when for example i log off and hit this action with jQuery ajax i could see that status code is 200, but it is should be 401.
$.ajax({
url: "/Cancel/",
type: "POST",
dataType: "text",
cache: false,
success: function (data, textStatus, xhr) {
alert(xhr.status); //200 here when unauthorized
}
});
So I really not able to execute the controller logic because it is not authorized, i checked that on debug, but why i am getting status code 200 in jquery ajax?
UPDATED:
In Fiddler it is saying status code 302 and i could see that request to /Acount/Login was made after /Cancel request.
/Cancel - 302
/Acount/Login - 200
In Chrome network Status Code:302 Found and also i could see that login controller(/Acount/Login) getting called after /Cancel was called.
/Cancel - 302
/Acount/Login - 200
Complete request details in Opera network
Could someone explain whats happening, why jquery didn't get correct status code?
Really what i want to do - a want to get correct status code and if it is 401 i want to redirect user to login page (window.location.href = " /Acount/Login")
Request details
POST /Cancel/ HTTP/1.1
User-Agent: Opera/9.80 (Windows NT 6.1; U; en) Presto/2.9.168 Version/11.50
Host: localhost:999
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate
Referer: http://localhost:999/Action
Cookie: style=normalText; ASP.NET_SessionId=latzewpi3kqmkq4meljv0ln5
Connection: Keep-Alive
Content-Length: 0
Accept: text/plain, */*; q=0.01
X-Requested-With: XMLHttpRequest
Content-Type: text/xml; charset=utf-8
Content-Transfer-Encoding: binary
Response details
HTTP/1.1 302 Found
Cache-Control: private
Content-Type: text/html; charset=utf-8
Location: /Account/LogOn?ReturnUrl=%2fCancel%2f
Server: Microsoft-IIS/7.5
X-AspNetMvc-Version: 3.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Thu, 11 Aug 2011 03:04:53 GMT
Content-Length: 169
FormsAuthentication have handler in its http module, that will redirect all 401 responses to login page instead of error page.
Looking at source code of FormsAuthenticationModule there should be (quite ugly) workaround - if you append ReturnUrl=/ to your query string, the module should do no redirection.
The best solution is probably to write own http module for authentication - you can open FormsAuthenticationModule in reflector and use it as reference.

Resources