outposts: add LDAP Binding using flows
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
a525d6c3a9
commit
6c9b3ebd2b
|
@ -6,6 +6,7 @@ require (
|
||||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||||
github.com/getsentry/sentry-go v0.10.0
|
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/analysis v0.20.1 // indirect
|
||||||
github.com/go-openapi/errors v0.20.0
|
github.com/go-openapi/errors v0.20.0
|
||||||
github.com/go-openapi/runtime v0.19.28
|
github.com/go-openapi/runtime v0.19.28
|
||||||
|
|
|
@ -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=
|
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=
|
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/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 h1:ZVN4Iat3runWOFLaBCDVU5a9X/XikSRBosye++6gojw=
|
||||||
github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss=
|
github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss=
|
||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
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/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-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/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-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 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
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-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-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-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.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
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=
|
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-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-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-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-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 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
|
|
|
@ -9,7 +9,7 @@ func (ls *LDAPServer) Refresh() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ls *LDAPServer) Start() error {
|
func (ls *LDAPServer) Start() error {
|
||||||
listen := "0.0.0.0:3389"
|
listen := "127.0.0.1:3390"
|
||||||
log.Debugf("Listening on %s", listen)
|
log.Debugf("Listening on %s", listen)
|
||||||
err := ls.s.ListenAndServe(listen)
|
err := ls.s.ListenAndServe(listen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,12 +1,114 @@
|
||||||
package ldap
|
package ldap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
goldap "github.com/go-ldap/ldap/v3"
|
||||||
|
|
||||||
"github.com/nmcclain/ldap"
|
"github.com/nmcclain/ldap"
|
||||||
|
"goauthentik.io/outpost/pkg/client/flows"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ls *LDAPServer) Bind(bindDN string, bindSimplePw string, conn net.Conn) (ldap.LDAPResultCode, error) {
|
type UIDResponse struct {
|
||||||
ls.log.WithField("dn", bindDN).WithField("pw", bindSimplePw).Debug("bind")
|
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.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)
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@ type LDAPServer struct {
|
||||||
s *ldap.Server
|
s *ldap.Server
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
ac *ak.APIController
|
ac *ak.APIController
|
||||||
|
|
||||||
|
// TODO: Make configurable
|
||||||
|
flowSlug string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(ac *ak.APIController) *LDAPServer {
|
func NewServer(ac *ak.APIController) *LDAPServer {
|
||||||
|
|
|
@ -42,7 +42,7 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "uid",
|
Name: "uid",
|
||||||
Values: []string{strconv.Itoa(int(g.Pk))},
|
Values: []string{string(g.Pk)},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "objectClass",
|
Name: "objectClass",
|
||||||
|
|
Reference in a new issue