Skip to content

Commit f16596d

Browse files
committed
Add integration tests to oauth2 and use.authentications
Signed-off-by: Matheus Cruz <matheuscruz.dev@gmail.com>
1 parent b626e95 commit f16596d

2 files changed

Lines changed: 267 additions & 0 deletions

File tree

experimental/test/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@
7979
<version>${project.version}</version>
8080
<scope>test</scope>
8181
</dependency>
82+
<dependency>
83+
<groupId>io.serverlessworkflow</groupId>
84+
<artifactId>serverlessworkflow-impl-jackson-jwt</artifactId>
85+
<version>${project.version}</version>
86+
<scope>test</scope>
87+
</dependency>
8288
<dependency>
8389
<groupId>org.glassfish.jersey.media</groupId>
8490
<artifactId>jersey-media-json-jackson</artifactId>
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.fluent.test;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import io.serverlessworkflow.api.types.OAuth2AuthenticationData;
22+
import io.serverlessworkflow.api.types.OAuth2AuthenticationDataClient;
23+
import io.serverlessworkflow.api.types.Workflow;
24+
import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder;
25+
import io.serverlessworkflow.fluent.func.dsl.FuncDSL;
26+
import io.serverlessworkflow.impl.WorkflowApplication;
27+
import java.io.IOException;
28+
import java.net.URI;
29+
import java.nio.charset.StandardCharsets;
30+
import java.time.Instant;
31+
import java.util.Base64;
32+
import java.util.Map;
33+
import java.util.concurrent.ConcurrentHashMap;
34+
import mockwebserver3.Dispatcher;
35+
import mockwebserver3.MockResponse;
36+
import mockwebserver3.MockWebServer;
37+
import mockwebserver3.RecordedRequest;
38+
import okhttp3.Headers;
39+
import org.junit.jupiter.api.AfterEach;
40+
import org.junit.jupiter.api.BeforeEach;
41+
import org.junit.jupiter.api.DisplayName;
42+
import org.junit.jupiter.api.Test;
43+
44+
public class FuncOAuth2HttpTest {
45+
46+
private static final ObjectMapper MAPPER = new ObjectMapper();
47+
48+
private record OAuth2Client(String clientId, String clientSecret, String baseUrl) {}
49+
50+
private WorkflowApplication app;
51+
private MockWebServer mockServer;
52+
53+
// Recorded token-request bodies, keyed by the request path.
54+
private final Map<String, String> tokenRequestBodies = new ConcurrentHashMap<>();
55+
// Recorded Authorization headers of the downstream API calls, keyed by the request path.
56+
private final Map<String, String> apiAuthHeaders = new ConcurrentHashMap<>();
57+
58+
@BeforeEach
59+
void setup() throws IOException {
60+
app = WorkflowApplication.builder().build();
61+
mockServer = new MockWebServer();
62+
mockServer.setDispatcher(
63+
new Dispatcher() {
64+
@Override
65+
public MockResponse dispatch(RecordedRequest request) {
66+
String path = request.getUrl().encodedPath();
67+
if (path.endsWith("/token")) {
68+
tokenRequestBodies.put(path, request.getBody().utf8());
69+
return new MockResponse(
70+
200, Headers.of("Content-Type", "application/json"), tokenResponse(fakeJwt()));
71+
}
72+
// The downstream API call ("/joogle" or "/jahoo").
73+
apiAuthHeaders.put(path, request.getHeaders().get("Authorization"));
74+
return new MockResponse(
75+
200,
76+
Headers.of("Content-Type", "application/json"),
77+
"{\"email\":\"" + path + "@example.com\"}");
78+
}
79+
});
80+
mockServer.start(0);
81+
}
82+
83+
@AfterEach
84+
void cleanup() {
85+
mockServer.close();
86+
app.close();
87+
}
88+
89+
@Test
90+
@DisplayName(
91+
"Two named OAuth2 client-credentials authentications, each used by a forked HTTP call")
92+
void test_multiple_oauth2_clients() throws Exception {
93+
String base = mockServer.url("/").toString();
94+
base = base.substring(0, base.length() - 1); // strip trailing slash
95+
96+
OAuth2Client joogle =
97+
new OAuth2Client("joogle-client-id", "joogle-client-secret", base + "/joogle-auth");
98+
OAuth2Client jahoo =
99+
new OAuth2Client("jahoo-client-id", "jahoo-client-secret", base + "/jahoo-auth");
100+
String wireMock = base;
101+
102+
Workflow workflow =
103+
FuncWorkflowBuilder.workflow("multiple-oauth2-clients", "quarkus-flow")
104+
.use(
105+
use ->
106+
use.authentications(
107+
auth -> {
108+
auth.authentication(
109+
"joogle",
110+
a ->
111+
a.oauth2(
112+
oauth2 ->
113+
oauth2
114+
.client(
115+
client ->
116+
client
117+
.id(joogle.clientId())
118+
.secret(joogle.clientSecret())
119+
.authentication(
120+
OAuth2AuthenticationDataClient
121+
.ClientAuthentication
122+
.CLIENT_SECRET_POST))
123+
.authority(joogle.baseUrl())
124+
.grant(
125+
OAuth2AuthenticationData
126+
.OAuth2AuthenticationDataGrant
127+
.CLIENT_CREDENTIALS)
128+
.build()));
129+
auth.authentication(
130+
"jahoo",
131+
a ->
132+
a.oauth2(
133+
oauth2 ->
134+
oauth2
135+
.client(
136+
client ->
137+
client
138+
.id(jahoo.clientId())
139+
.secret(jahoo.clientSecret()))
140+
.authority(jahoo.baseUrl())
141+
.grant(
142+
OAuth2AuthenticationData
143+
.OAuth2AuthenticationDataGrant
144+
.CLIENT_CREDENTIALS)
145+
.build()));
146+
}))
147+
.tasks(
148+
FuncDSL.fork(
149+
FuncDSL.http()
150+
.GET()
151+
.uri(URI.create(wireMock + "/joogle"), FuncDSL.use("joogle")),
152+
FuncDSL.http()
153+
.GET()
154+
.uri(URI.create(wireMock + "/jahoo"), FuncDSL.use("jahoo"))),
155+
FuncDSL.function("merge", o -> o))
156+
.build();
157+
158+
app.workflowDefinition(workflow).instance(Map.of()).start().join();
159+
160+
String joogleTokenBody = tokenRequestBodies.get("/joogle-auth/oauth2/token");
161+
String jahooTokenBody = tokenRequestBodies.get("/jahoo-auth/oauth2/token");
162+
163+
assertThat(joogleTokenBody)
164+
.as("joogle token request body")
165+
.isNotNull()
166+
.contains("grant_type=client_credentials")
167+
.contains("client_id=joogle-client-id")
168+
.contains("client_secret=joogle-client-secret");
169+
170+
assertThat(jahooTokenBody)
171+
.as("jahoo token request body")
172+
.isNotNull()
173+
.contains("grant_type=client_credentials")
174+
.contains("client_id=jahoo-client-id")
175+
.contains("client_secret=jahoo-client-secret");
176+
177+
// The token obtained from each authority is forwarded as a Bearer token on the API call.
178+
assertThat(apiAuthHeaders.get("/joogle")).as("joogle bearer").startsWith("Bearer ");
179+
assertThat(apiAuthHeaders.get("/jahoo")).as("jahoo bearer").startsWith("Bearer ");
180+
}
181+
182+
@Test
183+
@DisplayName("Custom endpoints.token overrides the default /oauth2/token path")
184+
void test_custom_token_endpoint() throws Exception {
185+
String base = mockServer.url("/").toString();
186+
base = base.substring(0, base.length() - 1); // strip trailing slash
187+
188+
OAuth2Client joogle =
189+
new OAuth2Client("joogle-client-id", "joogle-client-secret", base + "/joogle-auth");
190+
String wireMock = base;
191+
String customTokenPath = "/auth/realms/joogle/protocol/openid-connect/token";
192+
193+
// Same configuration as above, expressed with the FuncDSL auth helpers:
194+
// FuncDSL.auth(name, configurer) returns a chainable UseSpec (a Consumer<UseBuilder>),
195+
// and FuncDSL.oauth2(...) builds the policy (no explicit .build() needed).
196+
Workflow workflow =
197+
FuncWorkflowBuilder.workflow("custom-token-endpoint", "quarkus-flow")
198+
.use(
199+
FuncDSL.auth(
200+
"joogle",
201+
FuncDSL.oauth2(
202+
oauth2 ->
203+
oauth2
204+
.endpoints(e -> e.token(customTokenPath))
205+
.client(
206+
client ->
207+
client
208+
.id(joogle.clientId())
209+
.secret(joogle.clientSecret())
210+
.authentication(
211+
OAuth2AuthenticationDataClient.ClientAuthentication
212+
.CLIENT_SECRET_POST))
213+
.authority(joogle.baseUrl())
214+
.grant(
215+
OAuth2AuthenticationData.OAuth2AuthenticationDataGrant
216+
.CLIENT_CREDENTIALS))))
217+
.tasks(
218+
FuncDSL.http().GET().uri(URI.create(wireMock + "/joogle"), FuncDSL.use("joogle")))
219+
.build();
220+
221+
app.workflowDefinition(workflow).instance(Map.of()).start().join();
222+
223+
// The token path is resolved relative to the authority, so the default "/oauth2/token" is
224+
// replaced by the custom path.
225+
assertThat(tokenRequestBodies.get("/joogle-auth" + customTokenPath))
226+
.as("token request hit the custom endpoint")
227+
.isNotNull()
228+
.contains("client_id=joogle-client-id");
229+
assertThat(tokenRequestBodies).doesNotContainKey("/joogle-auth/oauth2/token");
230+
}
231+
232+
private static String tokenResponse(String jwt) {
233+
return """
234+
{
235+
"access_token": "%s",
236+
"token_type": "Bearer",
237+
"expires_in": 3600
238+
}
239+
"""
240+
.formatted(jwt);
241+
}
242+
243+
private static String fakeJwt() {
244+
try {
245+
long now = Instant.now().getEpochSecond();
246+
String header =
247+
MAPPER.writeValueAsString(Map.of("alg", "RS256", "typ", "Bearer", "kid", "test"));
248+
String payload =
249+
MAPPER.writeValueAsString(Map.of("sub", "test-subject", "exp", now + 3600, "iat", now));
250+
return b64Url(header) + "." + b64Url(payload) + ".sig";
251+
} catch (Exception e) {
252+
throw new RuntimeException(e);
253+
}
254+
}
255+
256+
private static String b64Url(String s) {
257+
return Base64.getUrlEncoder()
258+
.withoutPadding()
259+
.encodeToString(s.getBytes(StandardCharsets.UTF_8));
260+
}
261+
}

0 commit comments

Comments
 (0)