Skip to content

Commit 1c073b8

Browse files
authored
feat(spanner): implement OPAQUE login authentication for Spanner Omni (#20085)
### ⚠️ Note to Reviewers: > Of the ~3k lines changed in this PR, approximately ~1.3k lines are auto-generated protobuf classes. Specifically, the files `spanner/omni/authentication.pb.go` and `spanner/omni/login.pb.go` 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 (`omni/authentication.pb.go`, `omni/login.pb.go`) to handle the authentication handshake with Omni endpoints. * **OPAQUE Protocol Support**: Implemented [userAuthenticator](file:///usr/local/google/home/sagnikghosh/Documents/go/google-cloud-go/spanner/omni/opaque.go) to handle the secure OPAQUE cryptographic login flow, using timing-safe `crypto/subtle` checks for MAC and auth tag verification. * **omniTokenSource**: Added a new [omniTokenSource](file:///usr/local/google/home/sagnikghosh/Documents/go/google-cloud-go/spanner/omni/connection.go) (`oauth2.TokenSource` implementation) to dial Omni endpoints, perform authentication handshakes, and cache the returned OAuth2 token payload. * **Client Integration:** Updated [ClientConfig](file:///usr/local/google/home/sagnikghosh/Documents/go/google-cloud-go/spanner/client.go) and [OmniClientConfig](file:///usr/local/google/home/sagnikghosh/Documents/go/google-cloud-go/spanner/admin/database/apiv1/omni_client.go) to accept `Username` and `Password` credentials, automatically injecting the `omniTokenSource` as a client option. To run Integration Tests with auth login, run: ```bash go test -v -parallel 1 -run ^TestIntegration_ . -args \ -it.omni-endpoint="localhost:15000" \ -it.omni-ca-cert="/path/to/ca.crt" \ -it.omni-username="admin" \ -it.omni-password="admin" ```
1 parent a3a9bab commit 1c073b8

10 files changed

Lines changed: 3286 additions & 11 deletions

File tree

spanner/admin/database/apiv1/omni_client.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ type OmniClientConfig interface {
3535
GetCaCertificateFile() string
3636
GetClientCertificateFile() string
3737
GetClientKeyFile() string
38+
GetUsername() string
39+
GetPassword() []byte
3840
}
3941

4042
// NewDatabaseAdminClientWithConfig creates a new database admin client with
@@ -55,6 +57,18 @@ func NewDatabaseAdminClientWithConfig(ctx context.Context, config OmniClientConf
5557
return nil, err
5658
}
5759
opts = append(opts, omniOpts...)
60+
61+
hasUsername := config.GetUsername() != ""
62+
hasPassword := len(config.GetPassword()) > 0
63+
if hasUsername != hasPassword {
64+
return nil, status.Errorf(codes.InvalidArgument, "both Username and Password must be specified for Omni authentication")
65+
}
66+
if hasUsername && hasPassword {
67+
tsOpts := append([]option.ClientOption(nil), opts...)
68+
opts = append(opts, option.WithTokenSource(omni.NewTokenSource(ctx, config.GetUsername(), config.GetPassword(), tsOpts)))
69+
} else {
70+
opts = append(opts, option.WithoutAuthentication())
71+
}
5872
}
5973

6074
return NewDatabaseAdminClient(ctx, opts...)

