Spring Security SAML2, sending language code in <Extensions> -element - spring-security

I'm currently migrating from Spring Security SAML Extension to Spring Security SAML2 and use case requires language code to be sent in Extensions -element.
With Spring Security SAML Extension this was done by:
Extending SAMLEntryPoint and storing locale as relayState to SAMLMessageContext like this:
public class CustomSAMLEntryPoint extends SAMLEntryPoint {
private String relayState;
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
//read your request parameter
setRelayState(request.getParameter("locale"));
super.commence(request, response, authenticationException);
}
#Override
protected WebSSOProfileOptions getProfileOptions(SAMLMessageContext samlMessageContext, AuthenticationException authenticationException) throws MetadataProviderException {
//set the relayState to your SAML message context
samlMessageContext.setRelayState(getRelayState());
return super.getProfileOptions(samlMessageContext, authenticationException);
}
private void setRelayState(String relayState) {
this.relayState = relayState;
}
private String getRelayState() {
return relayState;
}
}
Extending WebSSOProfileImpl and using previously set relayState value to generate Extensions -element:
public class CustomWebSSOProfileImpl extends WebSSOProfileImpl {
#Override
protected AuthnRequest getAuthnRequest(SAMLMessageContext context, WebSSOProfileOptions options, AssertionConsumerService assertionConsumer, SingleSignOnService bindingService) throws SAMLException, MetadataProviderException {
AuthnRequest authnRequest = super.getAuthnRequest(context, options, assertionConsumer, bindingService);
authnRequest.setExtensions(buildExtensions(context.getRelayState()));
return authnRequest;
}
}
How could this same functionality be done with Spring Security Core SAML2? Is there some similar way than using SAMLMessageContext and relayState?
I could customize AuthenticationEntryPoint as well as authentication request creation but there seems to be no way to move locale between these two.
public AuthenticationEntryPoint authenticationEntryPoint() {
final AuthenticationEntryPoint authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint(
"/saml2/authenticate/sp");
return (request, response, exception) -> {
String locale = request.getParameter("locale");
// Where shoud locale be stored???
authenticationEntryPoint.commence(request, response, exception);
};
}
#Bean
public Saml2AuthenticationRequestFactory authenticationRequestFactory() {
final OpenSamlAuthenticationRequestFactory authenticationRequestFactory = new OpenSamlAuthenticationRequestFactory();
authenticationRequestFactory.setAuthenticationRequestContextConverter(context -> {
final AuthnRequest request = new AuthnRequestBuilder().buildObject();
request.setAssertionConsumerServiceURL(context.getAssertionConsumerServiceUrl());
request.setDestination(context.getDestination());
request.setID("A" + UUID.randomUUID());
request.setIssueInstant(new DateTime());
final Issuer issuer = new IssuerBuilder().buildObject();
issuer.setValue(context.getIssuer());
request.setIssuer(issuer);
// Where can locale be read from???
request.setExtensions(buildLanguageExtensions(???);
return request;
});
return authenticationRequestFactory;
}

Related

SPRING-SECURITY Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’

I have my service work and deployed with no problem, but i need to add more feature [Realtime Notification]. I'm using SockJS, StompJS, Spring security, and LDAP authentication.
Here is my AuthenticationFilter, i'm using token as my passcode it generated after login into LDAP.
#Log4j2
public class AuthenticationFilter extends OncePerRequestFilter {
#Autowired
private JWTUtil jwtUtil;
private final String authHeader = "token";
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
//CORS
response.addHeader("Access-Control-Allow-Origin", "*");
if (request.getHeader("Access-Control-Request-Method") != null && "OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.addHeader("Access-Control-Allow-Headers", "token");
response.addHeader("Access-Control-Allow-Headers", "Content-Type");
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
}
final String authHeader = request.getHeader(this.authHeader);
if (authHeader != null) {
String token = authHeader;
try {
Claims claims = jwtUtil.getAllClaimsFromToken(token);
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
new User(claims.getSubject()),
null,
authorities
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
log.debug("Error ", e);
}
}
if (!request.getMethod().equalsIgnoreCase("OPTIONS")) {
chain.doFilter(request, response);
}
}
}
Here is my WebSecurityConfig
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public UnauthorizedHandler unauthorizedHandler() throws Exception {
return new UnauthorizedHandler();
}
#Bean
public ForbiddenHandler forbiddenHandler() throws Exception {
return new ForbiddenHandler();
}
#Bean
public AuthenticationFilter authenticationFilterBean() throws Exception {
return new AuthenticationFilter();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// we don't need CSRF because our token is invulnerable
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler()).and()
.exceptionHandling().accessDeniedHandler(forbiddenHandler()).and()
// don't create session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// allow auth url
.antMatchers("/login","/v2/api-docs", "/configuration/ui", "/swagger-resources/**", "/configuration/**", "/swagger-ui.html", "/webjars/**", "/notif/**", "/mealnotif/**", "/topic/**", "/websocket/**", "/resources/**", "/META-INF/resources/**").permitAll()
.anyRequest().authenticated();
// custom JWT based security filter
httpSecurity.addFilterBefore(authenticationFilterBean(), UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity.headers().cacheControl();
}
}
And the last one is my WebSocketConfig
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/mealnotif");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/notif").setAllowedOrigins("*")
.withSockJS();
}
}
But in return My Angular project always return Cross-Origin Request Blocked
CORS Reason ERROR
How do i solve this?
i can solve this by adding whitelist of allowed origin in my application.properties
management.endpoints.web.cors.allowed-origins=http://localhost,http://localhost:4200
by this properties i managed to giving properly response to client
response

