Skip to content

Commit

Permalink
Add LDAP mode for basic authentication (#871)
Browse files Browse the repository at this point in the history
* add LDAP mode for basic authentication

* hold connection to avoid concurrenty problem
  • Loading branch information
samanhappy committed Dec 5, 2022
1 parent 4c9b3a1 commit a8eb6c7
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 2 deletions.
27 changes: 26 additions & 1 deletion doc/reference/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ basicAuth:
| jwt | [validator.JWTValidatorSpec](#validatorJWTValidatorSpec) | JWT validation rule, validates JWT token string from the `Authorization` header or cookies | No |
| signature | [signer.Spec](#signerSpec) | Signature validation rule, implements an [Amazon Signature V4](https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html) compatible signature validation validator, with customizable literal strings | No |
| oauth2 | [validator.OAuth2ValidatorSpec](#validatorOAuth2ValidatorSpec) | The `OAuth/2` method support `Token Introspection` mode and `Self-Encoded Access Tokens` mode, only one mode can be configured at a time | No |
| basicAuth | [basicauth.BasicAuthValidatorSpec](#basicauthBasicAuthValidatorSpec) | The `BasicAuth` method support `FILE` mode and `ETCD` mode, only one mode can be configured at a time. | No |
| basicAuth | [validator.BasicAuthValidatorSpec](#validatorBasicAuthValidatorSpec) | The `BasicAuth` method support `FILE`, `ETCD` and `LDAP` mode, only one mode can be configured at a time. | No |

### Results

Expand Down Expand Up @@ -1334,6 +1334,31 @@ The relationship between `methods` and `url` is `AND`.
| publicKey | string | The public key is used for `RS256`,`RS384`,`RS512`,`ES256`,`ES384`,`ES512` or `EdDSA` validation in hex encoding | Yes |
| secret | string | The secret is for `HS256`,`HS384`,`HS512` validation in hex encoding | Yes |

### validator.BasicAuthValidatorSpec

| Name | Type | Description | Required |
|--------------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------|----------|
| mode | string | The mode of basic authentication, valid values are `FILE`, `ETCD` and `LDAP` | Yes |
| userFile | string | The user file used for `FILE` mode | No |
| etcdPrefix | string | The etcd prefix used for `ETCD` mode | No |
| ldap | [basicAuth.LDAPSpec](#basicAuthLDAPSpec) | The LDAP configuration used for `LDAP` mode | No |

### basicAuth.LDAPSpec

| Name | Type | Description | Required |
|--------------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------|----------|
| host | string | The host of the LDAP server | Yes |
| port | int | The port of the LDAP server | Yes |
| baseDN | string | The base dn of the LDAP server, e.g. `ou=users,dc=example,dc=org` | Yes |
| uid | string | The user attribute used to bind user, e.g. `cn` | Yes |
| useSSL | bool | Whether to use SSL | No |
| skipTLS | bool | Whether to skip `StartTLS` | No |
| insecure | bool | Whether to skip verifying LDAP server's
certificate chain and host name | No |
| serverName | string | Server name used to verify certificate when `insecure` is `false` | No |
| certBase64 | string | Base64 encoded certificate | No |
| keyBase64 | string | Base64 encoded key | No |

### signer.Spec

| Name | Type | Description | Required |
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/hashicorp/consul/api v1.15.2
github.com/hashicorp/golang-lru v0.5.4
github.com/invopop/yaml v0.2.0
github.com/jtblin/go-ldap-client v0.0.0-20170223121919-b73f66626b33
github.com/libdns/alidns v1.0.2-x2
github.com/libdns/azure v0.2.0
github.com/libdns/cloudflare v0.1.0
Expand Down Expand Up @@ -84,6 +85,8 @@ require (
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.11.1 // indirect
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
gopkg.in/ldap.v2 v2.5.1 // indirect
)

require (
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtblin/go-ldap-client v0.0.0-20170223121919-b73f66626b33 h1:XDpFOMOZq0u0Ar4F0p/wklqQXp/AMV1pTF5T5bDoUfQ=
github.com/jtblin/go-ldap-client v0.0.0-20170223121919-b73f66626b33/go.mod h1:+0BcLY5d54TVv6irFzHoiFvwAHR6T0g9B+by/UaS9T0=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
Expand Down Expand Up @@ -1625,6 +1627,8 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand All @@ -1639,6 +1643,8 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU=
gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
Expand Down
83 changes: 82 additions & 1 deletion pkg/filters/validator/basicauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ package validator
import (
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"strings"
"time"

"github.com/fsnotify/fsnotify"
"github.com/jtblin/go-ldap-client"
"github.com/tg123/go-htpasswd"
"golang.org/x/crypto/bcrypt"

Expand All @@ -42,7 +44,7 @@ type (
// BasicAuthValidatorSpec defines the configuration of Basic Auth validator.
// There are 'file' and 'etcd' modes.
BasicAuthValidatorSpec struct {
Mode string `json:"mode" jsonschema:"omitempty,enum=FILE,enum=ETCD"`
Mode string `json:"mode" jsonschema:"omitempty,enum=FILE,enum=ETCD,enum=LDAP"`
// Required for 'FILE' mode.
// UserFile is path to file containing encrypted user credentials in apache2-utils/htpasswd format.
// To add user `userY`, use `sudo htpasswd /etc/apache2/.htpasswd userY`
Expand All @@ -58,6 +60,8 @@ type (
// Username and password are used for Basic Authentication. If "username" is empty, the value of "key"
// entry is used as username for Basic Auth.
EtcdPrefix string `json:"etcdPrefix" jsonschema:"omitempty"`
// Required for 'LDAP' mode.
LDAP *ldapSpec `json:"ldap,omitempty" jsonshema:"omitempty"`
}

// AuthorizedUsersCache provides cached lookup for authorized users.
Expand Down Expand Up @@ -85,6 +89,26 @@ type (
cancel context.CancelFunc
}

ldapUserCache struct {
spec *ldapSpec
client *ldap.LDAPClient
}

// ldapSpec defines the configuration of LDAP authentication
ldapSpec struct {
Host string `json:"host" jsonschema:"required"`
Port int `json:"port" jsonschema:"required"`
BaseDN string `json:"baseDN" jsonschema:"required"`
UID string `json:"uid" jsonschema:"required"`
UseSSL bool `json:"useSSL" jsonschema:"omitempty"`
SkipTLS bool `json:"skipTLS" jsonschema:"omitempty"`
Insecure bool `json:"insecure" jsonschema:"omitempty"`
ServerName string `json:"serverName" jsonschema:"omitempty"`
CertBase64 string `json:"certBase64" jsonschema:"omitempty,format=base64"`
KeyBase64 string `json:"keyBase64" jsonschema:"omitempty,format=base64"`
certificates []tls.Certificate
}

// BasicAuthValidator defines the Basic Auth validator
BasicAuthValidator struct {
spec *BasicAuthValidatorSpec
Expand Down Expand Up @@ -313,6 +337,61 @@ func (euc *etcdUserCache) Match(username string, password string) bool {
return euc.userFileObject.Match(username, password)
}

func newLDAPUserCache(spec *ldapSpec) *ldapUserCache {
if spec.CertBase64 != "" && spec.KeyBase64 != "" {
certPem, _ := base64.StdEncoding.DecodeString(spec.CertBase64)
keyPem, _ := base64.StdEncoding.DecodeString(spec.KeyBase64)
if cert, err := tls.X509KeyPair(certPem, keyPem); err == nil {
spec.certificates = append(spec.certificates, cert)
} else {
logger.Errorf("generates x509 key pair failed: %v", err)
}
}
client := &ldap.LDAPClient{
Host: spec.Host,
Port: spec.Port,
Base: spec.BaseDN,
UseSSL: spec.UseSSL,
SkipTLS: spec.SkipTLS,
InsecureSkipVerify: spec.Insecure,
ServerName: spec.ServerName,
ClientCertificates: spec.certificates,
}
return &ldapUserCache{
spec: spec,
client: client}
}

// make it mockable
var fnAuthLDAP = func(luc *ldapUserCache, username, password string) bool {
if err := luc.client.Connect(); err != nil {
logger.Warnf("failed to connect LDAP server %v", err)
return false
}

userdn := fmt.Sprintf("%s=%s,%s", luc.spec.UID, username, luc.spec.BaseDN)
if err := luc.client.Conn.Bind(userdn, password); err != nil {
logger.Warnf("failed to bind LDAP user %v", err)
return false
}

return true
}

func (luc *ldapUserCache) Match(username, password string) bool {
return fnAuthLDAP(luc, username, password)
}

func (luc *ldapUserCache) WatchChanges() {

}

func (luc *ldapUserCache) Close() {
if luc.client != nil {
luc.client.Close()
}
}

// NewBasicAuthValidator creates a new Basic Auth validator
func NewBasicAuthValidator(spec *BasicAuthValidatorSpec, supervisor *supervisor.Supervisor) *BasicAuthValidator {
var cache AuthorizedUsersCache
Expand All @@ -325,6 +404,8 @@ func NewBasicAuthValidator(spec *BasicAuthValidatorSpec, supervisor *supervisor.
cache = newEtcdUserCache(supervisor.Cluster(), spec.EtcdPrefix)
case "FILE":
cache = newHtpasswdUserCache(spec.UserFile, 1*time.Minute)
case "LDAP":
cache = newLDAPUserCache(spec.LDAP)
default:
logger.Errorf("BasicAuth validator spec unvalid.")
return nil
Expand Down
32 changes: 32 additions & 0 deletions pkg/filters/validator/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,4 +584,36 @@ lastEntry: "byebye"
assert.Equal("doge", header.Get("X-AUTH-USER"))
v.Close()
})

t.Run("credentials from LDAP", func(t *testing.T) {
assert := assert.New(t)

yamlConfig := `
kind: Validator
name: validator
basicAuth:
mode: LDAP
ldap:
host: localhost
port: 3893
baseDN: ou=superheros,dc=glauth,dc=com
uid: cn
skipTLS: true
`
// mock
fnAuthLDAP = func(luc *ldapUserCache, username, password string) bool {
return true
}

v := createValidator(yamlConfig, nil, nil)
for i := 0; i < 3; i++ {
ctx, header := prepareCtxAndHeader()
b64creds := base64.StdEncoding.EncodeToString([]byte(userIds[i] + ":" + passwords[i]))
header.Set("Authorization", "Basic "+b64creds)
result := v.Handle(ctx)
assert.True(result != resultInvalid)
}

v.Close()
})
}

0 comments on commit a8eb6c7

Please sign in to comment.