Spring with websocket and session interceptor + Tomcat = problem - java

I am facing weird problem. I am developing web application based on Spring Boot with WebSockets. I need to track anonymous web socket connections, therefore I am using custom Principal implementation which I am creating in custom DefaultHandshakeHandler -> determineUser method. To create such Principal object, I need some data from httpsession, therefore I am using HttpSessionHandshakeInterceptor to fill Map<String, Object> attributes attribute in mentioned method.
Everything works like a charm, until I switch application packaging from JAR (using embeded Tomcat) to WAR and deploy it on standalone Tomcat. Suddenly, attribute attributes is empty as if WS connection is using different session than HTTP connection and therefore HttpSessionHandshakeInterceptor will not pass required attributes. Any ideas, why it is behaving differently?
A few parts of the code:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/ws");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("ws-endpoint")
.setHandshakeHandler(new AnonymousHandshakeHandler())
.addInterceptors(new HttpSessionHandshakeInterceptor())
.withSockJS();
}
}
Custom handshake handler:
public class AnonymousHandshakeHandler extends DefaultHandshakeHandler {
#Override
protected Principal determineUser(ServerHttpRequest request,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
Principal principal = request.getPrincipal();
if (principal == null) {
SessionData sd = (SessionData) attributes.get(AppVariables.MODEL_PARAM_SESSION);
if (sd != null){
principal = new AnonymousPrincipal();
((AnonymousPrincipal) principal).setName(sd...);
}
}
return principal;
}
}
UPDATE:
Using custom handshake interceptor I can see that WS connections have different session IDs. That explains empty attributes, but why? Why requests on embeded Tomcat are using the same session but on standalone Tomcat they are creating new session on every request?
Handshake interceptor:
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession();
System.out.println(session.getId());
attributes.put(AppVariables.MODEL_PARAM_SESSION, session.getAttribute(AppVariables.MODEL_PARAM_SESSION));
}
return true;
}
#Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception ex) {
}
}

I resolved my problem. It consisted of two problems + my stupidity:
I am using <CookieProcessor sameSiteCookies="none" /> and I forgot about it. That is why every request was generating new session (sameSite without https was not creating JSESSIONID cookie).
My proxy was not set up well. That is why my AnonymousHandshakeHandler was not used. See: link

Related

Set custom response headers on Spring Security authentication success URL

After a successful SAML 2.0 login on a Spring Security application acting as service provider, I can redirect the user to a static success URL using:
#Configuration
#EnableWebSecurity
class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.saml2Login(saml2Login -> saml2Login.defaultSuccessUrl(mySuccessUrl);
}
}
How can I (dynamically) set these other pieces of information?
URL parameters
Response headers
Cookies
So far, I found a (rather cumbersome) solution: set an AuthenticationSuccessHandler with a custom RedirectStrategy.
SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
handler.setDefaultTargetUrl(relayState);
handler.setRedirectStrategy(new DefaultRedirectStrategy() {
#Override
public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
// do stuff ...
// set response headers ...
// set cookies ...
// add a query string to the redirect URL ...
super.sendRedirect(request, response, url);
}
});
http.saml2Login(saml2Login -> saml2Login.successHandler(handler));
Are there better alternatives?
I am using Spring Boot 2.3 and Spring Security 5.2.

Spring Boot with read-only session for single sign on

