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 ;)
Related
We have this
http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true)
and
http
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout-gimli-user")).permitAll()
.deleteCookies("JSESSIONID", "sessionid" /*, "sessdetail", "countfr"*/ );
and
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.invalidSessionUrl("/login?invalidSession") //dokunma
.maximumSessions(1) //dokunma
.maxSessionsPreventsLogin(true) //dokunma
.expiredUrl("/login?expired")
.sessionRegistry(sessionRegistry());
in
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
And in application.properties, we have
server.servlet.session.timeout=3m
When a user tries to login, user puts password + username, then one time code is sent to user. After this, user needs to put this code and then can view pages.
But if user does not put that code but only puts username+ password, and closes browser, logout is not working. Because logout is not invoked.
But timeout should work and kill after 3 minutes. Or tomcat should kill the session (because we deploy to external tomcat 9).
I tried this
https://stackoverflow.com/a/41450580/11369236
I added
#Bean
public static ServletListenerRegistrationBean httpSessionEventPublisher() {
return new ServletListenerRegistrationBean(new HttpSessionEventPublisher());
}
but still same.
I put
List<SessionInformation> sessions = sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
to
#Override
public Authentication authenticate(Authentication authentication) {
in public class CustomAuthenticationProviderWithRoles implements AuthenticationProvider {
and tried lots of logins without one time code confirm and saw that, sessions are increasing.
But when i try with putting one time code, it does not allow because of maxSessionsPreventsLogin
like here:
https://github.com/spring-projects/spring-security/issues/3078
Login page code:
<form method="POST" th:action="#{/login}">
<input autocomplete="off" class="form-control" id="mobile" name="username"
type="text">
<input autocomplete="off" class="form-control password" name="password"
type="password">
<button class="btn btn btn-block btn-primary btn-lg"
type="submit"
value="Log In">LOGIN
</button>
this is for
login:
http
.formLogin()
.loginPage("/login").permitAll()
and successhandler does this for successfull login:
response.sendRedirect("/otp");
Then, it sets the seconds which to count from for putting code. And sends another view to put code and which contains another form and submit button.
What can be best practice? For example user can close the page after putting user name password but session still remains. Despite there are timeouts.
I can use this and it solves it but I already session timeout in application.properties:
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response
request.getSession(false).setMaxInactiveInterval(11);
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())
}
My security configuration is as following:
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select username, password, activated from Person where username=?")
.authoritiesByUsernameQuery("select person.username, personrole.roleName from person, personrole, personandroles where person.username=? and personandroles.personid=person.personId and personrole.roleid=personandroles.roleid");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/empl").access("hasRole('employee')")
.antMatchers("/", "/index" , "/loginform", "/registerform","/approvelogin") .permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/loginform")
.loginProcessingUrl("/approvelogin")
.usernameParameter("username")
.passwordParameter("password")
.and()
.httpBasic().disable()
.and()
.exceptionHandling().accessDeniedPage("/403");
}
Where /approvelogin is the name of my login-controller. This controller expects a Modelattribute 'Person' and a request parameter 'role' (user may select a role as which to log in)
On the loginform I have this:
<form:form action="approvelogin" modelAttribute="userBean" method="POST">
<table>
<tr>
<td><label><b>Benutzername:</b></label>
<form:input type="text" path="username" required="true"/>
</td>
</tr>
<tr>
<td><label><b>Kennwort</b></label>
<form:input type="password" path="password" required="true">
</td>
<td>
<c:if test="${!empty roles}">
<select name="role">
<c:forEach var="r" items="${roles}">
<option value='${r.roleName}'>${r.roleName}</option>
</c:forEach>
</select>
</c:if>
</td>
<td> <input type="submit" value="Login"/>
</td>
</tr>
</table>
</form:form>
When I call /empl in the browser I am beeing redirected to loginform as expected. But after editing the correct user credencials I arrive on my 403.jsp.
Setting a breakpoint on the method successfullAuthentication(request, response, chain, authResult) of the class AuthenticationProcessingFilter shows that the 'authorities' collection is filled with the correct role 'employee'.
So I assume something else prevent my login controller from beeing entered at all.
The login controller:
#RequestMapping(value="/approvelogin", method=RequestMethod.POST)
public String login(#ModelAttribute("userBean") Person user, #RequestParam(value="role") String role, Model model){
if( user.getUsername().isEmpty() || user.getPassword().isEmpty() ){
model.addAttribute("error", "Please enter your username and password");
return "loginform";
}
else {
Person p = personDao.getPerson(user.getUsername(), user.getPassword());
if ( p == null) {
model.addAttribute("error", "Invalid Details, try again:");
return "loginform";
}
else{
String view="welcome";
model.addAttribute("loggedperson", p);
for( PersonRole r: p.getRoles() ){
if(r.getRoleName().equals(role)) {
view= role +"View";
break;
}
}
return view;
}
}
}
#RequestMapping(value="/empl")
public String employeeView(){
return "employeeView";
}
Your login controller will return String with role+View, are you sure that you have view with this name?
I think you want to redirect user after login to /empl.
Finally I found a solution. Instead of setting the
.loginProcessingUrl("/approvelogin")
I have to set the
.defaultSuccessUrl("/approvelogin")
Clearly after spring has done the authentication and has athorised the user to go on I am able to get to my custom login controller in order to redirect the user to the desired view (according to his role and request).
How can I display an error message in the very same JSP when a user submits a wrong input? I do not intend to throw an exception and show an error page.
Easiest would be to have placeholders for the validation error messages in your JSP.
The JSP /WEB-INF/foo.jsp:
<form action="${pageContext.request.contextPath}/foo" method="post">
<label for="foo">Foo</label>
<input id="foo" name="foo" value="${fn:escapeXml(param.foo)}">
<span class="error">${messages.foo}</span>
<br />
<label for="bar">Bar</label>
<input id="bar" name="bar" value="${fn:escapeXml(param.bar)}">
<span class="error">${messages.bar}</span>
<br />
...
<input type="submit">
<span class="success">${messages.success}</span>
</form>
In the servlet where you submit the form to, you can use a Map<String, String> to get hold of the messages which are to be displayed in JSP.
The Servlet #WebServlet("foo"):
#Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.getRequestDispatcher("/WEB-INF/foo.jsp").forward(request, response);
}
#Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Map<String, String> messages = new HashMap<String, String>();
request.setAttribute("messages", messages); // Now it's available by ${messages}
String foo = request.getParameter("foo");
if (foo == null || foo.trim().isEmpty()) {
messages.put("foo", "Please enter foo");
} else if (!foo.matches("\\p{Alnum}+")) {
messages.put("foo", "Please enter alphanumeric characters only");
}
String bar = request.getParameter("bar");
if (bar == null || bar.trim().isEmpty()) {
messages.put("bar", "Please enter bar");
} else if (!bar.matches("\\d+")) {
messages.put("bar", "Please enter digits only");
}
// ...
if (messages.isEmpty()) {
messages.put("success", "Form successfully submitted!");
}
request.getRequestDispatcher("/WEB-INF/foo.jsp").forward(request, response);
}
In case you create more JSP pages and servlets doing less or more the same, and start to notice yourself that this is after all a lot of repeated boilerplate code, then consider using a MVC framework instead.
See also:
Our Servlets wiki page
What is the best practice for validating parameters in JSP?
I see tag "form-validation", so maybe you should just use JavaScript and client form validation? If you need validation with JSP, handle form data, and redisplay the form with an error message or accept form data, if it's correct.
I'm not quite sure what you mean by "display error message". If you have a standard error handling, then you can always check for options:
<%
if(wrong option selected)
throw new Exception("Invalid option selected");
%>
Of course, this is just the notion; preferably, you'd have your own exception class. But then again, I'm not quite sure what you're after.
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>