Files
upstream-ory-kratos/selfservice/hook/password_migration_hook.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

139 lines
4.2 KiB
Go

// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hook
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/hashicorp/go-retryablehttp"
"github.com/pkg/errors"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.11.0"
"go.opentelemetry.io/otel/trace"
grpccodes "google.golang.org/grpc/codes"
"github.com/ory/herodot"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/request"
"github.com/ory/kratos/schema"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/x"
"github.com/ory/x/otelx"
)
type (
PasswordMigration struct {
deps webHookDependencies
conf *request.Config
}
PasswordMigrationRequest struct {
Identifier string `json:"identifier"`
Password string `json:"password"`
Identity *identity.Identity `json:"-"`
}
PasswordMigrationResponse struct {
Status string `json:"status"`
}
)
func NewPasswordMigrationHook(deps webHookDependencies, conf *request.Config) *PasswordMigration {
return &PasswordMigration{deps: deps, conf: conf}
}
func (p *PasswordMigration) Execute(ctx context.Context, req *http.Request, flow flow.Flow, data *PasswordMigrationRequest) (err error) {
var (
httpClient = p.deps.HTTPClient(ctx)
emitEvent = p.conf.EmitAnalyticsEvent == nil || *p.conf.EmitAnalyticsEvent // default true
tracer = trace.SpanFromContext(ctx).TracerProvider().Tracer("kratos-webhooks")
)
ctx, span := tracer.Start(ctx, "selfservice.login.password_migration")
defer otelx.End(span, &err)
if emitEvent {
InstrumentHTTPClientForEvents(ctx, httpClient, x.NewUUID(), "password_migration_hook")
}
builder, err := request.NewBuilder(p.conf, p.deps)
if err != nil {
return errors.WithStack(err)
}
var whReq *retryablehttp.Request
if p.conf.TemplateURI == "" {
whReq, err = builder.BuildRequest(ctx, nil) // passing a nil body here skips Jsonnet
if err != nil {
return err
}
rawData, err := json.Marshal(data)
if err != nil {
return errors.WithStack(err)
}
if err = whReq.SetBody(rawData); err != nil {
return errors.WithStack(err)
}
} else {
type templateContextMerged struct {
templateContext
Password string `json:"password"`
Identifier string `json:"identifier"`
}
whReq, err = builder.BuildRequest(ctx, templateContextMerged{
templateContext: templateContext{
Flow: flow,
RequestHeaders: req.Header,
RequestMethod: req.Method,
RequestURL: x.RequestURL(req).String(),
RequestCookies: cookies(req),
Identity: data.Identity,
},
Password: data.Password,
Identifier: data.Identifier,
})
if err != nil {
return err
}
}
p.deps.Logger().WithRequest(whReq.Request).Info("Dispatching password migration hook")
resp, err := httpClient.Do(whReq)
if err != nil {
return (&herodot.DefaultError{
CodeField: http.StatusBadGateway,
StatusField: http.StatusText(http.StatusBadGateway),
GRPCCodeField: grpccodes.Aborted,
ReasonField: "A third-party upstream service could not be reached. Please try again later.",
ErrorField: "calling the password migration hook failed",
}).WithWrap(errors.WithStack(err))
}
defer func() { _ = resp.Body.Close() }()
span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(resp.StatusCode)...)
switch resp.StatusCode {
case http.StatusOK:
// We now check if the response matches `{"status": "password_match" }`.
dec := json.NewDecoder(io.LimitReader(resp.Body, 1024)) // limit the response body to 1KB
var response PasswordMigrationResponse
if err := dec.Decode(&response); err != nil || response.Status != "password_match" {
return errors.WithStack(schema.NewInvalidCredentialsError())
}
return nil
case http.StatusForbidden:
return errors.WithStack(schema.NewInvalidCredentialsError())
default:
span.SetStatus(codes.Error, "Unexpected HTTP status code")
return &herodot.DefaultError{
CodeField: http.StatusBadGateway,
StatusField: http.StatusText(http.StatusBadGateway),
GRPCCodeField: grpccodes.Aborted,
ReasonField: "A third-party upstream service responded improperly. Please try again later.",
ErrorField: fmt.Sprintf("password migration hook failed with status code %v", resp.StatusCode),
}
}
}