Skip to content

[xDS] A97 - JWT token file call creds #12242

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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 xds/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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
64 changes: 63 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,10 @@

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.internal.GrpcUtil;
import io.grpc.internal.JsonUtil;
import io.grpc.xds.client.BootstrapperImpl;
import io.grpc.xds.client.XdsInitializationException;
Expand All @@ -33,6 +36,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 +46,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 @@ -90,7 +98,7 @@ protected String getJsonContent() throws XdsInitializationException, IOException
}

@Override
protected Object getImplSpecificConfig(Map<String, ?> serverConfig, String serverUri)
protected Object getImplSpecificChannelCredConfig(Map<String, ?> serverConfig, String serverUri)
throws XdsInitializationException {
return getChannelCredentials(serverConfig, serverUri);
}
Expand Down Expand Up @@ -135,4 +143,58 @@ private static ChannelCredentials parseChannelCredentials(List<Map<String, ?>> j
}
return null;
}

@Override
protected Object getImplSpecificCallCredConfig(Map<String, ?> serverConfig, String serverUri)
throws XdsInitializationException {
return getCallCredentials(serverConfig, serverUri);
}

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 {
CallCredentials callCredentials = null;
if (xdsBootstrapCallCredsEnabled) {
for (Map<String, ?> callCreds : jsonList) {
String type = JsonUtil.getString(callCreds, "type");
if (type != null) {
XdsCredentialsProvider provider = XdsCredentialsRegistry.getDefaultRegistry()
.getProvider(type);
if (provider != null) {
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'");
}

if (callCredentials == null) {
callCredentials = parsedCallCredentials;
} else {
callCredentials = new CompositeCallCredentials(
callCredentials, parsedCallCredentials);
}
}
}
}
}
return callCredentials;
}
}
20 changes: 17 additions & 3 deletions xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import io.grpc.CallOptions;
import io.grpc.ChannelCredentials;
import io.grpc.ClientCall;
import io.grpc.CompositeCallCredentials;
import io.grpc.CompositeChannelCredentials;
import io.grpc.Context;
import io.grpc.Grpc;
import io.grpc.ManagedChannel;
Expand Down Expand Up @@ -68,11 +70,23 @@ public GrpcXdsTransport(ManagedChannel channel) {

public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo, CallCredentials callCredentials) {
String target = serverInfo.target();
ChannelCredentials channelCredentials = (ChannelCredentials) serverInfo.implSpecificConfig();
this.channel = Grpc.newChannelBuilder(target, channelCredentials)
Object implSpecificConfig = serverInfo.implSpecificConfig();

this.channel = Grpc.newChannelBuilder(target, (ChannelCredentials) implSpecificConfig)
.keepAliveTime(5, TimeUnit.MINUTES)
.build();
this.callCredentials = callCredentials;

if (callCredentials != null && implSpecificConfig instanceof CompositeChannelCredentials) {
this.callCredentials =
new CompositeCallCredentials(
callCredentials,
((CompositeChannelCredentials) implSpecificConfig).getCallCredentials());
} else if (implSpecificConfig instanceof CompositeChannelCredentials) {
this.callCredentials =
((CompositeChannelCredentials) implSpecificConfig).getCallCredentials();
} else {
this.callCredentials = callCredentials;
}
}

@VisibleForTesting
Expand Down
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);
}
}
12 changes: 12 additions & 0 deletions xds/src/main/java/io/grpc/xds/XdsCredentialsProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.grpc.xds;

