Spring websocket message broker adding extra Access-Control-Allow-Origin header to respose - spring-boot

I have an application that includes a Spring cloud gateway that sits in front of an app which (among other things) supports web socket connections (sockJS). The gateway does a simple url rewrite when it forwards to the app. The two are currently running Spring-Boot 2.0.5.RELEASE and Spring-Cloud Finchley.RELEASE. According to the source I pulled down, this should be using spring-websockets-5.0.9.
When I try to upgrade to 2.1.2.RELEASE and Greenwich.RELEASE for Spring-Boot and Spring-Cloud respectively, my websocket connections start failing because an extra Access-Cloud-Allow-Origin is being injected into the response.
My gateway has a simple CORS filter like this (the values are constants and not relevant):
#Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
Mono<Void> result;
ServerHttpRequest request = ctx.getRequest();
if (CorsUtils.isCorsRequest(request)) {
ServerHttpResponse response = ctx.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);
headers.add("Access-Control-Max-Age", MAX_AGE);
headers.add("Access-Control-Allow-Headers",ALLOWED_HEADERS);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
result = Mono.empty();
} else {
result = chain.filter(ctx);
}
} else {
result = chain.filter(ctx);
}
return result;
};
}
And my web socket config on the downstream app is simply this:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS();
}
}
If I comment out the .setAllowedOrigins("*") in the registerStompEndpoints method, I correctly get 403 access denied responses, and the response only has the Access-Control-Allow-Origin header as injected by the gateway.
With the method in place as shown here, the websocket response completes as expected with a success response to the caller, but the response header contains both the access control header injected by the gateway plus another Access-Control-Allow-Origin header which is set to the value of the caller (in my case, http://localhost:4200 for the front-end application.) None of the other access control headers are duplicated.
How can I configure the Spring websocket message broker to not inject the Access-Control-Allow-Origin header? This was working, and still works if I roll back to 2.0.5/Finchley.

I faced this issue recently and I was able to resolve it by calling setSupressCors method. The documentation says that
This option can be used to disable automatic addition of CORS headers for SockJS requests.
Here is a code sample:
#Configuration
#EnableWebSocketMessageBroker
public class WebsocketMessageBrokerConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket/handshake")
.setAllowedOrigins("*")
.withSockJS()
.setSupressCors(true);
}
}

Related

Spring - Sock.js - websockets: blocked by CORS policy

I am implementing websockets to Spring App with sock.js + stomp.js on the client app.
When trying to connect I am getting the error:
Access to XMLHttpRequest at 'http://localhost:8080/ws/tracker/info?t=...' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
And in my WebsocketConfiguration :
#Configuration
#EnableWebSocketMessageBroker
#Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebsocketConfiguration implements WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic/", "/queue/");
registry.setApplicationDestinationPrefixes("/app");
}
}
Client libraries:
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.4.0/sockjs.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js"></script>
Client connection:
var socket = new SockJS('http://localhost:8080/websocket/tracker');
I have already implemented websockets in one of the earlier projects, so this is all copied from it, though I can't remember (nor find) how to solve this error.
Check if you have ClientForwardController, mapping should be like this:
#GetMapping(value = {"/{path:[^\\.]*}", "/{path:^(?!websocket).*}/**/{path:[^\\.]*}"})
public String forward() {
return "forward:/";
}

CORS error when accessing static resource

