mirror of
https://github.com/goauthentik/authentik
synced 2026-05-05 22:52:42 +02:00
Compare commits
6 Commits
next
...
providers/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2005651723 | ||
|
|
46b2a1e88f | ||
|
|
cce1d92cda | ||
|
|
3cae85fb52 | ||
|
|
514e73c3aa | ||
|
|
cb036e73c2 |
@@ -94,7 +94,12 @@ class LDAPProvider(OutpostModel, BackchannelProvider):
|
||||
return f"LDAP Provider {self.name}"
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
|
||||
required = [self, "authentik_core.view_user", "authentik_core.view_group"]
|
||||
required = [
|
||||
self,
|
||||
"authentik_core.view_user",
|
||||
"authentik_core.view_group",
|
||||
"authentik_stages_mtls.pass_outpost_certificate",
|
||||
]
|
||||
if self.certificate is not None:
|
||||
required.append(("authentik_crypto.view_certificatekeypair", self.certificate))
|
||||
required.append(
|
||||
|
||||
@@ -12,6 +12,7 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderAuthentikRemoteIP = "X-authentik-remote-ip"
|
||||
HeaderAuthentikOutpostToken = "X-authentik-outpost-token"
|
||||
HeaderAuthentikRemoteIP = "X-authentik-remote-ip"
|
||||
HeaderAuthentikOutpostToken = "X-authentik-outpost-token"
|
||||
HeaderAuthentikOutpostCertificate = "X-authentik-outpost-certificate"
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@ package direct
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/pem"
|
||||
"net/url"
|
||||
|
||||
"beryju.io/ldap"
|
||||
"github.com/getsentry/sentry-go"
|
||||
@@ -21,6 +23,13 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
|
||||
})
|
||||
fe.DelegateClientIP(req.RemoteAddr())
|
||||
fe.Params.Add("goauthentik.io/outpost/ldap", "true")
|
||||
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
|
||||
pem := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: req.TLS.PeerCertificates[0].Raw,
|
||||
})
|
||||
fe.AddHeader(flow.HeaderAuthentikOutpostCertificate, url.QueryEscape(string(pem)))
|
||||
}
|
||||
|
||||
fe.Answers[flow.StageIdentification] = username
|
||||
fe.SetSecrets(req.Password, db.si.GetMFASupport())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"time"
|
||||
|
||||
"beryju.io/ldap"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
type Credentials struct {
|
||||
DN string
|
||||
Password string
|
||||
Cert *x509.Certificate
|
||||
}
|
||||
|
||||
type SessionBinder struct {
|
||||
@@ -43,10 +45,18 @@ func NewSessionBinder(si server.LDAPServerInstance, oldBinder bind.Binder) *Sess
|
||||
return sb
|
||||
}
|
||||
|
||||
func certOrNil(req *bind.Request) *x509.Certificate {
|
||||
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
|
||||
return req.TLS.PeerCertificates[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sb *SessionBinder) Bind(username string, req *bind.Request) (ldap.LDAPResultCode, error) {
|
||||
item := sb.sessions.Get(Credentials{
|
||||
DN: req.BindDN,
|
||||
Password: req.Password,
|
||||
Cert: certOrNil(req),
|
||||
})
|
||||
if item != nil {
|
||||
sb.log.WithField("bindDN", req.BindDN).Info("authenticated from session")
|
||||
@@ -64,6 +74,7 @@ func (sb *SessionBinder) Bind(username string, req *bind.Request) (ldap.LDAPResu
|
||||
sb.sessions.Set(Credentials{
|
||||
DN: req.BindDN,
|
||||
Password: req.Password,
|
||||
Cert: certOrNil(req),
|
||||
}, result, time.Until(flags.Session.Expires))
|
||||
}
|
||||
return result, err
|
||||
|
||||
@@ -39,7 +39,6 @@ func NewRequest(req ldap.BindRequest, conn net.Conn) (*Request, *sentry.Span) {
|
||||
IPAddress: utils.GetIP(conn.RemoteAddr()),
|
||||
})
|
||||
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
return &Request{
|
||||
BindRequest: req,
|
||||
conn: conn,
|
||||
|
||||
@@ -42,6 +42,7 @@ func NewServer(ac *ak.APIController) ak.Outpost {
|
||||
|
||||
tlsConfig := utils.GetTLSConfig()
|
||||
tlsConfig.GetCertificate = ls.getCertificates
|
||||
tlsConfig.ClientAuth = tls.RequestClientCert
|
||||
s.StartTLS = tlsConfig
|
||||
|
||||
ls.s = s
|
||||
|
||||
@@ -38,6 +38,7 @@ func (ls *LDAPServer) getCertificates(info *tls.ClientHelloInfo) (*tls.Certifica
|
||||
|
||||
func (ls *LDAPServer) StartLDAPTLSServer(listen string) error {
|
||||
tlsConfig := utils.GetTLSConfig()
|
||||
tlsConfig.ClientAuth = tls.RequestClientCert
|
||||
tlsConfig.GetCertificate = ls.getCertificates
|
||||
|
||||
ln, err := net.Listen("tcp", listen)
|
||||
|
||||
@@ -86,7 +86,7 @@ func (pi *ProviderInstance) GetEAPSettings() protocol.Settings {
|
||||
fe.Answers[flow.StageIdentification] = ident
|
||||
fe.DelegateClientIP(utils.GetIP(ctx.Packet().RemoteAddr))
|
||||
fe.Params.Add("goauthentik.io/outpost/radius", "true")
|
||||
fe.AddHeader("X-Authentik-Outpost-Certificate", url.QueryEscape(string(pem)))
|
||||
fe.AddHeader(flow.HeaderAuthentikOutpostCertificate, url.QueryEscape(string(pem)))
|
||||
|
||||
passed, err := fe.Execute()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,20 +1,36 @@
|
||||
"""LDAP and Outpost e2e tests"""
|
||||
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
from time import sleep
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
|
||||
from ldap3 import (
|
||||
ALL,
|
||||
ALL_ATTRIBUTES,
|
||||
ALL_OPERATIONAL_ATTRIBUTES,
|
||||
EXTERNAL,
|
||||
SASL,
|
||||
SUBTREE,
|
||||
Connection,
|
||||
Server,
|
||||
Tls,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPInvalidCredentialsResult, LDAPSessionTerminatedByServerError
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint, reconcile_app
|
||||
from authentik.core.models import Application, AuthenticatedSession, User
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.core.tests.utils import create_test_cert, create_test_flow, create_test_user
|
||||
from authentik.endpoints.models import StageMode
|
||||
from authentik.enterprise.stages.mtls.models import MutualTLSStage
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType
|
||||
from authentik.providers.ldap.models import APIAccessMode, LDAPProvider
|
||||
from authentik.stages.user_login.models import UserLoginStage
|
||||
from tests.decorators import retry
|
||||
from tests.live import ChannelsE2ETestCase
|
||||
|
||||
@@ -32,6 +48,16 @@ def clean_response(response):
|
||||
class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
"""LDAP and Outpost e2e tests"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.kp = create_test_cert()
|
||||
self.cert = Path(gettempdir()) / generate_id()
|
||||
with open(self.cert, "w") as _cert:
|
||||
_cert.write(self.kp.certificate_data)
|
||||
self.key = Path(gettempdir()) / generate_id()
|
||||
with open(self.key, "w") as _key:
|
||||
_key.write(self.kp.key_data)
|
||||
|
||||
def assert_list_dict_equal(self, expected: list[dict], actual: list[dict], match_key="dn"):
|
||||
"""Assert a list of dictionaries is identical, ignoring the ordering of items"""
|
||||
self.assertEqual(len(expected), len(actual))
|
||||
@@ -54,15 +80,17 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def _prepare(self) -> User:
|
||||
def _prepare(self, **kwargs) -> User:
|
||||
"""prepare user, provider, app and container"""
|
||||
self.user.attributes["extraAttribute"] = "bar"
|
||||
self.user.save()
|
||||
|
||||
ldap: LDAPProvider = LDAPProvider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=Flow.objects.get(slug="default-authentication-flow"),
|
||||
search_mode=APIAccessMode.CACHED,
|
||||
kwargs.setdefault(
|
||||
"authorization_flow", Flow.objects.get(slug="default-authentication-flow")
|
||||
)
|
||||
|
||||
ldap = LDAPProvider.objects.create(
|
||||
name=generate_id(), search_mode=APIAccessMode.CACHED, **kwargs
|
||||
)
|
||||
self.user.assign_perms_to_managed_role(
|
||||
"authentik_providers_ldap.search_full_directory", ldap
|
||||
@@ -95,7 +123,7 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
password=self.user.username,
|
||||
)
|
||||
_connection.bind()
|
||||
self.assertTrue(
|
||||
self.assertIsNotNone(
|
||||
Event.objects.filter(
|
||||
action=EventAction.LOGIN,
|
||||
user={
|
||||
@@ -103,7 +131,7 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
"email": self.user.email,
|
||||
"username": self.user.username,
|
||||
},
|
||||
)
|
||||
).first()
|
||||
)
|
||||
|
||||
@retry()
|
||||
@@ -122,7 +150,7 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
password=self.user.username,
|
||||
)
|
||||
_connection.bind()
|
||||
self.assertTrue(
|
||||
self.assertIsNotNone(
|
||||
Event.objects.filter(
|
||||
action=EventAction.LOGIN,
|
||||
user={
|
||||
@@ -130,7 +158,7 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
"email": self.user.email,
|
||||
"username": self.user.username,
|
||||
},
|
||||
)
|
||||
).first()
|
||||
)
|
||||
|
||||
@retry()
|
||||
@@ -150,7 +178,7 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
)
|
||||
_connection.start_tls()
|
||||
_connection.bind()
|
||||
self.assertTrue(
|
||||
self.assertIsNotNone(
|
||||
Event.objects.filter(
|
||||
action=EventAction.LOGIN,
|
||||
user={
|
||||
@@ -158,7 +186,70 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
"email": self.user.email,
|
||||
"username": self.user.username,
|
||||
},
|
||||
)
|
||||
).first()
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
"default/flow-default-invalidation-flow.yaml",
|
||||
)
|
||||
def test_ldap_bind_success_starttls_sasl(self):
|
||||
"""Test SASL bind with ssl"""
|
||||
# Create flow with MTLS Stage
|
||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||
mtls_stage = MutualTLSStage.objects.create(
|
||||
name=generate_id(),
|
||||
mode=StageMode.REQUIRED,
|
||||
)
|
||||
mtls_stage.certificate_authorities.add(self.kp)
|
||||
login_stage = UserLoginStage.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
FlowStageBinding.objects.create(target=flow, stage=mtls_stage, order=0)
|
||||
FlowStageBinding.objects.create(target=flow, stage=login_stage, order=1)
|
||||
|
||||
self._prepare(authorization_flow=flow, certificate=self.kp)
|
||||
|
||||
tls = Tls(
|
||||
local_private_key_file=self.key,
|
||||
local_certificate_file=self.cert,
|
||||
)
|
||||
server = Server("ldap://localhost:3389", tls=tls)
|
||||
_connection = Connection(
|
||||
server,
|
||||
version=3,
|
||||
raise_exceptions=True,
|
||||
authentication=SASL,
|
||||
sasl_mechanism=EXTERNAL,
|
||||
sasl_credentials=f"dn:cn={self.user.username},ou=users,DC=ldap,DC=goauthentik,DC=io",
|
||||
)
|
||||
_connection.start_tls()
|
||||
with (
|
||||
patch(
|
||||
"authentik.enterprise.stages.mtls.stage.MTLSStageView.validate_cert",
|
||||
MagicMock(return_value=self.kp.certificate),
|
||||
),
|
||||
patch(
|
||||
"authentik.enterprise.stages.mtls.stage.MTLSStageView.check_if_user",
|
||||
MagicMock(return_value=self.user),
|
||||
) as check_if_user,
|
||||
):
|
||||
_connection.bind()
|
||||
check_if_user.assert_called_once_with(self.kp.certificate)
|
||||
event = Event.objects.filter(
|
||||
action=EventAction.LOGIN,
|
||||
user={
|
||||
"pk": self.user.pk,
|
||||
"email": self.user.email,
|
||||
"username": self.user.username,
|
||||
},
|
||||
).first()
|
||||
self.assertIsNotNone(event)
|
||||
self.assertEqual(event.context["auth_method"], "mtls")
|
||||
self.assertEqual(
|
||||
event.context["auth_method_args"]["certificate"]["fingerprint_sha256"],
|
||||
self.kp.fingerprint_sha256,
|
||||
)
|
||||
|
||||
@retry()
|
||||
@@ -178,7 +269,7 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
)
|
||||
with self.assertRaises(LDAPInvalidCredentialsResult):
|
||||
_connection.bind()
|
||||
self.assertTrue(
|
||||
self.assertIsNotNone(
|
||||
Event.objects.filter(
|
||||
action=EventAction.LOGIN_FAILED,
|
||||
user={
|
||||
@@ -186,7 +277,7 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
"email": self.user.email,
|
||||
"username": self.user.username,
|
||||
},
|
||||
).exists(),
|
||||
).first(),
|
||||
)
|
||||
|
||||
@retry()
|
||||
@@ -211,7 +302,7 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
password=self.user.username,
|
||||
)
|
||||
_connection.bind()
|
||||
self.assertTrue(
|
||||
self.assertIsNotNone(
|
||||
Event.objects.filter(
|
||||
action=EventAction.LOGIN,
|
||||
user={
|
||||
@@ -219,7 +310,7 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
"email": self.user.email,
|
||||
"username": self.user.username,
|
||||
},
|
||||
)
|
||||
).first()
|
||||
)
|
||||
|
||||
embedded_account = Outpost.objects.filter(managed=MANAGED_OUTPOST).first().user
|
||||
@@ -349,7 +440,7 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
password=user.username,
|
||||
)
|
||||
_connection.bind()
|
||||
self.assertTrue(
|
||||
self.assertIsNotNone(
|
||||
Event.objects.filter(
|
||||
action=EventAction.LOGIN,
|
||||
user={
|
||||
@@ -357,7 +448,7 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
"email": user.email,
|
||||
"username": user.username,
|
||||
},
|
||||
)
|
||||
).first()
|
||||
)
|
||||
|
||||
_connection.search(
|
||||
@@ -448,7 +539,7 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
password=self.user.username,
|
||||
)
|
||||
_connection.bind()
|
||||
self.assertTrue(
|
||||
self.assertIsNotNone(
|
||||
Event.objects.filter(
|
||||
action=EventAction.LOGIN,
|
||||
user={
|
||||
@@ -456,7 +547,7 @@ class TestProviderLDAP(ChannelsE2ETestCase):
|
||||
"email": self.user.email,
|
||||
"username": self.user.username,
|
||||
},
|
||||
)
|
||||
).first()
|
||||
)
|
||||
|
||||
embedded_account = Outpost.objects.filter(managed=MANAGED_OUTPOST).first().user
|
||||
|
||||
Reference in New Issue
Block a user