Send Spring Form from AuthenticationFailureHandler to Controller and create BindingResult - spring-security

I am development user authentication with Spring Security.
I wanna get Spring Form parameters through #Valid annotation in my Controller method. First, Spring security is performed, then, I need know to send Login object from AuthenticationFailureHandler to Controller method.
After, in Controller method i need create BindingResult to emulate in jsp Spring Form errors functionality.
Now i have this code in AuthenticationFailureHandler and Controller method.
public class AuthFailureHandler implements AuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException ae) throws IOException, ServletException {
Login login = new Login();
login.setType(request.getParameter("type"));
login.setEmail(request.getParameter("email"));
login.setPassword(request.getParameter("password"));
request.setAttribute("login", login);
response.sendRedirect(response.encodeRedirectURL("./login/error"));
}
}
In Controller method
#RequestMapping("/login/error")
public String loginError(Model model, #Valid Login login, BindingResult result) {
String message = this.messageSource.getMessage(
"NoAuth.loginManager",
null,
LocaleContextHolder.getLocale());
ObjectError error = new ObjectError("general", message);
result.addError(error);
model.addAttribute("login", new Login());
return "home";
}

Related

How make Spring security formLogin() use custom filter

I had been trying to make exactly what one member already did here Additional parameters in Spring Security Login, but in my case I can't make that the form authentication use the filter :
(I'm using Spring Boot 1.5.7)
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login.html")
.usernameParameter("username")
.passwordParameter("password").permitAll().defaultSuccessUrl("/").failureUrl("/error.html")
.and()
.logout().logoutUrl("/logout");
http.addFilterBefore(new WebAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
Pass always directly to the UserDetailsService implementation without pass through the filter. Also I had been trying using a Bean instead of 'new', but the result is the same:
http.addFilterBefore(webAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
#Bean
public WebAuthenticationFilter webAuthenticationFilter() throws Exception {
WebAuthenticationFilter auth = new WebAuthenticationFilter();
auth.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
auth.setAuthenticationManager(authenticationManagerBean());
return auth;
}
My custom filter is a extends of UsernamePasswordAuthenticationFilter and in the Override of the method attemptAuthentication this method is never call:
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
tenant = request.getParameter("selectTenant");
System.out.println("We are here WebAuthenticationFilter");
request.getSession().setAttribute(TENANT_KEY, tenant);
return super.attemptAuthentication(request, response);
}
The only solution that worked was injecting the HttpServletRequest class into my implementation of UserDetailsService so I take the new parameter from the request here.
public class myImpleentUserDetailsService implements UserDetailsService (
#Autowired(required = false)
private HttpServletRequest request;
public UserDetail loadUserVyUsername(String username) throws UsernameNotFoundException{
String myparameter = request.getParameter("myParameter");
request.setAttribute("app-parameter", myparameter);
user = userService.findById(username, myparameter);
...
}

How to prevent Spring from generating default simpSessionId?

I am trying to set up spring with websockets and STOMP.
On the client, I send a header variable
'simpSessionId':%session_id%
However, on receiving the message, spring it always places the supplied header in a key called nativeHeaders and a default simpSessionId in the header root.
{simpMessageType=MESSAGE, stompCommand=SEND, nativeHeaders={SPRING.SESSION.ID=[5b1f11d0-ad92-4855-ae44-b2052ecd76d8], Content-Type=[application/json], X-Requested-With=[XMLHttpRequest], simpSessionId=[5b1f11d0-ad92-4855-ae44-b2052ecd76d8], accept-version=[1.2,1.1,1.0], heart-beat=[0,0], destination=[/mobile-server/ping], content-length=[15]}, simpSessionAttributes={}, simpSessionId=1, simpDestination=/mobile-server/ping}
Any ideas how to have spring pick up the supplied session id instead?
Edited
Ok, I have a mobile phone app and a website hitting the same server. I need to be able to set up a webocket on the mobile phone app.
On the mobile phone app, I login to the server through a traditional REST endpoint, and I receive a session-id in the response if successful.
I use webstomp-client on the mobile phone, Spring 4.1.9, Spring Security 4.1, Spring Session 1.2.0.
I would ideally login to the STOMP websocket on the socket CONNECT using a token, but I understand that his is currently impossible because webstomp-client doesn't pass custom headers on CONNECT.
I have two problems:
How do I pass the session id that I retrieve on the REST Login in subsequent requests? I've tried adding headers such as SPRING.SESSION.ID, but stepping through the code I always see the message processing going back to the simpSessionId which is always defaulted to 1, 2 etc. I've tried extending the AbstractSessionWebsocketMessageBrokerConfigurer, but it doesn't pick up my session id, it always looks in the simpSessionAttributes, which is always empty.
The code also seems to try to get the http session, which is a web browser scenario. I'm assuming I should just ignore this
Sessions expire. What should be the strategy for a session that may have expired? Shouldn't I pass a remember-me style authentication token as well? Or should I rely on some everlasting stateless session? This is not clear to me and this aspect seems to be undocumented.
Obviously, I'm doing something very wrong. Here's my config:
#Configuration
#EnableRedisHttpSession(maxInactiveIntervalInSeconds=1200)
public class SessionConfig {
#Inject
ContentNegotiationManager contentNegotiationManager;
#Bean
public RedisConnectionFactory redisConnectionFactory(
#Value("${spring.redis.host}") String host,
#Value("${spring.redis.password}") String password,
#Value("${spring.redis.port}") Integer port) {
JedisConnectionFactory redis = new JedisConnectionFactory();
redis.setUsePool(true);
redis.setHostName(host);
redis.setPort(port);
redis.setPassword(password);
redis.afterPropertiesSet();
return redis;
}
#Bean
public RedisTemplate<String,ExpiringSession> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, ExpiringSession> template = new RedisTemplate<String, ExpiringSession>();
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(connectionFactory);
return template;
}
#Bean
public <S extends ExpiringSession>SessionRepositoryFilter<? extends ExpiringSession> sessionRepositoryFilter(SessionRepository<S> sessionRepository) {
return new SessionRepositoryFilter<S>(sessionRepository);
}
#Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
#Bean
public HttpSessionStrategy httpSessionStrategy(){
return new SmartSessionStrategy();
}
#Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setUseSecureCookie(true);
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
return serializer;
}
}
===
public class SessionWebApplicationInitializer extends AbstractHttpSessionApplicationInitializer {
public SessionWebApplicationInitializer() {
}
public SessionWebApplicationInitializer(Class<?>... configurationClasses) {
super(configurationClasses);
}
#Override
protected void beforeSessionRepositoryFilter(ServletContext servletContext) {
Dynamic registration = servletContext.addFilter("openSessionInViewFilter", new OpenSessionInViewFilter());
if (registration == null) {
throw new IllegalStateException(
"Duplicate Filter registration for openSessionInViewFilter. Check to ensure the Filter is only configured once.");
}
registration.setAsyncSupported(false);
EnumSet<DispatcherType> dispatcherTypes = getSessionDispatcherTypes();
registration.addMappingForUrlPatterns(dispatcherTypes, false,"/*");
}
}
==
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig<S extends ExpiringSession> extends AbstractSessionWebsocketMessageBrokerConfigurer<S>{
#Inject
SessionRepository<S> sessionRepository;
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/mobile-server");
config.setUserDestinationPrefix("/mobile-user");
}
#Override
public void configureStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/ws")
.setHandshakeHandler(new SessionHandShakeHandler(new TomcatRequestUpgradeStrategy()))
.setAllowedOrigins("*")
.withSockJS()
.setSessionCookieNeeded(false)
;
}
#Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(512 * 1024);
registration.setSendBufferSizeLimit(1024 * 1024);
registration.setSendTimeLimit(40000);
}
#Bean
public WebSocketConnectHandler<S> webSocketConnectHandler(SimpMessageSendingOperations messagingTemplate, UsorManager userMgr) {
return new WebSocketConnectHandler<S>(messagingTemplate, userMgr);
}
#Bean
public WebSocketDisconnectHandler<S> webSocketDisconnectHandler(SimpMessageSendingOperations messagingTemplate, WebSocketManager repository) {
return new WebSocketDisconnectHandler<S>(messagingTemplate, repository);
}
}
====
#Configuration
public class WebSocketSecurity extends AbstractSecurityWebSocketMessageBrokerConfigurer{
ApplicationContext context = null;
public void setApplicationContext(ApplicationContext context) {
this.context = context;
}
#Override
protected boolean sameOriginDisabled() {
return true;
}
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.nullDestMatcher().permitAll()
.simpSubscribeDestMatchers("/user/queue/errors").permitAll()
.simpDestMatchers("/mobile-server/ping").authenticated()
.simpDestMatchers("/mobile-server/csrf").authenticated()
.simpDestMatchers("/mobile-server/**").hasRole("ENDUSER")
.simpSubscribeDestMatchers("/user/**", "/topic/**").hasRole("ENDUSER")
.anyMessage().denyAll();
}
}
===
I have removed some additional security configurations I have here for brevity sake.
#Configuration
#EnableWebSecurity
#Order(100)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String REMEMBER_ME_COOKIE = "SPRING_SECURITY_REMEMBER_ME_COOKIE";
#Inject
FilterInvocationSecurityMetadataSource securityMetadataSource;
#Inject
SessionRepositoryFilter<? extends ExpiringSession> sessionRepositoryFilter;
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setSaltSource(saltSource);
provider.setUserDetailsService(userMgr);
provider.setPasswordEncoder(passwordEncoder);
provider.setMessageSource(messages);
auth.authenticationProvider(provider);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public AuthenticationTokenProcessingFilter authenticationTokenProcessingFilter() throws Exception{
return new AuthenticationTokenProcessingFilter(authenticationManagerBean());
}
#Bean
public FilterSecurityInterceptor myFilterSecurityInterceptor(
AuthenticationManager authenticationManager,
AccessDecisionManager accessDecisionManager,
FilterInvocationSecurityMetadataSource metadataSource){
FilterSecurityInterceptor interceptor = new FilterSecurityInterceptor();
interceptor.setAuthenticationManager(authenticationManager);
interceptor.setAccessDecisionManager(accessDecisionManager);
interceptor.setSecurityMetadataSource(securityMetadataSource);
interceptor.setSecurityMetadataSource(metadataSource);
return interceptor;
}
#Bean
public AccessDecisionManager accessDecisionManager(SiteConfig siteConfig){
URLBasedSecurityExpressionHandler expressionHandler = new URLBasedSecurityExpressionHandler();
expressionHandler.setSiteConfig(siteConfig);
WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
webExpressionVoter.setExpressionHandler(expressionHandler);
return new AffirmativeBased(Lists.newArrayList(
webExpressionVoter,
new RoleVoter(),
new AuthenticatedVoter()
));
}
public PasswordFixingAuthenticationProvider customAuthenticationProvider(PasswordEncoder passwordEncoder, SaltSource saltSource){
PasswordFixingAuthenticationProvider provider = new PasswordFixingAuthenticationProvider();
provider.setUserDetailsService(userMgr);
provider.setPasswordEncoder(passwordEncoder);
provider.setSaltSource(saltSource);
return provider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(sessionRepositoryFilter, ChannelProcessingFilter.class)
.antMatcher("/ws/**")
.exceptionHandling()
.accessDeniedPage("/mobile/403")
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/ws").permitAll()
.antMatchers("/ws/websocket").permitAll()
.antMatchers("/ws/**").denyAll();
.anyRequest().requiresSecure()
;
}
}
===
public class SmartSessionStrategy implements HttpSessionStrategy {
private HttpSessionStrategy browser;
private HttpSessionStrategy api;
private RequestMatcher browserMatcher = null;
public SmartSessionStrategy(){
this.browser = new CookieHttpSessionStrategy();
HeaderHttpSessionStrategy headerSessionStrategy = new HeaderHttpSessionStrategy();
headerSessionStrategy.setHeaderName(CustomSessionRepositoryMessageInterceptor.SPRING_SESSION_ID_ATTR_NAME);
this.api = headerSessionStrategy;
}
#Override
public String getRequestedSessionId(HttpServletRequest request) {
return getStrategy(request).getRequestedSessionId(request);
}
#Override
public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {
getStrategy(request).onNewSession(session, request, response);
}
#Override
public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) {
getStrategy(request).onInvalidateSession(request, response);
}
private HttpSessionStrategy getStrategy(HttpServletRequest request) {
if(this.browserMatcher != null)
return this.browserMatcher.matches(request) ? this.browser : this.api;
return SecurityRequestUtils.isApiRequest(request) ? this.api : this.browser;
}
}
I think the question is based on invalid expectations to begin with. You cannot pass the session id and it's not meant to be passed in. You cannot login at the STOMP protocol level, it's not how it it's designed to work.
Although the STOMP protocol does allow for user credentials to be passed in the CONNECT frame that's more useful with STOMP over TCP. In an HTTP scenario we already have authentication and authorization mechanisms in place to rely on. By the time you get to the STOMP CONNECT, you would have had to pass authentication and authorization for the WebSocket handshake URL.
I would start with the Spring reference documentation on Authentication for STOMP/WebSocket messaging if you haven't read that already:
When a WebSocket handshake is made and a new WebSocket session is
created, Spring’s WebSocket support automatically propagates the
java.security.Principal from the HTTP request to the WebSocket
session. After that every message flowing through the application on
that WebSocket session is enriched with the user information. It’s
present in the message as a header.
In other words authentication is the same as for existing web applications. The URL at which the WebSocket endpoint is exposed is just another HTTP endpoint of the application. The same way all other HTTP endpoints are secured is the way the WebSocket handshake is secured. Just like for other HTTP endpoints you don't pass the session id. Instead you're within an existing HTTP session maintained through a cookie.
The handshake cannot be established unless Spring Security authenticates and authorizes the HTTP URL first. From there the STOMP session will pick up the authenticated user and Spring Security offers further ways to authorize individual STOMP messages.
That should all work seamlessly. There is no need to login via STOMP or to pass the Spring Session id at any time.