We have a legacy Spring application (A) (that is not using spring-boot) that handles authentication and writes the session to Redis using spring-session (the data in Redis is stored as XML).
We now want to introduce a new application (B), using spring-boot 2.2.6.RELEASE and spring-session Corn-RC1, that should be useable if a user has signed into (A) with ROLE_ADMIN. I.e. this can be regarded as a very crude way of doing single sign on. A user should never be able to authenticate in B (it'd like to disable authentication if possible), it should only check that an existing user is authenticated in the session repository (redis) and has ROLE_ADMIN. Both A and B will be located under the same domain so cookies will be propagated by the browser. I've tried various different ways of getting this to work, for example:
#Configuration
#EnableWebSecurity
class ServiceBSpringSecurityConfig : WebSecurityConfigurerAdapter() {
#Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
}
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.formLogin()
.and()
.httpBasic().disable()
}
}
but this will show the default login screen:
I've also tried removing this part entirely:
#Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
}
but then it'll generate a default user and password and it does not seem to call the configure method (or the configuration doesn't work regardless).
How can I solve this?
What you need is to disable formLogin and httBasic on Application B and add a filter before spring's authentication filter AnonymousAuthenticationFilter or UsernamePasswordAuthenticationFilter. In the custom filter you will extract the cookie/header/token from the request object and based on that reach out to the redis cache for session details. This filter would then validate the session and create object of type org.springframework.security.core.Authentication and set that in the current SpringSecurityContext.
Below is the sudo code for this;
ServiceBSpringSecurityConfig
#Configuration
#EnableWebSecurity
public class ServiceBSpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(authEntryPoint()).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.httpBasic().disabled().and()
.formLogin().disabled().and()
.authorizeRequests().anyRequest().hasRole("ADMIN")
http.addFilterBefore(authTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
#Bean
public AuthTokenFilter authTokenFilter() {
return new AuthTokenFilter();
}
#Bean
public AuthEntryPoint authEntryPoint() {
return new AuthEntryPoint()
}
}
AuthEntryPoint
public class AuthEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPoint.class);
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// Very generic authEntryPoint which simply returns unauthorized
// Could implement additional functionality of forwarding the Application A login-page
logger.error("Unauthorized error: {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
}
}
AuthTokenFilter
public class AuthTokenFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// extract some sort of token or cookie value from request
token = request.getHeader("Token");
if (token != null) {
// Validate the token by retrieving session from redis cache
// Create org.springframework.security.core.Authentication from the token
Authentication auth = authFactory.getAuthentication(token);
// Set the spring security context with the auth
SecurityContextHolder.getContext().setAuthentication(auth);
} else {
// Do something if token not present at all
}
// Continue to to filter chain
filterChain.doFilter(request, response);
}
}
As mentioned this is sudo code so some adjustment might be required. However the general gist of token based auth remains the same.

Spring Security authenticate user via post

I have a react app running on a separate port (localhost:3000) that i want to use to authenticate users with, currently i have a proxy setup to my Spring backend (localhost:8080).
Can I somehow manually authenticate instead of http.httpBasic() by sending a POST request to my backend and getting back a session cookie then include the cookie with every request? It would simplify the auth process on iOS side aswell (using this process i could only store the session cookie value in keychain and pass it with every request made to my api)
How would I disable csrf for non-browser requests?
Is there a better approach to this? Diffrent paths for browser and mobile auth?
{
"username": "user",
"password": "12345678"
}
Handle the request in spring controller
#PostMapping(path = "/web")
public String authenticateUser() {
//Receive the auth data here... and perform auth
//Send back session cookie
return "Success?";
}
My WebSecurityConfigurerAdapter.java
#Configuration
#EnableWebSecurity
public class WebsecurityConfig extends WebSecurityConfigurerAdapter {
private final DetailService detailService;
public WebsecurityConfig(DetailService detailService) {
this.detailService = detailService;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(detailService).passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST,"/api/v1/authenticate/new").permitAll()
.antMatchers(HttpMethod.POST,"/api/v1/authenticate/web").permitAll()
.anyRequest().authenticated();
}
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "POST").allowedOrigins("http://localhost:8080");
}
};
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(14);
}
}
You can create an endpoint that takes user's credentials in a request body, perform authentication and then set tokens, and other required parameters in HttpOnly cookies.
After setting cookies, subsequent requests can read access/refresh token from cookies and add it in requests, you can then use custom CheckTokenEndpoint to verify tokens.
In the following example TokenParametersDto is a POJO that has username and password properties.
For issuing token (by verifying credentials) you can delegate call to TokenEndpoint#postAccessToken(....) or use its logic to your own method.
#PostMapping(path = "/oauth/http/token", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> issueToken(#RequestBody final #Valid #NotNull TokenParametersDto tokenParametersDto,
final HttpServletResponse response) {
final OAuth2AccessToken token = tokenService.issueToken(tokenParametersDto);
storeTokenInCookie(token, response);
return new ResponseEntity<>(HttpStatus.OK);
}
private void storeTokenInCookie(final OAuth2AccessToken token, final HttpServletResponse response) {
final Cookie accessToken = new Cookie("access_token", token.getValue());
accessToken.setHttpOnly(true);
accessToken.setSecure(sslEnabled);
accessToken.setPath("/");
accessToken.setMaxAge(cookieExpiration);
final Cookie tokenType = new Cookie("token_type", token.getTokenType());
tokenType.setHttpOnly(true);
tokenType.setSecure(sslEnabled);
tokenType.setPath("/");
tokenType.setMaxAge(cookieExpiration);
// Set Refresh Token and other required cookies.
response.addCookie(accessToken);
response.addCookie(tokenType);
}
Check this answer for disabling CSRF for a specific URL section.

