Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.devservices.oidc;

import static io.quarkus.deployment.bean.JavaBeanUtil.capitalize;
import static java.util.Base64.getDecoder;

import java.net.URI;
import java.net.URISyntaxException;
Expand Down Expand Up @@ -41,6 +42,7 @@
import io.quarkus.deployment.dev.devservices.DevServicesConfig;
import io.quarkus.runtime.configuration.ConfigUtils;
import io.smallrye.jwt.build.Jwt;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.mutiny.core.Vertx;
Expand Down Expand Up @@ -552,10 +554,8 @@ private static void token(RoutingContext rc) {

private static void passwordTokenEndpoint(RoutingContext rc) {
String scope = rc.request().formAttributes().get("scope");
String clientId = rc.request().formAttributes().get("client_id");
String username = rc.request().formAttributes().get("username");
if (clientId == null || clientId.isEmpty()) {
LOG.warn("Client id is not present, denying token request");
if (clientCredentialsAreInvalid(rc, false)) {
invalidTokenResponse(rc);
return;
}
Expand Down Expand Up @@ -583,9 +583,7 @@ private static void passwordTokenEndpoint(RoutingContext rc) {

private static void clientCredentialsTokenEndpoint(RoutingContext rc) {
String scope = rc.request().formAttributes().get("scope");
String clientId = rc.request().formAttributes().get("client_id");
if (clientId == null || clientId.isEmpty()) {
LOG.warn("Client id is not present, denying token request");
if (clientCredentialsAreInvalid(rc, false)) {
invalidTokenResponse(rc);
return;
}
Expand All @@ -603,20 +601,60 @@ private static void clientCredentialsTokenEndpoint(RoutingContext rc) {
.endAndForget(data);
}

private static void refreshTokenEndpoint(RoutingContext rc) {
String clientId = rc.request().formAttributes().get("client_id");
String clientSecret = rc.request().formAttributes().get("client_secret");
String scope = rc.request().formAttributes().get("scope");
if (clientId == null || clientId.isEmpty()) {
private static boolean clientCredentialsAreInvalid(RoutingContext rc, boolean requireClientSecret) {
// first try to get credentials form attributes
String reqClientId = rc.request().formAttributes().get("client_id");
String reqClientSecret = rc.request().formAttributes().get("client_secret");
// and fallback to basic authentication method
if (reqClientSecret == null || reqClientSecret.isEmpty()) {
String authorizationHeader = rc.request().getHeader(HttpHeaders.AUTHORIZATION);
if (authorizationHeader != null && authorizationHeader.startsWith("Basic ")) {
String encodedClientIdToSecret = authorizationHeader.substring("Basic ".length()).trim();
String[] clientIdAndSecret = decodeBasicCredentials(encodedClientIdToSecret).split(":");
if (clientIdAndSecret.length != 2) {
LOG.warn("Malformed client credentials submitted with the HTTP Authorization Basic scheme");
return true;
}
reqClientId = clientIdAndSecret[0];
reqClientSecret = clientIdAndSecret[1];
}
}

if (reqClientId == null || reqClientId.isEmpty()) {
LOG.warn("Client id is not present, denying token request");
invalidTokenResponse(rc);
return;
return true;
} else if (!reqClientId.equals(clientId)) {
LOG.warnf("Expected client id '%s', but got '%s', denying token request", clientId, reqClientId);
return true;
}

// TODO: this method verifies the client secret only when present or when required previously
// we should extend client credentials verification
if (reqClientSecret == null || reqClientSecret.isEmpty()) {
if (requireClientSecret) {
LOG.warn("Client secret is not present, denying token request");
return true;
} else {
LOG.debug("Client secret is not present");
}
} else if (!reqClientSecret.equals(clientSecret)) {
LOG.warnf("Expected client secret '%s', but got '%s', denying token request", clientSecret, reqClientSecret);
return true;
}
if (clientSecret == null || clientSecret.isEmpty()) {
LOG.warn("Client secret is not present, denying token request");

return false;
}

private static String decodeBasicCredentials(String basicCredentials) {
return new String(getDecoder().decode(basicCredentials.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
}

private static void refreshTokenEndpoint(RoutingContext rc) {
if (clientCredentialsAreInvalid(rc, true)) {
invalidTokenResponse(rc);
return;
}
String scope = rc.request().formAttributes().get("scope");
String refreshToken = rc.request().formAttributes().get("refresh_token");
UserAndRoles userAndRoles = decode(refreshToken);
if (userAndRoles == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package io.quarkus.oidc.client;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;

import io.quarkus.oidc.client.runtime.OidcClientsConfig;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

public abstract class AbstractOidcClientDevServiceTest {

protected static QuarkusUnitTest createQuarkusUnitTest(String applicationProperties) {
return new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(NamedOidcClientResource.class)
.addAsResource(applicationProperties, "application.properties"));
}

@Inject
OidcClientsConfig config;

@Test
public void testOidcClientDefaultIdIsSet() {
var defaultClientConfig = OidcClientsConfig.getDefaultClient(config);
// not set, so "Default" id should be set by Quarkus
assertEquals("Default", defaultClientConfig.id().orElse(null));
// not set, so named key "client1" should be set by Quarkus
assertEquals("client1", config.namedClients().get("client1").id().orElse(null));
// set to "client2" in application.properties
// we cannot set here any different value, because ATM OIDC client enforce that ID always equal named key anyway
assertEquals("client2", config.namedClients().get("client2").id().orElse(null));
// not set and named key "client3" is enclosed in double quotes, so named key "client3" should be set by Quarkus
assertEquals("client3", config.namedClients().get("client3").id().orElse(null));
}

@Test
public void testInjectedNamedOidcClients() {
String token1 = doTestGetTokenByNamedClient("client1");
String token2 = doTestGetTokenByNamedClient("client2");
validateTokens(token1, token2);
}

@Test
public void testInjectedNamedTokens() {
String token1 = doTestGetTokenByNamedTokensProvider("client1");
String token2 = doTestGetTokenByNamedTokensProvider("client2");
validateTokens(token1, token2);
}

private void validateTokens(String token1, String token2) {
assertThat(token1, is(not(equalTo(token2))));
assertThat(upn(token1), is("alice"));
assertThat(upn(token2), is("bob"));
}

private String upn(String token) {
return OidcCommonUtils.decodeJwtContent(token).getString("upn");
}

private String doTestGetTokenByNamedClient(String clientId) {
String token = RestAssured.given().get("/" + clientId + "/token").body().asString();
assertThat(token, is(notNullValue()));
return token;
}

private String doTestGetTokenByNamedTokensProvider(String clientId) {
String token = RestAssured.given().get("/" + clientId + "/token/singleton").body().asString();
assertThat(token, is(notNullValue()));
return token;
}
}
Original file line number Diff line number Diff line change
@@ -1,84 +1,17 @@
package io.quarkus.oidc.client;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.inject.Inject;

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

import io.quarkus.oidc.client.runtime.OidcClientsConfig;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

/**
* Test Keycloak Dev Service is started when OIDC extension is disabled (or not present, though indirectly).
* OIDC client auth server URL and client id and secret must be automatically configured for this test to pass.
* This test uses Dev Services for Keycloak.
*/
public class OidcClientKeycloakDevServiceTest {
public class OidcClientKeycloakDevServiceTest extends AbstractOidcClientDevServiceTest {

@RegisterExtension
static final QuarkusUnitTest test = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(NamedOidcClientResource.class)
.addAsResource("oidc-client-dev-service-test.properties", "application.properties"));

@Inject
OidcClientsConfig config;

@Test
public void testOidcClientDefaultIdIsSet() {
var defaultClientConfig = OidcClientsConfig.getDefaultClient(config);
// not set, so "Default" id should be set by Quarkus
assertEquals("Default", defaultClientConfig.id().orElse(null));
// not set, so named key "client1" should be set by Quarkus
assertEquals("client1", config.namedClients().get("client1").id().orElse(null));
// set to "client2" in application.properties
// we cannot set here any different value, because ATM OIDC client enforce that ID always equal named key anyway
assertEquals("client2", config.namedClients().get("client2").id().orElse(null));
// not set and named key "client3" is enclosed in double quotes, so named key "client3" should be set by Quarkus
assertEquals("client3", config.namedClients().get("client3").id().orElse(null));
}

@Test
public void testInjectedNamedOidcClients() {
String token1 = doTestGetTokenByNamedClient("client1");
String token2 = doTestGetTokenByNamedClient("client2");
validateTokens(token1, token2);
}

@Test
public void testInjectedNamedTokens() {
String token1 = doTestGetTokenByNamedTokensProvider("client1");
String token2 = doTestGetTokenByNamedTokensProvider("client2");
validateTokens(token1, token2);
}

private void validateTokens(String token1, String token2) {
assertThat(token1, is(not(equalTo(token2))));
assertThat(upn(token1), is("alice"));
assertThat(upn(token2), is("bob"));
}

private String upn(String token) {
return OidcCommonUtils.decodeJwtContent(token).getString("upn");
}

private String doTestGetTokenByNamedClient(String clientId) {
String token = RestAssured.given().get("/" + clientId + "/token").body().asString();
assertThat(token, is(notNullValue()));
return token;
}
static final QuarkusUnitTest test = createQuarkusUnitTest("oidc-client-dev-service-test.properties");

private String doTestGetTokenByNamedTokensProvider(String clientId) {
String token = RestAssured.given().get("/" + clientId + "/token/singleton").body().asString();
assertThat(token, is(notNullValue()));
return token;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.oidc.client;

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

import io.quarkus.test.QuarkusUnitTest;

/**
* This test uses OIDC Client extension with Dev Services for OIDC.
*/
public class OidcClientOidcDevServiceTest extends AbstractOidcClientDevServiceTest {

@RegisterExtension
static final QuarkusUnitTest test = createQuarkusUnitTest("oidc-client-oidc-dev-service-test.properties");

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# quarkus.oidc.enabled=false TODO: this shouldn't be necessary in the future!
quarkus.keycloak.devservices.enabled=false
quarkus.oidc.devservices.enabled=true

quarkus.oidc-client.client1.auth-server-url=${quarkus.oidc.auth-server-url}
quarkus.oidc-client.client1.client-id=${quarkus.oidc.client-id}
quarkus.oidc-client.client1.credentials.secret=${quarkus.oidc.credentials.secret}
quarkus.oidc-client.client1.grant.type=password
quarkus.oidc-client.client1.grant-options.password.username=alice
quarkus.oidc-client.client1.grant-options.password.password=alice

quarkus.oidc-client.client2.auth-server-url=${quarkus.oidc.auth-server-url}
quarkus.oidc-client.client2.client-id=${quarkus.oidc.client-id}
quarkus.oidc-client.client2.credentials.secret=${quarkus.oidc.credentials.secret}
quarkus.oidc-client.client2.grant.type=password
quarkus.oidc-client.client2.grant-options.password.username=bob
quarkus.oidc-client.client2.grant-options.password.password=bob
quarkus.oidc-client.client2.id=client2

quarkus.oidc-client."client3".auth-server-url=${quarkus.oidc.auth-server-url}
quarkus.oidc-client."client3".client-id=${quarkus.oidc.client-id}
quarkus.oidc-client."client3".credentials.secret=${quarkus.oidc.credentials.secret}
quarkus.oidc-client."client3".grant.type=password
quarkus.oidc-client."client3".grant-options.password.username=bob
quarkus.oidc-client."client3".grant-options.password.password=bob
Loading