mirror of https://github.com/dexidp/dex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
338 lines
8.7 KiB
338 lines
8.7 KiB
package connector |
|
|
|
import ( |
|
"crypto/tls" |
|
"crypto/x509" |
|
|
|
"fmt" |
|
|
|
"html/template" |
|
"io/ioutil" |
|
"net/http" |
|
"net/url" |
|
"path" |
|
"strings" |
|
"time" |
|
|
|
"github.com/coreos/dex/pkg/log" |
|
"github.com/coreos/go-oidc/oidc" |
|
|
|
"gopkg.in/ldap.v2" |
|
) |
|
|
|
const ( |
|
LDAPConnectorType = "ldap" |
|
LDAPLoginPageTemplateName = "ldap-login.html" |
|
) |
|
|
|
func init() { |
|
RegisterConnectorConfigType(LDAPConnectorType, func() ConnectorConfig { return &LDAPConnectorConfig{} }) |
|
} |
|
|
|
type LDAPConnectorConfig struct { |
|
ID string `json:"id"` |
|
ServerHost string `json:"serverHost"` |
|
ServerPort uint16 `json:"serverPort"` |
|
Timeout time.Duration `json:"timeout"` |
|
UseTLS bool `json:"useTLS"` |
|
UseSSL bool `json:"useSSL"` |
|
CertFile string `json:"certFile"` |
|
KeyFile string `json:"keyFile"` |
|
CaFile string `json:"caFile"` |
|
SkipCertVerification bool `json:"skipCertVerification"` |
|
BaseDN string `json:"baseDN"` |
|
NameAttribute string `json:"nameAttribute"` |
|
EmailAttribute string `json:"emailAttribute"` |
|
SearchBeforeAuth bool `json:"searchBeforeAuth"` |
|
SearchFilter string `json:"searchFilter"` |
|
SearchScope string `json:"searchScope"` |
|
SearchBindDN string `json:"searchBindDN"` |
|
SearchBindPw string `json:"searchBindPw"` |
|
BindTemplate string `json:"bindTemplate"` |
|
TrustedEmailProvider bool `json:"trustedEmailProvider"` |
|
} |
|
|
|
func (cfg *LDAPConnectorConfig) ConnectorID() string { |
|
return cfg.ID |
|
} |
|
|
|
func (cfg *LDAPConnectorConfig) ConnectorType() string { |
|
return LDAPConnectorType |
|
} |
|
|
|
type LDAPConnector struct { |
|
id string |
|
idp *LDAPIdentityProvider |
|
namespace url.URL |
|
trustedEmailProvider bool |
|
loginFunc oidc.LoginFunc |
|
loginTpl *template.Template |
|
} |
|
|
|
func (cfg *LDAPConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *template.Template) (Connector, error) { |
|
ns.Path = path.Join(ns.Path, httpPathCallback) |
|
tpl := tpls.Lookup(LDAPLoginPageTemplateName) |
|
if tpl == nil { |
|
return nil, fmt.Errorf("unable to find necessary HTML template") |
|
} |
|
|
|
// defaults |
|
const defaultNameAttribute = "cn" |
|
const defaultEmailAttribute = "mail" |
|
const defaultBindTemplate = "uid=%u,%b" |
|
const defaultSearchScope = ldap.ScopeWholeSubtree |
|
|
|
if cfg.UseTLS && cfg.UseSSL { |
|
return nil, fmt.Errorf("Invalid configuration. useTLS and useSSL are mutual exclusive.") |
|
} |
|
|
|
if len(cfg.CertFile) > 0 && len(cfg.KeyFile) == 0 { |
|
return nil, fmt.Errorf("Invalid configuration. Both certFile and keyFile must be specified.") |
|
} |
|
|
|
nameAttribute := defaultNameAttribute |
|
if len(cfg.NameAttribute) > 0 { |
|
nameAttribute = cfg.NameAttribute |
|
} |
|
|
|
emailAttribute := defaultEmailAttribute |
|
if len(cfg.EmailAttribute) > 0 { |
|
emailAttribute = cfg.EmailAttribute |
|
} |
|
|
|
bindTemplate := defaultBindTemplate |
|
if len(cfg.BindTemplate) > 0 { |
|
if cfg.SearchBeforeAuth { |
|
log.Warningf("bindTemplate not used when searchBeforeAuth specified.") |
|
} |
|
bindTemplate = cfg.BindTemplate |
|
} |
|
|
|
searchScope := defaultSearchScope |
|
if len(cfg.SearchScope) > 0 { |
|
switch { |
|
case strings.EqualFold(cfg.SearchScope, "BASE"): |
|
searchScope = ldap.ScopeBaseObject |
|
case strings.EqualFold(cfg.SearchScope, "ONE"): |
|
searchScope = ldap.ScopeSingleLevel |
|
case strings.EqualFold(cfg.SearchScope, "SUB"): |
|
searchScope = ldap.ScopeWholeSubtree |
|
default: |
|
return nil, fmt.Errorf("Invalid value for searchScope: '%v'. Must be one of 'base', 'one' or 'sub'.", cfg.SearchScope) |
|
} |
|
} |
|
|
|
if cfg.Timeout != 0 { |
|
ldap.DefaultTimeout = cfg.Timeout * time.Millisecond |
|
} |
|
|
|
tlsConfig := &tls.Config{ |
|
ServerName: cfg.ServerHost, |
|
InsecureSkipVerify: cfg.SkipCertVerification, |
|
} |
|
|
|
if (cfg.UseTLS || cfg.UseSSL) && len(cfg.CaFile) > 0 { |
|
buf, err := ioutil.ReadFile(cfg.CaFile) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
rootCertPool := x509.NewCertPool() |
|
ok := rootCertPool.AppendCertsFromPEM(buf) |
|
if ok { |
|
tlsConfig.RootCAs = rootCertPool |
|
} else { |
|
return nil, fmt.Errorf("%v: Unable to parse certificate data.", cfg.CaFile) |
|
} |
|
} |
|
|
|
if (cfg.UseTLS || cfg.UseSSL) && len(cfg.CertFile) > 0 && len(cfg.KeyFile) > 0 { |
|
cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile) |
|
if err != nil { |
|
return nil, err |
|
} |
|
tlsConfig.Certificates = []tls.Certificate{cert} |
|
} |
|
|
|
idp := &LDAPIdentityProvider{ |
|
serverHost: cfg.ServerHost, |
|
serverPort: cfg.ServerPort, |
|
useTLS: cfg.UseTLS, |
|
useSSL: cfg.UseSSL, |
|
baseDN: cfg.BaseDN, |
|
nameAttribute: nameAttribute, |
|
emailAttribute: emailAttribute, |
|
searchBeforeAuth: cfg.SearchBeforeAuth, |
|
searchFilter: cfg.SearchFilter, |
|
searchScope: searchScope, |
|
searchBindDN: cfg.SearchBindDN, |
|
searchBindPw: cfg.SearchBindPw, |
|
bindTemplate: bindTemplate, |
|
tlsConfig: tlsConfig, |
|
} |
|
|
|
idpc := &LDAPConnector{ |
|
id: cfg.ID, |
|
idp: idp, |
|
namespace: ns, |
|
trustedEmailProvider: cfg.TrustedEmailProvider, |
|
loginFunc: lf, |
|
loginTpl: tpl, |
|
} |
|
|
|
return idpc, nil |
|
} |
|
|
|
func (c *LDAPConnector) ID() string { |
|
return c.id |
|
} |
|
|
|
func (c *LDAPConnector) Healthy() error { |
|
ldapConn, err := c.idp.LDAPConnect() |
|
if err == nil { |
|
ldapConn.Close() |
|
} |
|
return err |
|
} |
|
|
|
func (c *LDAPConnector) LoginURL(sessionKey, prompt string) (string, error) { |
|
q := url.Values{} |
|
q.Set("session_key", sessionKey) |
|
q.Set("prompt", prompt) |
|
enc := q.Encode() |
|
|
|
return path.Join(c.namespace.Path, "login") + "?" + enc, nil |
|
} |
|
|
|
func (c *LDAPConnector) Register(mux *http.ServeMux, errorURL url.URL) { |
|
route := path.Join(c.namespace.Path, "login") |
|
mux.Handle(route, handleLoginFunc(c.loginFunc, c.loginTpl, c.idp, route, errorURL)) |
|
} |
|
|
|
func (c *LDAPConnector) Sync() chan struct{} { |
|
return make(chan struct{}) |
|
} |
|
|
|
func (c *LDAPConnector) TrustedEmailProvider() bool { |
|
return c.trustedEmailProvider |
|
} |
|
|
|
type LDAPIdentityProvider struct { |
|
serverHost string |
|
serverPort uint16 |
|
useTLS bool |
|
useSSL bool |
|
baseDN string |
|
nameAttribute string |
|
emailAttribute string |
|
searchBeforeAuth bool |
|
searchFilter string |
|
searchScope int |
|
searchBindDN string |
|
searchBindPw string |
|
bindTemplate string |
|
tlsConfig *tls.Config |
|
} |
|
|
|
func (m *LDAPIdentityProvider) LDAPConnect() (*ldap.Conn, error) { |
|
var err error |
|
var ldapConn *ldap.Conn |
|
|
|
log.Debugf("LDAPConnect()") |
|
if m.useSSL { |
|
ldapConn, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", m.serverHost, m.serverPort), m.tlsConfig) |
|
if err != nil { |
|
return nil, err |
|
} |
|
} else { |
|
ldapConn, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", m.serverHost, m.serverPort)) |
|
if err != nil { |
|
return nil, err |
|
} |
|
if m.useTLS { |
|
err = ldapConn.StartTLS(m.tlsConfig) |
|
if err != nil { |
|
return nil, err |
|
} |
|
} |
|
} |
|
|
|
return ldapConn, err |
|
} |
|
|
|
func (m *LDAPIdentityProvider) ParseString(template, username string) string { |
|
result := template |
|
result = strings.Replace(result, "%u", username, -1) |
|
result = strings.Replace(result, "%b", m.baseDN, -1) |
|
|
|
return result |
|
} |
|
|
|
func (m *LDAPIdentityProvider) Identity(username, password string) (*oidc.Identity, error) { |
|
var err error |
|
var bindDN, ldapUid, ldapName, ldapEmail string |
|
var ldapConn *ldap.Conn |
|
|
|
ldapConn, err = m.LDAPConnect() |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer ldapConn.Close() |
|
|
|
if m.searchBeforeAuth { |
|
err = ldapConn.Bind(m.searchBindDN, m.searchBindPw) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
filter := m.ParseString(m.searchFilter, username) |
|
|
|
attributes := []string{ |
|
m.nameAttribute, |
|
m.emailAttribute, |
|
} |
|
|
|
s := ldap.NewSearchRequest(m.baseDN, m.searchScope, ldap.NeverDerefAliases, 0, 0, false, filter, attributes, nil) |
|
|
|
sr, err := ldapConn.Search(s) |
|
if err != nil { |
|
return nil, err |
|
} |
|
if len(sr.Entries) == 0 { |
|
err = fmt.Errorf("Search returned no match. filter='%v' base='%v'", filter, m.baseDN) |
|
return nil, err |
|
} |
|
|
|
bindDN = sr.Entries[0].DN |
|
ldapName = sr.Entries[0].GetAttributeValue(m.nameAttribute) |
|
ldapEmail = sr.Entries[0].GetAttributeValue(m.emailAttribute) |
|
|
|
// drop to anonymous bind, prepare for bind as user |
|
err = ldapConn.Bind("", "") |
|
if err != nil { |
|
// unsupported or disallowed, reconnect |
|
log.Warningf("Re-connecting to LDAP Server after failure to bind anonymously: %v", err) |
|
ldapConn.Close() |
|
ldapConn, err = m.LDAPConnect() |
|
if err != nil { |
|
return nil, err |
|
} |
|
} |
|
} else { |
|
bindDN = m.ParseString(m.bindTemplate, username) |
|
} |
|
|
|
// authenticate user |
|
err = ldapConn.Bind(bindDN, password) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
ldapUid = bindDN |
|
|
|
return &oidc.Identity{ |
|
ID: ldapUid, |
|
Name: ldapName, |
|
Email: ldapEmail, |
|
}, nil |
|
}
|
|
|