Skip to content

Commit

Permalink
Merge pull request #16388 from fwippe/oidc_client_names
Browse files Browse the repository at this point in the history
Oidc-Client: Allow named injections of OidcClients and Tokens
  • Loading branch information
sberyozkin authored Apr 13, 2021
2 parents bff037b + 38ed3fc commit d97983a
Show file tree
Hide file tree
Showing 13 changed files with 410 additions and 18 deletions.
48 changes: 47 additions & 1 deletion docs/src/main/asciidoc/security-openid-connect-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ Please note that some OpenId Connect Providers will not return a refresh token i

=== OidcClients

`io.quarkus.oidc.client.OidcClients` is a container of `OidcClient`s - it includes a default `OidcClient` (which can also be injected directly as described above) and named clients which can be configured like this:
`io.quarkus.oidc.client.OidcClients` is a container of ``OidcClient``s - it includes a default `OidcClient` (which can also be injected directly as described above) and named clients which can be configured like this:

[source,properties]
----
Expand Down Expand Up @@ -270,6 +270,52 @@ public class OidcClientResource {
}
----

==== Inject named `OidcClient` and `Tokens`

In case of multiple configured ``OidcClient``s you can specify the `OidcClient` injection target by the extra qualifier `@NamedOidcClient` instead of working with `OidcClients`:

[source,java]
----
package io.quarkus.oidc.client;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@Path("/clients")
public class OidcClientResource {
@Inject
@NamedOidcClient("jwt-secret")
OidcClient client;
@GET
public String getResponse() {
// use client to get the token
}
}
----

The same qualifier can be used to specify the `OidcClient` used for a `Tokens` injection:

[source,java]
----
@Provider
@Priority(Priorities.AUTHENTICATION)
@RequestScoped
public class OidcClientRequestCustomFilter implements ClientRequestFilter {
@Inject
@NamedOidcClient("jwt-secret")
Tokens tokens;
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.getAccessToken());
}
}
----

