diff --git a/internal/outpost/flow/const.go b/internal/outpost/flow/const.go index 3ebcb9434..7b4436a63 100644 --- a/internal/outpost/flow/const.go +++ b/internal/outpost/flow/const.go @@ -13,3 +13,5 @@ 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 487f4534d..c70e3e2d9 100644 --- a/internal/outpost/flow/solvers.go +++ b/internal/outpost/flow/solvers.go @@ -3,10 +3,21 @@ 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)) @@ -19,23 +30,35 @@ func (fe *FlowExecutor) solveChallenge_Password(challenge *api.ChallengeTypes, r } func (fe *FlowExecutor) solveChallenge_AuthenticatorValidate(challenge *api.ChallengeTypes, req api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error) { - // We only support duo as authenticator, check if that's allowed + // We only support duo and code-based authenticators, check if that's allowed var deviceChallenge *api.DeviceChallenge + inner := api.NewAuthenticatorValidationChallengeResponseRequest() for _, devCh := range challenge.AuthenticatorValidationChallenge.DeviceChallenges { if devCh.DeviceClass == string(api.DEVICECLASSESENUM_DUO) { deviceChallenge = &devCh + devId, err := strconv.ParseInt(deviceChallenge.DeviceUid, 10, 32) + if err != nil { + return api.FlowChallengeResponseRequest{}, errors.New("failed to convert duo device id to int") + } + devId32 := int32(devId) + inner.SelectedChallenge = (*api.DeviceChallengeRequest)(deviceChallenge) + inner.Duo = &devId32 + } + 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) != "" { + deviceChallenge = &devCh + inner.SelectedChallenge = (*api.DeviceChallengeRequest)(deviceChallenge) + code := fe.getAnswer(StageAuthenticatorValidate) + inner.Code = &code + } } } if deviceChallenge == nil { return api.FlowChallengeResponseRequest{}, errors.New("no compatible authenticator class found") } - devId, err := strconv.Atoi(deviceChallenge.DeviceUid) - if err != nil { - return api.FlowChallengeResponseRequest{}, errors.New("failed to convert duo device id to int") - } - devId32 := int32(devId) - inner := api.NewAuthenticatorValidationChallengeResponseRequest() - inner.SelectedChallenge = (*api.DeviceChallengeRequest)(deviceChallenge) - inner.Duo = &devId32 return api.AuthenticatorValidationChallengeResponseRequestAsFlowChallengeResponseRequest(inner), nil } diff --git a/website/docs/providers/ldap/index.md b/website/docs/providers/ldap/index.md index 2cc771184..2bb8b33f8 100644 --- a/website/docs/providers/ldap/index.md +++ b/website/docs/providers/ldap/index.md @@ -72,11 +72,15 @@ The following stages are supported: - [Password](../../flow/stages/password/index.md) - [Authenticator validation](../../flow/stages/authenticator_validate/index.md) - Note: Authenticator validation currently only supports DUO devices + Note: Authenticator validation currently only supports DUO, TOTP and static authenticators. + + For code-based authenticators, the code must be given as part of the bind password, separated by a semicolon. For example for the password `example-password` and the code `123456`, the input must be `example-password;123456`. + + SMS-based authenticators are not supported as they require a code to be sent from authentik, which is not possible during the bind. #### Direct bind -In this mode, the outpost will always execute the configured flow when a new bind request arrives. +In this mode, the outpost will always execute the configured flow when a new bind request is received. #### Cached bind