Files
upstream-ory-kratos/selfservice/strategy/code/strategy_recovery_admin.go
upstream-mirror c60cc7ca75
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

251 lines
7.2 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package code
import (
"context"
"net/http"
"net/url"
"time"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"go.opentelemetry.io/otel/trace"
"github.com/ory/herodot"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/recovery"
"github.com/ory/kratos/selfservice/strategy"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x/events"
"github.com/ory/kratos/x/redir"
"github.com/ory/pop/v6"
"github.com/ory/x/decoderx"
"github.com/ory/x/httprouterx"
"github.com/ory/x/sqlcon"
"github.com/ory/x/urlx"
)
const (
RouteAdminCreateRecoveryCode = "/recovery/code"
)
func (s *Strategy) RegisterPublicRoutes(public *httprouterx.RouterPublic) {
s.deps.CSRFHandler().IgnorePath(RouteAdminCreateRecoveryCode)
public.POST(RouteAdminCreateRecoveryCode, redir.RedirectToAdminRoute(s.deps))
}
func (s *Strategy) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) {
wrappedCreateRecoveryCode := strategy.IsDisabled(s.deps, s.RecoveryStrategyID(), s.createRecoveryCodeForIdentity)
admin.POST(RouteAdminCreateRecoveryCode, wrappedCreateRecoveryCode)
}
// Create Recovery Code for Identity Parameters
//
// swagger:parameters createRecoveryCodeForIdentity
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type createRecoveryCodeForIdentity struct {
// in: body
Body createRecoveryCodeForIdentityBody
}
// Create Recovery Code for Identity Request Body
//
// swagger:model createRecoveryCodeForIdentityBody
type createRecoveryCodeForIdentityBody struct {
// Identity to Recover
//
// The identity's ID you wish to recover.
//
// required: true
IdentityID uuid.UUID `json:"identity_id"`
// Code Expires In
//
// The recovery code will expire after that amount of time has passed. Defaults to the configuration value of
// `selfservice.methods.code.config.lifespan`.
//
//
// pattern: ^([0-9]+([.][0-9]+)?(ns|us|µs|ms|s|m|h))+$
// example:
// - 1h
// - 1m
// - 1s
ExpiresIn string `json:"expires_in"`
// Flow Type
//
// The flow type for the recovery flow. Defaults to browser.
//
// required: false
FlowType *flow.Type `json:"flow_type"`
}
// Recovery Code for Identity
//
// Used when an administrator creates a recovery code for an identity.
//
// swagger:model recoveryCodeForIdentity
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type recoveryCodeForIdentity struct {
// RecoveryLink with flow
//
// This link opens the recovery UI with an empty `code` field.
//
// required: true
// format: uri
RecoveryLink string `json:"recovery_link"`
// RecoveryCode is the code that can be used to recover the account
//
// required: true
RecoveryCode string `json:"recovery_code"`
// Expires At is the timestamp of when the recovery flow expires
//
// The timestamp when the recovery code expires.
ExpiresAt time.Time `json:"expires_at"`
}
// swagger:route POST /admin/recovery/code identity createRecoveryCodeForIdentity
//
// # Create a Recovery Code
//
// This endpoint creates a recovery code which should be given to the user in order for them to recover
// (or activate) their account.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Security:
// oryAccessToken:
//
// Responses:
// 201: recoveryCodeForIdentity
// 400: errorGeneric
// 404: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-admin-low
func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http.Request) {
var p createRecoveryCodeForIdentityBody
if err := decoderx.Decode(r, &p, decoderx.HTTPJSONDecoder()); err != nil {
s.deps.Writer().WriteError(w, r, err)
return
}
ctx := r.Context()
config := s.deps.Config()
expiresIn := config.SelfServiceCodeMethodLifespan(ctx)
if len(p.ExpiresIn) > 0 {
// If an expiration of the code was supplied use it instead of the default duration
var err error
expiresIn, err = time.ParseDuration(p.ExpiresIn)
if err != nil {
s.deps.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().
WithReasonf(`Unable to parse "expires_in" as a duration string (e.g. "1h", "1m30s", "500ms"): %s`, p.ExpiresIn)))
return
}
}
if expiresIn <= 0 {
s.deps.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf(`Value from "expires_in" must result to a future time: %s`, expiresIn)))
return
}
flowType := flow.TypeBrowser
if p.FlowType != nil {
flowType = *p.FlowType
}
if !flowType.Valid() {
s.deps.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf(`Value from "flow_type" is not valid: %q`, flowType)))
return
}
recoveryFlow, err := recovery.NewFlow(s.deps, expiresIn, s.deps.GenerateCSRFToken(r), r, recovery.Strategies{s}, flowType)
if err != nil {
s.deps.Writer().WriteError(w, r, err)
return
}
recoveryFlow.DangerousSkipCSRFCheck = true
recoveryFlow.State = flow.StateEmailSent
recoveryFlow.UI.Nodes = node.Nodes{}
recoveryFlow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) {
a.Pattern = "[0-9]+"
a.MaxLength = CodeLength
a.Autocomplete = node.InputAttributeAutocompleteOneTimeCode
})).
WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()),
)
rawCode := GenerateCode()
recoveryFlow.UI.Nodes.
Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit).
WithMetaLabel(text.NewInfoNodeLabelContinue()))
id, err := s.deps.IdentityPool().GetIdentity(ctx, p.IdentityID, identity.ExpandDefault)
if notFoundErr := sqlcon.ErrNoRows(); errors.As(err, &notFoundErr) {
s.deps.Writer().WriteError(w, r, notFoundErr.WithReasonf("could not find identity"))
return
} else if err != nil {
s.deps.Writer().WriteError(w, r, err)
return
}
if err := s.deps.TransactionalPersisterProvider().Transaction(ctx, func(ctx context.Context, c *pop.Connection) error {
if err := s.deps.RecoveryFlowPersister().CreateRecoveryFlow(ctx, recoveryFlow); err != nil {
return err
}
if _, err := s.deps.RecoveryCodePersister().CreateRecoveryCode(ctx, &CreateRecoveryCodeParams{
RawCode: rawCode,
CodeType: RecoveryCodeTypeAdmin,
ExpiresIn: expiresIn,
FlowID: recoveryFlow.ID,
IdentityID: id.ID,
}); err != nil {
return err
}
return nil
}); err != nil {
s.deps.Writer().WriteError(w, r, err)
return
}
trace.SpanFromContext(r.Context()).AddEvent(
events.NewRecoveryInitiatedByAdmin(ctx, recoveryFlow.ID, id.ID, flowType.String(), "code"),
)
s.deps.Logger().
WithField("identity_id", id.ID).
WithSensitiveField("recovery_code", rawCode).
Info("A recovery code has been created.")
body := &recoveryCodeForIdentity{
ExpiresAt: recoveryFlow.ExpiresAt.UTC(),
RecoveryLink: urlx.CopyWithQuery(
s.deps.Config().SelfServiceFlowRecoveryUI(ctx),
url.Values{
"flow": {recoveryFlow.ID.String()},
}).String(),
RecoveryCode: rawCode,
}
s.deps.Writer().WriteCode(w, r, http.StatusCreated, body, herodot.UnescapedHTML)
}