Skip to content

Commit 112da99

Browse files
authored
feat: websocket authentication support (#1482)
* unauthorized response message, websocket tests Signed-off-by: achmelo <a.chmelo@gmail.com> * ignore auth header for /api/** Signed-off-by: achmelo <a.chmelo@gmail.com> * format Signed-off-by: achmelo <a.chmelo@gmail.com>
1 parent d487b34 commit 112da99

File tree

4 files changed

+193
-64
lines changed

4 files changed

+193
-64
lines changed

discoverable-client/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ dependencies {
4646
implementation libraries.spring_boot_starter_web
4747
implementation libraries.spring_boot_starter_websocket
4848
implementation libraries.spring_boot_starter_validation
49+
implementation libraries.spring_boot_starter_security
4950

5051
implementation libraries.bootstrap
5152
implementation libraries.jquery
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* This program and the accompanying materials are made available under the terms of the
3+
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
4+
* https://www.eclipse.org/legal/epl-v20.html
5+
*
6+
* SPDX-License-Identifier: EPL-2.0
7+
*
8+
* Copyright Contributors to the Zowe Project.
9+
*/
10+
package org.zowe.apiml.client.configuration;
11+
12+
import org.springframework.context.annotation.Configuration;
13+
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
14+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
15+
import org.springframework.security.config.annotation.web.builders.WebSecurity;
16+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
17+
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
18+
19+
@Configuration
20+
@EnableWebSecurity
21+
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
22+
23+
@Override
24+
protected void configure(HttpSecurity http) throws Exception {
25+
http.csrf().disable()
26+
27+
.authorizeRequests()
28+
.antMatchers("/ws/**").authenticated()
29+
.antMatchers("/**").permitAll()
30+
.and().httpBasic();
31+
}
32+
33+
@Override
34+
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
35+
auth.inMemoryAuthentication()
36+
.withUser("user")
37+
.password("{noop}pass").roles("ADMIN");
38+
}
39+
40+
@Override
41+
public void configure(WebSecurity web) {
42+
web.ignoring()
43+
.antMatchers("/api/**");
44+
}
45+
}

gateway-service/src/main/java/org/zowe/apiml/gateway/ws/WebSocketRoutedSession.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
import lombok.extern.slf4j.Slf4j;
1313
import org.eclipse.jetty.client.HttpClient;
1414
import org.eclipse.jetty.util.ssl.SslContextFactory;
15+
import org.eclipse.jetty.websocket.api.UpgradeException;
1516
import org.eclipse.jetty.websocket.client.WebSocketClient;
1617
import org.springframework.http.HttpHeaders;
18+
import org.springframework.http.HttpStatus;
1719
import org.springframework.util.concurrent.ListenableFuture;
1820
import org.springframework.web.socket.CloseStatus;
1921
import org.springframework.web.socket.WebSocketHttpHeaders;
@@ -24,6 +26,7 @@
2426
import java.io.IOException;
2527
import java.net.InetSocketAddress;
2628
import java.net.URI;
29+
import java.util.concurrent.ExecutionException;
2730
import java.util.concurrent.TimeUnit;
2831

