From 621245aece9e24aca469e6622f7cf38b66c06100 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 3 Sep 2022 21:29:58 +0200 Subject: [PATCH] internal: optimise outpost's flow executor to use less requests Signed-off-by: Jens Langhammer --- internal/outpost/flow/executor.go | 111 ++++++++++++++---------------- internal/outpost/flow/solvers.go | 41 +++++++++++ 2 files changed, 94 insertions(+), 58 deletions(-) create mode 100644 internal/outpost/flow/solvers.go diff --git a/internal/outpost/flow/executor.go b/internal/outpost/flow/executor.go index 778464a9b..702bafce5 100644 --- a/internal/outpost/flow/executor.go +++ b/internal/outpost/flow/executor.go @@ -7,7 +7,6 @@ import ( "net/http" "net/http/cookiejar" "net/url" - "strconv" "strings" "github.com/getsentry/sentry-go" @@ -31,11 +30,15 @@ var ( }, []string{"stage", "flow"}) ) +type SolverFunction func(*api.ChallengeTypes, api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error) + type FlowExecutor struct { Params url.Values Answers map[StageComponent]string Context context.Context + solvers map[StageComponent]SolverFunction + cip string api *api.APIClient flowSlug string @@ -68,6 +71,11 @@ func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Config cip: "", transport: transport, } + fe.solvers = map[StageComponent]SolverFunction{ + StageIdentification: fe.solveChallenge_Identification, + StagePassword: fe.solveChallenge_Password, + StageAuthenticatorValidate: fe.solveChallenge_AuthenticatorValidate, + } // Create new http client that also sets the correct ip config := api.NewConfiguration() config.Host = refConfig.Host @@ -98,7 +106,7 @@ func (fe *FlowExecutor) ApiClient() *api.APIClient { return fe.api } -type ChallengeInt interface { +type challengeInt interface { GetComponent() string GetType() api.ChallengeChoices GetResponseErrors() map[string][]api.ErrorDetail @@ -145,20 +153,23 @@ func (fe *FlowExecutor) WarmUp() error { } func (fe *FlowExecutor) Execute() (bool, error) { - return fe.solveFlowChallenge(1) + initial, err := fe.getInitialChallenge() + if err != nil { + return false, err + } + defer fe.sp.Finish() + return fe.solveFlowChallenge(initial, 1) } -func (fe *FlowExecutor) solveFlowChallenge(depth int) (bool, error) { - defer fe.sp.Finish() - +func (fe *FlowExecutor) getInitialChallenge() (*api.ChallengeTypes, error) { // Get challenge gcsp := sentry.StartSpan(fe.Context, "authentik.outposts.flow_executor.get_challenge") req := fe.api.FlowsApi.FlowsExecutorGet(gcsp.Context(), fe.flowSlug).Query(fe.Params.Encode()) challenge, _, err := req.Execute() if err != nil { - return false, errors.New("failed to get challenge") + return nil, err } - ch := challenge.GetActualInstance().(ChallengeInt) + ch := challenge.GetActualInstance().(challengeInt) fe.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got challenge") gcsp.SetTag("authentik.flow.challenge", string(ch.GetType())) gcsp.SetTag("authentik.flow.component", ch.GetComponent()) @@ -167,60 +178,16 @@ func (fe *FlowExecutor) solveFlowChallenge(depth int) (bool, error) { "stage": ch.GetComponent(), "flow": fe.flowSlug, }).Observe(float64(gcsp.EndTime.Sub(gcsp.StartTime))) + return challenge, nil +} +func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth int) (bool, error) { // Resole challenge scsp := sentry.StartSpan(fe.Context, "authentik.outposts.flow_executor.solve_challenge") responseReq := fe.api.FlowsApi.FlowsExecutorSolve(scsp.Context(), fe.flowSlug).Query(fe.Params.Encode()) - switch ch.GetComponent() { - case string(StageIdentification): - r := api.NewIdentificationChallengeResponseRequest(fe.getAnswer(StageIdentification)) - r.SetPassword(fe.getAnswer(StagePassword)) - responseReq = responseReq.FlowChallengeResponseRequest(api.IdentificationChallengeResponseRequestAsFlowChallengeResponseRequest(r)) - case string(StagePassword): - responseReq = responseReq.FlowChallengeResponseRequest(api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(api.NewPasswordChallengeResponseRequest(fe.getAnswer(StagePassword)))) - case string(StageAuthenticatorValidate): - // We only support duo as authenticator, check if that's allowed - var deviceChallenge *api.DeviceChallenge - for _, devCh := range challenge.AuthenticatorValidationChallenge.DeviceChallenges { - if devCh.DeviceClass == string(api.DEVICECLASSESENUM_DUO) { - deviceChallenge = &devCh - } - } - if deviceChallenge == nil { - return false, errors.New("no compatible authenticator class found") - } - devId, err := strconv.Atoi(deviceChallenge.DeviceUid) - if err != nil { - return false, errors.New("failed to convert duo device id to int") - } - devId32 := int32(devId) - inner := api.NewAuthenticatorValidationChallengeResponseRequest() - inner.SelectedChallenge = (*api.DeviceChallengeRequest)(deviceChallenge) - inner.Duo = &devId32 - responseReq = responseReq.FlowChallengeResponseRequest(api.AuthenticatorValidationChallengeResponseRequestAsFlowChallengeResponseRequest(inner)) - case string(StageAccessDenied): - return false, errors.New("got ak-stage-access-denied") - default: - return false, fmt.Errorf("unsupported challenge type %s", ch.GetComponent()) - } + ch := challenge.GetActualInstance().(challengeInt) - response, _, err := responseReq.Execute() - ch = response.GetActualInstance().(ChallengeInt) - fe.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got response") - scsp.SetTag("authentik.flow.challenge", string(ch.GetType())) - scsp.SetTag("authentik.flow.component", ch.GetComponent()) - scsp.Finish() - - switch ch.GetComponent() { - case string(StageAccessDenied): - return false, nil - } - if ch.GetType() == "redirect" { - return true, nil - } - if err != nil { - return false, fmt.Errorf("failed to submit challenge %w", err) - } + // Check for any validation errors that we might've gotten if len(ch.GetResponseErrors()) > 0 { for key, errs := range ch.GetResponseErrors() { for _, err := range errs { @@ -228,6 +195,34 @@ func (fe *FlowExecutor) solveFlowChallenge(depth int) (bool, error) { } } } + + switch ch.GetType() { + case api.CHALLENGECHOICES_REDIRECT: + return true, nil + case api.CHALLENGECHOICES_NATIVE: + if ch.GetComponent() == string(StageAccessDenied) { + return false, nil + } + solver, ok := fe.solvers[StageComponent(ch.GetComponent())] + if !ok { + return false, fmt.Errorf("unsupported challenge type %s", ch.GetComponent()) + } + rr, err := solver(challenge, responseReq) + if err != nil { + return false, err + } + responseReq = responseReq.FlowChallengeResponseRequest(rr) + } + + response, _, err := responseReq.Execute() + if err != nil { + return false, fmt.Errorf("failed to submit challenge %w", err) + } + ch = response.GetActualInstance().(challengeInt) + fe.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got response") + scsp.SetTag("authentik.flow.challenge", string(ch.GetType())) + scsp.SetTag("authentik.flow.component", ch.GetComponent()) + scsp.Finish() FlowTimingPost.With(prometheus.Labels{ "stage": ch.GetComponent(), "flow": fe.flowSlug, @@ -236,5 +231,5 @@ func (fe *FlowExecutor) solveFlowChallenge(depth int) (bool, error) { if depth >= 10 { return false, errors.New("exceeded stage recursion depth") } - return fe.solveFlowChallenge(depth + 1) + return fe.solveFlowChallenge(response, depth+1) } diff --git a/internal/outpost/flow/solvers.go b/internal/outpost/flow/solvers.go new file mode 100644 index 000000000..487f4534d --- /dev/null +++ b/internal/outpost/flow/solvers.go @@ -0,0 +1,41 @@ +package flow + +import ( + "errors" + "strconv" + + "goauthentik.io/api/v3" +) + +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)) + return api.IdentificationChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil +} + +func (fe *FlowExecutor) solveChallenge_Password(challenge *api.ChallengeTypes, req api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error) { + r := api.NewPasswordChallengeResponseRequest(fe.getAnswer(StagePassword)) + return api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil +} + +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 + var deviceChallenge *api.DeviceChallenge + for _, devCh := range challenge.AuthenticatorValidationChallenge.DeviceChallenges { + if devCh.DeviceClass == string(api.DEVICECLASSESENUM_DUO) { + deviceChallenge = &devCh + } + } + 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 +}