Spring Keycloak: Get User ID

I have developed a Spring Boot Webservice and use Keycloak for Access Management.
The website stores some userdata in a database. I try to connect these data with the user logged in.
At the moment I store the username with the data. But I like to store the user id instead the username. How can I do that?
I try to get SecurityContext by this:
#Bean
#Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public KeycloakSecurityContext getKeycloakSecurityContext() {
return ((KeycloakPrincipal<KeycloakSecurityContext>) getRequest().getUserPrincipal()).getKeycloakSecurityContext();
}
But I get an error:
There was an unexpected error (type=Internal Server Error, status=500).
Error creating bean with name 'scopedTarget.getKeycloakSecurityContext'
defined in com.SiteApplication: Bean instantiation via factory method
failed; nested exception is
org.springframework.beans.BeanInstantiationException: Failed to
instantiate [org.keycloak.KeycloakSecurityContext]: Factory method
'getKeycloakSecurityContext' threw exception; nested exception is
java.lang.ClassCastException:
org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken
cannot be cast to org.keycloak.KeycloakPrincipal
Is this the right way? What is missing?
Thank you!
I found a much simpler solution than the above:
#GetMapping
public ResponseEntity getEndpoint(String someParam, HttpServletRequest request) {
KeycloakAuthenticationToken principal = (KeycloakAuthenticationToken) request.getUserPrincipal();
String userId = principal.getAccount().getKeycloakSecurityContext().getIdToken().getSubject();
//....do something
return new ResponseEntity(HttpStatus.OK);
}
I think the exception you are getting above is because you are trying to cast getRequest().getUserPrincipal() to KeycloakPrincipal<KeycloakSecurityContext> while it is of type KeycloakAuthenticationToken, so ((KeycloakAuthenticationToken) getRequest().getUserPrincipal()) would work.
I've done something similar in our code.
public class AuthenticationTokenProcessingFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!(request instanceof HttpServletRequest)) {
throw new RuntimeException("Expecting a HTTP request");
}
RefreshableKeycloakSecurityContext context = (RefreshableKeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
if (context == null) {
handleNoSecurityContext(request, response, chain);
return;
}
AccessToken accessToken = context.getToken();
Integer userId = Integer.parseInt(accessToken.getOtherClaims().get("user_id").toString());
chain.doFilter(request, response);
}
}
Before you can do this you must add the user_id to the access tokens being issued by keycloak. You can do this through a mapper as shown in the screenshot below.
Also, don't forgot to add the processing filter from above to your application lifecycle by adding a #Bean method to your application class.
#Configuration
#EnableAutoConfiguration(exclude = {FallbackWebSecurityAutoConfiguration.class, SpringBootWebSecurityConfiguration.class, DataSourceAutoConfiguration.class})
#ComponentScan
#EnableAsync
public class MyServiceClass {
public static void main(String[] args) {
Properties properties = new Properties();
new SpringApplicationBuilder(MyServiceClass.class)
.properties(properties)
.bannerMode(Banner.Mode.OFF)
.run(args);
}
#Bean
public AuthenticationTokenProcessingFilter authFilter() {
return new AuthenticationTokenProcessingFilter();
}
}
All solutions above are using the (very) deprecated Keycloak Spring adapters, which was a solution 2 years ago, but isn't anymore.
2 Alternatives to Keycloak Spring adapters, both having very easy way to access-token claims from Authentication "auto-magically" injected by Spring as #Controller method parameter.
spring-addons-webmvc-jwt-resource-server
Sample here: almost everything configurable from properties
#GetMapping
#PreAuthorize("isAuthenticated()")
public ResponseEntity getEndpoint(OAuthentication<OpenidClaimSet> auth) {
final var preferredUsername = auth.getClaims().getPreferredUsername();
final var subject = auth.getClaims().getSubject();
//....do something
return new ResponseEntity(HttpStatus.OK);
}
spring-boot-starter-oauth2-resource-server
Sample there: requires much more Java config
#GetMapping
#PreAuthorize("isAuthenticated()")
public ResponseEntity getEndpoint(JwtAuthenticationToken auth) {
final var preferredUsername = auth.getToken().getClaimAsString(StandardClaimNames.SUB);
final var subject = auth.getToken().getClaimAsString(StandardClaimNames.PREFERRED_USERNAME);
//....do something
return new ResponseEntity(HttpStatus.OK);
}

