connector/ldap: support the StartTLS flow for secure connections

When connecting to an LDAP server, there are three ways to connect:

1. Insecurely through port 389 (LDAP).
2. Securely through port 696 (LDAPS).
3. Insecurely through port 389 then negotiate TLS (StartTLS).

This PR adds support for the 3rd flow, letting dex connect to the
standard LDAP port then negotiating TLS through the LDAP protocol
itself.

See a writeup here:

http://www.openldap.org/faq/data/cache/185.html
This commit is contained in:
Eric Chiang
2017-04-12 14:13:34 -07:00
parent 9b0af83604
commit 74f5eaf47e
8 changed files with 334 additions and 27 deletions

View File

@@ -21,6 +21,15 @@ import (
const envVar = "DEX_LDAP_TESTS"
// connectionMethod indicates how the test should connect to the LDAP server.
type connectionMethod int32
const (
connectStartTLS connectionMethod = iota
connectLDAPS
connectLDAP
)
// subtest is a login test against a given schema.
type subtest struct {
// Name of the sub-test.
@@ -110,7 +119,7 @@ userpassword: bar
},
}
runTests(t, schema, c, tests)
runTests(t, schema, connectLDAP, c, tests)
}
func TestGroupQuery(t *testing.T) {
@@ -198,7 +207,7 @@ member: cn=jane,ou=People,dc=example,dc=org
},
}
runTests(t, schema, c, tests)
runTests(t, schema, connectLDAP, c, tests)
}
func TestGroupsOnUserEntity(t *testing.T) {
@@ -295,7 +304,93 @@ gidNumber: 1002
},
},
}
runTests(t, schema, c, tests)
runTests(t, schema, connectLDAP, c, tests)
}
func TestStartTLS(t *testing.T) {
schema := `
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organization
o: Example Company
dc: example
dn: ou=People,dc=example,dc=org
objectClass: organizationalUnit
ou: People
dn: cn=jane,ou=People,dc=example,dc=org
objectClass: person
objectClass: inetOrgPerson
sn: doe
cn: jane
mail: janedoe@example.com
userpassword: foo
`
c := &Config{}
c.UserSearch.BaseDN = "ou=People,dc=example,dc=org"
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailAttr = "mail"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn"
tests := []subtest{
{
name: "validpassword",
username: "jane",
password: "foo",
want: connector.Identity{
UserID: "cn=jane,ou=People,dc=example,dc=org",
Username: "jane",
Email: "janedoe@example.com",
EmailVerified: true,
},
},
}
runTests(t, schema, connectStartTLS, c, tests)
}
func TestLDAPS(t *testing.T) {
schema := `
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organization
o: Example Company
dc: example
dn: ou=People,dc=example,dc=org
objectClass: organizationalUnit
ou: People
dn: cn=jane,ou=People,dc=example,dc=org
objectClass: person
objectClass: inetOrgPerson
sn: doe
cn: jane
mail: janedoe@example.com
userpassword: foo
`
c := &Config{}
c.UserSearch.BaseDN = "ou=People,dc=example,dc=org"
c.UserSearch.NameAttr = "cn"
c.UserSearch.EmailAttr = "mail"
c.UserSearch.IDAttr = "DN"
c.UserSearch.Username = "cn"
tests := []subtest{
{
name: "validpassword",
username: "jane",
password: "foo",
want: connector.Identity{
UserID: "cn=jane,ou=People,dc=example,dc=org",
Username: "jane",
Email: "janedoe@example.com",
EmailVerified: true,
},
},
}
runTests(t, schema, connectLDAPS, c, tests)
}
// runTests runs a set of tests against an LDAP schema. It does this by
@@ -305,7 +400,7 @@ gidNumber: 1002
// machine's PATH.
//
// The DEX_LDAP_TESTS must be set to "1"
func runTests(t *testing.T, schema string, config *Config, tests []subtest) {
func runTests(t *testing.T, schema string, connMethod connectionMethod, config *Config, tests []subtest) {
if os.Getenv(envVar) != "1" {
t.Skipf("%s not set. Skipping test (run 'export %s=1' to run tests)", envVar, envVar)
}
@@ -316,6 +411,11 @@ func runTests(t *testing.T, schema string, config *Config, tests []subtest) {
}
}
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
tempDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
@@ -324,7 +424,13 @@ func runTests(t *testing.T, schema string, config *Config, tests []subtest) {
configBytes := new(bytes.Buffer)
if err := slapdConfigTmpl.Execute(configBytes, tmplData{tempDir, includes(t)}); err != nil {
data := tmplData{
TempDir: tempDir,
Includes: includes(t, wd),
}
data.TLSCertPath, data.TLSKeyPath = tlsAssets(t, wd)
if err := slapdConfigTmpl.Execute(configBytes, data); err != nil {
t.Fatal(err)
}
@@ -344,7 +450,7 @@ func runTests(t *testing.T, schema string, config *Config, tests []subtest) {
cmd := exec.Command(
"slapd",
"-d", "any",
"-h", "ldap://localhost:10363/ ldaps://localhost:10636/ ldapi://"+socketPath,
"-h", "ldap://localhost:10389/ ldaps://localhost:10636/ ldapi://"+socketPath,
"-f", configPath,
)
cmd.Stdout = slapdOut
@@ -385,18 +491,30 @@ func runTests(t *testing.T, schema string, config *Config, tests []subtest) {
wg.Wait()
}()
// Wait for slapd to come up.
time.Sleep(100 * time.Millisecond)
// Try a few times to connect to the LDAP server. On slower machines
// it can take a while for it to come up.
connected := false
wait := 100 * time.Millisecond
for i := 0; i < 5; i++ {
time.Sleep(wait)
ldapadd := exec.Command(
"ldapadd", "-x",
"-D", "cn=admin,dc=example,dc=org",
"-w", "admin",
"-f", schemaPath,
"-H", "ldap://localhost:10363/",
)
if out, err := ldapadd.CombinedOutput(); err != nil {
t.Errorf("ldapadd: %s", out)
ldapadd := exec.Command(
"ldapadd", "-x",
"-D", "cn=admin,dc=example,dc=org",
"-w", "admin",
"-f", schemaPath,
"-H", "ldap://localhost:10389/",
)
if out, err := ldapadd.CombinedOutput(); err != nil {
t.Logf("ldapadd: %s", out)
wait = wait * 2 // backoff
continue
}
connected = true
break
}
if !connected {
t.Errorf("ldapadd command failed")
return
}
@@ -405,8 +523,19 @@ func runTests(t *testing.T, schema string, config *Config, tests []subtest) {
// We need to configure host parameters but don't want to overwrite user or
// group search configuration.
c.Host = "localhost:10363"
c.InsecureNoSSL = true
switch connMethod {
case connectStartTLS:
c.Host = "localhost:10389"
c.RootCA = "testdata/ca.crt"
c.StartTLS = true
case connectLDAPS:
c.Host = "localhost:10636"
c.RootCA = "testdata/ca.crt"
case connectLDAP:
c.Host = "localhost:10389"
c.InsecureNoSSL = true
}
c.BindDN = "cn=admin,dc=example,dc=org"
c.BindPW = "admin"
@@ -488,10 +617,16 @@ type tmplData struct {
TempDir string
// List of schema files to include.
Includes []string
// TLS assets for LDAPS.
TLSKeyPath string
TLSCertPath string
}
// Config template copied from:
// http://www.zytrax.com/books/ldap/ch5/index.html#step1-slapd
//
// TLS instructions found here:
// http://www.openldap.org/doc/admin24/tls.html
var slapdConfigTmpl = template.Must(template.New("").Parse(`
{{ range $i, $include := .Includes }}
include {{ $include }}
@@ -511,6 +646,9 @@ rootpw admin
# change path as necessary
directory {{ .TempDir }}
TLSCertificateFile {{ .TLSCertPath }}
TLSCertificateKeyFile {{ .TLSKeyPath }}
# Indices to maintain for this directory
# unique id so equality match only
index uid eq
@@ -534,11 +672,18 @@ cachesize 10000
checkpoint 128 15
`))
func includes(t *testing.T) (paths []string) {
wd, err := os.Getwd()
if err != nil {
t.Fatalf("getting working directory: %v", err)
func tlsAssets(t *testing.T, wd string) (certPath, keyPath string) {
certPath = filepath.Join(wd, "testdata", "server.crt")
keyPath = filepath.Join(wd, "testdata", "server.key")
for _, p := range []string{certPath, keyPath} {
if _, err := os.Stat(p); err != nil {
t.Fatalf("failed to find TLS asset file: %s %v", p, err)
}
}
return
}
func includes(t *testing.T, wd string) (paths []string) {
for _, f := range includeFiles {
p := filepath.Join(wd, "testdata", f)
if _, err := os.Stat(p); err != nil {