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
139 lines
4.2 KiB
Go
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),
|
|
}
|
|
}
|
|
}
|