Skip to content

Commit

Permalink
Merge pull request #17 from project-kessel/RHCLOUD-32406-Java-client-…
Browse files Browse the repository at this point in the history
…client-credentials-flow

Rhcloud 32406 java client client credentials flow
  • Loading branch information
merlante authored Aug 9, 2024
2 parents 1934df4 + 94d2d33 commit 084aee0
Show file tree
Hide file tree
Showing 21 changed files with 1,652 additions and 114 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
- name: Set up Zulu JDK 21
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
java-version: '21'
distribution: 'zulu'
cache: maven
- name: Build and test with Maven
run: ./mvnw -B package --file pom.xml
20 changes: 20 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@
<artifactId>pgv-java-stub</artifactId>
<version>1.0.4</version>
</dependency>
<!-- Default OIDC implementation. Must be provided by the user. -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>11.13</version>
<scope>provided</scope>
</dependency>
<!-- Included only for @RegisterForReflection to support native Quarkus apps if applicable -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core</artifactId>
<version>3.12.3</version>
<scope>provided</scope>
</dependency>
<dependency> <!-- necessary for Java 9+ -->
<groupId>org.apache.tomcat</groupId>
<artifactId>annotations-api</artifactId>
Expand All @@ -99,6 +113,12 @@
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
<!-- Use Weld as a CDI container for CDI testing. -->
<dependency>
<groupId>org.jboss.weld</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@ public class CDIManagedClients {
RelationsGrpcClientsManager getManager(Config config) {
var isSecureClients = config.isSecureClients();
var targetUrl = config.targetUrl();
var authnEnabled = config.authenticationConfig().map(t -> !t.mode().equals(Config.AuthMode.DISABLED)).orElse(false);

if (isSecureClients) {
if(authnEnabled) {
return RelationsGrpcClientsManager.forSecureClients(targetUrl, config.authenticationConfig().get());
}
return RelationsGrpcClientsManager.forSecureClients(targetUrl);
}

if(authnEnabled) {
return RelationsGrpcClientsManager.forInsecureClients(targetUrl, config.authenticationConfig().get());
}
return RelationsGrpcClientsManager.forInsecureClients(targetUrl);
}

Expand Down
28 changes: 28 additions & 0 deletions src/main/java/org/project_kessel/relations/client/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
import io.smallrye.config.WithName;

import java.util.Optional;

/**
* Interface for injecting config into container managed beans.
Expand All @@ -11,7 +14,32 @@
*/
@ConfigMapping(prefix = "relations-api")
public interface Config {
enum AuthMode {
DISABLED,
OIDC_CLIENT_CREDENTIALS
}

@WithDefault("false")
boolean isSecureClients();
String targetUrl();

@WithName("authn")
Optional<AuthenticationConfig> authenticationConfig();

interface AuthenticationConfig {
@WithDefault("disabled")
AuthMode mode();
@WithName("client")
Optional<OIDCClientCredentialsConfig> clientCredentialsConfig();
}

interface OIDCClientCredentialsConfig {
String issuer();
@WithName("id")
String clientId();
@WithName("secret")
String clientSecret();
Optional<String[]> scope();
Optional<String> oidcClientCredentialsMinterImplementation();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.project_kessel.relations.client;

import io.grpc.*;
import org.project_kessel.relations.client.authn.CallCredentialsFactory;

import java.util.HashMap;

Expand All @@ -18,6 +19,20 @@ public static synchronized RelationsGrpcClientsManager forInsecureClients(String
return insecureManagers.get(targetUrl);
}

public static synchronized RelationsGrpcClientsManager forInsecureClients(String targetUrl, Config.AuthenticationConfig authnConfig) throws RuntimeException {
if (!insecureManagers.containsKey(targetUrl)) {
try {
var manager = new RelationsGrpcClientsManager(targetUrl,
InsecureChannelCredentials.create(),
CallCredentialsFactory.create(authnConfig));
insecureManagers.put(targetUrl, manager);
} catch (CallCredentialsFactory.CallCredentialsCreationException e) {
throw new RuntimeException(e);
}
}
return insecureManagers.get(targetUrl);
}

public static synchronized RelationsGrpcClientsManager forSecureClients(String targetUrl) {
if (!secureManagers.containsKey(targetUrl)) {
var tlsChannelCredentials = TlsChannelCredentials.create();
Expand All @@ -27,6 +42,21 @@ public static synchronized RelationsGrpcClientsManager forSecureClients(String t
return secureManagers.get(targetUrl);
}

public static synchronized RelationsGrpcClientsManager forSecureClients(String targetUrl, Config.AuthenticationConfig authnConfig) {
if (!secureManagers.containsKey(targetUrl)) {
var tlsChannelCredentials = TlsChannelCredentials.create();
try {
var manager = new RelationsGrpcClientsManager(targetUrl,
tlsChannelCredentials,
CallCredentialsFactory.create(authnConfig));
secureManagers.put(targetUrl, manager);
} catch (CallCredentialsFactory.CallCredentialsCreationException e) {
throw new RuntimeException(e);
}
}
return secureManagers.get(targetUrl);
}

public static synchronized void shutdownAll() {
for (var manager : insecureManagers.values()) {
manager.closeClientChannel();
Expand Down Expand Up @@ -60,14 +90,23 @@ public static synchronized void shutdownManager(RelationsGrpcClientsManager mana
}

/**
*
* Bearer token and other things can be added to ChannelCredentials. New static factory methods can be added.
* Config management also required.
* Create a manager for a grpc channel with server credentials.
* @param targetUrl
* @param serverCredentials authenticates the server for TLS or are InsecureChannelCredentials
*/
private RelationsGrpcClientsManager(String targetUrl, ChannelCredentials serverCredentials) {
this.channel = Grpc.newChannelBuilder(targetUrl, serverCredentials).build();
}

/**
* Create a manager for a grpc channel with server credentials and credentials for per-rpc client authentication.
* @param targetUrl
* @param credentials
* @param serverCredentials authenticates the server for TLS or are InsecureChannelCredentials
* @param authnCredentials authenticates the client on each rpc
*/
private RelationsGrpcClientsManager(String targetUrl, ChannelCredentials credentials) {
this.channel = Grpc.newChannelBuilder(targetUrl, credentials).build();
private RelationsGrpcClientsManager(String targetUrl, ChannelCredentials serverCredentials, CallCredentials authnCredentials) {
this.channel = Grpc.newChannelBuilder(targetUrl,
CompositeChannelCredentials.create(serverCredentials, authnCredentials)).build();
}

private void closeClientChannel() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.project_kessel.relations.client.authn;

import io.grpc.CallCredentials;
import org.project_kessel.relations.client.Config;
import org.project_kessel.relations.client.authn.oidc.client.OIDCClientCredentialsCallCredentials;

public class CallCredentialsFactory {

private CallCredentialsFactory() {

}

public static CallCredentials create(Config.AuthenticationConfig authnConfig) throws CallCredentialsCreationException {
if (authnConfig == null) {
throw new CallCredentialsCreationException("AuthenticationConfig is required to create CallCredentials and must not be null.");
}

try {
switch (authnConfig.mode()) {
case DISABLED: return null;
case OIDC_CLIENT_CREDENTIALS: return new OIDCClientCredentialsCallCredentials(authnConfig);
}
} catch (OIDCClientCredentialsCallCredentials.OIDCClientCredentialsCallCredentialsException e) {
throw new CallCredentialsCreationException("Failed to create OIDCClientCredentialsCallCredentials.", e);
}

return null;
}

public static class CallCredentialsCreationException extends Exception {
public CallCredentialsCreationException(String message) {
super(message);
}

public CallCredentialsCreationException(String message, Throwable cause) {
super(message, cause);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.project_kessel.relations.client.authn.oidc.client;

import io.grpc.Metadata;
import io.grpc.Status;
import org.project_kessel.relations.client.Config;

import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;

public class OIDCClientCredentialsCallCredentials extends io.grpc.CallCredentials {
static final Metadata.Key<String> authorizationKey = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);

private final Config.OIDCClientCredentialsConfig clientCredentialsConfig;
private final OIDCClientCredentialsMinter minter;

private final AtomicReference<OIDCClientCredentialsMinter.BearerHeader> storedBearerHeaderRef = new AtomicReference<>();

public OIDCClientCredentialsCallCredentials(Config.AuthenticationConfig authnConfig) throws OIDCClientCredentialsCallCredentialsException {
this.clientCredentialsConfig = validateAndExtractConfig(authnConfig);

Optional<String> minterImpl = clientCredentialsConfig.oidcClientCredentialsMinterImplementation();
try {
if(minterImpl.isPresent()) {
this.minter = OIDCClientCredentialsMinter.forName(minterImpl.get());
} else {
this.minter = OIDCClientCredentialsMinter.forDefaultImplementation();
}
} catch (OIDCClientCredentialsMinter.OIDCClientCredentialsMinterException e) {
throw new OIDCClientCredentialsCallCredentialsException("Couldn't create GrpcCallCredentials because minter impl not instantiated.", e);
}
}

OIDCClientCredentialsCallCredentials(Config.OIDCClientCredentialsConfig clientCredentialsConfig, OIDCClientCredentialsMinter minter) {
this.clientCredentialsConfig = clientCredentialsConfig;
this.minter = minter;
}

@Override
public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) {
appExecutor.execute(() -> {
try {
synchronized (storedBearerHeaderRef) {
if (storedBearerHeaderRef.get() == null || storedBearerHeaderRef.get().isExpired()) {
storedBearerHeaderRef.set(minter.authenticateAndRetrieveAuthorizationHeader(clientCredentialsConfig));
}

Metadata headers = new Metadata();
headers.put(authorizationKey, storedBearerHeaderRef.get().getAuthorizationHeader());
applier.apply(headers);
}
} catch (Exception e) {
applier.fail(Status.UNAUTHENTICATED.withCause(e));
}
});
}

/**
* For unusual cases where stored credentials (i.e. token), which may be long-lived, is bad and needs to be flushed.
*/
public void flushStoredCredentials() {
synchronized (storedBearerHeaderRef) {
storedBearerHeaderRef.set(null);
}
}

/* We don't know that smallrye config validation will be used by clients, so do some validation here. */
static Config.OIDCClientCredentialsConfig validateAndExtractConfig(Config.AuthenticationConfig authnConfig) throws OIDCClientCredentialsCallCredentialsException {
if (authnConfig.clientCredentialsConfig().isEmpty()) {
throw new OIDCClientCredentialsCallCredentialsException("ClientCredentialsConfig is required for OIDC client credentials authentication method.");
}
if(authnConfig.clientCredentialsConfig().get().issuer() == null) {
throw new OIDCClientCredentialsCallCredentialsException("ClientCredentialsConfig Issuer must not be null.");
}
if(authnConfig.clientCredentialsConfig().get().clientId() == null) {
throw new OIDCClientCredentialsCallCredentialsException("ClientCredentialsConfig Client id must not be null.");
}
if(authnConfig.clientCredentialsConfig().get().clientSecret() == null) {
throw new OIDCClientCredentialsCallCredentialsException("ClientCredentialsConfig Client secret must not be null.");
}

return authnConfig.clientCredentialsConfig().get();
}

public static class OIDCClientCredentialsCallCredentialsException extends Exception {
public OIDCClientCredentialsCallCredentialsException(String message) {
super(message);
}

public OIDCClientCredentialsCallCredentialsException(String message, Throwable cause) {
super(message, cause);
}
}

}
Loading

0 comments on commit 084aee0

Please sign in to comment.