Skip to content

Commit c8d4533

Browse files
authored
Merge pull request quarkusio#36110 from sberyozkin/oidc_test_client
Add OidcTestClient
2 parents 1e6b053 + c7f419f commit c8d4533

File tree

5 files changed

+311
-3
lines changed

5 files changed

+311
-3
lines changed

docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,70 @@ public class CustomOidcWireMockStubTest {
515515
}
516516
----
517517

518+
[[integration-testing-oidc-test-client]]
519+
=== OidcTestClient
520+
521+
If you work with SaaS OIDC providers such as `Auth0` and would like to run tests against the test (development) domain or prefer to run tests against a remote Keycloak test realm, when you already have `quarkus.oidc.auth-server-url` configured, you can use `OidcTestClient`.
522+
523+
For example, lets assume you have the following configuration:
524+
525+
[source,properties]
526+
----
527+
%test.quarkus.oidc.auth-server-url=https://dev-123456.eu.auth0.com/
528+
%test.quarkus.oidc.client-id=test-auth0-client
529+
%test.quarkus.oidc.credentials.secret=secret
530+
----
531+
532+
Start with addding the same dependency as in the <<integration-testing-wiremock>> section, `quarkus-test-oidc-server`.
533+
534+
Next, write the test code like this:
535+
536+
[source, java]
537+
----
538+
package org.acme;
539+
540+
import org.junit.jupiter.api.AfterAll;
541+
import static io.restassured.RestAssured.given;
542+
import static org.hamcrest.CoreMatchers.is;
543+
544+
import java.util.Map;
545+
546+
import org.junit.jupiter.api.Test;
547+
548+
import io.quarkus.test.junit.QuarkusTest;
549+
import io.quarkus.test.oidc.client.OidcTestClient;
550+
551+
@QuarkusTest
552+
public class GreetingResourceTest {
553+
554+
static OidcTestClient oidcTestClient = new OidcTestClient();
555+
556+
@AfterAll
557+
public static void close() {
558+
client.close();
559+
}
560+
561+
@Test
562+
public void testHelloEndpoint() {
563+
given()
564+
.auth().oauth2(getAccessToken("alice", "alice"))
565+
.when().get("/hello")
566+
.then()
567+
.statusCode(200)
568+
.body(is("Hello, Alice"));
569+
}
570+
571+
private String getAccessToken(String name, String secret) {
572+
return oidcTestClient.getAccessToken(name, secret,
573+
Map.of("audience", "https://dev-123456.eu.auth0.com/api/v2/",
574+
"scope", "profile"));
575+
}
576+
}
577+
----
578+
579+
This test code acquires a token using a `password` grant from the test `Auth0` domain which has an application with the client id `test-auth0-client` registered, and which has a user `alice` with a password `alice` created. The test `Auth0` application must have the `password` grant enabled for a test like this one to work. This example code also shows how to pass additional parameters. For `Auth0`, these are the `audience` and `scope` parameters.
580+
581+
518582
[[integration-testing-keycloak-devservices]]
519583
==== Dev Services for Keycloak
520584

extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/OidcTokenPropagationTest.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@
22

33
import static org.hamcrest.Matchers.equalTo;
44

5-
import java.util.Set;
6-
5+
import org.junit.jupiter.api.AfterAll;
76
import org.junit.jupiter.api.Test;
87
import org.junit.jupiter.api.extension.RegisterExtension;
98

109
import io.quarkus.test.QuarkusUnitTest;
1110
import io.quarkus.test.common.QuarkusTestResource;
11+
import io.quarkus.test.oidc.client.OidcTestClient;
1212
import io.quarkus.test.oidc.server.OidcWiremockTestResource;
1313
import io.restassured.RestAssured;
1414