import io.grpc.CallCredentials;
import io.grpc.ChannelCredentials;
import io.grpc.Internal;
import java.util.Map;
Expand Down Expand Up @@ -49,6 +50,17 @@ public abstract class XdsCredentialsProvider {
*/
protected abstract ChannelCredentials newChannelCredentials(Map<String, ?> jsonConfig);

/**
* 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 credential name associated with this provider which makes it selectable
* via {@link XdsCredentialsRegistry#getProvider}. This is called only when the class is loaded.
Expand Down
10 changes: 8 additions & 2 deletions xds/src/main/java/io/grpc/xds/XdsCredentialsRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public static synchronized XdsCredentialsRegistry getDefaultRegistry() {
new XdsCredentialsProviderPriorityAccessor());
if (providerList.isEmpty()) {
logger.warning("No XdsCredsRegistry found via ServiceLoader, including for GoogleDefault, "
+ "TLS and Insecure. This is probably due to a broken build.");
+ "TLS, Insecure and JWT token file. This is probably due to a broken build.");
}
instance = new XdsCredentialsRegistry();
for (XdsCredentialsProvider provider : providerList) {
Expand Down Expand Up @@ -170,7 +170,13 @@ static List<Class<?>> getHardCodedClasses() {
} catch (ClassNotFoundException e) {
logger.log(Level.WARNING, "Unable to find TlsXdsCredentialsProvider", e);
}


try {
list.add(Class.forName("io.grpc.xds.internal.JwtTokenFileXdsCredentialsProvider"));
} catch (ClassNotFoundException e) {
logger.log(Level.WARNING, "Unable to find JwtTokenFileXdsCredentialsProvider", e);
}

return Collections.unmodifiableList(list);
}

Expand Down
28 changes: 22 additions & 6 deletions xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.grpc.CallCredentials;
import io.grpc.ChannelCredentials;
import io.grpc.CompositeChannelCredentials;
import io.grpc.Internal;
import io.grpc.InternalLogId;
import io.grpc.internal.GrpcUtil;
Expand Down Expand Up @@ -76,9 +79,13 @@ protected BootstrapperImpl() {

protected abstract String getJsonContent() throws IOException, XdsInitializationException;

protected abstract Object getImplSpecificConfig(Map<String, ?> serverConfig, String serverUri)
protected abstract Object getImplSpecificChannelCredConfig(
Map<String, ?> serverConfig, String serverUri)
throws XdsInitializationException;

protected abstract Object getImplSpecificCallCredConfig(
Map<String, ?> serverConfig, String serverUri)
throws XdsInitializationException;

/**
* Reads and parses bootstrap config. The config is expected to be in JSON format.
Expand Down Expand Up @@ -253,7 +260,9 @@ private List<ServerInfo> parseServerInfos(List<?> rawServerConfigs, XdsLogger lo
}
logger.log(XdsLogLevel.INFO, "xDS server URI: {0}", serverUri);

Object implSpecificConfig = getImplSpecificConfig(serverConfig, serverUri);
Object implSpecificChannelCredConfig =
getImplSpecificChannelCredConfig(serverConfig, serverUri);
Object implSpecificCallCredConfig = getImplSpecificCallCredConfig(serverConfig, serverUri);

boolean resourceTimerIsTransientError = false;
boolean ignoreResourceDeletion = false;
Expand All @@ -267,10 +276,17 @@ private List<ServerInfo> parseServerInfos(List<?> rawServerConfigs, XdsLogger lo
&& serverFeatures.contains(SERVER_FEATURE_RESOURCE_TIMER_IS_TRANSIENT_ERROR);
}
servers.add(
ServerInfo.create(serverUri, implSpecificConfig, ignoreResourceDeletion,
serverFeatures != null
&& serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER),
resourceTimerIsTransientError));
ServerInfo.create(
serverUri,
(implSpecificCallCredConfig != null)
? CompositeChannelCredentials.create(
(ChannelCredentials) implSpecificChannelCredConfig,
(CallCredentials) implSpecificCallCredConfig)
: implSpecificChannelCredConfig,
ignoreResourceDeletion,
serverFeatures != null
&& serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER),
resourceTimerIsTransientError));
}
return servers.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.grpc.xds.internal;

import io.grpc.CallCredentials;
import io.grpc.ChannelCredentials;
import io.grpc.alts.GoogleDefaultChannelCredentials;
import io.grpc.xds.XdsCredentialsProvider;
Expand All @@ -33,6 +34,11 @@ protected ChannelCredentials newChannelCredentials(Map<String, ?> jsonConfig) {
return GoogleDefaultChannelCredentials.create();
}

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

@Override
protected String getName() {
return CREDS_NAME;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.grpc.xds.internal;

import io.grpc.CallCredentials;
import io.grpc.ChannelCredentials;
import io.grpc.InsecureChannelCredentials;
import io.grpc.xds.XdsCredentialsProvider;
Expand All @@ -33,6 +34,11 @@ protected ChannelCredentials newChannelCredentials(Map<String, ?> jsonConfig) {
return InsecureChannelCredentials.create();
}

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

@Override
protected String getName() {
return CREDS_NAME;
Expand Down
Loading