Configuring an AuthenticationSuccessHandler with Spring Boot 1.3.2 (without spring-cloud-security) and #EnableOAuth2Sso

We have a Spring Boot 1.3.2/Webflow web app which we're converting to use SSO. I've followed the steps in the "Migrating OAuth2 Apps from Spring Boot 1.2 to 1.3" blog and have the app handing off to our Auth server for authentication and the web app using the token to populate it's security context correctly.
The only piece not working is the custom authentication success handler we have that configures a few bits in the users session before they continue to their landing page.
This is currently configured as follows in our security config, which extends WebSecurityConfigurerAdapter
#Override
protected void configure(HttpSecurity http) throws Exception {
// These are all the unprotected endpoints.
http.authorizeRequests()
.antMatchers(new String[] { "/", "/login", "/error",
"/loginFailed", "/static/**" })
.permitAll();
// Protect all the other endpoints with a login page.
http.authorizeRequests().anyRequest()
.hasAnyAuthority("USER", "ADMIN").and().formLogin().loginPage("/login").failureUrl("/loginFailed")
.successHandler(customAuthenticationSuccessHandler()).and().logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
#Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
if (accessDeniedException instanceof CsrfException) {
response.sendRedirect(request.getContextPath() + "/logout");
}
}
});
}
I can see the handler being configured during startup, but it is never called once the user has successfully logged in.
All of the questions I've found on the subject refer to using a OAuth2SsoConfigurerAdapter, however as we're no longer using spring-cloud-security this class is not available.
UPDATE: I've discovered that this is possible using a BeanPostProcessor:
public static class DefaultRolesPrefixPostProcessor implements BeanPostProcessor, PriorityOrdered {
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof FilterChainProxy) {
FilterChainProxy chains = (FilterChainProxy) bean;
for (SecurityFilterChain chain : chains.getFilterChains()) {
for (Filter filter : chain.getFilters()) {
if (filter instanceof OAuth2ClientAuthenticationProcessingFilter) {
OAuth2ClientAuthenticationProcessingFilter oAuth2ClientAuthenticationProcessingFilter = (OAuth2ClientAuthenticationProcessingFilter) filter;
oAuth2ClientAuthenticationProcessingFilter
.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler());
}
}
}
}
return bean;
}
}
Is there a better way to configure this though?
If you follow Dave Syers excellent Spring boot oauth2 tutorial, you will end up with a method that returns your ssoFilter
I added a setAuthenticationSuccessHandler to this filter
#Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
private Filter ssoFilter() {
OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");
OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
facebookFilter.setRestTemplate(facebookTemplate);
facebookFilter.setTokenServices(new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId()));
facebookFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
return facebookFilter;
}
And my CustomAuthenticationSuccessHandler was just a component that extended AuthenticationSuccessHandler
#Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//implementation
}
}

Categories