Skip to content

Commit

Permalink
Merge pull request quarkusio#36110 from sberyozkin/oidc_test_client
Browse files Browse the repository at this point in the history
Add OidcTestClient
  • Loading branch information
sberyozkin authored Sep 25, 2023
2 parents 1e6b053 + c7f419f commit c8d4533
Show file tree
Hide file tree
Showing 5 changed files with 311 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,70 @@ public class CustomOidcWireMockStubTest {
}
----

[[integration-testing-oidc-test-client]]
=== OidcTestClient

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`.

For example, lets assume you have the following configuration:

[source,properties]
----
%test.quarkus.oidc.auth-server-url=https://dev-123456.eu.auth0.com/
%test.quarkus.oidc.client-id=test-auth0-client
%test.quarkus.oidc.credentials.secret=secret
----

Start with addding the same dependency as in the <<integration-testing-wiremock>> section, `quarkus-test-oidc-server`.

Next, write the test code like this:

[source, java]
----
package org.acme;
import org.junit.jupiter.api.AfterAll;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import java.util.Map;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.client.OidcTestClient;
@QuarkusTest
public class GreetingResourceTest {
static OidcTestClient oidcTestClient = new OidcTestClient();
@AfterAll
public static void close() {
client.close();
}
@Test
public void testHelloEndpoint() {
given()
.auth().oauth2(getAccessToken("alice", "alice"))
.when().get("/hello")
.then()
.statusCode(200)
.body(is("Hello, Alice"));
}
private String getAccessToken(String name, String secret) {
return oidcTestClient.getAccessToken(name, secret,
Map.of("audience", "https://dev-123456.eu.auth0.com/api/v2/",
"scope", "profile"));
}
}
----

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.


[[integration-testing-keycloak-devservices]]
==== Dev Services for Keycloak

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@

import static org.hamcrest.Matchers.equalTo;

import java.util.Set;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.oidc.client.OidcTestClient;
import io.quarkus.test.oidc.server.OidcWiremockTestResource;
import io.restassured.RestAssured;

@QuarkusTestResource(OidcWiremockTestResource.class)
public class OidcTokenPropagationTest {

final static OidcTestClient client = new OidcTestClient();

private static Class<?>[] testClasses = {
FrontendResource.class,
ProtectedResource.class,
Expand All @@ -27,6 +29,11 @@ public class OidcTokenPropagationTest {
.addClasses(testClasses)
.addAsResource("application.properties"));

@AfterAll
public static void close() {
client.close();
}

@Test
public void testGetUserNameWithTokenPropagation() {
RestAssured.given().auth().oauth2(getBearerAccessToken())
Expand All @@ -37,7 +44,7 @@ public void testGetUserNameWithTokenPropagation() {
}

public String getBearerAccessToken() {
return OidcWiremockTestResource.getAccessToken("alice", Set.of("admin"));
return client.getAccessToken("alice", "alice");
}

}
8 changes: 8 additions & 0 deletions test-framework/oidc-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-common</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-mutiny-vertx-web-client</artifactId>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package io.quarkus.test.oidc.client;

import static org.awaitility.Awaitility.await;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;

import org.eclipse.microprofile.config.ConfigProvider;

import io.vertx.core.MultiMap;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.ext.web.client.WebClient;

public class OidcTestClient {

private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10);
private final static String CLIENT_AUTH_SERVER_URL_PROP = "client.quarkus.oidc.auth-server-url";
private final static String AUTH_SERVER_URL_PROP = "quarkus.oidc.auth-server-url";
private final static String CLIENT_ID_PROP = "quarkus.oidc.client-id";
private final static String CLIENT_SECRET_PROP = "quarkus.oidc.credentials.secret";

Vertx vertx = Vertx.vertx();
WebClient client = WebClient.create(vertx);

private String authServerUrl;
private String tokenUrl;

/**
* Get an access token a client_credentials grant.
* Client id must be configured with the `quarkus.oidc.client-id` property.
* Client secret must be configured with the `quarkus.oidc.credentials.secret` property.
*/
public String getClientAccessToken() {
return getClientAccessToken(null);
}

/**
* Get an access token a client_credentials grant with additional properties.
* Client id must be configured with the `quarkus.oidc.client-id` property.
* Client secret must be configured with the `quarkus.oidc.credentials.secret` property.
*/
public String getClientAccessToken(Map<String, String> extraProps) {
return getClientAccessToken(getClientId(), getClientSecret(), extraProps);
}

/**
* Get an access token from the default tenant realm using a client_credentials grant with a
* the provided client id and secret.
*/
public String getClientAccessToken(String clientId, String clientSecret) {
return getClientAccessToken(clientId, clientSecret, null);
}

/**
* Get an access token using a client_credentials grant with the provided client id and secret,
* and additional properties.
*/
public String getClientAccessToken(String clientId, String clientSecret, Map<String, String> extraProps) {
MultiMap requestMap = MultiMap.caseInsensitiveMultiMap();
requestMap.add("grant_type", "client_credentials")
.add("client_id", clientId);
if (clientSecret != null && !clientSecret.isBlank()) {
requestMap.add("client_secret", clientSecret);
}
return getAccessTokenInternal(requestMap, extraProps);
}

/**
* Get an access token from the default tenant realm using a password grant with the provided user name, user secret.
* Client id must be configured with the `quarkus.oidc.client-id` property.
* Client secret must be configured with the `quarkus.oidc.credentials.secret` property.
*/
public String getAccessToken(String userName, String userSecret) {
return getAccessToken(userName, userSecret, null);
}

/**
* Get an access token from the default tenant realm using a password grant with the provided user name, user secret,
* and additional properties.
* Client id must be configured with the `quarkus.oidc.client-id` property.
* Client secret must be configured with the `quarkus.oidc.credentials.secret` property.
*/
public String getAccessToken(String userName, String userSecret, Map<String, String> extraProps) {
return getAccessToken(getClientId(), getClientSecret(), userName, userSecret, extraProps);
}

/**
* Get an access token from the default tenant realm using a password grant with the provided client id, client secret, user
* name, user secret, client
* id and user secret.
*/
public String getAccessToken(String clientId, String clientSecret, String userName, String userSecret) {
return getAccessToken(userName, userSecret, clientId, clientSecret, null);
}

/**
* Get an access token using a password grant with the provided user name, user secret, client
* id and secret, and scopes.
*/
public String getAccessToken(String clientId, String clientSecret, String userName, String userSecret,
Map<String, String> extraProps) {

MultiMap requestMap = MultiMap.caseInsensitiveMultiMap();
requestMap.add("grant_type", "password")
.add("username", userName)
.add("password", userSecret);

requestMap.add("client_id", clientId);
if (clientSecret != null && !clientSecret.isBlank()) {
requestMap.add("client_secret", clientSecret);
}
return getAccessTokenInternal(requestMap, extraProps);
}

private String getAccessTokenInternal(MultiMap requestMap, Map<String, String> extraProps) {

if (extraProps != null) {
requestMap = requestMap.addAll(extraProps);
}

var result = client.postAbs(getTokenUrl())
.putHeader("Content-Type", "application/x-www-form-urlencoded")
.sendBuffer(encodeForm(requestMap));
await().atMost(REQUEST_TIMEOUT).until(result::isComplete);

return result.result().bodyAsJsonObject().getString("access_token");
}

private String getClientId() {
return getPropertyValue(CLIENT_ID_PROP);
}

private String getClientSecret() {
return getPropertyValue(CLIENT_SECRET_PROP);
}

/**
* Return URL string configured with a 'quarkus.oidc.auth-server' property.
*/
public String getAuthServerUrl() {
if (authServerUrl == null) {
authServerUrl = getOptionalPropertyValue(CLIENT_AUTH_SERVER_URL_PROP, AUTH_SERVER_URL_PROP);
}
return authServerUrl;
}

/**
* Return URL string configured with a 'quarkus.oidc.auth-server' property.
*/
public String getTokenUrl() {
if (tokenUrl == null) {
getAuthServerUrl();
var result = client.getAbs(authServerUrl + "/.well-known/openid-configuration")
.send();
await().atMost(REQUEST_TIMEOUT).until(result::isComplete);
tokenUrl = result.result().bodyAsJsonObject().getString("token_endpoint");
}
return tokenUrl;
}

private String getPropertyValue(String prop) {
return ConfigProvider.getConfig().getValue(prop, String.class);
}

private String getOptionalPropertyValue(String prop, String defaultProp) {
return ConfigProvider.getConfig().getOptionalValue(prop, String.class)
.orElseGet(() -> ConfigProvider.getConfig().getValue(defaultProp, String.class));
}

public static Buffer encodeForm(MultiMap form) {
Buffer buffer = Buffer.buffer();
for (Map.Entry<String, String> entry : form) {
if (buffer.length() != 0) {
buffer.appendByte((byte) '&');
}
buffer.appendString(entry.getKey());
buffer.appendByte((byte) '=');
buffer.appendString(urlEncode(entry.getValue()));
}
return buffer;
}

private static String urlEncode(String value) {
try {
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}

public void close() {
if (client != null) {
client.close();
client = null;
}
if (vertx != null) {
vertx.close().toCompletionStage().toCompletableFuture().join();
vertx = null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ public Map<String, String> start() {
"")
.withTransformers("response-template")));

definePasswordGrantTokenStub();
defineClientCredGrantTokenStub();

LOG.infof("Keycloak started in mock mode: %s", server.baseUrl());
Map<String, String> conf = new HashMap<>();
conf.put("keycloak.url", server.baseUrl() + "/auth");
Expand Down Expand Up @@ -293,6 +296,28 @@ private void defineCodeFlowAuthorizationMockTokenStub() {
"}")));
}

private void definePasswordGrantTokenStub() {
server.stubFor(post("/auth/realms/quarkus/token")
.withRequestBody(containing("grant_type=password"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\n" +
" \"access_token\": \""
+ getAccessToken("alice", getAdminRoles()) + "\",\n" +
" \"refresh_token\": \"07e08903-1263-4dd1-9fd1-4a59b0db5283\"}")));
}

private void defineClientCredGrantTokenStub() {
server.stubFor(post("/auth/realms/quarkus/token")
.withRequestBody(containing("grant_type=client_credentials"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\n" +
" \"access_token\": \""
+ getAccessToken("alice", getAdminRoles()) + "\",\n" +
" \"refresh_token\": \"07e08903-1263-4dd1-9fd1-4a59b0db5283\"}")));
}

private void defineCodeFlowAuthorizationMockEncryptedTokenStub() {
server.stubFor(post("/auth/realms/quarkus/encrypted-id-token")
.withRequestBody(containing("authorization_code"))
Expand Down

0 comments on commit c8d4533

Please sign in to comment.