Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OIDC client integration for GraphQL clients #36375

Merged
merged 1 commit into from
Oct 11, 2023
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
10 changes: 10 additions & 0 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,16 @@
<artifactId>quarkus-oidc-client-reactive-filter-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-graphql</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-graphql-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-token-propagation</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public enum Feature {
OIDC_CLIENT,
OIDC_CLIENT_FILTER,
OIDC_CLIENT_REACTIVE_FILTER,
OIDC_CLIENT_GRAPHQL_CLIENT_INTEGRATION,
OIDC_TOKEN_PROPAGATION,
OIDC_TOKEN_PROPAGATION_REACTIVE,
OPENSHIFT_CLIENT,
Expand Down
13 changes: 13 additions & 0 deletions devtools/bom-descriptor-json/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1565,6 +1565,19 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-graphql</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-reactive-filter</artifactId>
Expand Down
13 changes: 13 additions & 0 deletions docs/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1581,6 +1581,19 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-graphql-deployment</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-reactive-filter-deployment</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,66 @@ The `quarkus-oidc-token-propagation-reactive` extension provides `io.quarkus.oid
The `quarkus-oidc-token-propagation-reactive` extension (as opposed to the non-reactive `quarkus-oidc-token-propagation` extension) does not currently support the exchanging or resigning the tokens before the propagation.
However, these features may be added in the future.

[[oidc-client-graphql-client]]
== GraphQL client integration

The `quarkus-oidc-client-graphql` extension provides a way to integrate an
OIDC client into xref:smallrye-graphql-client.adoc[GraphQL clients]. This
works similarly as with REST clients. When this extension is present, any
configured (that means NOT created programmatically via the builder, but via
configuration properties) GraphQL client will attempt to use the OIDC client
to obtain an access token and set it as an `Authorization` header value.
OIDC client will also refresh expired access tokens.

To configure which OIDC client should be used by GraphQL client, select one of the configured OIDC clients with the `quarkus.oidc-client-graphql.client-name` property, for example:

----
quarkus.oidc-client-graphql.client-name=oidc-client-for-graphql

# example declaration of the OIDC client itself
quarkus.oidc-client.oidc-client-for-graphql.auth-server-url=${keycloak.url}
quarkus.oidc-client.oidc-client-for-graphql.grant.type=password
quarkus.oidc-client.oidc-client-for-graphql.grant-options.password.username=${username}
quarkus.oidc-client.oidc-client-for-graphql.grant-options.password.password=${password}
quarkus.oidc-client.oidc-client-for-graphql.client-id=${quarkus.oidc.client-id}
quarkus.oidc-client.oidc-client-for-graphql.credentials.client-secret.value=${keycloak.credentials.secret}
quarkus.oidc-client.oidc-client-for-graphql.credentials.client-secret.method=POST
----

NOTE: If you don't specify the `quarkus.oidc-client-graphql.client-name` property,
GraphQL clients will use the default OIDC client (without an explicit name).

Specifically for typesafe GraphQL clients, you can override this on a
per-client basis by annotating the `GraphQLClientApi` interface with
`@io.quarkus.oidc.client.filter.OidcClientFilter`. For example:

[source,java]
----
@GraphQLClientApi(configKey = "order-client")
@OidcClientFilter("oidc-client-for-graphql")
public interface OrdersGraphQLClient {
// queries, mutations and subscriptions go here...
}
----

To be able to use this with a programmatically created GraphQL client, both
builders (`VertxDynamicGraphQLClientBuilder` and
`VertxTypesafeGraphQLClientBuilder`) contain a method `dynamicHeader(String,
Uni<String>`) that allows you to plug in a header that might change for
every request. To plug an OIDC client into it, use

[source,java]
----
@Inject
OidcClients oidcClients;

VertxTypesafeGraphQLClientBuilder builder = ....;
Uni<String> tokenUni = oidcClients.getClient("OIDC_CLIENT_NAME")
.getTokens().map(t -> "Bearer " + t.getAccessToken());
builder.dynamicHeader("Authorization", tokenUni);
VertxDynamicGraphQLClient client = builder.build();
----

== References

* xref:security-openid-connect-client.adoc[OpenID Connect Client and Token Propagation Quickstart]
Expand Down
6 changes: 6 additions & 0 deletions docs/src/main/asciidoc/smallrye-graphql-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,9 @@ formats it for better readability by humans, for example by piping the output th

This example showed how to use both the dynamic and typesafe GraphQL clients to call an external
GraphQL service and explained the difference between the client types.

== References

* xref:security-openid-connect-client-reference.adoc#oidc-client-graphql-client[Integrating OIDC clients into GraphQL clients]
* https://smallrye.io/smallrye-graphql/latest/[Upstream SmallRye GraphQL Client documentation]

128 changes: 128 additions & 0 deletions extensions/oidc-client-graphql/deployment/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>quarkus-oidc-client-graphql-parent</artifactId>
<groupId>io.quarkus</groupId>
<version>999-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>quarkus-oidc-client-graphql-deployment</artifactId>
<name>Quarkus - OpenID Connect Client GraphQL integration - Deployment</name>

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-graphql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-graphql-client-deployment</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-graphql-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-deployment</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<testResources>
<testResource>
<directory>src/test/resources</directory>
<filtering>true</filtering>
</testResource>
</testResources>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${project.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>test-keycloak</id>
<activation>
<property>
<name>test-containers</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>false</skip>
<systemPropertyVariables>
<keycloak.docker.image>${keycloak.docker.legacy.image}</keycloak.docker.image>
<keycloak.use.https>false</keycloak.use.https>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.quarkus.oidc.client.graphql;

import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT;

import java.util.HashMap;
import java.util.Map;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;

import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem;
import io.quarkus.arc.deployment.BeanContainerBuildItem;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.oidc.client.graphql.runtime.OidcClientGraphQLConfig;
import io.quarkus.oidc.client.graphql.runtime.OidcGraphQLClientIntegrationRecorder;

public class OidcGraphQLClientIntegrationProcessor {

private static final DotName GRAPHQL_CLIENT_API = DotName
.createSimple("io.smallrye.graphql.client.typesafe.api.GraphQLClientApi");

private static final String OIDC_CLIENT_FILTER = "io.quarkus.oidc.client.filter.OidcClientFilter";

@BuildStep
void feature(BuildProducer<FeatureBuildItem> featureProducer) {
featureProducer.produce(new FeatureBuildItem(Feature.OIDC_CLIENT_GRAPHQL_CLIENT_INTEGRATION));
}

@BuildStep
@Record(RUNTIME_INIT)
void initialize(BeanContainerBuildItem containerBuildItem,
OidcGraphQLClientIntegrationRecorder recorder,
OidcClientGraphQLConfig config,
BeanArchiveIndexBuildItem index) {
Map<String, String> configKeysToOidcClients = new HashMap<>();
for (AnnotationInstance annotation : index.getIndex().getAnnotations(GRAPHQL_CLIENT_API)) {
ClassInfo clazz = annotation.target().asClass();
AnnotationInstance oidcClient = clazz.annotation(OIDC_CLIENT_FILTER);
if (oidcClient != null) {
String oidcClientName = oidcClient.valueWithDefault(index.getIndex(), "value").asString();
AnnotationValue configKeyValue = annotation.value("configKey");
String configKey = configKeyValue != null ? configKeyValue.asString() : null;
String actualConfigKey = (configKey != null && !configKey.equals("")) ? configKey : clazz.name().toString();
if (oidcClientName != null && !oidcClientName.isEmpty()) {
configKeysToOidcClients.put(actualConfigKey, oidcClientName);
}
}
}
recorder.enhanceGraphQLClientConfigurationWithOidc(configKeysToOidcClients, config.clientName.orElse(null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.oidc.client.graphql;

import org.eclipse.microprofile.graphql.Query;

import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi;

@GraphQLClientApi(configKey = "typesafe-annotation")
@OidcClientFilter("annotation")
public interface AnnotationTypesafeGraphQLClient {

@Query
String principalName();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.oidc.client.graphql;

import org.eclipse.microprofile.graphql.Query;

import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi;

@GraphQLClientApi(configKey = "typesafe-default")
@OidcClientFilter
public interface DefaultTypesafeGraphQLClient {

@Query
String principalName();

}
Loading
Loading