Problem Overview:
We are upgrading our system from CQ5.4 to AEM 6.1. In the existing code we have referred to POST.jsp using ajax, where we are invoking Authentication services to authenticate an external user to the system. We are getting the below error when we do an ajax post to the resource POST.jsp. in the error.log. com.adobe.granite.csrf.impl.CSRFFilter doFilter: the provided CSRF token is invalid
What have we tried:
Also we decomplied the com.adobe.granite.csrf.impl.CSRFFilter and found the below code:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; if ((request.getAuthType() != null) && (isFilteredMethod(request)) && (doFilterBasedOnUserAgent(request)) && (!isValidRequest(request))) { HttpServletResponse response = (HttpServletResponse)res; this.logger.info("doFilter: the provided CSRF token is invalid"); response.sendError(403); return; } chain.doFilter(req, res); }
In the above code, The isFilteredMethod checks if the current request(POST) is present in the configured filtered methods of Adobe Granite CSRF Filter.
The doFilterBasedOnUserAgent checks if the current request's User Agent is absent in the configured User agents.
request.getAuthType is "FORM" in our case. So this will not lead to (403).
The isValidRequest gets the request param :cq_csrf_token and checks if this parameter value is valid. (Observed this and is valid in our case and sends the parameter)
So what is the significance of filtered methods and user agents ? and why does CSRF sends 403 for POST.jsp although all CQ and AEM 5.x versions support this way of POSTING request? Please NOTE: We are facing this in our local dev environment in AUTHOR.
Solved! Go to Solution.
Views
Replies
Total Likes
The default expiration of the token is set to 10 minutes since it is issued. I hope the token which is submitted with the AJAX request is not expired.
You can verify the issued time and expired time of the token generated by doing base64 decoding of the claims set part (which is the first string before the dot).
Example -
{"token":"eyJleHAiOjE0NTE1MDEwNzksImlhdCI6MTQ1MTUwMDQ3OX0.brSE904pU-1_YCCcuAmmdwiguWblMXn0PF3In7AThBY"}
If you decode "eyJleHAiOjE0NTE1MDEwNzksImlhdCI6MTQ1MTUwMDQ3OX0" then you get the following string -
{"exp":1451501079,"iat":1451500479}
Views
Replies
Total Likes
Do you see in the browser a request being made to get the token?
Views
Replies
Total Likes
Have you added the necessary csrf client lib(granite.csrf.standalone) as dependency to your form component ?
Check details here.https://docs.adobe.com/docs/en/aem/6-1/develop/security/csrf-protection.html
Views
Replies
Total Likes
Thanks for replying.
Yes I do see a request made to http://localhost:4502/libs/granite/csrf/token.json And it provides the valid token as it is. The token value is first collected from this URL and later passed as an parameter value for parameter name :cq_csrf_token. The same is been retrieved in the doFilter method also mentioned in the question post.
Views
Replies
Total Likes
The default expiration of the token is set to 10 minutes since it is issued. I hope the token which is submitted with the AJAX request is not expired.
You can verify the issued time and expired time of the token generated by doing base64 decoding of the claims set part (which is the first string before the dot).
Example -
{"token":"eyJleHAiOjE0NTE1MDEwNzksImlhdCI6MTQ1MTUwMDQ3OX0.brSE904pU-1_YCCcuAmmdwiguWblMXn0PF3In7AThBY"}
If you decode "eyJleHAiOjE0NTE1MDEwNzksImlhdCI6MTQ1MTUwMDQ3OX0" then you get the following string -
{"exp":1451501079,"iat":1451500479}
Views
Replies
Total Likes
I'm also facing the same issue but with publisher. When i debugged, it seems to have problem in below method of com.adobe.granite.csrf.impl.CSRFFilter
protected boolean isValidRequest(HttpServletRequest request)
{
String csrf = request.getParameter(":cq_csrf_token");
if (StringUtils.isBlank(csrf))
{
csrf = request.getHeader("CSRF-Token");
if (StringUtils.isBlank(csrf))
{
this.logger.info("isValidRequest: empty CSRF token - rejecting");
return false;
}
}
String[] tokenParts = csrf.split("\\.");
if (ArrayUtils.getLength(tokenParts) != 2)
{
this.logger.info("isValidRequest: not well formed CSRF token - rejecting");
return false;
}
String token;
try
{
String user = request.getRemoteUser();
ClaimsSet claimsSet = ((JWT)new JWTReader().read("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + csrf)).getClaimsSet();
long expirationTime = claimsSet.getExpirationTime();
long buildAt = claimsSet.getIssuedAt();
long expiresIn = expirationTime - buildAt;
token = this.jwsBuilderFactory.getInstance("HS256").setSubject(user).setExpiresIn(expiresIn).setIssuedAt(claimsSet.getIssuedAt()).setCustomClaimsSetField("scope", ":aem_csrf_token_value").build();
}
catch (CryptoException e)
{
this.logger.error("isValidRequest: failed to generate CSRF token on validation", e);
return false;
}
boolean valid = this.jwsValidator.validate(token);
if (!valid)
{
this.logger.info("isValidRequest: the CSRF token is expired");
return false;
}
String signature = ((JWT)new JWTReader().read(token)).getSignature();
return signature.equals(tokenParts[1]);
}
Signature is not matching, hence returning false. Same configuration is there on author and working fine there. Any clue or pointer.
Exception:-
31.12.2015 01:35:09.021 *INFO* [127.0.0.1 [1451505909011] POST /bin/****Servlet HTTP/1.1] com.adobe.granite.csrf.impl.CSRFFilter doFilter: the provided CSRF token is invalid
Views
Replies
Total Likes
Have you replicated the crypto keys (/etc/keys/hmac) in all of your AEM boxes as mentioned in the below doc?
https://docs.adobe.com/docs/en/aem/6-1/develop/security/csrf-protection.html
Views
Replies
Total Likes
Views
Replies
Total Likes
i cannot see /etc/key/hmac in crx. What i have is /etc/key. We have hman binary file on key folder as property. Name of the property is hmac. Please see attached image.
Views
Replies
Total Likes
Yes it is a binary property. You need to create a package containing /etc/key node and install it on all of your AEM instances.
Views
Replies
Total Likes
Just to add one more thing. I'm directly hitting the publisher not dispatcher. So i think /etc/key/hmac can be out of sync as long as i'm directly hitting particular publisher.
Views
Replies
Total Likes
Thanks for the help kunal23. How can I modify the expiration date? I do see the expiration date and issue date is old.
Views
Replies
Total Likes
Have you written a custom javascript code to send the token as part of your AJAX request ? I guess the default CSRF javascript does not inject the tokens in AJAX calls. If you have written custom JS code then make sure to refresh the token periodically after every 5 minutes before you send the request via AJAX. Thats what the default OOTB CSRF JS does.
Views
Replies
Total Likes
Hi Kunal23 we do not send the token as part of our custom AJAX request we only send username and password. How can we identify which javascript sends the parameter :cq_csrf? To identify all the request params I used below code in the POST.jsp.
Map<String, String[]> parameters = request.getParameterMap(); for(String parameter : parameters.keySet()) { String[] values = parameters.get(parameter); log.info("Param name: "+ parameter + " value: "+ values[0]); }
Views
Replies
Total Likes
Hi Yash,
You can refer the code sample shared by @shopsinm.
The default CSRF JS adds the token field in the form when the form is loaded initially. The script intercepts the submission handler of the form and sets the token attribute again in the form with the refreshed value just before the form is submitted. I guess in your case since your firing AJAX post requests for the submissions the default script can not intercept the submission handler and set the token value again. So you need to always get the token again from the server and set it either as form parameter or in the request header before sending the request.
The default script file used for CSRF is - http://localhost:4502/etc/clientlibs/granite/jquery/granite/csrf.js
Views
Replies
Total Likes
Thanks @shopsinm, Kunal23 for the replies and code. Please correct my understanding added below.
The default script or the one shared by @shopsinm basically gets the token value from /libs/granite/csrf/token.json.
The default script intercepts and sets this value as a hidden parameter in the form which is then sent in the next POST request.
Alternatively script shared by @shopsinm fetches the value manually and sets it in the header.
However when I hit this URL http://localhost:4502/libs/granite/csrf/token.json in the browser the token value obtained is
eyJleHAiOjE0NTI1OTIzNDEsImlhdCI6MTQ1MjU5MTc0MX0
decodes to {"exp":1452592341,"iat":1452591741} converted to date and shows Sat Jan 17 1970 19:29:52 which is expired/old date.
So either default script or the one shared doesn't works for us.
Please let me know if I am misunderstanding this.
Views
Replies
Total Likes
Hi Yash,
I have checked the token values and the expired time is 10 minutes greater than the issued time which is as expected.
exp - 1452592341 --> GMT: Tue, 12 Jan 2016 09:52:21 GMT
iat - 1452591741 --> GMT: Tue, 12 Jan 2016 09:42:21 GMT
You can use http://www.epochconverter.com/ for doing this conversion.
-Kunal
Views
Replies
Total Likes
Views
Likes
Replies