With Spring Security 3.2.0.RELEASE, how can I get the CSRF token in a page that is purely HTML with no tag libs - spring

Today I upgraded from Spring Security 3.1.4 with the separate java config dependency, to the new 3.2.0 release which includes java config. CSRF is on by default and I know I can disable it in my overridden configure method with "http.csrf().disable()". But suppose I don't want to disable it, but I need the CSRF token on my login page where no JSP tag libs or Spring tag libs are being used.
My login page is purely HTML that I use in a Backbone app that I've generated using Yeoman. How would I go about including the CSRF token that's contained in the HttpSession in either the form or as a header so that I don't get the "Expected CSRF token not found. Has your session expired?" exception?

You can obtain the CSRF using the request attribute named _csrf as outlined in the reference. To add the CSRF to an HTML page, you will need to use JavaScript to obtain the token that needs to be included in the requests.
It is safer to return the token as a header than in the body as JSON since JSON in the body could be obtained by external domains. For example your JavaScript could request a URL processed by the following:
CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
// Spring Security will allow the Token to be included in this header name
response.setHeader("X-CSRF-HEADER", token.getHeaderName());
// Spring Security will allow the token to be included in this parameter name
response.setHeader("X-CSRF-PARAM", token.getParameterName());
// this is the value of the token to be included as either a header or an HTTP parameter
response.setHeader("X-CSRF-TOKEN", token.getToken());
Your JavaScript would then obtain the header name or the parameter name and the token from the response header and add it to the login request.

Although #rob-winch is right I would suggest to take token from session. If Spring-Security generates new token in SessionManagementFilter using CsrfAuthenticationStrategy it will set it to Session but not on Request. So it is possible you will end up with wrong csrf token.
public static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");
CsrfToken sessionToken = (CsrfToken) request.getSession().getAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME);

Note: I'm using CORS and AngularJS.
Note²: I found Stateless Spring Security Part 1: Stateless CSRF protection which would be interesting to keep the AngularJS' way to handle CSRF.
Instead of using Spring Security CSRF Filter which is based on answers (especially #Rob Winch's one), I used the method described in The Login Page: Angular JS and Spring Security Part II.
In addition to this, I had to add Access-Control-Allow-Headers: ..., X-CSRF-TOKEN (due to CORS).
Actually, I find this method cleaner than adding headers to the response.
Here is the code :
HttpHeaderFilter.java
#Component("httpHeaderFilter")
public class HttpHeaderFilter extends OncePerRequestFilter {
#Autowired
private List<HttpHeaderProvider> providerList;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
providerList.forEach(e -> e.filter(request, response));
if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
response.setStatus(HttpStatus.OK.value());
}
else {
filterChain.doFilter(request, response);
}
}
}
HttpHeaderProvider.java
public interface HttpHeaderProvider {
void filter(HttpServletRequest request, HttpServletResponse response);
}
CsrfHttpHeaderProvider.java
#Component
public class CsrfHttpHeaderProvider implements HttpHeaderProvider {
#Override
public void filter(HttpServletRequest request, HttpServletResponse response) {
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "X-CSRF-TOKEN");
}
}
CsrfTokenFilter.java
#Component("csrfTokenFilter")
public class CsrfTokenFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
CsrfToken csrf = (CsrfToken)request.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
String token = csrf.getToken();
if (cookie == null || token != null && !token.equals(cookie.getValue())) {
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
}
web.xml
...
<filter>
<filter-name>httpHeaderFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>httpHeaderFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
security-context.xml
...
<custom-filter ref="csrfTokenFilter" after="CSRF_FILTER"/>
...
app.js
...
.run(['$http', '$cookies', function ($http, $cookies) {
$http.defaults.transformResponse.unshift(function (data, headers) {
var csrfToken = $cookies['XSRF-TOKEN'];
if (!!csrfToken) {
$http.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken;
}
return data;
});
}]);

I use thymeleaf with Spring boot. I had the same problem. I diagnosed problem viewing source of returned html via browser. It should be look like this:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<form method="post" action="/login">
<div><label> User Name : <input type="text" name="username" /> </label></div>
<div><label> Password: <input type="password" name="password" /> </label></div>
<input type="hidden" name="_csrf" value=<!--"aaef0ba0-1c75-4434-b6cf-62c975dcc8ba"--> />
<div><input type="submit" value="Sign In" /></div>
</form>
</body>
</html>
If you can't see this html code. You may be forgot to put th: tag before name and value. <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}"> Invalid username and password. </div>
<div th:if="${param.logout}"> You have been logged out. </div>
<form th:action="#{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>

Related

Spring POST form data sends  as escape character for special characters not present in the browser request

