snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
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
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
This commit is contained in:
250
selfservice/strategy/code/strategy_recovery_admin.go
Normal file
250
selfservice/strategy/code/strategy_recovery_admin.go
Normal file
@@ -0,0 +1,250 @@
|
||||
// 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, ¬FoundErr) {
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user