outposts: Refactor session end signal and add LDAP support (#14539)

* outpost: promote session end signal to non-provider specific

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* implement server-side logout in ldap

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix previous import

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* use better retry logic

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* log

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make more generic if we switch from ws to something else

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make it possible to e2e test WS

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix ldap session id

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* ok I actually need to go to bed this took me an hour to fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format; add ldap test

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix leftover state

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove thread

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* use ws base for radius

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* separate test utils

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rename

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix missing super calls

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* websocket tests with browser 🎉

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add proxy test for sign out

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix install_id issue with channels tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix proxy basic auth test

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* big code dedupe

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* allow passing go build args

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* improve waiting for outpost

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rewrite ldap tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* ok actually fix the tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* undo a couple things that need more time to cook

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove unused lockfile-lint dependency since we use a shell script and SFE does not have a lockfile

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix session id for ldap

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix missing createTimestamp and modifyTimestamp ldap attributes

closes #10474

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2025-06-10 12:11:21 +02:00
committed by GitHub
parent 8bfc9ab7c9
commit 88fa7e37dc
25 changed files with 347 additions and 249 deletions

View File

@ -38,7 +38,14 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD
username, err := instance.binder.GetUsername(bindDN)
if err == nil {
selectedApp = instance.GetAppSlug()
return instance.binder.Bind(username, req)
c, err := instance.binder.Bind(username, req)
if c == ldap.LDAPResultSuccess {
f := instance.GetFlags(req.BindDN)
ls.connectionsSync.Lock()
ls.connections[f.SessionID()] = conn
ls.connectionsSync.Unlock()
}
return c, err
} else {
req.Log().WithError(err).Debug("Username not for instance")
}

View File

@ -27,8 +27,9 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
passed, err := fe.Execute()
flags := flags.UserFlags{
Session: fe.GetSession(),
UserPk: flags.InvalidUserPK,
Session: fe.SessionCookie(),
SessionJWT: fe.Session(),
UserPk: flags.InvalidUserPK,
}
// only set flags if we don't have flags for this DN yet
// as flags are only checked during the bind, we can remember whether a certain DN

View File

@ -0,0 +1,20 @@
package ldap
import "net"
func (ls *LDAPServer) Close(dn string, conn net.Conn) error {
ls.connectionsSync.Lock()
defer ls.connectionsSync.Unlock()
key := ""
for k, c := range ls.connections {
if c == conn {
key = k
break
}
}
if key == "" {
return nil
}
delete(ls.connections, key)
return nil
}

View File

@ -1,16 +1,30 @@
package flags
import (
"crypto/sha256"
"encoding/hex"
"net/http"
"github.com/golang-jwt/jwt/v5"
"goauthentik.io/api/v3"
"goauthentik.io/internal/outpost/flow"
)
const InvalidUserPK = -1
type UserFlags struct {
UserInfo *api.User
UserPk int32
CanSearch bool
Session *http.Cookie
UserInfo *api.User
UserPk int32
CanSearch bool
Session *http.Cookie
SessionJWT *jwt.Token
}
func (uf UserFlags) SessionID() string {
if uf.SessionJWT == nil {
return ""
}
h := sha256.New()
h.Write([]byte(uf.SessionJWT.Claims.(*flow.SessionCookieClaims).SessionID))
return hex.EncodeToString(h.Sum(nil))
}

View File

@ -18,21 +18,26 @@ import (
)
type LDAPServer struct {
s *ldap.Server
log *log.Entry
ac *ak.APIController
cs *ak.CryptoStore
defaultCert *tls.Certificate
providers []*ProviderInstance
s *ldap.Server
log *log.Entry
ac *ak.APIController
cs *ak.CryptoStore
defaultCert *tls.Certificate
providers []*ProviderInstance
connections map[string]net.Conn
connectionsSync sync.Mutex
}
func NewServer(ac *ak.APIController) ak.Outpost {
ls := &LDAPServer{
log: log.WithField("logger", "authentik.outpost.ldap"),
ac: ac,
cs: ak.NewCryptoStore(ac.Client.CryptoApi),
providers: []*ProviderInstance{},
log: log.WithField("logger", "authentik.outpost.ldap"),
ac: ac,
cs: ak.NewCryptoStore(ac.Client.CryptoApi),
providers: []*ProviderInstance{},
connections: map[string]net.Conn{},
connectionsSync: sync.Mutex{},
}
ac.AddEventHandler(ls.handleWSSessionEnd)
s := ldap.NewServer()
s.EnforceLDAP = true
@ -50,6 +55,7 @@ func NewServer(ac *ak.APIController) ak.Outpost {
s.BindFunc("", ls)
s.UnbindFunc("", ls)
s.SearchFunc("", ls)
s.CloseFunc("", ls)
return ls
}
@ -117,3 +123,23 @@ func (ls *LDAPServer) TimerFlowCacheExpiry(ctx context.Context) {
p.binder.TimerFlowCacheExpiry(ctx)
}
}
func (ls *LDAPServer) handleWSSessionEnd(ctx context.Context, msg ak.Event) error {
if msg.Instruction != ak.EventKindSessionEnd {
return nil
}
mmsg := ak.EventArgsSessionEnd{}
err := msg.ArgsAs(&mmsg)
if err != nil {
return err
}
ls.connectionsSync.Lock()
defer ls.connectionsSync.Unlock()
ls.log.Info("Disconnecting session due to session end event")
conn, ok := ls.connections[mmsg.SessionID]
if !ok {
return nil
}
delete(ls.connections, mmsg.SessionID)
return conn.Close()
}

View File

@ -44,38 +44,40 @@ func (ds *DirectSearcher) SearchSubschema(req *search.Request) (ldap.ServerSearc
{
Name: "attributeTypes",
Values: []string{
"( 2.5.4.0 NAME 'objectClass' SYNTAX '1.3.6.1.4.1.1466.115.121.1.38' NO-USER-MODIFICATION )",
"( 2.5.4.4 NAME 'sn' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.3 NAME 'cn' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.6 NAME 'c' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.7 NAME 'l' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.10 NAME 'o' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
"( 2.5.4.11 NAME 'ou' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
"( 2.5.4.12 NAME 'title' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.13 NAME 'description' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
"( 2.5.4.20 NAME 'telephoneNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.31 NAME 'member' SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' )",
"( 2.5.4.42 NAME 'givenName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.21.2 NAME 'dITContentRules' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' NO-USER-MODIFICATION )",
"( 2.5.21.5 NAME 'attributeTypes' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' NO-USER-MODIFICATION )",
"( 2.5.21.6 NAME 'objectClasses' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' NO-USER-MODIFICATION )",
"( 0.9.2342.19200300.100.1.1 NAME 'uid' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 0.9.2342.19200300.100.1.3 NAME 'mail' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 0.9.2342.19200300.100.1.41 NAME 'mobile' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.13 NAME 'displayName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.146 NAME 'company' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.102 NAME 'memberOf' SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' NO-USER-MODIFICATION )",
"( 1.2.840.113556.1.2.13 NAME 'displayName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.131 NAME 'co' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.141 NAME 'department' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.146 NAME 'company' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.1 NAME 'name' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE NO-USER-MODIFICATION )",
"( 1.2.840.113556.1.4.44 NAME 'homeDirectory' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.221 NAME 'sAMAccountName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.261 NAME 'division' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.44 NAME 'homeDirectory' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.750 NAME 'groupType' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.782 NAME 'objectCategory' SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' SINGLE-VALUE )",
"( 1.3.6.1.1.1.1.0 NAME 'uidNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
"( 1.3.6.1.1.1.1.1 NAME 'gidNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
"( 1.3.6.1.1.1.1.12 NAME 'memberUid' SYNTAX '1.3.6.1.4.1.1466.115.121.1.26' )",
"( 2.5.18.1 NAME 'createTimestamp' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION )",
"( 2.5.18.2 NAME 'modifyTimestamp' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION )",
"( 2.5.21.2 NAME 'dITContentRules' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' NO-USER-MODIFICATION )",
"( 2.5.21.5 NAME 'attributeTypes' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' NO-USER-MODIFICATION )",
"( 2.5.21.6 NAME 'objectClasses' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' NO-USER-MODIFICATION )",
"( 2.5.4.0 NAME 'objectClass' SYNTAX '1.3.6.1.4.1.1466.115.121.1.38' NO-USER-MODIFICATION )",
"( 2.5.4.10 NAME 'o' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
"( 2.5.4.11 NAME 'ou' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
"( 2.5.4.12 NAME 'title' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.13 NAME 'description' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
"( 2.5.4.20 NAME 'telephoneNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.3 NAME 'cn' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.31 NAME 'member' SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' )",
"( 2.5.4.4 NAME 'sn' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.42 NAME 'givenName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.6 NAME 'c' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.7 NAME 'l' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
// Custom attributes
// Temporarily use 1.3.6.1.4.1.26027.1.1 as a base