how grails spring security filter works

I would like to write a filter for a Facebook login, without to use the Facebook plugin. For Facebook authentification, I'll use Spring Social Facebook with annoation configuration.
The problem is who to write a filter.
I tried :
class FacebookAuthFilter extends AbstractAuthenticationProcessingFilter {
public FacebookAuthFilter() {
super("/j_facebook_security_check")
println "constructor"
}
#Override
public Authentication attemptAuthentication(HttpServletRequest arg0,
HttpServletResponse arg1) throws AuthenticationException,
IOException, ServletException {
println "attemptAuthentication"
return null;
}
}
In Bootstrap.goovy
SpringSecurityUtils.registerFilter ('facebookAuthFilter', SecurityFilterPosition.SECURITY_CONTEXT_FILTER.order + 10)
In resources.groovy
facebookAuthFilter(FacebookAuthFilter) {
authenticationManager = ref('authenticationManager')
rememberMeServices = ref('rememberMeServices')
}
At server start, I can see "constructor" in console. But not"attemptAuthentication".
How to enter in attemptAuthentication() ? I would like the map "/auth/facebook" to attemptAuthentication() method.
Thanks
EDIT :
Hi Igor, unfortunately, it's not work.
public FacebookAuthFilter() {
super("/myapp/facebook")
println "constructor"
}
#Override
public Authentication attemptAuthentication(HttpServletRequest arg0,
HttpServletResponse arg1) throws AuthenticationException,
IOException, ServletException {
println "attemptAuthentication"
return null;
}
then I go to
http://localhost:8080/myapp/facebook
Blank page and in console :
2014-09-26 18:09:57 [localhost].[/myapp] Initializing Spring FrameworkServlet 'grails-errorhandler'

