providers/ldap: improve password totp detection (#6006)

* providers/ldap: improve password totp detection

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

* add flag for totp mfa support

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

* keep support for static tokens

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

* fix migrations

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L
2023-06-20 12:09:13 +02:00
committed by GitHub
parent 962cbf9f6a
commit 01311929d1
25 changed files with 272 additions and 59 deletions

View File

@ -14,5 +14,3 @@ const (
HeaderAuthentikRemoteIP = "X-authentik-remote-ip"
HeaderAuthentikOutpostToken = "X-authentik-outpost-token"
)
const CodePasswordSeparator = ";"

View File

@ -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) != "" {

View File

@ -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
}

View File

@ -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()

View File

@ -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,
}

View File

@ -22,6 +22,7 @@ type LDAPServerInstance interface {
GetBaseGroupDN() string
GetBaseVirtualGroupDN() string
GetBaseUserDN() string
GetMFASupport() bool
GetUserDN(string) string
GetGroupDN(string) string