Grails Spring Security Shiro, how to create 2 login forms with 2 different authentication success urls?

I am working a project that leverages Spring Security Shiro. I would like to configure the application to have 2 different login forms with 2 different authentication success urls. What is the best way to perform this? I looked at the documentation but have not seen any solutions.
Thank you in advance for your help.
The easiest and the best way it's extending two separate WebSecurityConfigurerAdapter and creating there two different configuration (two login page and two authentication success urls):
#Configuration
#Order(94)
public class WebSecurityConf1 extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/first-resources**").formLogin().successForwardUrl();
}
}
and
#Configuration
#Order(95)
public class WebSecurityConf2 extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/second-resources**").formLogin().successForwardUrl();
}
}
Note that WebSecurityConf1 will be applied when .antMatcher("/first-resources**") is satisfied and the same for WebSecurityConf2. Also, configuration between WebSecurityConf1 and WebSecurityConf2 independent.
The simple way was to override the AuthenticationSuccessHandler with a custom one.
First create your success handler, my checks to see if the user is an admin user.
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler
import org.springframework.security.core.Authentication
import javax.servlet.ServletException
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import javax.servlet.http.HttpSession
class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
def requestCache
boolean administrator = false
#Override
protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) {
println administrator
if(administrator){
return "/admin"
} else {
return super.determineTargetUrl(request, response)
}
}
#Override
public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response,
final Authentication authentication) throws ServletException, IOException {
try {
checkSetAdministratorUser(authentication)
handle(request, response, authentication)
super.clearAuthenticationAttributes(request)
}catch(Exception e){
e.printStackTrace()
} finally {
// always remove the saved request
requestCache.removeRequest(request, response)
}
}
protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
String targetUrl = determineTargetUrl(request, response)
if (response.isCommitted()) {
logger.debug("Response has already been committed. Unable to redirect to " + targetUrl)
return
}
redirectStrategy.sendRedirect(request, response, targetUrl)
}
def checkSetAdministratorUser(authentication){
authentication.authorities.each(){ authority ->
if(authority.authority == "ROLE_ADMIN")administrator = true
}
}
}
Then I had to define the success handler in the beans section of resources.groovy
beans = {
authenticationSuccessHandler(CustomAuthenticationSuccessHandler) {
requestCache = ref('requestCache')
redirectStrategy = ref('redirectStrategy')
}
}
Then I was good to go. It worked well for my scenario.
Thanks: https://groggyman.com/2015/04/05/custom-authentication-success-handler-with-grails-and-spring-security/

Spring WebSocket. Get access to Cookie in Config

