Skip to content

Commit 1f112eb

Browse files
committed
GODRIVER-216 Add SCRAM-SHA-256 and negotiation testing
Change-Id: Ie0fbcad1c6c3b4253a29735dd764b635ad507ac6
1 parent 9b26cbb commit 1f112eb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+23276
-13
lines changed

core/auth/auth.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var authFactories = make(map[string]AuthenticatorFactory)
2525
func init() {
2626
RegisterAuthenticatorFactory("", newDefaultAuthenticator)
2727
RegisterAuthenticatorFactory(SCRAMSHA1, newScramSHA1Authenticator)
28+
RegisterAuthenticatorFactory(SCRAMSHA256, newScramSHA256Authenticator)
2829
RegisterAuthenticatorFactory(MONGODBCR, newMongoDBCRAuthenticator)
2930
RegisterAuthenticatorFactory(PLAIN, newPlainAuthenticator)
3031
RegisterAuthenticatorFactory(GSSAPI, newGSSAPIAuthenticator)
@@ -90,19 +91,23 @@ func RegisterAuthenticatorFactory(name string, factory AuthenticatorFactory) {
9091
// })
9192
// }
9293

93-
// HandshakeOptions packages options that can be passed to the Handshaker() function
94+
// HandshakeOptions packages options that can be passed to the Handshaker()
95+
// function. DBUser is optional but must be of the form <dbname.username>;
96+
// if non-empty, then the connection will do SASL mechanism negotiation.
9497
type HandshakeOptions struct {
9598
AppName string
9699
Authenticator Authenticator
97100
Compressors []string
101+
DBUser string
98102
}
99103

100104
// Handshaker creates a connection handshaker for the given authenticator.
101105
func Handshaker(h connection.Handshaker, options *HandshakeOptions) connection.Handshaker {
102106
return connection.HandshakerFunc(func(ctx context.Context, addr address.Address, rw wiremessage.ReadWriter) (description.Server, error) {
103107
desc, err := (&command.Handshake{
104-
Client: command.ClientDoc(options.AppName),
105-
Compressors: options.Compressors,
108+
Client: command.ClientDoc(options.AppName),
109+
Compressors: options.Compressors,
110+
SaslSupportedMechs: options.DBUser,
106111
}).Handshake(ctx, addr, rw)
107112

108113
if err != nil {

core/auth/auth_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func TestCreateAuthenticator(t *testing.T) {
2626
}{
2727
{name: "", auther: &DefaultAuthenticator{}},
2828
{name: "SCRAM-SHA-1", auther: &ScramSHA1Authenticator{}},
29+
{name: "SCRAM-SHA-256", auther: &ScramSHA256Authenticator{}},
2930
{name: "MONGODB-CR", auther: &MongoDBCRAuthenticator{}},
3031
{name: "PLAIN", auther: &PlainAuthenticator{}},
3132
{name: "MONGODB-X509", auther: &MongoDBX509Authenticator{}},

core/auth/default.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,14 @@ type DefaultAuthenticator struct {
2929
func (a *DefaultAuthenticator) Auth(ctx context.Context, desc description.Server, rw wiremessage.ReadWriter) error {
3030
var actual Authenticator
3131
var err error
32-
if err = description.ScramSHA1Supported(desc.WireVersion); err != nil {
33-
actual, err = newMongoDBCRAuthenticator(a.Cred)
34-
} else {
32+
33+
switch chooseAuthMechanism(desc) {
34+
case SCRAMSHA256:
35+
actual, err = newScramSHA256Authenticator(a.Cred)
36+
case SCRAMSHA1:
3537
actual, err = newScramSHA1Authenticator(a.Cred)
38+
default:
39+
actual, err = newMongoDBCRAuthenticator(a.Cred)
3640
}
3741

3842
if err != nil {
@@ -41,3 +45,23 @@ func (a *DefaultAuthenticator) Auth(ctx context.Context, desc description.Server
4145

4246
return actual.Auth(ctx, desc, rw)
4347
}
48+
49+
// If a server provides a list of supported mechanisms, we choose
50+
// SCRAM-SHA-256 if it exists or else MUST use SCRAM-SHA-1.
51+
// Otherwise, we decide based on what is supported.
52+
func chooseAuthMechanism(desc description.Server) string {
53+
if desc.SaslSupportedMechs != nil {
54+
for _, v := range desc.SaslSupportedMechs {
55+
if v == SCRAMSHA256 {
56+
return v
57+
}
58+
}
59+
return SCRAMSHA1
60+
}
61+
62+
if err := description.ScramSHA1Supported(desc.WireVersion); err == nil {
63+
return SCRAMSHA1
64+
}
65+
66+
return MONGODBCR
67+
}

core/auth/scramsha256.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (C) MongoDB, Inc. 2018-present.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
// not use this file except in compliance with the License. You may obtain
5+
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
package auth
8+
9+
import (
10+
"context"
11+
"fmt"
12+
13+
"github.com/mongodb/mongo-go-driver/core/description"
14+
"github.com/mongodb/mongo-go-driver/core/wiremessage"
15+
"github.com/xdg/scram"
16+
"github.com/xdg/stringprep"
17+
)
18+
19+
// SCRAMSHA256 is the mechanism name for SCRAM-SHA-256.
20+
const SCRAMSHA256 = "SCRAM-SHA-256"
21+
22+
func newScramSHA256Authenticator(cred *Cred) (Authenticator, error) {
23+
passprep, err := stringprep.SASLprep.Prepare(cred.Password)
24+
if err != nil {
25+
return nil, newAuthError(fmt.Sprintf("error SASLprepping password '%s'", cred.Password), err)
26+
}
27+
client, err := scram.SHA256.NewClientUnprepped(cred.Username, passprep, "")
28+
if err != nil {
29+
return nil, newAuthError("error initializing SCRAM-SHA-256 client", err)
30+
}
31+
client.WithMinIterations(4096)
32+
return &ScramSHA256Authenticator{
33+
DB: cred.Source,
34+
client: client,
35+
}, nil
36+
}
37+
38+
// ScramSHA256Authenticator uses the SCRAM-SHA-256 algorithm over SASL to authenticate a connection.
39+
type ScramSHA256Authenticator struct {
40+
DB string
41+
client *scram.Client
42+
}
43+
44+
// Auth authenticates the connection.
45+
func (a *ScramSHA256Authenticator) Auth(ctx context.Context, desc description.Server, rw wiremessage.ReadWriter) error {
46+
adapter := &scramSaslAdapter{conversation: a.client.NewConversation()}
47+
err := ConductSaslConversation(ctx, desc, rw, a.DB, adapter)
48+
if err != nil {
49+
return newAuthError("sasl conversation error", err)
50+
}
51+
return nil
52+
}
53+
54+
type scramSaslAdapter struct {
55+
conversation *scram.ClientConversation
56+
}
57+
58+
func (a *scramSaslAdapter) Start() (string, []byte, error) {
59+
step, err := a.conversation.Step("")
60+
if err != nil {
61+
return SCRAMSHA256, nil, err
62+
}
63+
return SCRAMSHA256, []byte(step), nil
64+
}
65+
66+
func (a *scramSaslAdapter) Next(challenge []byte) ([]byte, error) {
67+
step, err := a.conversation.Step(string(challenge))
68+
if err != nil {
69+
return nil, err
70+
}
71+
return []byte(step), nil
72+
}
73+
74+
func (a *scramSaslAdapter) Completed() bool {
75+
return a.conversation.Done()
76+
}

core/command/handshake.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ import (
2323
//
2424
// The isMaster and buildInfo commands are used to build a server description.
2525
type Handshake struct {
26-
Client *bson.Document
27-
Compressors []string
26+
Client *bson.Document
27+
Compressors []string
28+
SaslSupportedMechs string
2829

2930
ismstr result.IsMaster
3031
err error
@@ -33,7 +34,11 @@ type Handshake struct {
3334
// Encode will encode the handshake commands into a wire message containing isMaster
3435
func (h *Handshake) Encode() (wiremessage.WireMessage, error) {
3536
var wm wiremessage.WireMessage
36-
ismstr, err := (&IsMaster{Client: h.Client, Compressors: h.Compressors}).Encode()
37+
ismstr, err := (&IsMaster{
38+
Client: h.Client,
39+
Compressors: h.Compressors,
40+
SaslSupportedMechs: h.SaslSupportedMechs,
41+
}).Encode()
3742
if err != nil {
3843
return wm, err
3944
}

core/command/ismaster.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ import (
2222
//
2323
// Since IsMaster can only be run on a connection, there is no Dispatch method.
2424
type IsMaster struct {
25-
Client *bson.Document
26-
Compressors []string
25+
Client *bson.Document
26+
Compressors []string
27+
SaslSupportedMechs string
2728

2829
err error
2930
res result.IsMaster
@@ -35,6 +36,9 @@ func (im *IsMaster) Encode() (wiremessage.WireMessage, error) {
3536
if im.Client != nil {
3637
cmd.Append(bson.EC.SubDocument("client", im.Client))
3738
}
39+
if im.SaslSupportedMechs != "" {
40+
cmd.Append(bson.EC.String("saslSupportedMechs", im.SaslSupportedMechs))
41+
}
3842

3943
// always send compressors even if empty slice
4044
array := bson.NewArray()

core/connstring/connstring_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func TestAuthMechanism(t *testing.T) {
4848
err bool
4949
}{
5050
{s: "authMechanism=scram-sha-1", expected: "scram-sha-1"},
51+
{s: "authMechanism=scram-sha-256", expected: "scram-sha-256"},
5152
{s: "authMechanism=mongodb-CR", expected: "mongodb-CR"},
5253
{s: "authMechanism=plain", expected: "plain"},
5354
}

core/description/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ type Server struct {
5050
Tags tag.Set
5151
Kind ServerKind
5252
WireVersion *VersionRange
53+
54+
SaslSupportedMechs []string // user-specific from server handshake
5355
}
5456

5557
// NewServer creates a new server description from the given parameters.
@@ -65,6 +67,7 @@ func NewServer(addr address.Address, isMaster result.IsMaster) Server {
6567
MaxBatchCount: isMaster.MaxWriteBatchSize,
6668
MaxDocumentSize: isMaster.MaxBSONObjectSize,
6769
MaxMessageSize: isMaster.MaxMessageSizeBytes,
70+
SaslSupportedMechs: isMaster.SaslSupportedMechs,
6871
SessionTimeoutMinutes: isMaster.LogicalSessionTimeoutMinutes,
6972
SetName: isMaster.SetName,
7073
SetVersion: isMaster.SetVersion,

core/integration/main_test.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"sync"
1717
"testing"
1818

19+
"github.com/mongodb/mongo-go-driver/core/auth"
1920
"github.com/mongodb/mongo-go-driver/core/connection"
2021
"github.com/mongodb/mongo-go-driver/core/connstring"
2122
)
@@ -52,11 +53,21 @@ func TestMain(m *testing.M) {
5253
func noerr(t *testing.T, err error) {
5354
if err != nil {
5455
t.Helper()
55-
t.Errorf("Unepexted error: %v", err)
56+
t.Errorf("Unexpected error: %v", err)
5657
t.FailNow()
5758
}
5859
}
5960

61+
func autherr(t *testing.T, err error) {
62+
t.Helper()
63+
switch err.(type) {
64+
case *auth.Error:
65+
return
66+
default:
67+
t.Fatal("Expected auth error and didn't get one")
68+
}
69+
}
70+
6071
// addTLSConfigToURI checks for the environmental variable indicating that the tests are being run
6172
// on an SSL-enabled server, and if so, returns a new URI with the necessary configuration.
6273
func addTLSConfigToURI(uri string) string {

0 commit comments

Comments
 (0)