From 01311929d1174d1f5e90ab3b17cb370f779f7b60 Mon Sep 17 00:00:00 2001
From: Jens L
Date: Tue, 20 Jun 2023 12:09:13 +0200
Subject: [PATCH] providers/ldap: improve password totp detection (#6006)
* providers/ldap: improve password totp detection
Signed-off-by: Jens Langhammer
* add flag for totp mfa support
Signed-off-by: Jens Langhammer
* keep support for static tokens
Signed-off-by: Jens Langhammer
* fix migrations
Signed-off-by: Jens Langhammer
---------
Signed-off-by: Jens Langhammer
---
...vider_backchannel_applications_and_more.py | 2 +-
authentik/providers/ldap/api.py | 2 +
.../0003_ldapprovider_mfa_support_and_more.py | 37 +++++++++++++
authentik/providers/ldap/models.py | 15 +++++-
blueprints/schema.json | 9 +++-
internal/outpost/flow/const.go | 2 -
internal/outpost/flow/solvers.go | 13 -----
internal/outpost/ldap/bind/direct/bind.go | 46 ++++++++++++++++
internal/outpost/ldap/instance.go | 5 ++
internal/outpost/ldap/refresh.go | 7 +--
internal/outpost/ldap/server/base.go | 1 +
locale/en/LC_MESSAGES/django.po | 19 +++++--
schema.yml | 44 +++++++++++++---
.../admin/providers/ldap/LDAPProviderForm.ts | 21 ++++++++
web/xliff/de.xlf | 6 +++
web/xliff/en.xlf | 6 +++
web/xliff/es.xlf | 6 +++
web/xliff/fr_FR.xlf | 6 +++
web/xliff/pl.xlf | 6 +++
web/xliff/pseudo-LOCALE.xlf | 6 +++
web/xliff/tr.xlf | 6 +++
web/xliff/zh-Hans.xlf | 52 +++++++++++--------
web/xliff/zh-Hant.xlf | 6 +++
web/xliff/zh_TW.xlf | 6 +++
website/docs/providers/ldap/index.md | 2 +
25 files changed, 272 insertions(+), 59 deletions(-)
create mode 100644 authentik/providers/ldap/migrations/0003_ldapprovider_mfa_support_and_more.py
diff --git a/authentik/core/migrations/0029_provider_backchannel_applications_and_more.py b/authentik/core/migrations/0029_provider_backchannel_applications_and_more.py
index 07cb4f2df..f34c59ccb 100644
--- a/authentik/core/migrations/0029_provider_backchannel_applications_and_more.py
+++ b/authentik/core/migrations/0029_provider_backchannel_applications_and_more.py
@@ -11,7 +11,7 @@ def backport_is_backchannel(apps: Apps, schema_editor: BaseDatabaseSchemaEditor)
for model in BackchannelProvider.__subclasses__():
try:
- for obj in model.objects.all():
+ for obj in model.objects.only("is_backchannel"):
obj.is_backchannel = True
obj.save()
except (DatabaseError, InternalError, ProgrammingError):
diff --git a/authentik/providers/ldap/api.py b/authentik/providers/ldap/api.py
index 870d66ee5..21438ef43 100644
--- a/authentik/providers/ldap/api.py
+++ b/authentik/providers/ldap/api.py
@@ -29,6 +29,7 @@ class LDAPProviderSerializer(ProviderSerializer):
"outpost_set",
"search_mode",
"bind_mode",
+ "mfa_support",
]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
@@ -99,6 +100,7 @@ class LDAPOutpostConfigSerializer(ModelSerializer):
"gid_start_number",
"search_mode",
"bind_mode",
+ "mfa_support",
]
diff --git a/authentik/providers/ldap/migrations/0003_ldapprovider_mfa_support_and_more.py b/authentik/providers/ldap/migrations/0003_ldapprovider_mfa_support_and_more.py
new file mode 100644
index 000000000..f5cd61cfa
--- /dev/null
+++ b/authentik/providers/ldap/migrations/0003_ldapprovider_mfa_support_and_more.py
@@ -0,0 +1,37 @@
+# Generated by Django 4.1.7 on 2023-06-19 17:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("authentik_providers_ldap", "0002_ldapprovider_bind_mode"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="ldapprovider",
+ name="mfa_support",
+ field=models.BooleanField(
+ default=True,
+ help_text="When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
+ verbose_name="MFA Support",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="ldapprovider",
+ name="gid_start_number",
+ field=models.IntegerField(
+ default=4000,
+ help_text="The start for gidNumbers, this number is added to a number generated from the group.pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="ldapprovider",
+ name="uid_start_number",
+ field=models.IntegerField(
+ default=2000,
+ help_text="The start for uidNumbers, this number is added to the user.pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber",
+ ),
+ ),
+ ]
diff --git a/authentik/providers/ldap/models.py b/authentik/providers/ldap/models.py
index 581d50230..d03f5e1f3 100644
--- a/authentik/providers/ldap/models.py
+++ b/authentik/providers/ldap/models.py
@@ -50,7 +50,7 @@ class LDAPProvider(OutpostModel, BackchannelProvider):
uid_start_number = models.IntegerField(
default=2000,
help_text=_(
- "The start for uidNumbers, this number is added to the user.Pk to make sure that the "
+ "The start for uidNumbers, this number is added to the user.pk to make sure that the "
"numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't "
"collide with local users uidNumber"
),
@@ -60,7 +60,7 @@ class LDAPProvider(OutpostModel, BackchannelProvider):
default=4000,
help_text=_(
"The start for gidNumbers, this number is added to a number generated from the "
- "group.Pk to make sure that the numbers aren't too low for POSIX groups. Default "
+ "group.pk to make sure that the numbers aren't too low for POSIX groups. Default "
"is 4000 to ensure that we don't collide with local groups or users "
"primary groups gidNumber"
),
@@ -69,6 +69,17 @@ class LDAPProvider(OutpostModel, BackchannelProvider):
bind_mode = models.TextField(default=APIAccessMode.DIRECT, choices=APIAccessMode.choices)
search_mode = models.TextField(default=APIAccessMode.DIRECT, choices=APIAccessMode.choices)
+ mfa_support = models.BooleanField(
+ default=True,
+ verbose_name="MFA Support",
+ help_text=_(
+ "When enabled, code-based multi-factor authentication can be used by appending a "
+ "semicolon and the TOTP code to the password. This should only be enabled if all "
+ "users that will bind to this provider have a TOTP device configured, as otherwise "
+ "a password may incorrectly be rejected if it contains a semicolon."
+ ),
+ )
+
@property
def launch_url(self) -> Optional[str]:
"""LDAP never has a launch URL"""
diff --git a/blueprints/schema.json b/blueprints/schema.json
index 8f41c9449..69c046195 100644
--- a/blueprints/schema.json
+++ b/blueprints/schema.json
@@ -3620,14 +3620,14 @@
"minimum": -2147483648,
"maximum": 2147483647,
"title": "Uid start number",
- "description": "The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber"
+ "description": "The start for uidNumbers, this number is added to the user.pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber"
},
"gid_start_number": {
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647,
"title": "Gid start number",
- "description": "The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber"
+ "description": "The start for gidNumbers, this number is added to a number generated from the group.pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber"
},
"search_mode": {
"type": "string",
@@ -3644,6 +3644,11 @@
"cached"
],
"title": "Bind mode"
+ },
+ "mfa_support": {
+ "type": "boolean",
+ "title": "MFA Support",
+ "description": "When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon."
}
},
"required": []
diff --git a/internal/outpost/flow/const.go b/internal/outpost/flow/const.go
index acecf3bd9..26fcb2f9b 100644
--- a/internal/outpost/flow/const.go
+++ b/internal/outpost/flow/const.go
@@ -14,5 +14,3 @@ const (
HeaderAuthentikRemoteIP = "X-authentik-remote-ip"
HeaderAuthentikOutpostToken = "X-authentik-outpost-token"
)
-
-const CodePasswordSeparator = ";"
diff --git a/internal/outpost/flow/solvers.go b/internal/outpost/flow/solvers.go
index a809571a3..5006bbd75 100644
--- a/internal/outpost/flow/solvers.go
+++ b/internal/outpost/flow/solvers.go
@@ -3,21 +3,10 @@ package flow
import (
"errors"
"strconv"
- "strings"
"goauthentik.io/api/v3"
)
-func (fe *FlowExecutor) checkPasswordMFA() {
- password := fe.getAnswer(StagePassword)
- if !strings.Contains(password, CodePasswordSeparator) || fe.Answers[StageAuthenticatorValidate] != "" {
- return
- }
- idx := strings.LastIndex(password, CodePasswordSeparator)
- fe.Answers[StagePassword] = password[:idx]
- fe.Answers[StageAuthenticatorValidate] = password[idx+1:]
-}
-
func (fe *FlowExecutor) solveChallenge_Identification(challenge *api.ChallengeTypes, req api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error) {
r := api.NewIdentificationChallengeResponseRequest(fe.getAnswer(StageIdentification))
r.SetPassword(fe.getAnswer(StagePassword))
@@ -25,7 +14,6 @@ func (fe *FlowExecutor) solveChallenge_Identification(challenge *api.ChallengeTy
}
func (fe *FlowExecutor) solveChallenge_Password(challenge *api.ChallengeTypes, req api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error) {
- fe.checkPasswordMFA()
r := api.NewPasswordChallengeResponseRequest(fe.getAnswer(StagePassword))
return api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil
}
@@ -52,7 +40,6 @@ func (fe *FlowExecutor) solveChallenge_AuthenticatorValidate(challenge *api.Chal
}
if devCh.DeviceClass == string(api.DEVICECLASSESENUM_STATIC) ||
devCh.DeviceClass == string(api.DEVICECLASSESENUM_TOTP) {
- fe.checkPasswordMFA()
// Only use code-based devices if we have a code in the entered password,
// and we haven't selected a push device yet
if deviceChallenge == nil && fe.getAnswer(StageAuthenticatorValidate) != "" {
diff --git a/internal/outpost/ldap/bind/direct/bind.go b/internal/outpost/ldap/bind/direct/bind.go
index 0ec9649bb..0806bff88 100644
--- a/internal/outpost/ldap/bind/direct/bind.go
+++ b/internal/outpost/ldap/bind/direct/bind.go
@@ -2,6 +2,9 @@ package direct
import (
"context"
+ "regexp"
+ "strconv"
+ "strings"
"beryju.io/ldap"
"github.com/getsentry/sentry-go"
@@ -13,6 +16,10 @@ import (
"goauthentik.io/internal/outpost/ldap/metrics"
)
+const CodePasswordSeparator = ";"
+
+var alphaNum = regexp.MustCompile(`^[a-zA-Z0-9]*$`)
+
func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResultCode, error) {
fe := flow.NewFlowExecutor(req.Context(), db.si.GetAuthenticationFlowSlug(), db.si.GetAPIClient().GetConfig(), log.Fields{
"bindDN": req.BindDN,
@@ -24,6 +31,7 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
fe.Answers[flow.StageIdentification] = username
fe.Answers[flow.StagePassword] = req.BindPW
+ db.CheckPasswordMFA(fe)
passed, err := fe.Execute()
flags := flags.UserFlags{
@@ -96,3 +104,41 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
uisp.Finish()
return ldap.LDAPResultSuccess, nil
}
+
+func (db *DirectBinder) CheckPasswordMFA(fe *flow.FlowExecutor) {
+ if !db.si.GetMFASupport() {
+ return
+ }
+ password := fe.Answers[flow.StagePassword]
+ // We already have an authenticator answer
+ if fe.Answers[flow.StageAuthenticatorValidate] != "" {
+ return
+ }
+ // password doesn't contain the separator
+ if !strings.Contains(password, CodePasswordSeparator) {
+ return
+ }
+ // password ends with the separator, so it won't contain an answer
+ if strings.HasSuffix(password, CodePasswordSeparator) {
+ return
+ }
+ idx := strings.LastIndex(password, CodePasswordSeparator)
+ authenticator := password[idx+1:]
+ // Authenticator is either 6 chars (totp code) or 8 chars (long totp or static)
+ if len(authenticator) == 6 {
+ // authenticator answer isn't purely numerical, so won't be value
+ if _, err := strconv.Atoi(authenticator); err != nil {
+ return
+ }
+ } else if len(authenticator) == 8 {
+ // 8 chars can be a long totp or static token, so it needs to be alphanumerical
+ if !alphaNum.MatchString(authenticator) {
+ return
+ }
+ } else {
+ // Any other length, doesn't contain an answer
+ return
+ }
+ fe.Answers[flow.StagePassword] = password[:idx]
+ fe.Answers[flow.StageAuthenticatorValidate] = authenticator
+}
diff --git a/internal/outpost/ldap/instance.go b/internal/outpost/ldap/instance.go
index 23fbc9fb5..fe6ef7b71 100644
--- a/internal/outpost/ldap/instance.go
+++ b/internal/outpost/ldap/instance.go
@@ -42,6 +42,7 @@ type ProviderInstance struct {
uidStartNumber int32
gidStartNumber int32
+ mfaSupport bool
}
func (pi *ProviderInstance) GetAPIClient() *api.APIClient {
@@ -68,6 +69,10 @@ func (pi *ProviderInstance) GetOutpostName() string {
return pi.outpostName
}
+func (pi *ProviderInstance) GetMFASupport() bool {
+ return pi.mfaSupport
+}
+
func (pi *ProviderInstance) GetFlags(dn string) *flags.UserFlags {
pi.boundUsersMutex.RLock()
defer pi.boundUsersMutex.RUnlock()
diff --git a/internal/outpost/ldap/refresh.go b/internal/outpost/ldap/refresh.go
index 0d121ad46..2ecd9a759 100644
--- a/internal/outpost/ldap/refresh.go
+++ b/internal/outpost/ldap/refresh.go
@@ -66,7 +66,7 @@ func (ls *LDAPServer) Refresh() error {
}
providers[idx] = &ProviderInstance{
- BaseDN: *provider.BaseDn,
+ BaseDN: provider.GetBaseDn(),
VirtualGroupDN: virtualGroupDN,
GroupDN: groupDN,
UserDN: userDN,
@@ -79,8 +79,9 @@ func (ls *LDAPServer) Refresh() error {
s: ls,
log: logger,
tlsServerName: provider.TlsServerName,
- uidStartNumber: *provider.UidStartNumber,
- gidStartNumber: *provider.GidStartNumber,
+ uidStartNumber: provider.GetUidStartNumber(),
+ gidStartNumber: provider.GetGidStartNumber(),
+ mfaSupport: provider.GetMfaSupport(),
outpostName: ls.ac.Outpost.Name,
outpostPk: provider.Pk,
}
diff --git a/internal/outpost/ldap/server/base.go b/internal/outpost/ldap/server/base.go
index d6227e77e..ff6649a03 100644
--- a/internal/outpost/ldap/server/base.go
+++ b/internal/outpost/ldap/server/base.go
@@ -22,6 +22,7 @@ type LDAPServerInstance interface {
GetBaseGroupDN() string
GetBaseVirtualGroupDN() string
GetBaseUserDN() string
+ GetMFASupport() bool
GetUserDN(string) string
GetGroupDN(string) string
diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po
index 44b6ccb85..e588ad891 100644
--- a/locale/en/LC_MESSAGES/django.po
+++ b/locale/en/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-06-13 11:23+0000\n"
+"POT-Creation-Date: 2023-06-19 17:34+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -861,7 +861,7 @@ msgstr ""
#: authentik/providers/ldap/models.py:53
msgid ""
-"The start for uidNumbers, this number is added to the user.Pk to make sure "
+"The start for uidNumbers, this number is added to the user.pk to make sure "
"that the numbers aren't too low for POSIX users. Default is 2000 to ensure "
"that we don't collide with local users uidNumber"
msgstr ""
@@ -869,16 +869,25 @@ msgstr ""
#: authentik/providers/ldap/models.py:62
msgid ""
"The start for gidNumbers, this number is added to a number generated from "
-"the group.Pk to make sure that the numbers aren't too low for POSIX groups. "
+"the group.pk to make sure that the numbers aren't too low for POSIX groups. "
"Default is 4000 to ensure that we don't collide with local groups or users "
"primary groups gidNumber"
msgstr ""
-#: authentik/providers/ldap/models.py:97
+#: authentik/providers/ldap/models.py:76
+msgid ""
+"When enabled, code-based multi-factor authentication can be used by "
+"appending a semicolon and the TOTP code to the password. This should only be "
+"enabled if all users that will bind to this provider have a TOTP device "
+"configured, as otherwise a password may incorrectly be rejected if it "
+"contains a semicolon."
+msgstr ""
+
+#: authentik/providers/ldap/models.py:108
msgid "LDAP Provider"
msgstr ""
-#: authentik/providers/ldap/models.py:98
+#: authentik/providers/ldap/models.py:109
msgid "LDAP Providers"
msgstr ""
diff --git a/schema.yml b/schema.yml
index 857e4d2a1..211e1e084 100644
--- a/schema.yml
+++ b/schema.yml
@@ -30796,7 +30796,7 @@ components:
type: integer
maximum: 2147483647
minimum: -2147483648
- description: The start for uidNumbers, this number is added to the user.Pk
+ description: The start for uidNumbers, this number is added to the user.pk
to make sure that the numbers aren't too low for POSIX users. Default
is 2000 to ensure that we don't collide with local users uidNumber
gid_start_number:
@@ -30804,13 +30804,20 @@ components:
maximum: 2147483647
minimum: -2147483648
description: The start for gidNumbers, this number is added to a number
- generated from the group.Pk to make sure that the numbers aren't too low
+ generated from the group.pk to make sure that the numbers aren't too low
for POSIX groups. Default is 4000 to ensure that we don't collide with
local groups or users primary groups gidNumber
search_mode:
$ref: '#/components/schemas/LDAPAPIAccessMode'
bind_mode:
$ref: '#/components/schemas/LDAPAPIAccessMode'
+ mfa_support:
+ type: boolean
+ description: When enabled, code-based multi-factor authentication can be
+ used by appending a semicolon and the TOTP code to the password. This
+ should only be enabled if all users that will bind to this provider have
+ a TOTP device configured, as otherwise a password may incorrectly be rejected
+ if it contains a semicolon.
required:
- application_slug
- bind_flow_slug
@@ -30966,7 +30973,7 @@ components:
type: integer
maximum: 2147483647
minimum: -2147483648
- description: The start for uidNumbers, this number is added to the user.Pk
+ description: The start for uidNumbers, this number is added to the user.pk
to make sure that the numbers aren't too low for POSIX users. Default
is 2000 to ensure that we don't collide with local users uidNumber
gid_start_number:
@@ -30974,7 +30981,7 @@ components:
maximum: 2147483647
minimum: -2147483648
description: The start for gidNumbers, this number is added to a number
- generated from the group.Pk to make sure that the numbers aren't too low
+ generated from the group.pk to make sure that the numbers aren't too low
for POSIX groups. Default is 4000 to ensure that we don't collide with
local groups or users primary groups gidNumber
outpost_set:
@@ -30986,6 +30993,13 @@ components:
$ref: '#/components/schemas/LDAPAPIAccessMode'
bind_mode:
$ref: '#/components/schemas/LDAPAPIAccessMode'
+ mfa_support:
+ type: boolean
+ description: When enabled, code-based multi-factor authentication can be
+ used by appending a semicolon and the TOTP code to the password. This
+ should only be enabled if all users that will bind to this provider have
+ a TOTP device configured, as otherwise a password may incorrectly be rejected
+ if it contains a semicolon.
required:
- assigned_application_name
- assigned_application_slug
@@ -31041,7 +31055,7 @@ components:
type: integer
maximum: 2147483647
minimum: -2147483648
- description: The start for uidNumbers, this number is added to the user.Pk
+ description: The start for uidNumbers, this number is added to the user.pk
to make sure that the numbers aren't too low for POSIX users. Default
is 2000 to ensure that we don't collide with local users uidNumber
gid_start_number:
@@ -31049,13 +31063,20 @@ components:
maximum: 2147483647
minimum: -2147483648
description: The start for gidNumbers, this number is added to a number
- generated from the group.Pk to make sure that the numbers aren't too low
+ generated from the group.pk to make sure that the numbers aren't too low
for POSIX groups. Default is 4000 to ensure that we don't collide with
local groups or users primary groups gidNumber
search_mode:
$ref: '#/components/schemas/LDAPAPIAccessMode'
bind_mode:
$ref: '#/components/schemas/LDAPAPIAccessMode'
+ mfa_support:
+ type: boolean
+ description: When enabled, code-based multi-factor authentication can be
+ used by appending a semicolon and the TOTP code to the password. This
+ should only be enabled if all users that will bind to this provider have
+ a TOTP device configured, as otherwise a password may incorrectly be rejected
+ if it contains a semicolon.
required:
- authorization_flow
- name
@@ -36721,7 +36742,7 @@ components:
type: integer
maximum: 2147483647
minimum: -2147483648
- description: The start for uidNumbers, this number is added to the user.Pk
+ description: The start for uidNumbers, this number is added to the user.pk
to make sure that the numbers aren't too low for POSIX users. Default
is 2000 to ensure that we don't collide with local users uidNumber
gid_start_number:
@@ -36729,13 +36750,20 @@ components:
maximum: 2147483647
minimum: -2147483648
description: The start for gidNumbers, this number is added to a number
- generated from the group.Pk to make sure that the numbers aren't too low
+ generated from the group.pk to make sure that the numbers aren't too low
for POSIX groups. Default is 4000 to ensure that we don't collide with
local groups or users primary groups gidNumber
search_mode:
$ref: '#/components/schemas/LDAPAPIAccessMode'
bind_mode:
$ref: '#/components/schemas/LDAPAPIAccessMode'
+ mfa_support:
+ type: boolean
+ description: When enabled, code-based multi-factor authentication can be
+ used by appending a semicolon and the TOTP code to the password. This
+ should only be enabled if all users that will bind to this provider have
+ a TOTP device configured, as otherwise a password may incorrectly be rejected
+ if it contains a semicolon.
PatchedLDAPSourceRequest:
type: object
description: LDAP Source Serializer
diff --git a/web/src/admin/providers/ldap/LDAPProviderForm.ts b/web/src/admin/providers/ldap/LDAPProviderForm.ts
index 6c240b2c2..0a4df7ada 100644
--- a/web/src/admin/providers/ldap/LDAPProviderForm.ts
+++ b/web/src/admin/providers/ldap/LDAPProviderForm.ts
@@ -187,6 +187,27 @@ export class LDAPProviderFormPage extends ModelForm {
${msg("Configure how the outpost queries the core authentik server's users.")}
+
+
+
+ ${msg(
+ "When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
+ )}
+
+
+
${msg("Protocol settings")}
diff --git a/web/xliff/de.xlf b/web/xliff/de.xlf
index a90848884..ec897f2f5 100644
--- a/web/xliff/de.xlf
+++ b/web/xliff/de.xlf
@@ -5741,6 +5741,12 @@ Bindings to groups/users are checked against the user of the event.
+
+
+
+
+
+