spanner/client.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,14 @@ type ClientConfig struct {
390390
// If unspecified, it defaults to CLOUD.
391391
Type InstanceType
392392

393+
// Username is the username for logging into Spanner Omni.
394+
// Note: This field is only applicable when Type is OMNI.
395+
Username string
396+
397+
// Password is the password for logging into Spanner Omni.
398+
// Note: This field is only applicable when Type is OMNI.
399+
Password []byte
400+
393401
// UsePlainText specifies whether to use plain text for the connection.
394402
UsePlainText bool
395403

@@ -444,6 +452,16 @@ func (c ClientConfig) GetUsePlainText() bool {
444452
return c.UsePlainText
445453
}
446454

455+
// GetUsername returns the username for OPAQUE login.
456+
func (c ClientConfig) GetUsername() string {
457+
return c.Username
458+
}
459+
460+
// GetPassword returns the password for OPAQUE login.
461+
func (c ClientConfig) GetPassword() []byte {
462+
return c.Password
463+
}
464+
447465
// GetCaCertificateFile returns the CA certificate file path.
448466
func (c ClientConfig) GetCaCertificateFile() string {
449467
return c.CaCertificateFile
@@ -624,6 +642,18 @@ func newClientWithConfig(ctx context.Context, database string, config ClientConf
624642
return nil, err
625643
}
626644
opts = append(opts, omniOpts...)
645+
646+
hasUsername := config.Username != ""
647+
hasPassword := len(config.Password) > 0
648+
if hasUsername != hasPassword {
649+
return nil, spannerErrorf(codes.InvalidArgument, "both Username and Password must be specified for Omni authentication")
650+
}
651+
if hasUsername && hasPassword {
652+
tsOpts := append([]option.ClientOption(nil), opts...)
653+
opts = append(opts, option.WithTokenSource(omni.NewTokenSource(ctx, config.Username, config.Password, tsOpts)))
654+
} else {
655+
opts = append(opts, option.WithoutAuthentication())
656+
}
627657
}
628658

629659
// Validate database path.

spanner/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require (
2222
go.opentelemetry.io/otel/sdk v1.44.0
2323
go.opentelemetry.io/otel/sdk/metric v1.44.0
2424
go.opentelemetry.io/otel/trace v1.44.0
25+
golang.org/x/crypto v0.53.0
2526
golang.org/x/oauth2 v0.36.0
2627
golang.org/x/sync v0.21.0
2728
google.golang.org/api v0.287.1
@@ -55,7 +56,6 @@ require (
5556
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
5657
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
5758
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
58-
golang.org/x/crypto v0.53.0 // indirect
5959
golang.org/x/net v0.56.0 // indirect
6060
golang.org/x/sys v0.46.0 // indirect
6161
golang.org/x/text v0.38.0 // indirect

spanner/integration_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,8 @@ func init() {
411411
flag.StringVar(&omniConfig.caCertificateFile, "it.omni-ca-cert", "", "Path to CA certificate file for Spanner Omni connection")
412412
flag.StringVar(&omniConfig.clientCertificateFile, "it.omni-client-cert", "", "Path to client certificate file for Spanner Omni connection")
413413
flag.StringVar(&omniConfig.clientKeyFile, "it.omni-client-key", "", "Path to client key file for Spanner Omni connection")
414+
flag.StringVar(&omniConfig.username, "it.omni-username", "", "Username for Spanner Omni connection")
415+
flag.StringVar(&omniConfig.password, "it.omni-password", "", "Password for Spanner Omni connection")
414416
}
415417

416418
type directPathTestConfig struct {
@@ -425,6 +427,8 @@ type spannerOmniTestConfig struct {
425427
caCertificateFile string
426428
clientCertificateFile string
427429
clientKeyFile string
430+
username string
431+
password string
428432
}
429433

430434
func omniConnectionConfig() ClientConfig {
@@ -438,6 +442,8 @@ func omniConnectionConfig() ClientConfig {
438442
CaCertificateFile: omniConfig.caCertificateFile,
439443
ClientCertificateFile: omniConfig.clientCertificateFile,
440444
ClientKeyFile: omniConfig.clientKeyFile,
445+
Username: omniConfig.username,
446+
Password: []byte(omniConfig.password),
441447
}
442448
}
443449

@@ -5639,6 +5645,7 @@ func TestIntegration_Foreign_Key_Delete_Cascade_Action(t *testing.T) {
56395645

56405646
func TestIntegration_GFE_Latency(t *testing.T) {
56415647
skipDirectPathTest(t)
5648+
skipSpannerOmniTest(t)
56425649
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
56435650
defer cancel()
56445651

@@ -6402,6 +6409,8 @@ func createClient(ctx context.Context, dbPath string, config ClientConfig) (clie
64026409
config.CaCertificateFile = omniConf.CaCertificateFile
64036410
config.ClientCertificateFile = omniConf.ClientCertificateFile
64046411
config.ClientKeyFile = omniConf.ClientKeyFile
6412+
config.Username = omniConf.Username
6413+
config.Password = omniConf.Password
64056414
}
64066415
client, err = makeClientWithConfig(ctx, dbPath, config, serverAddress, opts...)
64076416
if err != nil {
@@ -6429,6 +6438,8 @@ func createClientWithRole(ctx context.Context, dbPath string, spc SessionPoolCon
64296438
config.CaCertificateFile = omniConf.CaCertificateFile
64306439
config.ClientCertificateFile = omniConf.ClientCertificateFile
64316440
config.ClientKeyFile = omniConf.ClientKeyFile
6441+
config.Username = omniConf.Username
6442+
config.Password = omniConf.Password
64326443
}
64336444
client, err = makeClientWithConfig(ctx, dbPath, config, serverAddress, opts...)
64346445
if err != nil {
@@ -6455,6 +6466,8 @@ func createClientForProtoColumns(ctx context.Context, dbPath string, spc Session
64556466
config.CaCertificateFile = omniConf.CaCertificateFile
64566467
config.ClientCertificateFile = omniConf.ClientCertificateFile
64576468
config.ClientKeyFile = omniConf.ClientKeyFile
6469+
config.Username = omniConf.Username
6470+
config.Password = omniConf.Password
64586471
}
64596472
client, err = NewClientWithConfig(ctx, dbPath, config, opts...)
64606473
if err != nil {

0 commit comments

Comments
 (0)