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:
@ -14,5 +14,3 @@ const (
|
||||
HeaderAuthentikRemoteIP = "X-authentik-remote-ip"
|
||||
HeaderAuthentikOutpostToken = "X-authentik-outpost-token"
|
||||
)
|
||||
|
||||
const CodePasswordSeparator = ";"
|
||||
|
@ -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) != "" {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ type LDAPServerInstance interface {
|
||||
GetBaseGroupDN() string
|
||||
GetBaseVirtualGroupDN() string
|
||||
GetBaseUserDN() string
|
||||
GetMFASupport() bool
|
||||
|
||||
GetUserDN(string) string
|
||||
GetGroupDN(string) string
|
||||
|
Reference in New Issue
Block a user