Grails/SpringSecurity: Getting locale in LogoutHandler

In a grails app, I'm successfully getting the language the user has selected(added to url, "...?lang=xx_XX") like this:
def locale = RequestContextUtils.getLocale(request)
Using springsecurity, and got a special logout handler set up that works fine
grails.plugins.springsecurity.logout.handlerNames = ['securityContextLogoutHandler', 'myLogoutHandler']
I need to get the user selected locale in myLogoutHandler, but the following is not working (it only shows the browser default locale, not the one selected by the user)
class MyLogoutHandler implements LogoutHandler {
void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
def locale2 = RequestContextUtils.getLocale(httpServletRequest);
}
}
I have also tried getting the session locale with:
RequestContextHolder.currentRequestAttributes().getProperties()
but that gives the same result, anyone got an idea how to get the locale from the MyLogoutHandler?
Workaround
The session appears to be cleared by spring-security but you can still send in parameters.
so in the controller for the logout page I got:
def index = {
def myParam = "bar"
redirect(uri: SpringSecurityUtils.securityConfig.logout.filterProcessesUrl + "?foo=" + myParam) // '/j_spring_security_logout'
}
And I just get the parameter in the LogoutHandler:
void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
def myParam = httpServletRequest.parameterMap.get("foo")[0]
....
}
I can get the locales by using the code below:
class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private static final ThreadLocal<Authentication> AUTH_HOLDER = new ThreadLocal<Authentication>()
void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
AUTH_HOLDER.set authentication
// reading locales...
request.locales.each {locale ->println locale.toString()}
try {
super.handle(request, response, authentication)
}
finally {
AUTH_HOLDER.remove()
}
}
#Override
protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) {
Authentication auth = AUTH_HOLDER.get()
String url = super.determineTargetUrl(request, response)
// do something with the url based on session data..
url
}
}