I'm trying to configure WebSocket via Spring with STOMP, OAuth 2 and SockJS.
New spec tells us how to implement it using Interceptors.
The case is: if user is authenticated there is an Bearer Token in Native header of CONNECT request and there is no problem to set principal via Token.
But my task is to use BrowserToken for unauthorized users (which is saved in Cookies). How can i get it from the request?
I've found a solution:
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket/tracker")
.withSockJS()
.setInterceptors(httpSessionHandshakeInterceptor());
}
#Bean
public HandshakeInterceptor httpSessionHandshakeInterceptor() {
return new HandshakeInterceptor() {
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletServerRequest = (ServletServerHttpRequest) request;
HttpServletRequest servletRequest = servletServerRequest.getServletRequest();
Cookie token = WebUtils.getCookie(servletRequest, "key");
attributes.put("token", token.getValue());
}
return true;
}
#Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
};
}
And finally
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Map<String, Object> sessionAttributes = accessor.getSessionAttributes();
List<String> authorization = accessor.getNativeHeader("Authorization");
Principal user = ... ; // get Principal using authentication / browser token
accessor.setUser(user);
}
return message;
}
});
}

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 to set a custom invalid session strategy in Spring Security

I'm developing a web application, based on Spring-Boot - 1.1.6, Spring -Security -3.2.5 and more.
I'm using Java based configuration:
#Configuration
#EnableWebMvcSecurity
public class SecurityCtxConfig extends WebSecurityConfigurerAdapter {
#Bean
DelegatingAuthenticationEntryPoint delegatingAuthenticationEntryPoint() {
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> map = new LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>();
Http403ForbiddenEntryPoint defaultEntryPoint = new Http403ForbiddenEntryPoint();
map.put(AnyRequestMatcher.INSTANCE, defaultEntryPoint);
DelegatingAuthenticationEntryPoint retVal = new DelegatingAuthenticationEntryPoint(map);
retVal.setDefaultEntryPoint(defaultEntryPoint);
return retVal;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling = http.exceptionHandling();
exceptionHandling.authenticationEntryPoint(delegatingAuthenticationEntryPoint());
http.logout().logoutSuccessHandler(new LogoutSuccessHandler() {
#Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication arg2)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
}
});
}
}
The requirement is to return Http status 401 in case that the session cookie is invalid or missing(no matter the reason)
I see the InvalidSessionStrategy but I don't find a way to set it on the SessionManagementFilter.
Can some one please instract me how to implement my plan or another one that will fulfill the requirement
Using SpringBoot this works for me:
#Configuration
#EnableWebSecurity
public class UISecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...
http.addFilterAfter(expiredSessionFilter(), SessionManagementFilter.class);
...
}
private Filter expiredSessionFilter() {
SessionManagementFilter smf = new SessionManagementFilter(new HttpSessionSecurityContextRepository());
smf.setInvalidSessionStrategy((request, response) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Session go BOOM!"));
return smf;
}
}
We had the exact same problem and I did this hack to solve it (yes I know, this is a hack, therefore the name...).
I create a BeanPostProcessor and search for the SessionManagementFilter to reconfigure it...
#Bean
public HackyBeanPostProcessor myBeanPostProcessor() {
return new HackyBeanPostProcessor();
}
protected static class HackyBeanPostProcessor implements BeanPostProcessor {
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
// FIXME check if a new spring-security version allows this in an
// other way (current: 3.2.5.RELEASE)
if (bean instanceof SessionManagementFilter) {
SessionManagementFilter filter = (SessionManagementFilter) bean;
filter.setInvalidSessionStrategy(new InvalidSessionStrategy() {
#Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
});
}
return bean;
}
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
return bean;
}
}
Since I'm using AspectJ (I mean, compile time weaving and not Spring AOP), it was quite easy to hack the SessionManagementFilter creation by setting my custom InvalidSessionStrategy after the SessionManagementFilter is constructed:
#Aspect
public class SessionManagementAspect {
private static final Log logger = LogFactory.getLog();
#AfterReturning("execution( org.springframework.security.web.session.SessionManagementFilter.new(..))&&this(smf)")
public void creation(JoinPoint pjp, SessionManagementFilter smf) throws Throwable {
logger.debug("Adding/Replacing the invalid session detection policy to return 401 in case of an invalid session");
smf.setInvalidSessionStrategy(new InvalidSessionStrategy() {
#Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
logInvalidSession(request, "invalid cookie");
if (!response.isCommitted())
response.sendError(HttpStatus.UNAUTHORIZED.value());
}
});
}
}
If you are not using AspectJ, try adding #Component and add this Aspect to your context, it might work if the SessionManagementFilter is a bean (Since Spring-AOP applias only on spring beans)

Resources