I'm using a spring jsp form to send some data back to a controller.
<head>
<meta charset="utf-8">
</head>
(...)
<spring:url value="pathA" var="formActionVar"/>
<form:form action="${formActionVar}" modelAttribute="modelA" id="formIdA" method="post">
<input name="itemA" value="${modelA.itemA}" autocomplete="off"/>
<input name="itemB" value="${modelA.itemB}" autocomplete="off"/>
<button id="sendBtn" type="submit">
</form:form>
Looking at the browser request, the form data with Content-Type: application/x-www-form-urlencoded is as follows:
itemA=12%C2%A7&itemB=potatoes
The problem is that itemA is being converted to 12§ (like an escape char to all special chars, §, £, ...) meaning that there is something in between the browser request and the server doing that conversion.
Custom class that extends CharacterEncodingFilter:
public class CustomCharEncodingFilter extends CharacterEncodingFilter {
public CustomCharEncodingFilter() {
super("UTF-8", true, true);
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println(request.getCharacterEncoding()); // <-- null
System.out.println("Encoded param: " + request.getParameter("itemA")); // <-- 12§
request.setCharacterEncoding("UTF-8"); // <-- tested with these 2 lines but no effect
setForceEncoding(true);
System.out.println(request.getCharacterEncoding()); // <-- UTF-8
System.out.println("Encoded param: " + request.getParameter("itemA")); // <-- 12§
super.doFilterInternal(request, response, filterChain);
}
}
But as you can see the value was already previously converted somewhere disregarding the character encoding.
The weird part is that doing the request using ajax with a parsed form representation does not convert the 12§ value to 12§ and the controller gets the correct value.
Edit:
Just a quick note to mention that using enctype="multipart/form-data" in the jsp form changes the itemA value to it's clear form (12§) but still gets the value to be "server-side rendered" as 12§

How to handle session creation and adding hidden input csrf token for any page containing a form for an anonymous user in Spring Boot?

I Introduce the problem:
when I launch the application and I enter the url "/home". The home page is displayed but not correctly (the template is not well organized) and I receive an exception TemplateInputException. After a while, If I refresh the home page and the other pages It comes back to normal but if I go to "/login", and I logout which redirects me to the home view the same issue comes back again.
The Stacktrace Console:
org.thymeleaf.exceptions.TemplateInputException: An error happened
during template parsing (template: "class path resource
[templates/home.html]") ...
Caused by: org.attoparser.ParseException: Error during execution of processor
'org.thymeleaf.spring4.processor.SpringActionTagProcessor' (template:
"home" - line 2494, col 10) at
org.attoparser.MarkupParser.parseDocument(MarkupParser.java:393)
~[attoparser-2.0.4.RELEASE.jar:2.0.4.RELEASE]...
Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Error
during execution of processor
'org.thymeleaf.spring4.processor.SpringActionTagProcessor' (template:
"home" - line 2494, col 10)
Caused by: java.lang.IllegalStateException: Cannot create a session after the response has been committed at
org.apache.catalina.connector.Request.doGetSession(Request.java:2995)
~[tomcat-embed-core-8.5.14.jar:8.5.14]
...
at org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.saveToken(HttpSessionCsrfTokenRepository.java:63)
~[spring-security-web-4.2.0.RELEASE.jar:4.2.0.RELEASE] at
org.springframework.security.web.csrf.LazyCsrfTokenRepository$SaveOnAccessCsrfToken.saveTokenIfNecessary(LazyCsrfTokenRepository.java:176)
~[spring-security-web-4.2.0.RELEASE.jar:4.2.0.RELEASE] at
org.springframework.security.web.csrf.LazyCsrfTokenRepository$SaveOnAccessCsrfToken.getToken(LazyCsrfTokenRepository.java:128)
~[spring-security-web-4.2.0.RELEASE.jar:4.2.0.RELEASE] at
org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor.getExtraHiddenFields(CsrfRequestDataValueProcessor.java:71)
~[spring-security-web-4.2.0.RELEASE.jar:4.2.0.RELEASE] ...
The Code source:
The issue is in the Contact Form of the home.html in this line: th:action="#{/home/contact}" th:object="${mailForm}":
<form id="contact-form" method="post" action="/home/contact}"
th:action="#{/home/contact}" th:object="${mailForm}"
role="form">
<!-- <input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" /> -->
<input type="text" name="senderName" th:field="*{senderName}">
<input type="text" name="senderLastName" th:field="*{senderLastName}">
<input type="email" name="senderEmail" th:field="*{senderEmail}">
<textarea name="message" th:field="*{message}"></textarea>
<button type="submit">Send Message</button>
</form>
I think it's a problem with csrf token. I tried to add this line <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> in my form and the csrf protection is enabled by default by Spring Security but it did not work.
The Controller that calls the service to send mails:
#Controller
public class HomeController {
#Autowired
private EmailService emailService;
#Autowired
private MailValidator mailValidator;
// some other code like #InitBinder methode ...
// method to post mailForm
#PostMapping("/home/contact")
public String contactUsHome(#Valid #ModelAttribute("mailForm") final MailForm mailForm, BindingResult bindingResult)
throws MessagingException {
if (bindingResult.hasErrors()) {
return HOME_VIEW;
} else {
mailForm.setRecipientEmail(recipientEmail);
Mail mail = DTOUtil.map(mailForm, Mail.class);
emailService.sendSimpleMail(mail);
return REDIRECT_HOME_VIEW;
}
}
}
This is how to fix the issue "Cannot create a session and CSRF token".
In the spring security configuration class, I just added this line .and().csrf().csrfTokenRepository(..) and everything works well.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//some another line of code...
.and().csrf().csrfTokenRepository(new HttpSessionCsrfTokenRepository())
}

Same form in multiple locations in Thymeleaf 3 & Spring 4

I'd like to have my login form (for example) on multiple pages, and I want to pre-populate it with the current user's login (assuming that user is recognized via cookie). But I don't want every controller method for every possible page to have to provide a LoginForm bean for the form. I do want all the validation magic when the form is submitted, and then of course I want the result of the form to the same page the user was on when they submitted it.
I can't quite figure out how to accomplish this right now. Is it even possible?
EDIT:
I've got a Thymeleaf form like this:
<form action="#" data-th-action="#{/users/login}" data-th-object="${loginForm}" method="post">
<input type="text" placeholder="Email or Username" data-th-field="${loginForm.login}">
<input type="password" placeholder="Password" data-th-field="${loginForm.password}">
<button type="submit" name="login">Sign in</button>
<button type="submit" name="register">Register</button>
</form>
If I don’t create a LoginForm (my class) bean and stick it in the model under loginForm, then I get an exception on GET, when rendering the page.
You don't need to pass the LoginForm to multiple controllers, rather, you can centralize your code at a single place with a http filter by validating the Login form for the required urls as below:
LoginFormValidationFilter class:
#Component
public class LoginFormValidationFilter implements Filter {
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String url = request.getRequestURL();
if(url.equals(YOUR_LOGIN_URL)) {
//validate the request here for the required urls
//If request is invalid, send an error message back
}
chain.doFilter(req, res);
}
#Override
public void init(FilterConfig filterConfig) {
}
#Override
public void destroy() {
}
}
Web.xml filter mapping:
<filter>
<filter-name>LoginFormValidationFilter </filter-name>
<filter-class>xyz.LoginFormValidationFilter </filter-class>
</filter>
<filter-mapping>
<filter-name>LoginFormValidationFilter </filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