Despite having the following config, accessing http://localhost:8080/rooms/rooms.json gives me a CORS error - No 'Access-Control-Allow-Origin' header is present on the requested resource.
I have no problem to request any other path which is mapped by controller. What is the problem with static resources? How to allow cors request or exclude the resource paths without spring security?
Spring Boot 2.0.5
Spring Boot Web Starter 2.0.5
#Configuration
#EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
#Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/rooms/**")
.addResourceLocations("classpath:/rooms/")
.setCachePeriod(3600)
.resourceChain(true)
.addResolver(new PathResourceResolver());
}
#Override
public void addCorsMappings(final CorsRegistry registry) {
registry.addMapping("/**");
}
}
I got it working with the following configuration bean:
#Configuration
public class StaticResourcesCorsConfig
{
#Bean
public WebMvcConfigurer corsConfigurer()
{
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*");
}
};
}
}
Note, that Spring Boot will only send the Access-Control-Allow-Origin header back on a GET request, if the Origin-header is present on the request.
Update addCorsMappings like below it could work
#Configuration
#EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
#Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/rooms/**")
.addResourceLocations("classpath:/rooms/")
.setCachePeriod(3600)
.resourceChain(true)
.addResolver(new PathResourceResolver());
}
#Override
public void addCorsMappings(final CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("POST", "GET")
//.allowedHeaders("header1", "header2", "header3")
//.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
}
}
Add #CrossOrigin(value = "*") to your controller class. You can replace * with any particular URL in case to allow that origin only.
#CrossOrigin(origins = "http://localhost")
#GetMapping("/rooms/")
public Object rooms() {
// your implementation
}
You can do in this way.
https://www.viator.com/orion/nova/public/mmblite/styles-53929dcb.css
Experienced the same problem actually, but found the root cause and a solution.
Your request was most probably cached by intermediary: load balancer, CDN or caching HTTP server in front of your application as regular non-CORS request. Then you have sent request with Origin:, but the intermediary returned you the same cached response because from point of view of the cache responses by default are identified by /path + METHOD + Host:-header which were the same. To tell caches that the request with Origin: and the regular request (without Origin: need to be cached as independent entries in any cache we need Vary: Origin header in both responses. This was fixed/implemented in Spring 5.2.x (in my case it was Spring 5.1.9), in your case it was 5.0.9 (as dependency of Spring Boot 2.0.5.). Once I upgraded to Spring 5.2.0 all was fixed once caches on the intermediary had expired. I recommend to upgrade beyond 5.2.6 (cause there were further changes in CORS handling, which are nice to have).
here is the line (which made the difference) they (Pivotal) commited into Spring: https://github.com/spring-projects/spring-framework/commit/d27b5d0ab6e8b91a77e272ad57ae83c7d81d810b#r36264428
and their bug description: https://github.com/spring-projects/spring-framework/issues/22273

Send User-specific message with spring

I have an endpoint (/create) which has some logic and it takes 3-4 min to process so I used rabbitmq and as soon as the endpoint receive the request it takes the body and post the message in rabbitmq, the listener listens to the message and process the request now I want to notify the user that his request is successfully processed.
Is websocket correct choice for this requirement
Is there other better way through which i can achieve my goal?
So I went forward with websocket since I am using oauth based authentication I am unable to get web-socket work
Here is my code I have written:
SocketConfig.java
#Configuration
#EnableWebSocketMessageBroker
public class SocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic","/secured/queue");
//config.setApplicationDestinationPrefixes("/app");
//config.setUserDestinationPrefix("/secured/user");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/secured/messagereg").setAllowedOrigins("*").withSockJS();
}
SocketHandler.java
#Configuration
public class SocketHandler extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected boolean sameOriginDisabled() {
return true;
}
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpDestMatchers("/secured/**", "/secured/**/**").authenticated()
.anyMessage().authenticated();
}
}
WebSecurityConfig.java
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Profile("!test")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private Auth0PropertyConfig config;
#Override
protected void configure(HttpSecurity http) throws Exception {
JwtWebSecurityConfigurer
.forRS256(config.getAudience(), config.getIssuer())
.configure(http)
.cors()
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
}
}
clientCode
const socket = new SockJs("/secured/messagereg/?access_token="+token);
this.setState({ clientRef: Stomp.over(socket) }, () =>
this.state.clientRef.connect({},
frame => {
this.setState({ connection: true });
this.state.clientRef.subscribe("/user/secured/queue/reply", message => {
console.log("asd received ----------" + message.body);
this.setState(prevs => ({
message: [...prevs.message, message]
}));
});
},
error => {
console.log("Stomp protocol error." + error);
}
)
);
I am getting 401 unauthorized while connecting to socket.
In my opinion: a push messaging pattern (for example using STOMP) is suitable for this scenario, but that ultimately depends on your architectural principles. You could also poll the server for result (using REST API) which has both advantages (shared security architecture) and disadvantages (client code, traffic, and reaction-time overheads).
Answer:
In order to get your code working, I think you need one more method in your SocketConfig.java, which will hook into your OAUTH filter (or whatever method you may have in place).
Important - websocket auth does not reuse existing Spring Security context. That's why you need to implement auth again, for example in the SocketConfig class using the WebSocketMessageBrokerConfigurer's method configureClientInboundChannel.
The following example assumes you have already obtained the OAUTH token previously, and it's only used to reauthenticate the websocket connection. Setting the user reference in StompHeaderAccessor (3rd last line) will enable your code to send a message to the correct user.
It also requires that the OAUTH token is set in the message header, as opposed to the endpoint parameter in your example. I think that may be safer for websocks messaging as the message itself is encrypted on protocol level if you use wss.
#Autowired
private YourOauthService auth;
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message,
StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String token = accessor.removeNativeHeader("Authorization").get(0);
Authentication user = auth.getAuthentication(token);
accessor.setUser(user);
}
return message;
}
});
}
I found some more interesting examples in https://robertleggett.wordpress.com/2015/05/27/websockets-with-spring-spring-security/

