// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package verification import ( "net/http" "github.com/pkg/errors" "github.com/ory/herodot" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/hydra" "github.com/ory/kratos/identity" "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/errorx" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/session" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" "github.com/ory/kratos/x/nosurfx" "github.com/ory/kratos/x/redir" "github.com/ory/nosurf" "github.com/ory/x/clock" "github.com/ory/x/httprouterx" "github.com/ory/x/httpx" "github.com/ory/x/logrusx" "github.com/ory/x/sqlcon" "github.com/ory/x/urlx" ) const ( RouteInitBrowserFlow = "/self-service/verification/browser" RouteInitAPIFlow = "/self-service/verification/api" RouteGetFlow = "/self-service/verification/flows" RouteSubmitFlow = "/self-service/verification" ) type ( HandlerProvider interface { VerificationHandler() *Handler } handlerDependencies interface { clock.Provider errorx.ManagementProvider identity.ManagementProvider identity.PrivilegedPoolProvider config.Provider hydra.Provider session.PersistenceProvider session.ManagementProvider nosurfx.CSRFTokenGeneratorProvider httpx.WriterProvider nosurfx.CSRFProvider logrusx.Provider FlowPersistenceProvider ErrorHandlerProvider StrategyProvider HookExecutorProvider } Handler struct { d handlerDependencies } ) func NewHandler(d handlerDependencies) *Handler { return &Handler{d: d} } func (h *Handler) RegisterPublicRoutes(public *httprouterx.RouterPublic) { h.d.CSRFHandler().IgnorePath(RouteInitAPIFlow) h.d.CSRFHandler().IgnorePath(RouteSubmitFlow) public.GET(RouteInitBrowserFlow, h.createBrowserVerificationFlow) public.GET(RouteInitAPIFlow, h.createNativeVerificationFlow) public.GET(RouteGetFlow, h.getVerificationFlow) public.POST(RouteSubmitFlow, h.updateVerificationFlow) public.GET(RouteSubmitFlow, h.updateVerificationFlow) } func (h *Handler) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) { admin.GET(RouteInitBrowserFlow, redir.RedirectToPublicRoute(h.d)) admin.GET(RouteInitAPIFlow, redir.RedirectToPublicRoute(h.d)) admin.GET(RouteGetFlow, redir.RedirectToPublicRoute(h.d)) admin.POST(RouteSubmitFlow, redir.RedirectToPublicRoute(h.d)) admin.GET(RouteSubmitFlow, redir.RedirectToPublicRoute(h.d)) } type FlowOption func(f *Flow) func WithFlowReturnTo(returnTo string) FlowOption { return func(f *Flow) { f.ReturnTo = returnTo } } func (h *Handler) NewVerificationFlow(w http.ResponseWriter, r *http.Request, ft flow.Type, opts ...FlowOption) (*Flow, error) { strategies, _, err := h.d.GetActiveVerificationStrategies(r.Context()) if err != nil { return nil, err } f, err := NewFlow(h.d, h.d.Config().SelfServiceFlowVerificationRequestLifespan(r.Context()), h.d.GenerateCSRFToken(r), r, strategies, ft) if err != nil { return nil, err } for _, o := range opts { o(f) } if err := h.d.VerificationExecutor().PreVerificationHook(w, r, f); err != nil { return nil, err } if err := h.d.VerificationFlowPersister().CreateVerificationFlow(r.Context(), f); err != nil { return nil, err } return f, nil } // Create Verification Flow Parameters for Native Apps // // swagger:parameters createNativeVerificationFlow // //nolint:deadcode,unused //lint:ignore U1000 Used to generate Swagger and OpenAPI definitions type createNativeVerificationFlow struct { // A URL contained in the return_to key of the verification flow. // This piece of data has no effect on the actual logic of the flow and is purely informational. // // in: query ReturnTo string `json:"return_to"` } // swagger:route GET /self-service/verification/api frontend createNativeVerificationFlow // // # Create Verification Flow for Native Apps // // This endpoint initiates a verification flow for API clients such as mobile devices, smart TVs, and so on. // // To fetch an existing verification flow call `/self-service/verification/flows?flow=`. // // You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server // Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make // you vulnerable to a variety of CSRF attacks. // // This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...). // // More information can be found at [Ory Email and Phone Verification Documentation](https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation). // // Schemes: http, https // // Responses: // 200: verificationFlow // 400: errorGeneric // default: errorGeneric // // Extensions: // x-ory-ratelimit-bucket: kratos-public-medium func (h *Handler) createNativeVerificationFlow(w http.ResponseWriter, r *http.Request) { if !h.d.Config().SelfServiceFlowVerificationEnabled(r.Context()) { h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Verification is not allowed because it was disabled."))) return } req, err := h.NewVerificationFlow(w, r, flow.TypeAPI) if err != nil { h.d.Writer().WriteError(w, r, err) return } h.d.Writer().Write(w, r, req) } // Create Browser Verification Flow Parameters // // swagger:parameters createBrowserVerificationFlow // //nolint:deadcode,unused //lint:ignore U1000 Used to generate Swagger and OpenAPI definitions type createBrowserVerificationFlow struct { // The URL to return the browser to after the flow was completed. // // in: query ReturnTo string `json:"return_to"` } // swagger:route GET /self-service/verification/browser frontend createBrowserVerificationFlow // // # Create Verification Flow for Browser Clients // // This endpoint initializes a browser-based account verification flow. Once initialized, the browser will be redirected to // `selfservice.flows.verification.ui_url` with the flow ID set as the query parameter `?flow=`. // // If this endpoint is called via an AJAX request, the response contains the recovery flow without any redirects. // // This endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...). // // More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation). // // Schemes: http, https // // Responses: // 200: verificationFlow // 303: emptyResponse // default: errorGeneric // // Extensions: // x-ory-ratelimit-bucket: kratos-public-medium func (h *Handler) createBrowserVerificationFlow(w http.ResponseWriter, r *http.Request) { if !h.d.Config().SelfServiceFlowVerificationEnabled(r.Context()) { h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Verification is not allowed because it was disabled."))) return } req, err := h.NewVerificationFlow(w, r, flow.TypeBrowser) if err != nil { h.d.Writer().WriteError(w, r, err) return } redirTo := req.AppendTo(h.d.Config().SelfServiceFlowVerificationUI(r.Context())).String() x.SendFlowCompletedAsRedirectOrJSON(w, r, h.d.Writer(), req, redirTo) } // Get Verification Flow Parameters // // swagger:parameters getVerificationFlow // //nolint:deadcode,unused //lint:ignore U1000 Used to generate Swagger and OpenAPI definitions type getVerificationFlow struct { // The Flow ID // // The value for this parameter comes from `request` URL Query parameter sent to your // application (e.g. `/verification?flow=abcde`). // // required: true // in: query FlowID string `json:"id"` // HTTP Cookies // // When using the SDK on the server side you must include the HTTP Cookie Header // originally sent to your HTTP handler here. // // in: header // name: Cookie Cookie string `json:"cookie"` } // swagger:route GET /self-service/verification/flows frontend getVerificationFlow // // # Get Verification Flow // // This endpoint returns a verification flow's context with, for example, error details and other information. // // Browser flows expect the anti-CSRF cookie to be included in the request's HTTP Cookie Header. // For AJAX requests you must ensure that cookies are included in the request or requests will fail. // // If you use the browser-flow for server-side apps, the services need to run on a common top-level-domain // and you need to forward the incoming HTTP Cookie header to this endpoint: // // ```js // // pseudo-code example // router.get('/recovery', async function (req, res) { // const flow = await client.getVerificationFlow(req.header('cookie'), req.query['flow']) // // res.render('verification', flow) // }) // ``` // // More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation). // // Produces: // - application/json // // Schemes: http, https // // Responses: // 200: verificationFlow // 403: errorGeneric // 404: errorGeneric // default: errorGeneric // // Extensions: // x-ory-ratelimit-bucket: kratos-public-high func (h *Handler) getVerificationFlow(w http.ResponseWriter, r *http.Request) { if !h.d.Config().SelfServiceFlowVerificationEnabled(r.Context()) { h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Verification is not allowed because it was disabled."))) return } rid := x.ParseUUID(r.URL.Query().Get("id")) req, err := h.d.VerificationFlowPersister().GetVerificationFlow(r.Context(), rid) if err != nil { h.d.Writer().WriteError(w, r, err) return } // Browser flows must include the CSRF token // // Resolves: https://github.com/ory/kratos/issues/1282 if req.Type == flow.TypeBrowser && !nosurf.VerifyToken(h.d.GenerateCSRFToken(r), req.CSRFToken) { h.d.Writer().WriteError(w, r, nosurfx.CSRFErrorReason(r, h.d)) return } if req.ExpiresAt.Before(h.d.Clock().Now().UTC()) { if req.Type == flow.TypeBrowser { redirectURL := flow.GetFlowExpiredRedirectURL(r.Context(), h.d.Config(), RouteInitBrowserFlow, req.ReturnTo) h.d.Writer().WriteError(w, r, errors.WithStack(nosurfx.ErrGone(). WithReason("The verification flow has expired. Redirect the user to the verification flow init endpoint to initialize a new verification flow."). WithDetail("redirect_to", redirectURL.String()). WithDetail("return_to", req.ReturnTo))) return } h.d.Writer().WriteError(w, r, errors.WithStack(nosurfx.ErrGone(). WithReason("The verification flow has expired. Call the verification flow init API endpoint to initialize a new verification flow."). WithDetail("api", urlx.AppendPaths(h.d.Config().SelfPublicURL(r.Context()), RouteInitAPIFlow).String()))) return } h.d.Writer().Write(w, r, req) } // Update Verification Flow Parameters // // swagger:parameters updateVerificationFlow // //nolint:deadcode,unused //lint:ignore U1000 Used to generate Swagger and OpenAPI definitions type updateVerificationFlow struct { // The Verification Flow ID // // The value for this parameter comes from `flow` URL Query parameter sent to your // application (e.g. `/verification?flow=abcde`). // // required: true // in: query Flow string `json:"flow"` // Verification Token // // The verification token which completes the verification request. If the token // is invalid (e.g. expired) an error will be shown to the end-user. // // This parameter is usually set in a link and not used by any direct API call. // // in: query Token string `json:"token" form:"token"` // in: body // required: true Body updateVerificationFlowBody // HTTP Cookies // // When using the SDK in a browser app, on the server side you must include the HTTP Cookie Header // sent by the client to your server here. This ensures that CSRF and session cookies are respected. // // in: header // name: Cookie Cookies string `json:"Cookie"` } // Update Verification Flow Request Body // // swagger:model updateVerificationFlowBody // //nolint:deadcode,unused //lint:ignore U1000 Used to generate Swagger and OpenAPI definitions type updateVerificationFlowBody struct{} // swagger:route POST /self-service/verification frontend updateVerificationFlow // // # Complete Verification Flow // // Use this endpoint to complete a verification flow. This endpoint // behaves differently for API and browser flows and has several states: // // - `choose_method` expects `flow` (in the URL query) and `email` (in the body) to be sent // and works with API- and Browser-initiated flows. // - For API clients and Browser clients with HTTP Header `Accept: application/json` it either returns a HTTP 200 OK when the form is valid and HTTP 400 OK when the form is invalid // and a HTTP 303 See Other redirect with a fresh verification flow if the flow was otherwise invalid (e.g. expired). // - For Browser clients without HTTP Header `Accept` or with `Accept: text/*` it returns a HTTP 303 See Other redirect to the Verification UI URL with the Verification Flow ID appended. // - `sent_email` is the success state after `choose_method` when using the `link` method and allows the user to request another verification email. It // works for both API and Browser-initiated flows and returns the same responses as the flow in `choose_method` state. // - `passed_challenge` expects a `token` to be sent in the URL query and given the nature of the flow ("sending a verification link") // does not have any API capabilities. The server responds with a HTTP 303 See Other redirect either to the Settings UI URL // (if the link was valid) and instructs the user to update their password, or a redirect to the Verification UI URL with // a new Verification Flow ID which contains an error message that the verification link was invalid. // // More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation). // // Consumes: // - application/json // - application/x-www-form-urlencoded // // Produces: // - application/json // // Schemes: http, https // // Responses: // 200: verificationFlow // 303: emptyResponse // 400: verificationFlow // 410: errorGeneric // default: errorGeneric // // Extensions: // x-ory-ratelimit-bucket: kratos-public-low func (h *Handler) updateVerificationFlow(w http.ResponseWriter, r *http.Request) { rid, err := flow.GetFlowID(r) if err != nil { h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, nil, node.DefaultGroup, err) return } ctx := r.Context() f, err := h.d.VerificationFlowPersister().GetVerificationFlow(ctx, rid) if errors.Is(err, sqlcon.ErrNoRows()) { h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, nil, node.DefaultGroup, errors.WithStack(herodot.ErrNotFound().WithReasonf("The verification request could not be found. Please restart the flow."))) return } else if err != nil { h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, nil, node.DefaultGroup, err) return } if err := f.Valid(h.d.Clock()); err != nil { h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) return } var g node.UiNodeGroup var found bool for _, ss := range h.d.AllVerificationStrategies() { // If a primary strategy is set, but it does not match the current strategy, that strategy is not responsible anyways. if ps, isPrimary := ss.(PrimaryStrategy); isPrimary && f.Active.String() != "" && f.Active.String() != ps.VerificationStrategyID() { continue } err := ss.Verify(w, r, f) if errors.Is(err, flow.ErrStrategyNotResponsible) { continue } else if errors.Is(err, flow.ErrCompletedByStrategy) { return } else if err != nil { h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, ss.NodeGroup(), err) return } found = true g = ss.NodeGroup() break } if !found { h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, errors.WithStack(schema.NewNoVerificationStrategyResponsible())) return } // API flows can receive requests from the browser, if the link strategy is used. // However, x.IsBrowserRequest only checks for form submissions, not JSON requests made from a browser context if x.IsBrowserRequest(r) || (f.Type == flow.TypeBrowser && x.IsJSONRequest(r)) { // Special case: If we ended up here through a OAuth2 login challenge, we need to accept the login request // and redirect back to the OAuth2 provider. if flow.HasReachedState(flow.StatePassedChallenge, f.State) && f.OAuth2LoginChallenge.String() != "" { if !f.IdentityID.Valid || !f.SessionID.Valid { h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, herodot.ErrBadRequest().WithReasonf("No session was found for this flow. Please retry the authentication.")) return } callbackURL, err := h.d.Hydra().AcceptLoginRequest(ctx, hydra.AcceptLoginRequestParams{ LoginChallenge: string(f.OAuth2LoginChallenge), IdentityID: f.IdentityID.UUID.String(), SessionID: f.SessionID.UUID.String(), AuthenticationMethods: f.AMR, }) if err != nil { h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) return } sess, err := h.d.SessionPersister().GetSession(ctx, f.SessionID.UUID, session.ExpandDefault) if err != nil { h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) return } err = h.d.SessionManager().IssueCookie(ctx, w, r, sess) if err != nil { h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) return } if x.IsJSONRequest(r) { // This intentionally works differently than the "form browser" flow, // as it _does_ show the `verification success` UI, but the "Continue" // button contains the link to the OAuth2 provider with the `login_verifier`. continueNode := f.UI.Nodes.Find("continue") if continueNode != nil { if attr, ok := continueNode.Attributes.(*node.AnchorAttributes); ok { attr.HREF = callbackURL if err := h.d.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil { h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) return } h.d.Writer().Write(w, r, f) return } } // The flow does not have the `continue` node, which is an unknown state. // This should never happen. } else { http.Redirect(w, r, callbackURL, http.StatusSeeOther) return } } else if x.IsBrowserRequest(r) { http.Redirect(w, r, f.AppendTo(h.d.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther) return } } updatedFlow, err := h.d.VerificationFlowPersister().GetVerificationFlow(ctx, f.ID) if err != nil { h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, g, err) return } h.d.Writer().Write(w, r, updatedFlow) }