1515
@QuarkusTestResource(OidcWiremockTestResource.class)
1616
public class OidcTokenPropagationTest {
1717

18+
final static OidcTestClient client = new OidcTestClient();
19+
1820
private static Class<?>[] testClasses = {
1921
FrontendResource.class,
2022
ProtectedResource.class,
@@ -27,6 +29,11 @@ public class OidcTokenPropagationTest {
2729
.addClasses(testClasses)
2830
.addAsResource("application.properties"));
2931

32+
@AfterAll
33+
public static void close() {
34+
client.close();
35+
}
36+
3037
@Test
3138
public void testGetUserNameWithTokenPropagation() {
3239
RestAssured.given().auth().oauth2(getBearerAccessToken())
@@ -37,7 +44,7 @@ public void testGetUserNameWithTokenPropagation() {
3744
}
3845

3946
public String getBearerAccessToken() {
40-
return OidcWiremockTestResource.getAccessToken("alice", Set.of("admin"));
47+
return client.getAccessToken("alice", "alice");
4148
}
4249

4350
}

test-framework/oidc-server/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929
<groupId>io.quarkus</groupId>
3030
<artifactId>quarkus-test-common</artifactId>
3131
</dependency>
32+
<dependency>
33+
<groupId>io.smallrye.reactive</groupId>
34+
<artifactId>smallrye-mutiny-vertx-web-client</artifactId>
35+
</dependency>
36+
<dependency>
37+
<groupId>org.awaitility</groupId>
38+
<artifactId>awaitility</artifactId>
39+
</dependency>
3240
</dependencies>
3341

3442
</project>
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package io.quarkus.test.oidc.client;
2+
3+
import static org.awaitility.Awaitility.await;
4+
5+
import java.net.URLEncoder;
6+
import java.nio.charset.StandardCharsets;
7+
import java.time.Duration;
8+
import java.util.Map;
9+
10+
import org.eclipse.microprofile.config.ConfigProvider;
11+
12+
import io.vertx.core.MultiMap;
13+
import io.vertx.core.Vertx;
14+
import io.vertx.core.buffer.Buffer;
15+
import io.vertx.ext.web.client.WebClient;
16+
17+
public class OidcTestClient {
18+
19+
private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10);
20+
private final static String CLIENT_AUTH_SERVER_URL_PROP = "client.quarkus.oidc.auth-server-url";
21+
private final static String AUTH_SERVER_URL_PROP = "quarkus.oidc.auth-server-url";
22+
private final static String CLIENT_ID_PROP = "quarkus.oidc.client-id";
23+
private final static String CLIENT_SECRET_PROP = "quarkus.oidc.credentials.secret";
24+
25+
Vertx vertx = Vertx.vertx();
26+
WebClient client = WebClient.create(vertx);
27+
28+
private String authServerUrl;
29+
private String tokenUrl;
30+
31+
/**
32+
* Get an access token a client_credentials grant.
33+
* Client id must be configured with the `quarkus.oidc.client-id` property.
34+
* Client secret must be configured with the `quarkus.oidc.credentials.secret` property.
35+
*/
36+
public String getClientAccessToken() {
37+
return getClientAccessToken(null);
38+
}
39+
40+
/**
41+
* Get an access token a client_credentials grant with additional properties.
42+
* Client id must be configured with the `quarkus.oidc.client-id` property.
43+
* Client secret must be configured with the `quarkus.oidc.credentials.secret` property.
44+
*/
45+
public String getClientAccessToken(Map<String, String> extraProps) {
46+
return getClientAccessToken(getClientId(), getClientSecret(), extraProps);
47+
}
48+
49+
/**
50+
* Get an access token from the default tenant realm using a client_credentials grant with a
51+
* the provided client id and secret.
52+
*/
53+
public String getClientAccessToken(String clientId, String clientSecret) {
54+
return getClientAccessToken(clientId, clientSecret, null);
55+
}
56+
57+
/**
58+
* Get an access token using a client_credentials grant with the provided client id and secret,
59+
* and additional properties.
60+
*/
61+
public String getClientAccessToken(String clientId, String clientSecret, Map<String, String> extraProps) {
62+
MultiMap requestMap = MultiMap.caseInsensitiveMultiMap();
63+
requestMap.add("grant_type", "client_credentials")
64+
.add("client_id", clientId);
65+
if (clientSecret != null && !clientSecret.isBlank()) {
66+
requestMap.add("client_secret", clientSecret);
67+
}
68+
return getAccessTokenInternal(requestMap, extraProps);
69+
}
70+
71+
/**
72+
* Get an access token from the default tenant realm using a password grant with the provided user name, user secret.
73+
* Client id must be configured with the `quarkus.oidc.client-id` property.
74+
* Client secret must be configured with the `quarkus.oidc.credentials.secret` property.
75+
*/
76+
public String getAccessToken(String userName, String userSecret) {
77+
return getAccessToken(userName, userSecret, null);
78+
}
79+
80+
/**
81+
* Get an access token from the default tenant realm using a password grant with the provided user name, user secret,
82+
* and additional properties.
83+
* Client id must be configured with the `quarkus.oidc.client-id` property.
84+
* Client secret must be configured with the `quarkus.oidc.credentials.secret` property.
85+
*/
86+
public String getAccessToken(String userName, String userSecret, Map<String, String> extraProps) {
87+
return getAccessToken(getClientId(), getClientSecret(), userName, userSecret, extraProps);
88+
}
89+
90+
/**
91+
* Get an access token from the default tenant realm using a password grant with the provided client id, client secret, user
92+
* name, user secret, client
93+
* id and user secret.
94+
*/
95+
public String getAccessToken(String clientId, String clientSecret, String userName, String userSecret) {
96+
return getAccessToken(userName, userSecret, clientId, clientSecret, null);
97+
}
98+
99+
/**
100+
* Get an access token using a password grant with the provided user name, user secret, client
101+
* id and secret, and scopes.
102+
*/
103+
public String getAccessToken(String clientId, String clientSecret, String userName, String userSecret,
104+
Map<String, String> extraProps) {
105+
106+
MultiMap requestMap = MultiMap.caseInsensitiveMultiMap();
107+
requestMap.add("grant_type", "password")
108+
.add("username", userName)
109+
.add("password", userSecret);
110+
111+
requestMap.add("client_id", clientId);
112+
if (clientSecret != null && !clientSecret.isBlank()) {
113+
requestMap.add("client_secret", clientSecret);
114+
}
115+
return getAccessTokenInternal(requestMap, extraProps);
116+
}
117+
118+
private String getAccessTokenInternal(MultiMap requestMap, Map<String, String> extraProps) {
119+
120+
if (extraProps != null) {
121+
requestMap = requestMap.addAll(extraProps);
122+
}
123+
124+
var result = client.postAbs(getTokenUrl())
125+
.putHeader("Content-Type", "application/x-www-form-urlencoded")
126+
.sendBuffer(encodeForm(requestMap));
127+
await().atMost(REQUEST_TIMEOUT).until(result::isComplete);
128+
129+
return result.result().bodyAsJsonObject().getString("access_token");
130+
}
131+
132+
private String getClientId() {
133+
return getPropertyValue(CLIENT_ID_PROP);
134+
}
135+
136+
private String getClientSecret() {
137+
return getPropertyValue(CLIENT_SECRET_PROP);
138+
}
139+
140+
/**
141+
* Return URL string configured with a 'quarkus.oidc.auth-server' property.
142+
*/
143+
public String getAuthServerUrl() {
144+
if (authServerUrl == null) {
145+
authServerUrl = getOptionalPropertyValue(CLIENT_AUTH_SERVER_URL_PROP, AUTH_SERVER_URL_PROP);
146+
}
147+
return authServerUrl;
148+
}
149+
150+
/**
151+
* Return URL string configured with a 'quarkus.oidc.auth-server' property.
152+
*/
153+
public String getTokenUrl() {
154+
if (tokenUrl == null) {
155+
getAuthServerUrl();
156+
var result = client.getAbs(authServerUrl + "/.well-known/openid-configuration")
157+
.send();
158+
await().atMost(REQUEST_TIMEOUT).until(result::isComplete);
159+
tokenUrl = result.result().bodyAsJsonObject().getString("token_endpoint");
160+
}
161+
return tokenUrl;
162+
}
163+
164+
private String getPropertyValue(String prop) {
165+
return ConfigProvider.getConfig().getValue(prop, String.class);
166+
}
167+
168+
private String getOptionalPropertyValue(String prop, String defaultProp) {
169+
return ConfigProvider.getConfig().getOptionalValue(prop, String.class)
170+
.orElseGet(() -> ConfigProvider.getConfig().getValue(defaultProp, String.class));
171+
}
172+
173+
public static Buffer encodeForm(MultiMap form) {
174+
Buffer buffer = Buffer.buffer();
175+
for (Map.Entry<String, String> entry : form) {
176+
if (buffer.length() != 0) {
177+
buffer.appendByte((byte) '&');
178+
}
179+
buffer.appendString(entry.getKey());
180+
buffer.appendByte((byte) '=');
181+
buffer.appendString(urlEncode(entry.getValue()));
182+
}
183+
return buffer;
184+
}
185+
186+
private static String urlEncode(String value) {
187+
try {
188+
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
189+
} catch (Exception ex) {
190+
throw new RuntimeException(ex);
191+
}
192+
}
193+
194+
public void close() {
195+
if (client != null) {
196+
client.close();
197+
client = null;
198+
}
199+
if (vertx != null) {
200+
vertx.close().toCompletionStage().toCompletableFuture().join();
201+
vertx = null;
202+
}
203+
}
204+
}

test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@ public Map<String, String> start() {
205205
"")
206206
.withTransformers("response-template")));
207207