spring : enable cross origin not working

I have a springboot app in which I got a WS that i'm able to request with a REST client.
I also have an angular app and I try to request the WS in it but I got this message :
Cross origin requests are only supported for protocol schemes: http,
data, chrome, chrome-extension, https.
I've added this code :
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
};
}
in my configuration class, like said in this tuto : https://spring.io/guides/gs/rest-service-cors/ but it seems that it's not enough. What do I miss? Do I have to do something in the angular app?
Thx

JSON Web Token (JWT) with Spring based SockJS / STOMP Web Socket

Background
I am in the process of setting up a RESTful web application using Spring Boot (1.3.0.BUILD-SNAPSHOT) that includes a STOMP/SockJS WebSocket, which I intend to consume from an iOS app as well as web browsers. I want to use JSON Web Tokens (JWT) to secure the REST requests and the WebSocket interface but I’m having difficulty with the latter.
The app is secured with Spring Security:-
#Configuration
#EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
public WebSecurityConfiguration() {
super(true);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("steve").password("steve").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling().and()
.anonymous().and()
.servletApi().and()
.headers().cacheControl().and().and()
// Relax CSRF on the WebSocket due to needing direct access from apps
.csrf().ignoringAntMatchers("/ws/**").and()
.authorizeRequests()
//allow anonymous resource requests
.antMatchers("/", "/index.html").permitAll()
.antMatchers("/resources/**").permitAll()
//allow anonymous POSTs to JWT
.antMatchers(HttpMethod.POST, "/rest/jwt/token").permitAll()
// Allow anonymous access to websocket
.antMatchers("/ws/**").permitAll()
//all other request need to be authenticated
.anyRequest().hasRole("USER").and()
// Custom authentication on requests to /rest/jwt/token
.addFilterBefore(new JWTLoginFilter("/rest/jwt/token", authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)
// Custom JWT based authentication
.addFilterBefore(new JWTTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
The WebSocket configuration is standard:-
#Configuration
#EnableScheduling
#EnableWebSocketMessageBroker
public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
}
I also have a subclass of AbstractSecurityWebSocketMessageBrokerConfigurer to secure the WebSocket:-
#Configuration
public class WebSocketSecurityConfiguration extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.anyMessage().hasRole("USER");
}
#Override
protected boolean sameOriginDisabled() {
// We need to access this directly from apps, so can't do cross-site checks
return true;
}
}
There is also a couple of #RestController annotated classes to handle various bits of functionality and these are secured successfully via the JWTTokenFilter registered in my WebSecurityConfiguration class.
Problem
However I can't seem to get the WebSocket to be secured with JWT. I am using SockJS 1.1.0 and STOMP 1.7.1 in the browser and can't figure out how to pass the token. It would appear that SockJS does not allow parameters to be sent with the initial /info and/or handshake requests.
The Spring Security for WebSockets documentation states that the AbstractSecurityWebSocketMessageBrokerConfigurer ensures that:
Any inbound CONNECT message requires a valid CSRF token to enforce Same Origin Policy
Which seems to imply that the initial handshake should be unsecured and authentication invoked at the point of receiving a STOMP CONNECT message. Unfortunately I can't seem to find any information with regards to implementing this. Additionally this approach would require additional logic to disconnect a rogue client that opens a WebSocket connection and never sends a STOMP CONNECT.
Being (very) new to Spring I'm also not sure if or how Spring Sessions fits into this. While the documentation is very detailed there doesn't appear to a nice and simple (aka idiots) guide to how the various components fit together / interact with each other.
Question
How do I go about securing the SockJS WebSocket by providing a JSON Web Token, preferably at the point of handshake (is it even possible)?
Current Situation
UPDATE 2016-12-13 : the issue referenced below is now marked fixed, so the hack below is no longer necessary which Spring 4.3.5 or above. See https://github.com/spring-projects/spring-framework/blob/master/src/docs/asciidoc/web/websocket.adoc#token-authentication.
Previous Situation
Currently (Sep 2016), this is not supported by Spring except via query parameter as answered by #rossen-stoyanchev, who wrote a lot (all?) of the Spring WebSocket support. I don't like the query parameter approach because of potential HTTP referrer leakage and storage of the token in server logs. In addition, if the security ramifications don't bother you, note that I have found this approach works for true WebSocket connections, but if you are using SockJS with fallbacks to other mechanisms, the determineUser method is never called for the fallback. See Spring 4.x token-based WebSocket SockJS fallback authentication.
I've created a Spring issue to improve support for token-based WebSocket authentication: https://jira.spring.io/browse/SPR-14690
Hacking It
In the meantime, I've found a hack that works well in testing. Bypass the built-in Spring connection-level Spring auth machinery. Instead, set the authentication token at the message-level by sending it in the Stomp headers on the client side (this nicely mirrors what you are already doing with regular HTTP XHR calls) e.g.:
stompClient.connect({'X-Authorization': 'token'}, ...);
stompClient.subscribe(..., {'X-Authorization': 'token'});
stompClient.send("/wherever", {'X-Authorization': 'token'}, ...);
On the server-side, obtain the token from the Stomp message using a ChannelInterceptor
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
Message<*> preSend(Message<*> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
List tokenList = accessor.getNativeHeader("X-Authorization");
String token = null;
if(tokenList == null || tokenList.size < 1) {
return message;
} else {
token = tokenList.get(0);
if(token == null) {
return message;
}
}
// validate and convert to a Principal based on your own requirements e.g.
// authenticationManager.authenticate(JwtAuthentication(token))
Principal yourAuth = [...];
accessor.setUser(yourAuth);
// not documented anywhere but necessary otherwise NPE in StompSubProtocolHandler!
accessor.setLeaveMutable(true);
return MessageBuilder.createMessage(message.payload, accessor.messageHeaders)
}
})
This is simple and gets us 85% of the way there, however, this approach does not support sending messages to specific users. This is because Spring's machinery to associate users to sessions is not affected by the result of the ChannelInterceptor. Spring WebSocket assumes authentication is done at the transport layer, not the message layer, and thus ignores the message-level authentication.
The hack to make this work anyway, is to create our instances of DefaultSimpUserRegistry and DefaultUserDestinationResolver, expose those to the environment, and then use the interceptor to update those as if Spring itself was doing it. In other words, something like:
#Configuration
#EnableWebSocketMessageBroker
#Order(HIGHEST_PRECEDENCE + 50)
class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer() {
private DefaultSimpUserRegistry userRegistry = new DefaultSimpUserRegistry();
private DefaultUserDestinationResolver resolver = new DefaultUserDestinationResolver(userRegistry);
#Bean
#Primary
public SimpUserRegistry userRegistry() {
return userRegistry;
}
#Bean
#Primary
public UserDestinationResolver userDestinationResolver() {
return resolver;
}
#Override
public configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue", "/topic");
}
#Override
public registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/stomp")
.withSockJS()
.setWebSocketEnabled(false)
.setSessionCookieNeeded(false);
}
#Override public configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
Message<*> preSend(Message<*> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
List tokenList = accessor.getNativeHeader("X-Authorization");
accessor.removeNativeHeader("X-Authorization");
String token = null;
if(tokenList != null && tokenList.size > 0) {
token = tokenList.get(0);
}
// validate and convert to a Principal based on your own requirements e.g.
// authenticationManager.authenticate(JwtAuthentication(token))
Principal yourAuth = token == null ? null : [...];
if (accessor.messageType == SimpMessageType.CONNECT) {
userRegistry.onApplicationEvent(SessionConnectedEvent(this, message, yourAuth));
} else if (accessor.messageType == SimpMessageType.SUBSCRIBE) {
userRegistry.onApplicationEvent(SessionSubscribeEvent(this, message, yourAuth));
} else if (accessor.messageType == SimpMessageType.UNSUBSCRIBE) {
userRegistry.onApplicationEvent(SessionUnsubscribeEvent(this, message, yourAuth));
} else if (accessor.messageType == SimpMessageType.DISCONNECT) {
userRegistry.onApplicationEvent(SessionDisconnectEvent(this, message, accessor.sessionId, CloseStatus.NORMAL));
}
accessor.setUser(yourAuth);
// not documented anywhere but necessary otherwise NPE in StompSubProtocolHandler!
accessor.setLeaveMutable(true);
return MessageBuilder.createMessage(message.payload, accessor.messageHeaders);
}
})
}
}
Now Spring is fully aware of the the authentication i.e. it injects the Principal into any controller methods that require it, exposes it to the context for Spring Security 4.x, and associates the user to the WebSocket session for sending messages to specific users/sessions.
Spring Security Messaging
Lastly, if you use Spring Security 4.x Messaging support, make sure to set the #Order of your AbstractWebSocketMessageBrokerConfigurer to a higher value than Spring Security's AbstractSecurityWebSocketMessageBrokerConfigurer (Ordered.HIGHEST_PRECEDENCE + 50 would work, as shown above). That way, your interceptor sets the Principal before Spring Security executes its check and sets the security context.
Creating a Principal (Update June 2018)
Lots of people seem to be confused by this line in the code above:
// validate and convert to a Principal based on your own requirements e.g.
// authenticationManager.authenticate(JwtAuthentication(token))
Principal yourAuth = [...];
This is pretty much out of scope for the question as it is not Stomp-specific, but I'll expand on it a little bit anyway, because its related to using auth tokens with Spring. When using token-based authentication, the Principal you need will generally be a custom JwtAuthentication class that extends Spring Security's AbstractAuthenticationToken class. AbstractAuthenticationToken implements the Authentication interface which extends the Principal interface, and contains most of the machinery to integrate your token with Spring Security.
So, in Kotlin code (sorry I don't have the time or inclination to translate this back to Java), your JwtAuthentication might look something like this, which is a simple wrapper around AbstractAuthenticationToken:
import my.model.UserEntity
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.core.GrantedAuthority
class JwtAuthentication(
val token: String,
// UserEntity is your application's model for your user
val user: UserEntity? = null,
authorities: Collection<GrantedAuthority>? = null) : AbstractAuthenticationToken(authorities) {
override fun getCredentials(): Any? = token
override fun getName(): String? = user?.id
override fun getPrincipal(): Any? = user
}
Now you need an AuthenticationManager that knows how to deal with it. This might look something like the following, again in Kotlin:
#Component
class CustomTokenAuthenticationManager #Inject constructor(
val tokenHandler: TokenHandler,
val authService: AuthService) : AuthenticationManager {
val log = logger()
override fun authenticate(authentication: Authentication?): Authentication? {
return when(authentication) {
// for login via username/password e.g. crash shell
is UsernamePasswordAuthenticationToken -> {
findUser(authentication).let {
//checkUser(it)
authentication.withGrantedAuthorities(it).also { setAuthenticated(true) }
}
}
// for token-based auth
is JwtAuthentication -> {
findUser(authentication).let {
val tokenTypeClaim = tokenHandler.parseToken(authentication.token)[CLAIM_TOKEN_TYPE]
when(tokenTypeClaim) {
TOKEN_TYPE_ACCESS -> {
//checkUser(it)
authentication.withGrantedAuthorities(it).also { setAuthenticated(true) }
}
TOKEN_TYPE_REFRESH -> {
//checkUser(it)
JwtAuthentication(authentication.token, it, listOf(SimpleGrantedAuthority(Authorities.REFRESH_TOKEN)))
}
else -> throw IllegalArgumentException("Unexpected token type claim $tokenTypeClaim.")
}
}
}
else -> null
}
}
private fun findUser(authentication: JwtAuthentication): UserEntity =
authService.login(authentication.token) ?:
throw BadCredentialsException("No user associated with token or token revoked.")
private fun findUser(authentication: UsernamePasswordAuthenticationToken): UserEntity =
authService.login(authentication.principal.toString(), authentication.credentials.toString()) ?:
throw BadCredentialsException("Invalid login.")
#Suppress("unused", "UNUSED_PARAMETER")
private fun checkUser(user: UserEntity) {
// TODO add these and lock account on x attempts
//if(!user.enabled) throw DisabledException("User is disabled.")
//if(user.accountLocked) throw LockedException("User account is locked.")
}
fun JwtAuthentication.withGrantedAuthorities(user: UserEntity): JwtAuthentication {
return JwtAuthentication(token, user, authoritiesOf(user))
}
fun UsernamePasswordAuthenticationToken.withGrantedAuthorities(user: UserEntity): UsernamePasswordAuthenticationToken {
return UsernamePasswordAuthenticationToken(principal, credentials, authoritiesOf(user))
}
private fun authoritiesOf(user: UserEntity) = user.authorities.map(::SimpleGrantedAuthority)
}
The injected TokenHandler abstracts away the JWT token parsing, but should use a common JWT token library like jjwt. The injected AuthService is your abstraction that actually creates your UserEntity based on the claims in the token, and may talk to your user database or other backend system(s).
Now, coming back to the line we started with, it might look something like this, where authenticationManager is an AuthenticationManager injected into our adapter by Spring, and is an instance of CustomTokenAuthenticationManager we defined above:
Principal yourAuth = token == null ? null : authenticationManager.authenticate(new JwtAuthentication(token));
This principal is then attached to the message as described above. HTH!
With the latest SockJS 1.0.3 you can pass query parameters as a part of connection URL. Thus you can send some JWT token to authorize a session.
var socket = new SockJS('http://localhost/ws?token=AAA');
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
stompClient.subscribe('/topic/echo', function(data) {
// topic handler
});
}
}, function(err) {
// connection error
});
Now all the requests related to websocket will have parameter "?token=AAA"
http://localhost/ws/info?token=AAA&t=1446482506843
http://localhost/ws/515/z45wjz24/websocket?token=AAA
Then with Spring you can setup some filter which will identify a session using provided token.
Seems like support for a query string was added to the SockJS client, see https://github.com/sockjs/sockjs-client/issues/72.
As of now, it is possible either to add auth token as a request parameter and handle it on a handshake, or add it as a header on a connection to stomp endpoint, and handle it on the CONNECT command in the interceptor.
Best thing would be to use header, but the problem is that you can't access native header on the handshake step, so you wouldn't be able to handle the auth there then.
Let me give some example code:
Config:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-test")
.setHandshakeHandler(new SecDefaultHandshakeHandler())
.addInterceptors(new HttpHandshakeInterceptor())
.withSockJS()
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new JwtChannelInterceptor())
}
}
Handshake interceptor:
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Map<String, Object> attributes) {
attributes.put("token", request.getServletRequest().getParameter("auth_token")
return true
}
}
Handshake handler:
public class SecDefaultHandshakeHandler extends DefaultHandshakeHandler {
#Override
public Principal determineUser(ServerHttpRequest request, WebSocketHandler handler, Map<String, Object> attributes) {
Object token = attributes.get("token")
//handle authorization here
}
}
Channel Interceptor:
public class JwtChannelInterceptor implements ChannelInterceptor {
#Override
public void postSend(Message message, MessageChannel channel, Boolean sent) {
MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class)
if (StompCommand.DISCONNECT == accessor.getCommand()) {
//retrieve Principal here via accessor.getUser()
//or get auth header from the accessor and handle authorization
}
}
}
Sorry for possible compile mistakes, I was converting manually from Kotlin code =)
As you mentioned that you have both web and mobile clients for your WebSockets, please mind that there are some difficulties maintaining same codebase for all clients. Please see my thread: Spring Websocket ChannelInterceptor not firing CONNECT event
I spend a lot of time to find simple solution. For me solution of Raman didn't work.
All you need is define custom bearerTokenResolver method and put access token into cookies or parameter.
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**")
.hasAuthority("SCOPE_read")
.antMatchers(HttpMethod.POST, "/api/foos")
.hasAuthority("SCOPE_write")
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer()
.jwt().and().bearerTokenResolver(this::tokenExtractor);
}
...
}
public String tokenExtractor(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header != null)
return header.replace("Bearer ", "");
Cookie cookie = WebUtils.getCookie(request, "access_token");
if (cookie != null)
return cookie.getValue();
return null;
}

Resources