I'm working on a Spring framework 3.2 version project. Requirement is to call an external Web-service which requires basic authentication.
Currently, I'm using HttpClient and adding basic authentication into header. but this is being done for each requests. I know in Spring Boot we can achieve that by using RestTemplateBuilder. IS there a way we can maintain a single authentication for all the requests by adding authentication only once?
TIA
You can define a RestTemplate bean with authentication details like this:
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.basicAuthorization("user", "secret").build();
}
Updated
An alternative way to do this is by implementing an interceptor. RestTemplate extends the InterceptingHttpAccessor interface which has a setInterceptors() method. You can use it to inject an interceptor to set request headers as needed.
public class MyInterceptor implements ClientHttpRequestInterceptor {
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// ... set headers on the request ...
return execution.execute(request, body);
}
}
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList(new MyInterceptor()))
return restTemplate
}
Note: untested code, but should work. You can check the docs for more details.
https://docs.spring.io/autorepo/docs/spring-framework/3.2.0.RELEASE/javadoc-api/index.html?org/springframework/web/client/RestTemplate.html
The HttpClient can be built from the HttpClientBuilder.
This builder has setDefaultHeaders(Collection<? extends org.apache.http.Header> defaultHeaders) method which you can set the headers you want. Then when you want an HttpClient, call httpClientBuilder.build().
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);
}
}
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/
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;
}
I'm doing experiments with Spring 4 websockets and stomp, and I have a hard time figuring out how to get/set the current user and other session attributes in a message handling method annotated with #MessageMapping.
The documentation says that the message handling methods can take a Principal as argument, and I found that the principal is retrieved by Spring by calling getUserPrincipal() on the native socket session, and then associated with the socket session, but I haven't found any way to easily customize this behavior, other than writing a servlet filter and wrap the original request into a wrapper returning the principal found in my cookie.
So my questions are:
How to manually set the principal to the socket session, when the client connects (I have this information thanks to a custom cookie, and I don't use Spring security)?
If 1 is not possible, how to add additional attributes to the socket session when the client connects?
How to access the socket session and its attributes from a message handling method?
Is there a way to access the login and passcode sent by the browser at connection time. They seem to be completely ignore by Spring and not accessible.
UPDATE: With Spring 4.1 it is possible to set the user on the handshake for #1 from above. Per the Spring documentation you can create a new class which extends DefaultHandshakeHandler and override the determineUser method. Additionally you can also create a security filter which sets the principal as well if you have a token. I have implemented the second one myself and I include some sample code for both below.
For #2 and #3 I do not think that it is possible still. For #4 Spring intentionally ignores these per the documentation here.
SAMPLE CODE FOR DefaultHandshakeHandler SUBCLASS:
#Configuration
#EnableWebSocketMessageBroker
public class ApplicationWebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
public class MyHandshakeHandler extends DefaultHandshakeHandler {
#Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler,
Map<String, Object> attributes) {
// add your own code to determine the user
return null;
}
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/myEndPoint").setHandshakeHandler(new MyHandshakeHandler());
}
}
SAMPLE CODE FOR SECURITY FILTER:
public class ApplicationSecurityTokenFilter extends GenericFilterBean {
private final static String AUTHENTICATION_PARAMETER = "authentication";
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (servletRequest instanceof HttpServletRequest) {
// check to see if already authenticated before trying again
Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
if ((existingAuth == null) || !existingAuth.isAuthenticated()) {
HttpServletRequest request = (HttpServletRequest)servletRequest;
UsernamePasswordAuthenticationToken token = extractToken(request);
// dump token into security context (for authentication-provider to pick up)
if (token != null) { // if it exists
SecurityContextHolder.getContext().setAuthentication(token);
}
}
}
filterChain.doFilter(servletRequest,servletResponse);
}
private UsernamePasswordAuthenticationToken extractToken( HttpServletRequest request ) {
UsernamePasswordAuthenticationToken authenticationToken = null;
// do what you need to extract the information for a token
// in this example we assume a query string that has an authenticate
// parameter with a "user:password" string. A new UsernamePasswordAuthenticationToken
// is created and then normal authentication happens using this info.
// This is just a sample and I am sure there are more secure ways to do this.
if (request.getQueryString() != null) {
String[] pairs = request.getQueryString().split("&");
for (String pair : pairs) {
String[] pairTokens = pair.split("=");
if (pairTokens.length == 2) {
if (AUTHENTICATION_PARAMETER.equals(pairTokens[0])) {
String[] tokens = pairTokens[1].split(":");
if (tokens.length == 2) {
log.debug("Using credentials: " + pairTokens[1]);
authenticationToken = new UsernamePasswordAuthenticationToken(tokens[0], tokens[1]);
}
}
}
}
}
return authenticationToken;
}
}
// set up your web security for the area in question
#Configuration
public class SubscriptionWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/myEndPoint**","/myEndPoint/**").and()
.addFilterBefore(new ApplicationSecurityTokenFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic() // leave this if you want non web browser clients to connect and add an auth header
.and()
.csrf().disable();
}
}
** NOTE: ** DO NOT declare your filter as a Bean. If you do then it will also be picked up (at least using Spring Boot) in the generic filters so it will fire on every request.
This is impossible for the time being (Spring 4.0). An issue has been opened (and considered) at Spring: https://jira.springsource.org/browse/SPR-11228