Spring Redirect Url: SavedRequest is null when session expires

I'm accessing the SavedRequest in the login controller to set the redirect url in the login form. When the session expires (or first login after server is started), savedRequest is null.
If I access a url in a fresh browser session, saved request is available.
Any way i can access the redirect url consistently from the login controller?
NB. I'm using spring 3.2.3..from what I can tell savedRequest is no longer accessbile from the session.
login.jsp
<form action="${actionUrl}" method='POST' autocomplete='off'>
<span class="box-label">Username:</span>
<input type="text" name="j_username" data-dojo-type="dijit/form/TextBox" data-dojo-props="style:{width:'150px'}">
<span class="box-label">Password:</span>
<input type="password" name="j_password" data-dojo-type="dijit/form/TextBox" data-dojo-props="style:{width:'150px'}">
<input type="submit" data-dojo-type="dijit/form/Button" data-dojo-props="label:'Login'"/>
<input type="hidden" name="redirect" value="${redirect}">
</form>
Login Controller:
#Controller
public class LoginController {
#RequestMapping(value = "/login", method = RequestMethod.GET)
public String login(final HttpSession session, final Model model, final HttpServletRequest request,
final HttpServletResponse response) {
addRedirectUrlToModel(model, request, response);
return "login";
}
private void addRedirectUrlToModel(final Model model, final HttpServletRequest request, final HttpServletResponse response) {
SavedRequest savedRequest = new HttpSessionRequestCache().getRequest(request, response);
if (savedRequest != null) {
model.addAttribute("redirect", savedRequest.getRedirectUrl());
} else {
System.out.println("saved request is null");
}
}
Security config:
<sec:http auto-config="true" use-expressions="true">
<sec:form-login login-page="/login"
authentication-success-handler-ref="authSuccHandler"
authentication-failure-handler-ref="authFailHandler" />
<sec:logout delete-cookies="JSESSIONID" invalidate-session="true" />
<sec:access-denied-handler error-page="/error/not-authorised"/>
<sec:session-management session-fixation-protection="none"
invalid-session-url="/login/sessionExpired"
session-authentication-error-url="/login/alreadyLoggedIn"
>
<sec:concurrency-control max-sessions="1"
expired-url="/login/sessionExpiredDuplicateLogin"
error-if-maximum-exceeded="false"/>
</sec:session-management>
When you try to get the savedrequest of the expire session, it will come null. You can fix this by creating a new DefaultSavedRequest. After creating the DefaultSavedRequest, you can set into the HttpServletRequest.getSession() and move it during the session in different requests.
private void addRedirectUrlToModel(final Model model, final HttpServletRequest request, final HttpServletResponse response) {
SavedRequest savedRequest = new DefaultSavedRequest(request, new PortResolverImpl());
request.getSession().setAttribute("SPRING_SECURITY_SAVED_REQUEST", savedRequest);
if (savedRequest != null) {
model.addAttribute("redirect", savedRequest.getRedirectUrl());
} else {
System.out.println("saved request is null");
}
}

Display error messages in Spring login

I am using Spring security for authenticating users. I created a custom authentication provider and now I am wondering how I can get error messages from the provider into my form. This is the authenticate() method in my custom authentication provider:
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UserProfile profile = userProfileService.findByEmail(authentication.getPrincipal().toString());
if(profile == null){
throw new UsernameNotFoundException(String.format("Invalid credentials", authentication.getPrincipal()));
}
String suppliedPasswordHash = DigestUtils.shaHex(authentication.getCredentials().toString());
if(!profile.getPasswordHash().equals(suppliedPasswordHash)){
throw new BadCredentialsException("Invalid credentials");
}
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(profile, null, profile.getAuthorities());
return token;
}
This is my form:
<form name='f' action="<c:url value='j_spring_security_check' />" method='POST'>
<div id="data-entry-form">
<div class="form-entry">
<label><spring:message code="login.form.label.email"/></label>
<input type='text' name='j_username' value=''>
</div>
<div class="form-entry">
<label><spring:message code="login.form.label.password"/></label>
<input type='password' name='j_password'/>
</div>
<div class="form-entry">
<input type="submit" value="Verzenden"/>
</div>
</div>
How would I get error messages into my form? From the moment I press the login button, Spring takes over, so the only method I could generate error messages in would be the authenticate() method...
3 Steps of the safest way (we don't rely on the LAST_EXCEPTION):
Specify error page (for example "login-error") in configuration for your custom authentication provider
httpSecurity
.authorizeRequests()
.antMatchers("/css/**", "/js/**", "/img/**").permitAll()
.anyRequest().fullyAuthenticated()
.and()
.formLogin().loginPage("/login").permitAll()
.failureUrl("/login-error")
.and()
.logout().permitAll()
Create controller for url /login-error that returns view of your custom login page (for example "login") with the next code:
#Controller
public class LoginController {
#GetMapping("/login-error")
public String login(HttpServletRequest request, Model model) {
HttpSession session = request.getSession(false);
String errorMessage = null;
if (session != null) {
AuthenticationException ex = (AuthenticationException) session
.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
if (ex != null) {
errorMessage = ex.getMessage();
}
}
model.addAttribute("errorMessage", errorMessage);
return "login";
}
}
Get the error message into your page finally (ThymeLeaf tags for example):
<!--/*#thymesVar id="errorMessage" type="java.lang.String"*/-->
<div class="alert" th:if="${errorMessage}" th:text="${errorMessage}"></div>
I was able to solve it like this:
<c:if test="${param.auth eq 'failure'}">
<div class="error">
<c:out value="${SPRING_SECURITY_LAST_EXCEPTION.message}" />
</div>
</c:if>
Note that you need to detect whether there was an error via a special parameter which you can set in your spring-security config like this:
<security:form-login [...] authentication-failure-url="/login?auth=failure" />
EDIT:
Actually, passing that parameter is not necessary. Instead, one can simply check whether SPRING_SECURITY_LAST_EXCEPTION.message is defined, like this:
<c:if test="${not empty SPRING_SECURITY_LAST_EXCEPTION.message}">
<div class="error">
<c:out value="${SPRING_SECURITY_LAST_EXCEPTION.message}" />
</div>
</c:if>
I think that you should be able to get the messages in the same way than using the "standard" authenticators.
If an exception (or more than one) is thrown in the authentication process, the last exception is stored in a session attribute: SPRING_SECURITY_LAST_EXCEPTION.
So, to get the last exception message from the JSP you can use something like this:
<%=((Exception) request.getSession().getAttribute("SPRING_SECURITY_LAST_EXCEPTION")).getMessage()%>;
Of course, if you have a controller you should probably get the message from the controller and pass only the string to the jsp. This is only an example ;)

Resources