2932
/**
@@ -86,11 +89,30 @@ private WebSocketSession createWebSocketClientSession(WebSocketSession webSocket
8689
} catch (InterruptedException e) {
8790
Thread.currentThread().interrupt();
8891
throw webSocketProxyException(targetUrl, e, webSocketServerSession, false);
92+
} catch (ExecutionException e) {
93+
throw handleExecutionException(targetUrl, e, webSocketServerSession, false);
8994
} catch (Exception e) {
9095
throw webSocketProxyException(targetUrl, e, webSocketServerSession, false);
9196
}
9297
}
9398

99+
private WebSocketProxyError handleExecutionException(String targetUrl, ExecutionException cause, WebSocketSession webSocketServerSession, boolean logError) {
100+
if (cause.getCause() != null && cause.getCause().getCause() instanceof UpgradeException) {
101+
UpgradeException upgradeException = (UpgradeException) cause.getCause().getCause();
102+
if (upgradeException.getResponseStatusCode() == HttpStatus.UNAUTHORIZED.value()) {
103+
String message = "Invalid login credentials";
104+
if (logError) {
105+
log.debug(message);
106+
}
107+
return new WebSocketProxyError(message, cause, webSocketServerSession);
108+
} else {
109+
return webSocketProxyException(targetUrl, cause, webSocketServerSession, logError);
110+
}
111+
} else {
112+
return webSocketProxyException(targetUrl, cause, webSocketServerSession, logError);
113+
}
114+
}
115+
94116
private WebSocketProxyError webSocketProxyException(String targetUrl, Exception cause, WebSocketSession webSocketServerSession, boolean logError) {
95117
String message = String.format("Error opening session to WebSocket service at %s: %s", targetUrl, cause.getMessage());
96118
if (logError) {

integration-tests/src/test/java/org/zowe/apiml/integration/proxy/WebSocketProxyTest.java

Lines changed: 125 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
package org.zowe.apiml.integration.proxy;
1111

1212
import org.apache.http.client.utils.URIBuilder;
13+
import org.junit.jupiter.api.BeforeAll;
14+
import org.junit.jupiter.api.Nested;
1315
import org.junit.jupiter.api.Test;
1416
import org.springframework.web.socket.CloseStatus;
1517
import org.springframework.web.socket.TextMessage;
@@ -27,6 +29,7 @@
2729

2830
import java.net.URI;
2931
import java.net.URISyntaxException;
32+
import java.util.Base64;
3033
import java.util.concurrent.TimeUnit;
3134
import java.util.concurrent.atomic.AtomicInteger;
3235

@@ -39,9 +42,26 @@
3942
class WebSocketProxyTest implements TestWithStartedInstances {
4043
private final GatewayServiceConfiguration serviceConfiguration = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration();
4144

42-
private final static int WAIT_TIMEOUT_MS = 10000;
43-
private final static String UPPERCASE_URL = "/ws/v1/discoverableclient/uppercase";
44-
private final static String HEADER_URL = "/ws/v1/discoverableclient/header";
45+
private static final int WAIT_TIMEOUT_MS = 10000;
46+
private static final String UPPERCASE_URL = "/ws/v1/discoverableclient/uppercase";
47+
private static final String HEADER_URL = "/ws/v1/discoverableclient/header";
48+
49+
private static final WebSocketHttpHeaders VALID_AUTH_HEADERS = new WebSocketHttpHeaders();
50+
private static final WebSocketHttpHeaders INVALID_AUTH_HEADERS = new WebSocketHttpHeaders();
51+
private static final String validToken = "apimlAuthenticationToken=tokenValue";
52+
53+
54+
@BeforeAll
55+
static void setup() {
56+
String plainCred = "user:pass";
57+
String base64cred = Base64.getEncoder().encodeToString(plainCred.getBytes());
58+
VALID_AUTH_HEADERS.add("Authorization", "Basic " + base64cred);
59+
60+
String invalidPlainCred = "user:invalidPass";
61+
String invalidBase64cred = Base64.getEncoder().encodeToString(invalidPlainCred.getBytes());
62+
INVALID_AUTH_HEADERS.add("Authorization", "Basic " + invalidBase64cred);
63+
64+
}
4565

4666
private TextWebSocketHandler appendResponseHandler(StringBuilder target, int countToNotify) {
4767
final AtomicInteger counter = new AtomicInteger(countToNotify);
@@ -89,86 +109,127 @@ private WebSocketSession appendingWebSocketSession(String url, StringBuilder res
89109
return appendingWebSocketSession(url, null, response, countToNotify);
90110
}
91111

92-
@Test
93-
void shouldRouteWebSocketSession() throws Exception {
94-
final StringBuilder response = new StringBuilder();
95-
WebSocketSession session = appendingWebSocketSession(discoverableClientGatewayUrl(UPPERCASE_URL), response, 1);
96112

97-
session.sendMessage(new TextMessage("hello world!"));
98-
synchronized (response) {
99-
response.wait(WAIT_TIMEOUT_MS);
100-
}
113+
@Nested
114+
class WhenRoutingSession {
115+
@Nested
116+
class Authentication {
117+
@Nested
118+
class WhenValid {
119+
@Nested
120+
class ReturnSuccess {
121+
@Test
122+
void message() throws Exception {
123+
final StringBuilder response = new StringBuilder();
101124

102-
assertEquals("HELLO WORLD!", response.toString());
103-
session.close();
104-
}
125+
WebSocketSession session = appendingWebSocketSession(discoverableClientGatewayUrl(UPPERCASE_URL), VALID_AUTH_HEADERS, response, 1);
105126

106-
@Test
107-
void shouldRouteHeaders() throws Exception {
108-
final StringBuilder response = new StringBuilder();
109-
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
110-
headers.add("X-Test", "value");
111-
WebSocketSession session = appendingWebSocketSession(discoverableClientGatewayUrl(HEADER_URL), headers, response, 1);
127+
session.sendMessage(new TextMessage("hello world!"));
128+
synchronized (response) {
129+
response.wait(WAIT_TIMEOUT_MS);
130+
}
112131

113-
session.sendMessage(new TextMessage("gimme those headers"));
114-
synchronized (response) {
115-
response.wait(WAIT_TIMEOUT_MS);
116-
}
132+
assertEquals("HELLO WORLD!", response.toString());
133+
session.close();
134+
}
117135

118-
assertTrue(response.toString().contains("x-test:\"value\""));
119-
session.sendMessage(new TextMessage("bye"));
120-
session.close();
121-
}
136+
@Test
137+
void headers() throws Exception {
138+
final StringBuilder response = new StringBuilder();
139+
VALID_AUTH_HEADERS.add("X-Test", "value");
140+
VALID_AUTH_HEADERS.add("Cookie", validToken);
141+
WebSocketSession session = appendingWebSocketSession(discoverableClientGatewayUrl(HEADER_URL), VALID_AUTH_HEADERS, response, 1);
142+
143+
session.sendMessage(new TextMessage("gimme those headers"));
144+
synchronized (response) {
145+
response.wait(WAIT_TIMEOUT_MS);
146+
}
147+
148+
assertTrue(response.toString().contains("x-test:\"value\""));
149+
assertTrue(response.toString().contains(validToken));
150+
session.sendMessage(new TextMessage("bye"));
151+
session.close();
152+
}
153+
}
122154

123-
@Test
124-
void shouldCloseSessionAfterClientServerCloses() throws Exception {
125-
final StringBuilder response = new StringBuilder();
126-
WebSocketSession session = appendingWebSocketSession(discoverableClientGatewayUrl(UPPERCASE_URL), response, 2);
155+
@Nested
156+
class ReturnError {
157+
@Test
158+
void whenPathIsNotCorrect() throws Exception {
159+
final StringBuilder response = new StringBuilder();
160+
appendingWebSocketSession(discoverableClientGatewayUrl(UPPERCASE_URL + "bad"), VALID_AUTH_HEADERS, response, 1);
127161

128-
session.sendMessage(new TextMessage("bye"));
129-
synchronized (response) {
130-
response.wait(WAIT_TIMEOUT_MS);
131-
}
162+
synchronized (response) {
163+
response.wait(WAIT_TIMEOUT_MS);
164+
}
132165

133-
assertEquals("BYECloseStatus[code=1000, reason=null]", response.toString());
134-
}
166+
System.out.println("Response: " + response.toString());
167+
assertEquals(0, response.toString().indexOf("CloseStatus[code=1003,"));
168+
}
135169

136-
@Test
137-
void shouldFailIfPathIsNotCorrect() throws Exception {
138-
final StringBuilder response = new StringBuilder();
139-
appendingWebSocketSession(discoverableClientGatewayUrl(UPPERCASE_URL + "bad"), response, 1);
170+
@Test
171+
void whenServiceIsNotCorrect() throws Exception {
172+
final StringBuilder response = new StringBuilder();
173+
appendingWebSocketSession(discoverableClientGatewayUrl("/ws/v1/wrong-service/uppercase"), VALID_AUTH_HEADERS, response, 1);
140174

141-
synchronized (response) {
142-
response.wait(WAIT_TIMEOUT_MS);
143-
}
175+
synchronized (response) {
176+
response.wait(WAIT_TIMEOUT_MS);
177+
}
144178

145-
System.out.println("Response: " + response.toString());
146-
assertEquals(0, response.toString().indexOf("CloseStatus[code=1003,"));
147-
}
179+
assertEquals("CloseStatus[code=1003, reason=Requested service wrong-service is not known by the gateway]",
180+
response.toString());
181+
}
182+
183+
@Test
184+
void whenUrlFormatIsNotCorrect() throws Exception {
185+
final StringBuilder response = new StringBuilder();
186+
appendingWebSocketSession(discoverableClientGatewayUrl("/ws/wrong"), response, 1);
187+
188+
synchronized (response) {
189+
response.wait(WAIT_TIMEOUT_MS);
190+
}
191+
192+
assertEquals("CloseStatus[code=1003, reason=Invalid URL format]", response.toString());
193+
}
194+
}
195+
}
196+
197+
@Nested
198+
class WhenInvalid {
199+
@Test
200+
void returnError() throws Exception {
201+
final StringBuilder response = new StringBuilder();
148202

149-
@Test
150-
void shouldFailIfServiceIsNotCorrect() throws Exception {
151-
final StringBuilder response = new StringBuilder();
152-
appendingWebSocketSession(discoverableClientGatewayUrl("/ws/v1/wrong-service/uppercase"), response, 1);
203+
WebSocketSession session = appendingWebSocketSession(discoverableClientGatewayUrl(UPPERCASE_URL), INVALID_AUTH_HEADERS, response, 1);
153204

154-
synchronized (response) {
155-
response.wait(WAIT_TIMEOUT_MS);
205+
session.sendMessage(new TextMessage("hello world!"));
206+
synchronized (response) {
207+
response.wait(WAIT_TIMEOUT_MS);
208+
}
209+
210+
assertEquals("CloseStatus[code=1003, reason=Invalid login credentials]", response.toString());
211+
session.close();
212+
}
213+
}
156214
}
157215

158-
assertEquals("CloseStatus[code=1003, reason=Requested service wrong-service is not known by the gateway]",
159-
response.toString());
160216
}
161217

162-
@Test
163-
void shouldFailIfUrlFormatIsNotCorrect() throws Exception {
164-
final StringBuilder response = new StringBuilder();
165-
appendingWebSocketSession(discoverableClientGatewayUrl("/ws/wrong"), response, 1);
218+
@Nested
219+
class WhenClosingSession {
220+
@Test
221+
void getCorrectResponse() throws Exception {
222+
final StringBuilder response = new StringBuilder();
223+
WebSocketSession session = appendingWebSocketSession(discoverableClientGatewayUrl(UPPERCASE_URL), VALID_AUTH_HEADERS, response, 2);
166224

167-
synchronized (response) {
168-
response.wait(WAIT_TIMEOUT_MS);
169-
}
225+
session.sendMessage(new TextMessage("bye"));
226+
synchronized (response) {
227+
response.wait(WAIT_TIMEOUT_MS);
228+
}
170229

171-
assertEquals("CloseStatus[code=1003, reason=Invalid URL format]", response.toString());
230+
assertEquals("BYECloseStatus[code=1000, reason=null]", response.toString());
231+
}
172232
}
173233

234+
174235
}

0 commit comments

Comments
 (0)