Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 55930b4

Browse filesBrowse files
authored
feat(spanner): auth login support for Spanner Omni endpoints (#13470)
**⚠️ Note to Reviewers:** > **Of the ~14k lines changed in this PR, approximately ~12.5k lines are auto-generated protobuf and gRPC classes.** > Specifically, the files `Login.java`, `Authentication.java`, and `LoginServiceGrpc.java` account for the vast majority of the diff. > > The actual hand-written logic is small and contained within the core auth implementation and connection classes. This PR introduces native authentication support for Spanner Omni endpoints using the OPAQUE password-authenticated key exchange protocol. **Key Changes:** * **Omni Login Protocol:** Added generated protobufs (`Login.java`, `Authentication.java`, `LoginServiceGrpc.java`) and a gRPC `LoginClient` to handle the authentication handshake with Omni endpoints. * **LoginClient & OPAQUE Protocol**: Implements the `LoginClient` utilizing `OpaqueUtil` to perform the secure two-step OPAQUE authentication flow over gRPC. * **SpannerOmniCredentials**: A new credentials provider that manages Omni authentication tokens, incorporating automatic background token refresh mechanisms. * **Security Considerations**: Securely handles raw passwords using char arrays, ensuring sensitive credentials are zeroed out of memory buffers immediately after processing. * **Client Integration:** Updated `ConnectionOptions` and `SpannerOptions.Builder` with a new `login(username, password)` method to wire up Omni credentials. The channel initialization logic was moved to `prepareBuilder()` to ensure the builder pattern remains order-independent. To run Integration Tests with auth login run below command with default username/password ``` mvn clean -pl java-spanner/google-cloud-spanner -B verify \ -DskipUnitTests=true \ -DskipITs=false \ -Dspanner.omni.host=https://localhost:15000 \ -Dspanner.testenv.instance=projects/default/instances/default \ -Denforcer.skip=true \ -Dspanner.username=admin \ -Dspanner.password=admin ``` **Design Document:** [Spanner Omni Auth Login](https://docs.google.com/document/d/1hFxZmS1WYIW58qIVN2QO5kkfa8sXvxiLtgE395gEW60/edit?resourcekey=0-NLqNwe9NOCoJ1wnE671gTA&tab=t.0) ***
1 parent 6cf0567 commit 55930b4
Copy full SHA for 55930b4

16 files changed

+14,031-4Lines changed: 14031 additions & 4 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎java-spanner/google-cloud-spanner/pom.xml‎

Copy file name to clipboardExpand all lines: java-spanner/google-cloud-spanner/pom.xml
+9Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,15 @@
532532
<version>2.94.0-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-trace-v1:current} -->
533533
<scope>test</scope>
534534
</dependency>
535+
<dependency>
536+
<groupId>org.bouncycastle</groupId>
537+
<artifactId>bcprov-jdk18on</artifactId>
538+
</dependency>
539+
<dependency>
540+
<groupId>com.google.crypto.tink</groupId>
541+
<artifactId>tink</artifactId>
542+
<version>1.13.0</version>
543+
</dependency>
535544
</dependencies>
536545
<profiles>
537546
<profile>
Collapse file

‎java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java‎

Copy file name to clipboardExpand all lines: java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java
+42-2Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings;
5454
import com.google.cloud.spanner.admin.instance.v1.InstanceAdminSettings;
5555
import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings;
56+
import com.google.cloud.spanner.omni.SpannerOmniCredentials;
5657
import com.google.cloud.spanner.spi.SpannerRpcFactory;
5758
import com.google.cloud.spanner.spi.v1.ChannelEndpointCacheFactory;
5859
import com.google.cloud.spanner.spi.v1.GapicSpannerRpc;
@@ -66,6 +67,7 @@
6667
import com.google.common.collect.ImmutableMap;
6768
import com.google.common.collect.ImmutableSet;
6869
import com.google.common.util.concurrent.ThreadFactoryBuilder;
70+
import com.google.crypto.tink.util.SecretBytes;
6971
import com.google.spanner.v1.DirectedReadOptions;
7072
import com.google.spanner.v1.ExecuteSqlRequest;
7173
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
@@ -1239,8 +1241,22 @@ private static Builder prepareBuilder(Builder builder) {
12391241
builder.sessionPoolOptions =
12401242
builder.sessionPoolOptions.toBuilder().setExperimentalHost().build();
12411243
}
1242-
if (builder.credentials == null) {
1243-
builder.setCredentials(environment.getDefaultSpannerOmniCredentials());
1244+
if (builder.username != null && builder.secretBytes != null) {
1245+
builder.setCredentials(
1246+
new SpannerOmniCredentials(builder.username, builder.secretBytes, builder.host));
1247+
} else if (builder.credentials == null) {
1248+
GoogleCredentials defaultCreds = environment.getDefaultSpannerOmniCredentials();
1249+
if (defaultCreds != null) {
1250+
builder.setCredentials(defaultCreds);
1251+
}
1252+
}
1253+
if (builder.credentials instanceof SpannerOmniCredentials) {
1254+
((SpannerOmniCredentials) builder.credentials)
1255+
.initChannel(builder.usePlainText, builder.mTLSContext);
1256+
}
1257+
} else {
1258+
if (builder.username != null || builder.secretBytes != null) {
1259+
throw new IllegalStateException("login() can only be used with InstanceType.OMNI.");
12441260
}
12451261
}
12461262
return builder;
@@ -1296,6 +1312,8 @@ private static Builder prepareBuilder(Builder builder) {
12961312
DEFAULT_ADMIN_REQUESTS_LIMIT_EXCEEDED_RETRY_SETTINGS;
12971313
private boolean autoThrottleAdministrativeRequests = false;
12981314
private boolean trackTransactionStarter = false;
1315+
private String username;
1316+
private SecretBytes secretBytes;
12991317
private Map<DatabaseId, QueryOptions> defaultQueryOptions = new HashMap<>();
13001318
private boolean enableGrpcGcpOtelMetrics =
13011319
SpannerOptions.environment.isEnableGrpcGcpOtelMetrics();
@@ -1910,6 +1928,28 @@ public Builder setType(InstanceType instanceType) {
19101928
return this;
19111929
}
19121930

1931+
/**
1932+
* Authenticates to Spanner Omni using the provided username and password, and configures the
1933+
* resulting token for use in subsequent Spanner API calls.
1934+
*
1935+
* <p>Note: The provided {@code password} array will be cleared (zeroed out) by this method for
1936+
* security purposes.
1937+
*
1938+
* @param username The username for login.
1939+
* @param password The password for login.
1940+
* @return this builder
1941+
*/
1942+
public Builder login(String username, char[] password) {
1943+
Preconditions.checkArgument(
1944+
username != null && !username.isEmpty(), "username cannot be null or empty");
1945+
Preconditions.checkArgument(
1946+
password != null && password.length > 0, "password cannot be null or empty");
1947+
1948+
this.username = username;
1949+
this.secretBytes = SpannerOmniCredentials.convertToSecretBytes(password);
1950+
return this;
1951+
}
1952+
19131953
/** Enables gRPC-GCP extension with the default settings. This option is enabled by default. */
19141954
public Builder enableGrpcGcpExtension() {
19151955
return this.enableGrpcGcpExtension(null);
Collapse file

‎java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java‎

Copy file name to clipboardExpand all lines: java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
+29-2Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,14 @@
8989
import com.google.cloud.spanner.SpannerOptions;
9090
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.GrpcInterceptorProviderConverter;
9191
import com.google.cloud.spanner.connection.StatementExecutor.StatementExecutorType;
92+
import com.google.cloud.spanner.omni.SpannerOmniCredentials;
9293
import com.google.common.annotations.VisibleForTesting;
9394
import com.google.common.base.MoreObjects;
9495
import com.google.common.base.Preconditions;
9596
import com.google.common.base.Strings;
9697
import com.google.common.base.Suppliers;
9798
import com.google.common.collect.ImmutableMap;
99+
import com.google.crypto.tink.util.SecretBytes;
98100
import io.grpc.Deadline;
99101
import io.grpc.Deadline.Ticker;
100102
import io.opentelemetry.api.OpenTelemetry;
@@ -154,6 +156,8 @@ public class ConnectionOptions {
154156
static final boolean DEFAULT_USE_PLAIN_TEXT = false;
155157
static final boolean DEFAULT_IS_EXPERIMENTAL_HOST = false;
156158
static final SpannerOptions.InstanceType DEFAULT_TYPE = SpannerOptions.InstanceType.CLOUD;
159+
static final String DEFAULT_USERNAME = "";
160+
static final String DEFAULT_PASSWORD = "";
157161
static final boolean DEFAULT_AUTOCOMMIT = true;
158162
static final boolean DEFAULT_READONLY = false;
159163
static final boolean DEFAULT_RETRY_ABORTS_INTERNALLY = true;
@@ -224,6 +228,12 @@ public class ConnectionOptions {
224228
/** The type of Spanner instance to connect to (cloud, omni, or emulator). */
225229
public static final String TYPE_PROPERTY_NAME = "type";
226230

231+
/** Username for OPAQUE login */
232+
public static final String USERNAME_PROPERTY_NAME = "username";
233+
234+
/** Password for OPAQUE login */
235+
public static final String PASSWORD_PROPERTY_NAME = "password";
236+
227237
/** Client certificate path to establish mTLS */
228238
static final String CLIENT_CERTIFICATE_PROPERTY_NAME = "clientCertificate";
229239

@@ -775,6 +785,8 @@ private ConnectionOptions(Builder builder) {
775785
System.getenv());
776786
GoogleCredentials defaultSpannerOmniCredentials =
777787
SpannerOptions.getDefaultSpannerOmniCredentialsFromSysEnv();
788+
String username = getInitialConnectionPropertyValue(ConnectionProperties.USERNAME);
789+
String password = getInitialConnectionPropertyValue(ConnectionProperties.PASSWORD);
778790
// Using credentials on a plain text connection is not allowed, so if the user has not specified
779791
// any credentials and is using a plain text connection, we should not try to get the
780792
// credentials from the environment, but default to NoCredentials.
@@ -783,14 +795,29 @@ && getInitialConnectionPropertyValue(CREDENTIALS_URL) == null
783795
&& getInitialConnectionPropertyValue(ENCODED_CREDENTIALS) == null
784796
&& getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) == null
785797
&& getInitialConnectionPropertyValue(OAUTH_TOKEN) == null
798+
&& Strings.isNullOrEmpty(getInitialConnectionPropertyValue(ConnectionProperties.USERNAME))
786799
&& usePlainText) {
787800
this.credentials = NoCredentials.getInstance();
788801
} else if (getInitialConnectionPropertyValue(OAUTH_TOKEN) != null) {
789802
this.credentials =
790803
new GoogleCredentials(
791804
new AccessToken(getInitialConnectionPropertyValue(OAUTH_TOKEN), null));
792-
} else if ((isSpannerOmniPattern || isSpannerOmni()) && defaultSpannerOmniCredentials != null) {
793-
this.credentials = defaultSpannerOmniCredentials;
805+
} else if (isSpannerOmniPattern || isSpannerOmni()) {
806+
if (!Strings.isNullOrEmpty(username) && !Strings.isNullOrEmpty(password)) {
807+
SecretBytes secretBytes =
808+
SpannerOmniCredentials.convertToSecretBytes(password.toCharArray());
809+
this.credentials = new SpannerOmniCredentials(username, secretBytes, this.host);
810+
// Clear the password from the initial connection state to allow it to be GC'd.
811+
this.initialConnectionState.setValue(
812+
ConnectionProperties.PASSWORD,
813+
DEFAULT_PASSWORD,
814+
ConnectionProperty.Context.STARTUP,
815+
/* inTransaction= */ false);
816+
} else if (defaultSpannerOmniCredentials != null) {
817+
this.credentials = defaultSpannerOmniCredentials;
818+
} else {
819+
this.credentials = NoCredentials.getInstance();
820+
}
794821
} else if (getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) != null) {
795822
try {
796823
this.credentials = getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER).getCredentials();
Collapse file

‎java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java‎

Copy file name to clipboardExpand all lines: java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java
+14Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,20 @@ public class ConnectionProperties {
279279
},
280280
InstanceTypeConverter.INSTANCE,
281281
Context.STARTUP);
282+
static final ConnectionProperty<String> USERNAME =
283+
create(
284+
ConnectionOptions.USERNAME_PROPERTY_NAME,
285+
"The username to use for OPAQUE login.",
286+
ConnectionOptions.DEFAULT_USERNAME,
287+
StringValueConverter.INSTANCE,
288+
Context.STARTUP);
289+
static final ConnectionProperty<String> PASSWORD =
290+
create(
291+
ConnectionOptions.PASSWORD_PROPERTY_NAME,
292+
"The password to use for OPAQUE login.",
293+
ConnectionOptions.DEFAULT_PASSWORD,
294+
StringValueConverter.INSTANCE,
295+
Context.STARTUP);
282296
static final ConnectionProperty<String> CLIENT_CERTIFICATE =
283297
create(
284298
CLIENT_CERTIFICATE_PROPERTY_NAME,

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.