Reputation: 721
In one of my projects I configured both rest services and websockets, both go through spring security filter that check for JWT. For websockets on the client side, application uses sockjs & stomp (on Angular2) and Spring websockets on the server side (Tomcat 8). When I open connection with Spring security enabled then I get below error two seconds after it gets opened. However when I open connection without spring security enabled connection does not get dropped.
angular2 connect()/subscribe()/send() - all go with JWT token
public connect() : void {
let sockjs = new SockJS('/rest/add?jwt=' + this.authService.getToken());
let headers : any = this.authService.getAuthHeader();
this.stompClient = Stomp.over(sockjs);
this.stompClient.connect(this.token, (frame) => {
this.log.d("frame", "My Frame: " + frame);
this.log.d("connected()", "connected to /add");
this.stompClient.subscribe('/topic/addMessage', this.authService.getAuthHeader(), (stompResponse) => {
// this.stompSubject.next(JSON.parse(stompResponse.body));
this.log.d("result of WS call: ", JSON.parse(stompResponse.body).message);
}, (error) => {
this.log.d(error);
});
});
}
public send(payload: string) {
this.stompClient.send("/app/add", this.token, JSON.stringify({'message': payload}));
}
JwtAuthenticationFilter.java
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthenticationFilter() {
super("/rest/**");
}
@Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return true;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String token = null;
String param = request.getParameter("jwt");
if(param == null) {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
throw new JwtAuthenticationException("No JWT token found in request headers");
}
token = header.substring(7);
} else {
token = param;
}
JwtAuthenticationToken authRequest = new JwtAuthenticationToken(token);
return getAuthenticationManager().authenticate(authRequest);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
// As this authentication is in HTTP header, after success we need to continue the request normally
// and return the response as if the resource was not secured at all
chain.doFilter(request, response);
}
}
JwtAuthenticationProvider.java
@Service
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Autowired
private SecurityService securityService;
@Override
public boolean supports(Class<?> authentication) {
return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
}
@Override
@Transactional(readOnly=true)
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
String token = jwtAuthenticationToken.getToken();
User user = securityService.parseToken(token);
if (user == null) {
throw new JwtAuthenticationException("JWT token is not valid");
}
return new AuthenticatedUser(user);
}
}
JwtAuthenticationSuccessHandler.java
@Service
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// We do not need to do anything extra on REST authentication success, because there is no page to redirect to
}
}
RestAuthenticationEntryPoint.java
@Service
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
// This is invoked when user tries to access a secured REST resource without supplying any credentials
// We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
Weboscket configuration:
<websocket:message-broker
application-destination-prefix="/app">
<websocket:stomp-endpoint path="/add">
<websocket:sockjs />
</websocket:stomp-endpoint>
<websocket:simple-broker prefix="/topic, /queue" />
</websocket:message-broker>
and my spring security
<context:component-scan base-package="com.myapp.ws.security"/>
<sec:global-method-security pre-post-annotations="enabled" />
<!-- everyone can try to login -->
<sec:http pattern="/rest/login/" security="none" />
<!--<sec:http pattern="/rest/add/**" security="none" />-->
<!-- only users with valid JWT can access protected resources -->
<sec:http pattern="/rest/**" entry-point-ref="restAuthenticationEntryPoint" create-session="stateless">
<!-- JWT is used to disabled-->
<sec:csrf disabled="true" />
<!-- don't redirect to UI login form -->
<sec:custom-filter before="FORM_LOGIN_FILTER" ref="jwtAuthenticationFilter" />
</sec:http>
<bean id="jwtAuthenticationFilter" class="com.myapp.ws.security.JwtAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="authenticationSuccessHandler" ref="jwtAuthenticationSuccessHandler" />
</bean>
<sec:authentication-manager alias="authenticationManager">
<sec:authentication-provider ref="jwtAuthenticationProvider" />
</sec:authentication-manager>
Upvotes: 4
Views: 5069
Reputation: 721
@user1516873 finally I got it working:
Problem was that I was using angular-cli server on port 4200 with proxy file like this:
{ "/rest": { "target": "http://localhost:8080", "secure": false } }
but should have been like this:
{ "/rest": { "target": "http://localhost:8080", "secure": false, "ws": true, "logLevel": "debug" } }
so through all the combinations of configuration I was always checking through 4200 proxy server and very rarely through native 8080 directly. I just didn't know that angular-cli proxy does not support when spring security is applied. I will accept your answer as you helped a lot!
Upvotes: 1
Reputation: 5173
Your problem doesn't related to security. You just pass wrong arguments in Stomp connect and subscribe functions.
The connect() method also accepts two other variants if you need to pass additional headers:
client.connect(headers, connectCallback);
client.connect(headers, connectCallback, errorCallback);where header is a map and connectCallback and errorCallback are functions.
this.stompClient.connect(this.token, (frame) => {
should be
this.stompClient.connect({}, (frame) => {
and
You can use the subscribe() method to subscribe to a destination. The method takes 2 mandatory arguments: destination, a String corresponding to the destination and callback, a function with one message argument and an optional argument headers, a JavaScript object for additional headers.
var subscription = client.subscribe("/queue/test", callback);
this.stompClient.subscribe('/topic/addMessage', this.authService.getAuthHeader(), (stompResponse) => {
should be
this.stompClient.subscribe('/topic/addMessage', (stompResponse) => {
Documentation http://jmesnil.net/stomp-websocket/doc/
Upvotes: 2