mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-06 22:30:56 +03:00
add an example auth program that allow to authenticate against LDAP
External authentication is the way to go to authenticate against LDAP, at least for now. Closes #99
This commit is contained in:
@@ -47,4 +47,6 @@ else
|
|||||||
fi
|
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.
|
||||||
|
|||||||
48
examples/ldapauth/README.md
Normal file
48
examples/ldapauth/README.md
Normal file
@@ -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!
|
||||||
10
examples/ldapauth/go.mod
Normal file
10
examples/ldapauth/go.mod
Normal file
@@ -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
|
||||||
|
)
|
||||||
13
examples/ldapauth/go.sum
Normal file
13
examples/ldapauth/go.sum
Normal file
@@ -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=
|
||||||
135
examples/ldapauth/main.go
Normal file
135
examples/ldapauth/main.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user