Expand my Community achievements bar.

SOLVED

Need help in resolving and understanding com.adobe.granite.csrf.impl.CSRFFilter doFilter: the provided CSRF token is invalid in AEM 6.1

Avatar

Level 2

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:

  1. To fix this we tried the solution as per AEM 6.1 docs https://docs.adobe.com/docs/en/aem/6-1/administer/security/security-checklist.html#par_title_1046104....
    But it didn't worked.
  2. We tried removing POST from the filter methods in Adobe Granite CSRF Filter configuration from system/console/configMgr. This worked but this affects the security of our system since it allows other external system to POST data. (Correct me if I am wrong on security)
  3. We tried adding google chrome browser user agent in the Safe User Agents in Adobe Granite CSRF Filter configuration from system/console/configMgr. This worked but the application can be used from various other user agents which we cannot keep on whitelisting in the Safe User Agents.

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.

1 Accepted Solution

Avatar

Correct answer by
Employee Advisor

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}

View solution in original post

16 Replies

Avatar

Employee

Do you see in the browser a request being made to get the token?

Avatar

Level 3

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

Avatar

Level 2

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.

Avatar

Correct answer by
Employee Advisor

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}

Avatar

Level 1

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

Avatar

Employee Advisor

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

Avatar

Level 1
As we are on the process of upgrade to AEM 6.1. We do not have whole set up ready in full working condition. Replication is not working. But as per the process I've created the package from author for the same and installed it to publisher.

Avatar

Level 1

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.

Avatar

Employee Advisor

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. 

Avatar

Level 1

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.

Avatar

Level 2

Thanks for the help kunal23. How can I modify the expiration date? I do see the expiration date and issue date is old.

Avatar

Employee Advisor

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. 

Avatar

Level 2

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]); }

Avatar

Employee Advisor

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

Avatar

Level 2

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.

Avatar

Employee Advisor

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