Get original request url in Spring Security form-login page

I have the following declared in my spring security configuration file (http://www.springframework.org/schema/security/spring-security-2.0.1.xsd):
<form-login login-page="/login.html" />
What Spring Security does is redirect the user to that page if they don't have the correct authentication credentials. How can I get the url of the page the user was trying to get to?
Original request is represented by the SavedRequest object, which can be accessed as a session attribute named SPRING_SECURITY_SAVED_REQUEST_KEY.
in my case i did something like this and it worked for me.
#Autowired
private LoggedUserListener loggedUserListener;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/find/**","/","/Application/**")
.access("hasRole('ROLE_USER')")
.successHandler(loggedUserListener)
//some other stuff
}
#Component
public class LoggedUserListener implements AuthenticationSuccessHandler{
#Autowired
private UserRepo userRepo;
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
HttpSession session = request.getSession();
SavedRequest savedRequest = (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST");
if(savedRequest != null) {
User u = userRepo.findByUsername(authentication.getName());
u.setLastLogin(new Date());
u.setAccountNonLocked(false);
userRepo.save(u);
response.sendRedirect(savedRequest.getRedirectUrl());
}
}
}
In Spring Security 4
The original request is represented by the DefaultSavedRequest object, which can be accessed as a session attribute named SPRING_SECURITY_SAVED_REQUEST.
#RequestMapping(value = "/login", method = RequestMethod.GET)
public String login(HttpSession session) {
DefaultSavedRequest savedRequest = (DefaultSavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST");
}

Resources