Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LDAP mode for basic authentication #871

Merged
merged 2 commits into from
Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
})
}