Skip to content
Open
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
2 changes: 2 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [
"com.google.errorprone:error_prone_annotations:2.36.0",
"com.google.guava:failureaccess:1.0.1",
"com.google.guava:guava:33.4.8-android",
"com.google.http-client:google-http-client:1.44.2",
"com.google.http-client:google-http-client-gson:1.44.2",
"com.google.re2j:re2j:1.8",
"com.google.s2a.proto.v2:s2a-proto:0.1.2",
"com.google.truth:truth:1.4.2",
Expand Down
2 changes: 2 additions & 0 deletions repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [
"com.google.errorprone:error_prone_annotations:2.36.0",
"com.google.guava:failureaccess:1.0.1",
"com.google.guava:guava:33.4.8-android",
"com.google.http-client:google-http-client:1.44.2",
"com.google.http-client:google-http-client-gson:1.44.2",
"com.google.re2j:re2j:1.8",
"com.google.s2a.proto.v2:s2a-proto:0.1.2",
"com.google.truth:truth:1.4.2",
Expand Down
2 changes: 2 additions & 0 deletions xds/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ java_library(
"@com_google_protobuf//:protobuf_java",
"@com_google_protobuf//:protobuf_java_util",
"@maven//:com_google_auth_google_auth_library_oauth2_http",
"@maven//:com_google_http_client_google_http_client",
"@maven//:com_google_http_client_google_http_client_gson",
artifact("com.google.code.findbugs:jsr305"),
artifact("com.google.code.gson:gson"),
artifact("com.google.errorprone:error_prone_annotations"),
Expand Down
67 changes: 66 additions & 1 deletion xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import io.grpc.CallCredentials;
import io.grpc.ChannelCredentials;
import io.grpc.CompositeCallCredentials;
import io.grpc.CompositeChannelCredentials;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.JsonUtil;
import io.grpc.xds.client.BootstrapperImpl;
import io.grpc.xds.client.XdsInitializationException;
Expand All @@ -33,6 +37,8 @@ class GrpcBootstrapperImpl extends BootstrapperImpl {
private static final String BOOTSTRAP_PATH_SYS_PROPERTY = "io.grpc.xds.bootstrap";
private static final String BOOTSTRAP_CONFIG_SYS_ENV_VAR = "GRPC_XDS_BOOTSTRAP_CONFIG";
private static final String BOOTSTRAP_CONFIG_SYS_PROPERTY = "io.grpc.xds.bootstrapConfig";
private static final String GRPC_EXPERIMENTAL_XDS_BOOTSTRAP_CALL_CREDS =
"GRPC_EXPERIMENTAL_XDS_BOOTSTRAP_CALL_CREDS";
@VisibleForTesting
String bootstrapPathFromEnvVar = System.getenv(BOOTSTRAP_PATH_SYS_ENV_VAR);
@VisibleForTesting
Expand All @@ -41,6 +47,9 @@ class GrpcBootstrapperImpl extends BootstrapperImpl {
String bootstrapConfigFromEnvVar = System.getenv(BOOTSTRAP_CONFIG_SYS_ENV_VAR);
@VisibleForTesting
String bootstrapConfigFromSysProp = System.getProperty(BOOTSTRAP_CONFIG_SYS_PROPERTY);
@VisibleForTesting
static boolean xdsBootstrapCallCredsEnabled = GrpcUtil.getFlag(
GRPC_EXPERIMENTAL_XDS_BOOTSTRAP_CALL_CREDS, false);

GrpcBootstrapperImpl() {
super();
Expand Down Expand Up @@ -92,7 +101,12 @@ protected String getJsonContent() throws XdsInitializationException, IOException
@Override
protected Object getImplSpecificConfig(Map<String, ?> serverConfig, String serverUri)
throws XdsInitializationException {
return getChannelCredentials(serverConfig, serverUri);
ChannelCredentials channelCreds = getChannelCredentials(serverConfig, serverUri);
CallCredentials callCreds = getCallCredentials(serverConfig, serverUri);
if (callCreds != null) {
channelCreds = CompositeChannelCredentials.create(channelCreds, callCreds);
}
return channelCreds;
}

private static ChannelCredentials getChannelCredentials(Map<String, ?> serverConfig,
Expand Down Expand Up @@ -135,4 +149,55 @@ private static ChannelCredentials parseChannelCredentials(List<Map<String, ?>> j
}
return null;
}

private static CallCredentials getCallCredentials(Map<String, ?> serverConfig,
String serverUri)
throws XdsInitializationException {
List<?> rawCallCredsList = JsonUtil.getList(serverConfig, "call_creds");
if (rawCallCredsList == null || rawCallCredsList.isEmpty()) {
return null;
}
CallCredentials callCredentials =
parseCallCredentials(JsonUtil.checkObjectList(rawCallCredsList), serverUri);
return callCredentials;
}

@Nullable
private static CallCredentials parseCallCredentials(List<Map<String, ?>> jsonList,
String serverUri)
throws XdsInitializationException {
if (!xdsBootstrapCallCredsEnabled) {
return null;
}

CallCredentials callCredentials = null;
for (Map<String, ?> callCreds : jsonList) {
String type = JsonUtil.getString(callCreds, "type");
if (type == null) {
continue;
}

XdsCallCredentialsProvider provider = XdsCallCredentialsRegistry.getDefaultRegistry()
.getProvider(type);
if (provider == null) {
continue;
}

Map<String, ?> config = JsonUtil.getObject(callCreds, "config");
if (config == null) {
config = ImmutableMap.of();
}
CallCredentials parsedCallCredentials = provider.newCallCredentials(config);
if (parsedCallCredentials == null) {
throw new XdsInitializationException(
"Invalid bootstrap: server " + serverUri + " with invalid 'config' for " + type
+ " 'call_creds'");
}

callCredentials = (callCredentials == null)
? parsedCallCredentials
: new CompositeCallCredentials(callCredentials, parsedCallCredentials);
}
return callCredentials;
}
}
66 changes: 66 additions & 0 deletions xds/src/main/java/io/grpc/xds/JwtTokenFileCallCredentials.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2025 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc.xds;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.OAuth2Credentials;
import com.google.common.io.Files;
import io.grpc.CallCredentials;
import io.grpc.auth.MoreCallCredentials;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Date;

/**
* JWT token file call credentials.
* See gRFC A97 (https://github.com/grpc/proposal/pull/492).
*/
public final class JwtTokenFileCallCredentials extends OAuth2Credentials {
private static final long serialVersionUID = 0L;
private final String path;

private JwtTokenFileCallCredentials(String path) {
this.path = checkNotNull(path, "path");
}

@Override
public AccessToken refreshAccessToken() throws IOException {
String tokenString = new String(Files.toByteArray(new File(path)), StandardCharsets.UTF_8);
Long expTime = JsonWebSignature.parse(new GsonFactory(), tokenString)
.getPayload()
.getExpirationTimeSeconds();
if (expTime == null) {
throw new IOException("No expiration time found for JWT token");
}

return AccessToken.newBuilder()
.setTokenValue(tokenString)
.setExpirationTime(new Date(expTime * 1000L))
.build();
}

// using {@link MoreCallCredentials} adapter to be compatible with {@link CallCredentials} iface
public static CallCredentials create(String path) {
JwtTokenFileCallCredentials jwtTokenFileCallCredentials = new JwtTokenFileCallCredentials(path);
return MoreCallCredentials.from(jwtTokenFileCallCredentials);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2025 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc.xds;

import io.grpc.CallCredentials;
import io.grpc.internal.JsonUtil;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* A wrapper class that supports {@link JwtTokenFileXdsCallCredentialsProvider} for
* xDS by implementing {@link XdsCredentialsProvider}.
*/
public final class JwtTokenFileXdsCallCredentialsProvider extends XdsCallCredentialsProvider {
private static final Logger logger = Logger.getLogger(
JwtTokenFileXdsCallCredentialsProvider.class.getName());
private static final String CREDS_NAME = "jwt_token_file";

@Override
protected CallCredentials newCallCredentials(Map<String, ?> jsonConfig) {
if (jsonConfig == null) {
return null;
}

String jwtTokenPath = JsonUtil.getString(jsonConfig, getName());
if (jwtTokenPath == null) {
logger.log(Level.WARNING, "jwt_token_file credential requires jwt_token_file in the config");
return null;
}

return JwtTokenFileCallCredentials.create(jwtTokenPath);
}

@Override
protected String getName() {
return CREDS_NAME;
}
}
47 changes: 47 additions & 0 deletions xds/src/main/java/io/grpc/xds/XdsCallCredentialsProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2025 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc.xds;

import io.grpc.CallCredentials;
import io.grpc.Internal;
import java.util.Map;

/**
* Provider of credentials data which will be propagated to the server for each RPC. The actual call
* credential to be used for a particular xDS communication will be chosen based on the bootstrap
* configuration.
*/
@Internal
public abstract class XdsCallCredentialsProvider {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not wild about io.grpc.xds.internal (JwtTokenFileCallCredentials) depending on io.grpc.xds (XdsCallCredentialsProvider) which depends on io.grpc.xds.internal (XdsCallCredentialsRegistry). Can we move this class to internal, or the JWT stuff to io.grpc.xds?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved JWT classes to io.grpc.xds package

/**
* Creates a {@link CallCredentials} from the given jsonConfig, or
* {@code null} if the given config is invalid. The provider is free to ignore
* the config if it's not needed for producing the call credentials.
*
* @param jsonConfig json config that can be consumed by the provider to create
* the call credentials
*
*/
protected abstract CallCredentials newCallCredentials(Map<String, ?> jsonConfig);

/**
* Returns the xDS call credential name associated with this provider which makes it selectable
* via {@link XdsCallCredentialsRegistry#getProvider}. This is called only when the class is
* loaded. It shouldn't change, and there is no point doing so.
*/
protected abstract String getName();
}
75 changes: 75 additions & 0 deletions xds/src/main/java/io/grpc/xds/XdsCallCredentialsRegistry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2025 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc.xds;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;

/**
* Registry of {@link XdsCallCredentialsProvider}s. The {@link #getDefaultRegistry default
* instance} loads hardcoded providers at runtime.
*/
@ThreadSafe
final class XdsCallCredentialsRegistry {
private static XdsCallCredentialsRegistry instance;

private final Map<String, XdsCallCredentialsProvider> registeredProviders =
new HashMap<>();

/**
* Returns the default registry that loads hardcoded providers at runtime.
*/
public static synchronized XdsCallCredentialsRegistry getDefaultRegistry() {
if (instance == null) {
instance = newRegistry().register(new JwtTokenFileXdsCallCredentialsProvider());
}
return instance;
}

@VisibleForTesting
static XdsCallCredentialsRegistry newRegistry() {
return new XdsCallCredentialsRegistry();
}

@VisibleForTesting
XdsCallCredentialsRegistry register(XdsCallCredentialsProvider... providers) {
for (XdsCallCredentialsProvider provider : providers) {
registeredProviders.put(provider.getName(), provider);
}
return this;
}

@VisibleForTesting
synchronized Map<String, XdsCallCredentialsProvider> providers() {
return registeredProviders;
}

/**
* Returns the registered provider for the given xDS call credential name, or {@code null} if no
* suitable provider can be found.
* Each provider declares its name via {@link XdsCallCredentialsProvider#getName}.
*/
@Nullable
public synchronized XdsCallCredentialsProvider getProvider(String name) {
return registeredProviders.get(checkNotNull(name, "name"));
}
}
Loading