From 6c9b3ebd2b61c97a11c5a758a9753c78cff3a802 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sun, 25 Apr 2021 22:07:12 +0200 Subject: [PATCH] outposts: add LDAP Binding using flows Signed-off-by: Jens Langhammer --- outpost/go.mod | 1 + outpost/go.sum | 7 +++ outpost/pkg/ldap/api.go | 2 +- outpost/pkg/ldap/bind.go | 108 +++++++++++++++++++++++++++++++++++-- outpost/pkg/ldap/ldap.go | 3 ++ outpost/pkg/ldap/search.go | 2 +- 6 files changed, 118 insertions(+), 5 deletions(-) diff --git a/outpost/go.mod b/outpost/go.mod index f9089d9a2..bb46b635c 100644 --- a/outpost/go.mod +++ b/outpost/go.mod @@ -6,6 +6,7 @@ require ( github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/coreos/go-oidc v2.2.1+incompatible github.com/getsentry/sentry-go v0.10.0 + github.com/go-ldap/ldap/v3 v3.3.0 // indirect github.com/go-openapi/analysis v0.20.1 // indirect github.com/go-openapi/errors v0.20.0 github.com/go-openapi/runtime v0.19.28 diff --git a/outpost/go.sum b/outpost/go.sum index 9d458aa6e..291cb571a 100644 --- a/outpost/go.sum +++ b/outpost/go.sum @@ -34,6 +34,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28= +github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb h1:ZVN4Iat3runWOFLaBCDVU5a9X/XikSRBosye++6gojw= github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= @@ -136,6 +138,8 @@ github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NB github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8= +github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= @@ -143,6 +147,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-ldap/ldap/v3 v3.3.0 h1:lwx+SJpgOHd8tG6SumBQZXCmNX51zM8B1cfxJ5gv4tQ= +github.com/go-ldap/ldap/v3 v3.3.0/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= @@ -668,6 +674,7 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= diff --git a/outpost/pkg/ldap/api.go b/outpost/pkg/ldap/api.go index c5c7eb69c..142826483 100644 --- a/outpost/pkg/ldap/api.go +++ b/outpost/pkg/ldap/api.go @@ -9,7 +9,7 @@ func (ls *LDAPServer) Refresh() error { } func (ls *LDAPServer) Start() error { - listen := "0.0.0.0:3389" + listen := "127.0.0.1:3390" log.Debugf("Listening on %s", listen) err := ls.s.ListenAndServe(listen) if err != nil { diff --git a/outpost/pkg/ldap/bind.go b/outpost/pkg/ldap/bind.go index 745ef959c..79f9f7e3a 100644 --- a/outpost/pkg/ldap/bind.go +++ b/outpost/pkg/ldap/bind.go @@ -1,12 +1,114 @@ package ldap import ( + "context" + "errors" + "fmt" "net" + "net/http" + "net/http/cookiejar" + "strings" + + goldap "github.com/go-ldap/ldap/v3" "github.com/nmcclain/ldap" + "goauthentik.io/outpost/pkg/client/flows" ) -func (ls *LDAPServer) Bind(bindDN string, bindSimplePw string, conn net.Conn) (ldap.LDAPResultCode, error) { - ls.log.WithField("dn", bindDN).WithField("pw", bindSimplePw).Debug("bind") - return ldap.LDAPResultSuccess, nil +type UIDResponse struct { + UIDFIeld string `json:"uid_field"` +} + +type PasswordResponse struct { + Password string `json:"password"` +} + +func (ls *LDAPServer) getUsername(dn string) (string, error) { + if !strings.HasSuffix(dn, ls.BaseDN) { + return "", errors.New("invalid base DN") + } + dns, err := goldap.ParseDN(dn) + if err != nil { + return "", err + } + for _, part := range dns.RDNs { + for _, attribute := range part.Attributes { + if attribute.Type == "DN" { + return attribute.Value, nil + } + } + } + return "", errors.New("failed to find dn") +} + +func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { + username, err := ls.getUsername(bindDN) + if err != nil { + ls.log.WithError(err).Warning("failed to parse user dn") + return ldap.LDAPResultInvalidCredentials, nil + } + ls.log.WithField("dn", username).Debug("bind") + jar, err := cookiejar.New(nil) + if err != nil { + ls.log.WithError(err).Warning("Failed to create cookiejar") + return ldap.LDAPResultOperationsError, nil + } + client := &http.Client{ + Jar: jar, + } + passed, err := ls.solveFlowChallenge(username, bindPW, client) + if err != nil { + ls.log.WithError(err).Warning("failed to solve challenge") + return ldap.LDAPResultOperationsError, nil + } + if passed { + return ldap.LDAPResultSuccess, nil + } + return ldap.LDAPResultInvalidCredentials, nil +} + +func (ls *LDAPServer) solveFlowChallenge(bindDN string, password string, client *http.Client) (bool, error) { + challenge, err := ls.ac.Client.Flows.FlowsExecutorGet(&flows.FlowsExecutorGetParams{ + FlowSlug: ls.flowSlug, + Query: "ldap=true", + Context: context.Background(), + HTTPClient: client, + }, ls.ac.Auth) + if err != nil { + ls.log.WithError(err).Warning("Failed to get challenge") + return false, err + } + ls.log.WithField("component", challenge.Payload.Component).WithField("type", *challenge.Payload.Type).Debug("Got challenge") + responseParams := &flows.FlowsExecutorSolveParams{ + FlowSlug: ls.flowSlug, + Query: "ldap=true", + Context: context.Background(), + HTTPClient: client, + } + switch challenge.Payload.Component { + case "ak-stage-identification": + responseParams.Data = &UIDResponse{UIDFIeld: bindDN} + case "ak-stage-password": + responseParams.Data = &PasswordResponse{Password: password} + default: + return false, fmt.Errorf("unsupported challenge type: %s", challenge.Payload.Component) + } + response, err := ls.ac.Client.Flows.FlowsExecutorSolve(responseParams, ls.ac.Auth) + ls.log.WithField("component", response.Payload.Component).WithField("type", *response.Payload.Type).Debug("Got response") + if *response.Payload.Type == "redirect" { + return true, nil + } + if err != nil { + ls.log.WithError(err).Warning("Failed to submit challenge") + return false, err + } + if len(response.Payload.ResponseErrors) > 0 { + for key, errs := range response.Payload.ResponseErrors { + for _, err := range errs { + ls.log.WithField("key", key).WithField("code", *err.Code).Debug(*err.String) + return false, nil + } + } + } + return ls.solveFlowChallenge(bindDN, password, client) } diff --git a/outpost/pkg/ldap/ldap.go b/outpost/pkg/ldap/ldap.go index 105f2be4f..dd707a150 100644 --- a/outpost/pkg/ldap/ldap.go +++ b/outpost/pkg/ldap/ldap.go @@ -22,6 +22,9 @@ type LDAPServer struct { s *ldap.Server log *log.Entry ac *ak.APIController + + // TODO: Make configurable + flowSlug string } func NewServer(ac *ak.APIController) *LDAPServer { diff --git a/outpost/pkg/ldap/search.go b/outpost/pkg/ldap/search.go index b0f818ddf..b4d0dde2e 100644 --- a/outpost/pkg/ldap/search.go +++ b/outpost/pkg/ldap/search.go @@ -42,7 +42,7 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n }, { Name: "uid", - Values: []string{strconv.Itoa(int(g.Pk))}, + Values: []string{string(g.Pk)}, }, { Name: "objectClass",