208+
definePasswordGrantTokenStub();
209+
defineClientCredGrantTokenStub();
210+
208211
LOG.infof("Keycloak started in mock mode: %s", server.baseUrl());
209212
Map<String, String> conf = new HashMap<>();
210213
conf.put("keycloak.url", server.baseUrl() + "/auth");
@@ -293,6 +296,28 @@ private void defineCodeFlowAuthorizationMockTokenStub() {
293296
"}")));
294297
}
295298

299+
private void definePasswordGrantTokenStub() {
300+
server.stubFor(post("/auth/realms/quarkus/token")
301+
.withRequestBody(containing("grant_type=password"))
302+
.willReturn(aResponse()
303+
.withHeader("Content-Type", "application/json")
304+
.withBody("{\n" +
305+
" \"access_token\": \""
306+
+ getAccessToken("alice", getAdminRoles()) + "\",\n" +
307+
" \"refresh_token\": \"07e08903-1263-4dd1-9fd1-4a59b0db5283\"}")));
308+
}
309+
310+
private void defineClientCredGrantTokenStub() {
311+
server.stubFor(post("/auth/realms/quarkus/token")
312+
.withRequestBody(containing("grant_type=client_credentials"))
313+
.willReturn(aResponse()
314+
.withHeader("Content-Type", "application/json")
315+
.withBody("{\n" +
316+
" \"access_token\": \""
317+
+ getAccessToken("alice", getAdminRoles()) + "\",\n" +
318+
" \"refresh_token\": \"07e08903-1263-4dd1-9fd1-4a59b0db5283\"}")));
319+
}
320+
296321
private void defineCodeFlowAuthorizationMockEncryptedTokenStub() {
297322
server.stubFor(post("/auth/realms/quarkus/encrypted-id-token")
298323
.withRequestBody(containing("authorization_code"))

0 commit comments

Comments
 (0)