// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package link import ( "context" "encoding/json" "net/http" "net/url" "time" "github.com/gofrs/uuid" "github.com/pkg/errors" "go.opentelemetry.io/otel/attribute" "github.com/ory/kratos/identity" "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/text" "github.com/ory/kratos/ui/container" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" "github.com/ory/x/decoderx" "github.com/ory/x/otelx" "github.com/ory/x/sqlcon" "github.com/ory/x/sqlxx" "github.com/ory/x/urlx" ) func (s *Strategy) VerificationStrategyID() string { return string(verification.VerificationStrategyLink) } func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.Flow) error { f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) f.UI.GetNodes().Upsert( // v0.5: form.Field{Name: "email", Type: "email", Required: true} node.NewInputField("email", nil, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) f.UI.GetNodes().Append(node.NewInputField("method", s.VerificationStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue())) return nil } type verificationSubmitPayload struct { Method string `json:"method" form:"method"` Token string `json:"token" form:"token"` CSRFToken string `json:"csrf_token" form:"csrf_token"` Flow string `json:"flow" form:"flow"` Email string `json:"email" form:"email"` TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) decodeVerification(r *http.Request) (*verificationSubmitPayload, error) { var body verificationSubmitPayload compiler, err := decoderx.HTTPRawJSONSchemaCompiler(verificationMethodSchema) if err != nil { return nil, errors.WithStack(err) } if err := decoderx.Decode(r, &body, compiler, decoderx.HTTPDecoderUseQueryAndBody(), decoderx.HTTPKeepRequestBody(true), decoderx.HTTPDecoderAllowedMethods("POST", "GET"), decoderx.HTTPDecoderSetValidatePayloads(true), decoderx.HTTPDecoderJSONFollowsFormFormat(), ); err != nil { return nil, errors.WithStack(err) } return &body, nil } // handleVerificationError is a convenience function for handling all types of errors that may occur (e.g. validation error). func (s *Strategy) handleVerificationError(r *http.Request, f *verification.Flow, body *verificationSubmitPayload, err error) error { if f != nil { f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) email := "" if body != nil { email = body.Email } f.UI.GetNodes().Upsert( // v0.5: form.Field{Name: "email", Type: "email", Required: true, Value: body.Body.Email} node.NewInputField("email", email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) } return err } // Update Verification Flow with Link Method // // swagger:model updateVerificationFlowWithLinkMethod // //nolint:deadcode,unused //lint:ignore U1000 Used to generate Swagger and OpenAPI definitions type updateVerificationFlowWithLinkMethod struct { // Email to Verify // // Needs to be set when initiating the flow. If the email is a registered // verification email, a verification link will be sent. If the email is not known, // a email with details on what happened will be sent instead. // // format: email // required: true Email string `json:"email"` // Sending the anti-csrf token is only required for browser login flows. CSRFToken string `form:"csrf_token" json:"csrf_token"` // Method is the method that should be used for this verification flow // // Allowed values are `link` and `code` // // required: true Method verification.VerificationStrategy `json:"method"` // Transient data to pass along to any webhooks // // required: false TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verification.Flow) (err error) { ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.link.Strategy.Verify") span.SetAttributes(attribute.String("selfservice_flows_verification_use", s.d.Config().SelfServiceFlowVerificationUse(ctx))) defer otelx.End(span, &err) body, err := s.decodeVerification(r) if err != nil { return s.handleVerificationError(r, nil, body, err) } f.TransientPayload = body.TransientPayload if len(body.Token) > 0 { if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.VerificationStrategyID(), s.VerificationStrategyID(), s.d); err != nil { return s.handleVerificationError(r, nil, body, err) } return s.verificationUseToken(ctx, w, r, body, f) } if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.VerificationStrategyID(), body.Method, s.d); err != nil { return s.handleVerificationError(r, f, body, err) } if err := f.Valid(s.d.Clock()); err != nil { return s.handleVerificationError(r, f, body, err) } switch f.State { case flow.StateChooseMethod, flow.StateEmailSent: return s.verificationHandleFormSubmission(ctx, r, f) case flow.StatePassedChallenge: return s.retryVerificationFlowWithMessage(ctx, w, r, f.Type, text.NewErrorValidationVerificationRetrySuccess()) default: return s.retryVerificationFlowWithMessage(ctx, w, r, f.Type, text.NewErrorValidationVerificationStateFailure()) } } func (s *Strategy) verificationHandleFormSubmission(ctx context.Context, r *http.Request, f *verification.Flow) error { body, err := s.decodeVerification(r) if err != nil { return s.handleVerificationError(r, f, body, err) } if len(body.Email) == 0 { return s.handleVerificationError(r, f, body, schema.NewRequiredError("#/email", "email")) } if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, body.CSRFToken); err != nil { return s.handleVerificationError(r, f, body, err) } if err := s.d.LinkSender().SendVerificationLink(ctx, f, identity.AddressTypeEmail, body.Email); err != nil { if !errors.Is(err, ErrUnknownAddress) { return s.handleVerificationError(r, f, body, err) } // Continue execution } f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) f.UI.GetNodes().Upsert( // v0.5: form.Field{Name: "email", Type: "email", Required: true, Value: body.Body.Email} node.NewInputField("email", body.Email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) f.Active = sqlxx.NullString(s.NodeGroup()) f.State = flow.StateEmailSent f.UI.Messages.Set(text.NewVerificationEmailSent()) if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil { return s.handleVerificationError(r, f, body, err) } return nil } func (s *Strategy) verificationUseToken(ctx context.Context, w http.ResponseWriter, r *http.Request, body *verificationSubmitPayload, f *verification.Flow) error { token, err := s.d.VerificationTokenPersister().UseVerificationToken(ctx, f.ID, body.Token) if err != nil { if errors.Is(err, sqlcon.ErrNoRows()) { return s.retryVerificationFlowWithMessage(ctx, w, r, flow.TypeBrowser, text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed()) } return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) } if err := token.Valid(s.d.Clock()); err != nil { return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) } var identityID uuid.UUID var addressVia string var ptcIdentity *identity.Identity if token.VerifiableAddress == nil { ptc, ptcErr := s.d.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, f.ID) if ptcErr != nil { if errors.Is(ptcErr, sqlcon.ErrNoRows()) { return s.retryVerificationFlowWithMessage(ctx, w, r, flow.TypeBrowser, text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed()) } return s.retryVerificationFlowWithError(ctx, w, r, f.Type, ptcErr) } updatedIdentity, applyErr := s.d.SettingsHookExecutor().ApplyPendingTraitsChange(ctx, w, r, ptc) if applyErr != nil { if errors.Is(applyErr, identity.ErrConcurrentModification) || errors.Is(applyErr, settings.ErrPendingTraitsChangeSessionInvalid) { f.UI.Messages.Clear() f.UI.Messages.Add(text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed()) if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil { return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err) } if x.IsBrowserRequest(r) { http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther) } else { s.d.Writer().Write(w, r, f) } return errors.WithStack(flow.ErrCompletedByStrategy) } return s.retryVerificationFlowWithError(ctx, w, r, f.Type, applyErr) } ptcIdentity = updatedIdentity addressVia = ptc.NewAddressVia identityID = ptc.IdentityID } else { // Normal path: update the existing verifiable address directly. address := token.VerifiableAddress address.Verified = true verifiedAt := sqlxx.NullTime(time.Now().UTC()) address.VerifiedAt = &verifiedAt address.Status = identity.VerifiableAddressStatusCompleted if err := s.d.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, address, "verified", "verified_at", "status"); err != nil { return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) } addressVia = address.Via identityID = address.IdentityID } var i *identity.Identity if ptcIdentity != nil { i = ptcIdentity } else { i, err = s.d.IdentityPool().GetIdentity(ctx, identityID, identity.ExpandDefault) if err != nil { return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) } } // Remainder is shared between both paths. returnTo := f.ContinueURL(ctx, s.d.Config()) f.UI = &container.Container{ Method: "GET", Action: returnTo.String(), } f.State = flow.StatePassedChallenge // See https://github.com/ory/kratos/issues/1547 f.SetCSRFToken(flow.GetCSRFToken(s.d, w, r, f.Type)) if addressVia == identity.AddressTypeSMS { f.UI.Messages.Set(text.NewInfoSelfServiceVerificationPhoneSuccessful()) } else { f.UI.Messages.Set(text.NewInfoSelfServiceVerificationSuccessful()) } f.UI.Nodes.Append(node.NewAnchorField("continue", returnTo.String(), node.LinkGroup, text.NewInfoNodeLabelContinue()). WithMetaLabel(text.NewInfoNodeLabelContinue())) if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil { return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) } if err := s.d.VerificationExecutor().PostVerificationHook(w, r, f, i); err != nil { return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err) } return nil } func (s *Strategy) retryVerificationFlowWithMessage(ctx context.Context, w http.ResponseWriter, r *http.Request, ft flow.Type, message *text.Message) error { s.d.Logger().WithRequest(r).WithField("message", message).Debug("A verification flow is being retried because a validation error occurred.") f, err := verification.NewFlow(s.d, s.d.Config().SelfServiceFlowVerificationRequestLifespan(ctx), s.d.CSRFHandler().RegenerateToken(w, r), r, verification.Strategies{s}, ft) if err != nil { return s.handleVerificationError(r, f, nil, err) } f.UI.Messages.Add(message) if err := s.d.VerificationFlowPersister().CreateVerificationFlow(ctx, f); err != nil { return s.handleVerificationError(r, f, nil, err) } if ft == flow.TypeBrowser { http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther) } else { http.Redirect(w, r, urlx.CopyWithQuery(urlx.AppendPaths(s.d.Config().SelfPublicURL(ctx), verification.RouteGetFlow), url.Values{"id": {f.ID.String()}}).String(), http.StatusSeeOther) } return errors.WithStack(flow.ErrCompletedByStrategy) } func (s *Strategy) retryVerificationFlowWithError(ctx context.Context, w http.ResponseWriter, r *http.Request, ft flow.Type, verErr error) error { s.d.Logger().WithRequest(r).WithError(verErr).Debug("A verification flow is being retried because an error occurred.") f, err := verification.NewFlow(s.d, s.d.Config().SelfServiceFlowVerificationRequestLifespan(ctx), s.d.CSRFHandler().RegenerateToken(w, r), r, verification.Strategies{s}, ft) if err != nil { return s.handleVerificationError(r, f, nil, err) } if expired := new(flow.ExpiredError); errors.As(verErr, &expired) { return s.retryVerificationFlowWithMessage(ctx, w, r, ft, text.NewErrorValidationVerificationFlowExpired(s.d.Clock(), expired.ExpiredAt)) } else { if err := f.UI.ParseError(node.LinkGroup, verErr); err != nil { return err } } if err := s.d.VerificationFlowPersister().CreateVerificationFlow(ctx, f); err != nil { return s.handleVerificationError(r, f, nil, err) } if ft == flow.TypeBrowser { http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther) } else { http.Redirect(w, r, urlx.CopyWithQuery(urlx.AppendPaths(s.d.Config().SelfPublicURL(ctx), verification.RouteGetFlow), url.Values{"id": {f.ID.String()}}).String(), http.StatusSeeOther) } return errors.WithStack(flow.ErrCompletedByStrategy) } func (s *Strategy) SendVerificationCode(ctx context.Context, f *verification.Flow, i *identity.Identity, addressLike identity.VerifiableAddressLike) error { token := NewSelfServiceVerificationToken(addressLike, f, s.d.Config().SelfServiceLinkMethodLifespan(ctx)) if err := s.d.VerificationTokenPersister().CreateVerificationToken(ctx, token); err != nil { return err } return s.d.LinkSender().SendVerificationTokenTo(ctx, f, i, addressLike, token) }