// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package verification import ( "net/http" "net/url" "go.opentelemetry.io/otel/attribute" "github.com/ory/kratos/x/nosurfx" "github.com/ory/x/clock" "github.com/ory/x/httpx" "github.com/ory/x/logrusx" "github.com/ory/x/otelx" "github.com/gofrs/uuid" "go.opentelemetry.io/otel/trace" "github.com/ory/kratos/x/events" "github.com/ory/kratos/ui/node" "github.com/pkg/errors" "github.com/ory/x/sqlxx" "github.com/ory/x/urlx" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/selfservice/errorx" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/text" "github.com/ory/kratos/x" ) var ErrHookAbortFlow = errors.New("aborted verification hook execution") type ( errorHandlerDependencies interface { clock.Provider errorx.ManagementProvider httpx.WriterProvider logrusx.Provider otelx.Provider nosurfx.CSRFProvider nosurfx.CSRFTokenGeneratorProvider config.Provider FlowPersistenceProvider StrategyProvider } ErrorHandlerProvider interface { VerificationFlowErrorHandler() *ErrorHandler } ErrorHandler struct { d errorHandlerDependencies } ) func NewErrorHandler(d errorHandlerDependencies) *ErrorHandler { return &ErrorHandler{d: d} } func (s *ErrorHandler) WriteFlowError( w http.ResponseWriter, r *http.Request, f *Flow, group node.UiNodeGroup, err error, ) { ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.flow.verification.ErrorHandler.WriteFlowError", trace.WithAttributes( attribute.String("error", err.Error()), )) r = r.WithContext(ctx) defer otelx.End(span, &err) logger := s.d.Logger(). WithError(err). WithRequest(r). WithField("verification_flow", f.ToLoggerField()) logger. Info("Encountered self-service verification error.") if f == nil { trace.SpanFromContext(r.Context()).AddEvent(events.NewVerificationFailed(r.Context(), uuid.Nil, "", "", err)) s.forward(w, r, nil, err) return } span.SetAttributes(attribute.String("flow_id", f.ID.String())) trace.SpanFromContext(r.Context()).AddEvent(events.NewVerificationFailed(r.Context(), f.ID, string(f.Type), f.Active.String(), err)) if e := new(flow.ExpiredError); errors.As(err, &e) { strategies, _, err := s.d.VerificationStrategies(r.Context()).ActiveStrategies(f.Active.String()) if err != nil { strategies, _, err = s.d.GetActiveVerificationStrategies(r.Context()) // Can't retry the verification if no primary strategy has been set if err != nil { s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) return } } // create new flow because the old one is not valid a, err := FromOldFlow(s.d, s.d.Config().SelfServiceFlowVerificationRequestLifespan(r.Context()), s.d.CSRFHandler().RegenerateToken(w, r), r, strategies, f) if err != nil { // failed to create a new session and redirect to it, handle that error as a new one s.WriteFlowError(w, r, f, group, err) return } a.UI.Messages.Add(text.NewErrorValidationVerificationFlowExpired(s.d.Clock(), e.ExpiredAt)) if err := s.d.VerificationFlowPersister().CreateVerificationFlow(r.Context(), a); err != nil { s.forward(w, r, a, err) return } // We need to use the new flow, as that flow will be a browser flow. Bug fix for: // // https://github.com/ory/kratos/issues/2049!! if a.Type == flow.TypeAPI || x.IsJSONRequest(r) { http.Redirect(w, r, urlx.CopyWithQuery(urlx.AppendPaths(s.d.Config().SelfPublicURL(r.Context()), RouteGetFlow), url.Values{"id": {a.ID.String()}}).String(), http.StatusSeeOther) } else { http.Redirect(w, r, a.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(r.Context())).String(), http.StatusSeeOther) } return } if err := f.UI.ParseError(group, err); err != nil { s.forward(w, r, f, err) return } f.Active = sqlxx.NullString(group) if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(r.Context(), f); err != nil { s.forward(w, r, f, err) return } if f.Type == flow.TypeBrowser && !x.IsJSONRequest(r) { http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(r.Context())).String(), http.StatusSeeOther) return } updatedFlow, innerErr := s.d.VerificationFlowPersister().GetVerificationFlow(r.Context(), f.ID) if innerErr != nil { s.forward(w, r, updatedFlow, innerErr) return } s.d.Writer().WriteCode(w, r, x.RecoverStatusCode(err, http.StatusBadRequest), updatedFlow) } func (s *ErrorHandler) forward(w http.ResponseWriter, r *http.Request, rr *Flow, err error) { if rr == nil { if x.IsJSONRequest(r) { s.d.Writer().WriteError(w, r, err) return } s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) return } if rr.Type == flow.TypeAPI || x.IsJSONRequest(r) { s.d.Writer().WriteErrorCode(w, r, x.RecoverStatusCode(err, http.StatusBadRequest), err) } else { s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) } }