dex/connector/connector_ldap.go
2016-02-16 23:58:41 +01:00

339 lines
8.7 KiB
Go

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
}