diff --git a/docs/external-auth.md b/docs/external-auth.md index 2ccc4b01..8ae5dadf 100644 --- a/docs/external-auth.md +++ b/docs/external-auth.md @@ -47,4 +47,6 @@ else fi ``` -If you have an external authentication hook that could be useful for others too, please let us know and/or send a pull request. +An example authentication program that allow SFTPGo to authenticate against LDAP can be found inside the source tree [ldapauth](../examples/ldapauth) directory. + +If you have an external authentication hook that could be useful to others too, please let us know and/or please send a pull request. diff --git a/examples/ldapauth/README.md b/examples/ldapauth/README.md new file mode 100644 index 00000000..f7ce1355 --- /dev/null +++ b/examples/ldapauth/README.md @@ -0,0 +1,48 @@ +## LDAPAuth + +This is an example for an external authentication program that performs authentication against an LDAP server. +It is tested against [389ds](https://directory.fedoraproject.org/) and can be used as starting point to authenticate using any LDAP server including Active Directory. + +You need to change the LDAP connection parameters and the user search query to match your environment. +You can build this example using the following command: + +``` +go build -i -ldflags "-s -w" -o ldapauth +``` + +This program assumes that the 389ds schema was extended to add support for public keys using the following ldif file placed in `/etc/dirsrv/schema/98openssh-ldap.ldif`: + +``` +dn: cn=schema +changetype: modify +add: attributetypes +attributetypes: ( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey' DESC 'MANDATORY: OpenSSH Public key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 ) +- +add: objectclasses +objectClasses: ( 1.3.6.1.4.1.24552.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY DESC 'MANDATORY: OpenSSH LPK objectclass' MUST ( uid ) MAY ( sshPublicKey ) ) +- + +dn: cn=sshpublickey,cn=default indexes,cn=config,cn=ldbm database,cn=plugins,cn=config +changetype: add +cn: sshpublickey +nsIndexType: eq +nsIndexType: pres +nsSystemIndex: false +objectClass: top +objectClass: nsIndex + +dn: cn=sshpublickey_self_manage,ou=groups,dc=example,dc=com +changetype: add +objectClass: top +objectClass: groupofuniquenames +cn: sshpublickey_self_manage +description: Members of this group gain the ability to edit their own sshPublicKey field + +dn: dc=example,dc=com +changetype: modify +add: aci +aci: (targetattr = "sshPublicKey") (version 3.0; acl "Allow members of sshpublickey_self_manage to edit their keys"; allow(write) (groupdn = "ldap:///cn=sshpublickey_self_manage,ou=groups,dc=example,dc=com" and userdn="ldap:///self" ); ) +- +``` + +Please feel free to send pull requests to improve this example authentication program, thanks! \ No newline at end of file diff --git a/examples/ldapauth/go.mod b/examples/ldapauth/go.mod new file mode 100644 index 00000000..dcdd54f2 --- /dev/null +++ b/examples/ldapauth/go.mod @@ -0,0 +1,10 @@ +module github.com/drakkan/ldapauth + +go 1.14 + +require ( + github.com/go-asn1-ber/asn1-ber v1.4.1 // indirect + github.com/go-ldap/ldap/v3 v3.1.8 + golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 + golang.org/x/sys v0.0.0-20200409092240-59c9f1ba88fa // indirect +) diff --git a/examples/ldapauth/go.sum b/examples/ldapauth/go.sum new file mode 100644 index 00000000..5d0be384 --- /dev/null +++ b/examples/ldapauth/go.sum @@ -0,0 +1,13 @@ +github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-asn1-ber/asn1-ber v1.4.1 h1:qP/QDxOtmMoJVgXHCXNzDpA0+wkgYB2x5QoLMVOciyw= +github.com/go-asn1-ber/asn1-ber v1.4.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-ldap/ldap/v3 v3.1.8 h1:5vU/2jOh9HqprwXp8aF915s9p6Z8wmbSEVF7/gdTFhM= +github.com/go-ldap/ldap/v3 v3.1.8/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 h1:DOmugCavvUtnUD114C1Wh+UgTgQZ4pMLzXxi1pSt+/Y= +golang.org/x/crypto v0.0.0-20200406173513-056763e48d71/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200409092240-59c9f1ba88fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/examples/ldapauth/main.go b/examples/ldapauth/main.go new file mode 100644 index 00000000..df93e93a --- /dev/null +++ b/examples/ldapauth/main.go @@ -0,0 +1,135 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "strconv" + + "github.com/go-ldap/ldap/v3" + "golang.org/x/crypto/ssh" +) + +const ( + bindUsername = "cn=Directory Manager" + bindPassword = "YOUR_ADMIN_PASSWORD_HERE" + bindURL = "ldap://192.168.1.103:389" +) + +type userFilters struct { + DeniedLoginMethods []string `json:"denied_login_methods,omitempty"` +} + +type minimalSFTPGoUser struct { + Status int `json:"status,omitempty"` + Username string `json:"username"` + HomeDir string `json:"home_dir,omitempty"` + UID int `json:"uid,omitempty"` + GID int `json:"gid,omitempty"` + Permissions map[string][]string `json:"permissions"` + Filters userFilters `json:"filters"` +} + +func exitError() { + u := minimalSFTPGoUser{ + Username: "", + } + json, _ := json.Marshal(u) + fmt.Printf("%v\n", string(json)) + os.Exit(1) +} + +func printSuccessResponse(username, homeDir string, uid, gid int) { + u := minimalSFTPGoUser{ + Username: username, + HomeDir: homeDir, + UID: uid, + GID: gid, + Status: 1, + } + u.Permissions = make(map[string][]string) + u.Permissions["/"] = []string{"*"} + // uncomment the next line to require publickey+password authentication + //u.Filters.DeniedLoginMethods = []string{"publickey", "password", "keyboard-interactive", "publickey+keyboard-interactive"} + json, _ := json.Marshal(u) + fmt.Printf("%v\n", string(json)) + os.Exit(0) +} + +func main() { + // get credentials from env vars + username := os.Getenv("SFTPGO_AUTHD_USERNAME") + password := os.Getenv("SFTPGO_AUTHD_PASSWORD") + publickey := os.Getenv("SFTPGO_AUTHD_PUBLIC_KEY") + l, err := ldap.DialURL(bindURL) + if err != nil { + exitError() + } + defer l.Close() + // bind to the ldap server with an account that can read users + err = l.Bind(bindUsername, bindPassword) + if err != nil { + exitError() + } + + // search the user trying to login and fetch some attributes, this search string is tested against 389ds using the default configuration + searchRequest := ldap.NewSearchRequest( + "dc=example,dc=com", + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=nsPerson)(uid=%s))", username), + []string{"dn", "homeDirectory", "uidNumber", "gidNumber", "nsSshPublicKey"}, + nil, + ) + + sr, err := l.Search(searchRequest) + if err != nil { + exitError() + } + + // we expect exactly one user + if len(sr.Entries) != 1 { + exitError() + } + + if len(publickey) > 0 { + // check public key + userKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publickey)) + if err != nil { + exitError() + } + authOk := false + for _, k := range sr.Entries[0].GetAttributeValues("nsSshPublicKey") { + key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k)) + // we skip an invalid public key stored inside the LDAP server + if err != nil { + continue + } + if bytes.Equal(key.Marshal(), userKey.Marshal()) { + authOk = true + break + } + } + if !authOk { + exitError() + } + } else { + // bind to the LDAP server with the user dn and the given password to check the password + userdn := sr.Entries[0].DN + err = l.Bind(userdn, password) + if err != nil { + exitError() + } + } + + uid, err := strconv.Atoi(sr.Entries[0].GetAttributeValue("uidNumber")) + if err != nil { + exitError() + } + gid, err := strconv.Atoi(sr.Entries[0].GetAttributeValue("gidNumber")) + if err != nil { + exitError() + } + // return the authenticated user + printSuccessResponse(username, sr.Entries[0].GetAttributeValue("homeDirectory"), uid, gid) +}