[[oidc-client-authentication]]
=== OidcClient Authentication

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ public void filter(ClientRequestContext requestContext) throws IOException {

private String getAccessToken() {
// It should be reactive when run with Resteasy Reactive
return getTokens().await().indefinitely().getAccessToken();
return awaitTokens().getAccessToken();
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
package io.quarkus.oidc.client.deployment;

import java.util.Arrays;
import java.util.List;
import java.lang.reflect.Modifier;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.stream.Collectors;

import javax.enterprise.context.RequestScoped;
import javax.inject.Singleton;

import org.jboss.jandex.DotName;

import io.quarkus.arc.BeanDestroyer;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.deployment.ApplicationArchive;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem;
import io.quarkus.deployment.builditem.EnableAllSecurityServicesBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.ClassOutput;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.oidc.client.NamedOidcClient;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.OidcClients;
import io.quarkus.oidc.client.Tokens;
import io.quarkus.oidc.client.runtime.AbstractTokensProducer;
import io.quarkus.oidc.client.runtime.OidcClientBuildTimeConfig;
import io.quarkus.oidc.client.runtime.OidcClientRecorder;
import io.quarkus.oidc.client.runtime.OidcClientsConfig;
Expand Down Expand Up @@ -49,31 +68,145 @@ void runtimeInitializeTokenHelper(BuildProducer<RuntimeInitializedClassBuildItem
runtime.produce(new RuntimeInitializedClassBuildItem(TokensHelper.class.getName()));
}

@BuildStep(onlyIf = IsEnabled.class)
void extractInjectedOidcClientNames(
ApplicationArchivesBuildItem beanArchiveIndex,
BuildProducer<OidcClientNamesBuildItem> oidcClientNames) {

oidcClientNames.produce(new OidcClientNamesBuildItem(oidcClientNamesOf(beanArchiveIndex)));
}

private Set<String> oidcClientNamesOf(ApplicationArchivesBuildItem beanArchiveIndex) {
return beanArchiveIndex.getAllApplicationArchives().stream()
.map(ApplicationArchive::getIndex)
.flatMap(archive -> archive.getAnnotations(DotName.createSimple(NamedOidcClient.class.getName())).stream())
.map(annotation -> annotation.value().asString())
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}

@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep(onlyIf = IsEnabled.class)
public List<SyntheticBeanBuildItem> setup(
public void setup(
OidcClientsConfig oidcConfig,
TlsConfig tlsConfig,
OidcClientRecorder recorder,
CoreVertxBuildItem vertxBuildItem) {
CoreVertxBuildItem vertxBuildItem,
OidcClientNamesBuildItem oidcClientNames,
BuildProducer<SyntheticBeanBuildItem> syntheticBean) {

OidcClients clients = recorder.setup(oidcConfig, tlsConfig, vertxBuildItem.getVertx());

SyntheticBeanBuildItem oidcClientBuildItem = SyntheticBeanBuildItem.configure(OidcClient.class).unremovable()
syntheticBean.produce(SyntheticBeanBuildItem.configure(OidcClient.class).unremovable()
.types(OidcClient.class)
.supplier(recorder.createOidcClientBean(clients))
.scope(Singleton.class)
.setRuntimeInit()
.destroyer(BeanDestroyer.CloseableDestroyer.class)
.done();
SyntheticBeanBuildItem oidcClientsBuildItem = SyntheticBeanBuildItem.configure(OidcClients.class).unremovable()
.done());

syntheticBean.produce(SyntheticBeanBuildItem.configure(OidcClients.class).unremovable()
.types(OidcClients.class)
.supplier(recorder.createOidcClientsBean(clients))
.scope(Singleton.class)
.setRuntimeInit()
.destroyer(BeanDestroyer.CloseableDestroyer.class)
.done());

produceNamedOidcClientBeans(syntheticBean, oidcClientNames.oidcClientNames(), recorder, clients);
}

private void produceNamedOidcClientBeans(BuildProducer<SyntheticBeanBuildItem> syntheticBean,
Set<String> injectedOidcClientNames,
OidcClientRecorder recorder, OidcClients clients) {
injectedOidcClientNames.stream()
.map(clientName -> syntheticNamedOidcClientBeanFor(clientName, recorder, clients))
.forEach(syntheticBean::produce);
}

private SyntheticBeanBuildItem syntheticNamedOidcClientBeanFor(String clientName, OidcClientRecorder recorder,
OidcClients clients) {
return SyntheticBeanBuildItem.configure(OidcClient.class).unremovable()
.types(OidcClient.class)
.supplier(recorder.createOidcClientBean(clients, clientName))
.scope(Singleton.class)
.addQualifier().annotation(NamedOidcClient.class).addValue("value", clientName).done()
.setRuntimeInit()
.destroyer(BeanDestroyer.CloseableDestroyer.class)
.done();
return Arrays.asList(oidcClientBuildItem, oidcClientsBuildItem);
}

@BuildStep(onlyIf = IsEnabled.class)
public void createNonDefaultTokensProducers(
BuildProducer<GeneratedBeanBuildItem> generatedBean,
OidcClientNamesBuildItem oidcClientNames) {

ClassOutput classOutput = new GeneratedBeanGizmoAdaptor(generatedBean);

String targetPackage = DotNames
.internalPackageNameWithTrailingSlash(DotName.createSimple(TokensProducer.class.getName()));

for (String oidcClientName : oidcClientNames.oidcClientNames()) {
createNamedTokensProducerFor(classOutput, targetPackage, oidcClientName);
}
}

/**
* Creates a Tokens producer class like follows:
*
* <pre>
* &#64;Singleton
* public class TokensProducer_oidcClientName extends AbstractTokensProducer {
* &#64;Produces
* &#64;NamedOidcClient("oidcClientName")
* &#64;RequestScoped
* public Tokens produceTokens() {
* return awaitTokens();
* }
*
* &#64;Override
* protected Optional<String> clientId() {
* return Optional.of("oidcClientName");
* }
* }
* </pre>
*/
private String createNamedTokensProducerFor(ClassOutput classOutput, String targetPackage, String oidcClientName) {
String generatedName = targetPackage + "TokensProducer_" + sanitize(oidcClientName);

try (ClassCreator tokensProducer = ClassCreator.builder().classOutput(classOutput).className(generatedName)
.superClass(AbstractTokensProducer.class)
.build()) {
tokensProducer.addAnnotation(DotNames.SINGLETON.toString());

try (MethodCreator produceMethod = tokensProducer.getMethodCreator("produceTokens", Tokens.class)) {
produceMethod.setModifiers(Modifier.PUBLIC);

produceMethod.addAnnotation(DotNames.PRODUCES.toString());
produceMethod.addAnnotation(NamedOidcClient.class.getName()).addValue("value", oidcClientName);
produceMethod.addAnnotation(RequestScoped.class.getName());

ResultHandle tokensResult = produceMethod.invokeVirtualMethod(
MethodDescriptor.ofMethod(AbstractTokensProducer.class, "awaitTokens", Tokens.class),
produceMethod.getThis());

produceMethod.returnValue(tokensResult);
}

try (MethodCreator clientIdMethod = tokensProducer.getMethodCreator("clientId", Optional.class)) {
clientIdMethod.setModifiers(Modifier.PROTECTED);

clientIdMethod.returnValue(clientIdMethod.invokeStaticMethod(
MethodDescriptor.ofMethod(Optional.class, "of", Optional.class, Object.class),
clientIdMethod.load(oidcClientName)));
}
}

return generatedName.replace('/', '.');
}

private String sanitize(String oidcClientName) {
return oidcClientName.replaceAll("\\W+", "");
}

public static class IsEnabled implements BooleanSupplier {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.oidc.client.deployment;

import java.util.Collections;
import java.util.Set;

import io.quarkus.builder.item.SimpleBuildItem;

/**
* Contains non-default names of OIDC Clients.
*/
public final class OidcClientNamesBuildItem extends SimpleBuildItem {
private final Set<String> oidcClientNames;

OidcClientNamesBuildItem(Set<String> oidcClientNames) {
this.oidcClientNames = Collections.unmodifiableSet(oidcClientNames);
}

Set<String> oidcClientNames() {
return oidcClientNames;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public Map<String, String> start() {

realm.getClients().add(createClient("quarkus-app"));
realm.getUsers().add(createUser("alice", "user"));
realm.getUsers().add(createUser("bob", "user"));

RestAssured
.given()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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 org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.oidc.runtime.OidcUtils;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.QuarkusTestResource;
import io.restassured.RestAssured;

@QuarkusTestResource(KeycloakRealmUserPasswordManager.class)
public class NamedOidcClientInjectionTestCase {

private static Class<?>[] testClasses = {
NamedOidcClientResource.class
};

@RegisterExtension
static final QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(testClasses)
.addAsResource("application-named-oidc-client-credentials.properties", "application.properties"));

@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(preferredUserOf(token1), is("alice"));
assertThat(preferredUserOf(token2), is("bob"));
}

private String preferredUserOf(String token) {
return OidcUtils.decodeJwtContent(token).getString("preferred_username");
}

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

private String doTestGetTokenByNamedTokensProvider(String clientId) {
String token = RestAssured.when().get("/" + clientId + "/token/singleton").body().asString();
assertThat(token, is(notNullValue()));
return token;
}
}
Loading

0 comments on commit d97983a

Please sign in to comment.