mirror of https://github.com/ory/kratos
feat: webhooks that update identities
Introduces a new configuration `response.parse` in webhooks. This enables updating of identity data during registration, including admin/public metadata, identity traits, enabling/disabling identity, and modifying verified/recovery addresses. Please note that `can_interrupt` is being deprecated in favor of `response.parse`. Closes #2161
This commit is contained in:
parent
586eaf9e3f
commit
2cbee3e8ee
|
|
@ -174,7 +174,23 @@
|
|||
"type": "boolean",
|
||||
"description": "Ignore the response from the web hook. If enabled the request will be made asynchronously which can be useful if you only wish to notify another system but do not parse the response.",
|
||||
"default": false
|
||||
},
|
||||
"parse": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "If enabled parses the response before saving the flow result. Set this value to true if you would like to modify the identity, for example identity metadata, before saving it during registration. When enabled, you may also abort the registration, verification, login or settings flow due to, for example, a validation flow. Head over to the [web hook documentation](https://www.ory.sh/docs/kratos/hooks/configure-hooks) for more information."
|
||||
}
|
||||
},
|
||||
"not": {
|
||||
"properties": {
|
||||
"ignore": {
|
||||
"const": true
|
||||
},
|
||||
"parse": {
|
||||
"const": true
|
||||
}
|
||||
},
|
||||
"required":["ignore","parse"]
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
|
|
@ -211,7 +227,7 @@
|
|||
"can_interrupt": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "If enabled allows the web hook to interrupt / abort the self-service flow. It only applies to certain flows (registration/verification/login/settings) and requires a valid response format."
|
||||
"description": "Deprecated, please use `response.parse` instead. If enabled allows the web hook to interrupt / abort the self-service flow. It only applies to certain flows (registration/verification/login/settings) and requires a valid response format."
|
||||
},
|
||||
"auth": {
|
||||
"type": "object",
|
||||
|
|
@ -1208,7 +1224,10 @@
|
|||
"title": "Verification Strategy",
|
||||
"description": "The strategy to use for verification requests",
|
||||
"type": "string",
|
||||
"enum": ["link", "code"],
|
||||
"enum": [
|
||||
"link",
|
||||
"code"
|
||||
],
|
||||
"default": "code"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ory/x/pointerx"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
|
|
@ -257,9 +259,26 @@ func (p *Persister) normalizeAllAddressess(ctx context.Context, id *identity.Ide
|
|||
|
||||
func (p *Persister) normalizeVerifiableAddresses(ctx context.Context, id *identity.Identity) {
|
||||
for k := range id.VerifiableAddresses {
|
||||
id.VerifiableAddresses[k].IdentityID = id.ID
|
||||
id.VerifiableAddresses[k].NID = p.NetworkID(ctx)
|
||||
id.VerifiableAddresses[k].Value = stringToLowerTrim(id.VerifiableAddresses[k].Value)
|
||||
v := id.VerifiableAddresses[k]
|
||||
|
||||
v.IdentityID = id.ID
|
||||
v.NID = p.NetworkID(ctx)
|
||||
v.Value = stringToLowerTrim(v.Value)
|
||||
v.Via = x.Coalesce(v.Via, identity.AddressTypeEmail)
|
||||
if len(v.Status) == 0 {
|
||||
if v.Verified {
|
||||
v.Status = identity.VerifiableAddressStatusCompleted
|
||||
} else {
|
||||
v.Status = identity.VerifiableAddressStatusPending
|
||||
}
|
||||
}
|
||||
|
||||
// If verified is true but no timestamp is set, we default to time.Now
|
||||
if v.Verified && (v.VerifiedAt == nil || time.Time(*v.VerifiedAt).IsZero()) {
|
||||
v.VerifiedAt = pointerx.Ptr(sqlxx.NullTime(time.Now()))
|
||||
}
|
||||
|
||||
id.VerifiableAddresses[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -268,6 +287,7 @@ func (p *Persister) normalizeRecoveryAddresses(ctx context.Context, id *identity
|
|||
id.RecoveryAddresses[k].IdentityID = id.ID
|
||||
id.RecoveryAddresses[k].NID = p.NetworkID(ctx)
|
||||
id.RecoveryAddresses[k].Value = stringToLowerTrim(id.RecoveryAddresses[k].Value)
|
||||
id.RecoveryAddresses[k].Via = x.Coalesce(id.RecoveryAddresses[k].Via, identity.AddressTypeEmail)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"credentials": {
|
||||
"password": {
|
||||
"type": "password",
|
||||
"identifiers": [
|
||||
"test"
|
||||
],
|
||||
"config": {
|
||||
"hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA"
|
||||
},
|
||||
"version": 0,
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"schema_id": "default",
|
||||
"schema_url": "file://stub/default.schema.json",
|
||||
"state": "active",
|
||||
"traits": {
|
||||
"email": "some@example.org"
|
||||
},
|
||||
"verifiable_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"verified": false,
|
||||
"via": "email",
|
||||
"status": "pending",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"recovery_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"via": "email",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"metadata_public": {
|
||||
"public": "data"
|
||||
},
|
||||
"metadata_admin": {
|
||||
"admin": "data"
|
||||
},
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"credentials": {
|
||||
"password": {
|
||||
"type": "password",
|
||||
"identifiers": [
|
||||
"test"
|
||||
],
|
||||
"config": {
|
||||
"hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA"
|
||||
},
|
||||
"version": 0,
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"schema_id": "default",
|
||||
"schema_url": "file://stub/default.schema.json",
|
||||
"state": "active",
|
||||
"traits": {
|
||||
"email": "some@example.org"
|
||||
},
|
||||
"verifiable_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"verified": false,
|
||||
"via": "email",
|
||||
"status": "pending",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"recovery_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"via": "email",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"metadata_public": {
|
||||
"public": "data"
|
||||
},
|
||||
"metadata_admin": {
|
||||
"admin": "data"
|
||||
},
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"credentials": {
|
||||
"password": {
|
||||
"type": "password",
|
||||
"identifiers": [
|
||||
"test"
|
||||
],
|
||||
"config": {
|
||||
"hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA"
|
||||
},
|
||||
"version": 0,
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"schema_id": "default",
|
||||
"schema_url": "file://stub/default.schema.json",
|
||||
"state": "active",
|
||||
"traits": {
|
||||
"email": "some@example.org"
|
||||
},
|
||||
"verifiable_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"verified": false,
|
||||
"via": "email",
|
||||
"status": "pending",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"recovery_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"via": "email",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"metadata_public": {
|
||||
"useful": "metadata"
|
||||
},
|
||||
"metadata_admin": {
|
||||
"admin": "data"
|
||||
},
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"credentials": {
|
||||
"password": {
|
||||
"type": "password",
|
||||
"identifiers": [
|
||||
"test"
|
||||
],
|
||||
"config": {
|
||||
"hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA"
|
||||
},
|
||||
"version": 0,
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"schema_id": "default",
|
||||
"schema_url": "file://stub/default.schema.json",
|
||||
"state": "active",
|
||||
"traits": {
|
||||
"email": "some@other-example.org"
|
||||
},
|
||||
"verifiable_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"verified": false,
|
||||
"via": "email",
|
||||
"status": "pending",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"recovery_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@other-example.org",
|
||||
"via": "email",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"metadata_public": {
|
||||
"public": "data"
|
||||
},
|
||||
"metadata_admin": {
|
||||
"admin": "data"
|
||||
},
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"credentials": {
|
||||
"password": {
|
||||
"type": "password",
|
||||
"identifiers": [
|
||||
"test"
|
||||
],
|
||||
"config": {
|
||||
"hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA"
|
||||
},
|
||||
"version": 0,
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"schema_id": "default",
|
||||
"schema_url": "file://stub/default.schema.json",
|
||||
"state": "inactive",
|
||||
"traits": {
|
||||
"email": "some@example.org"
|
||||
},
|
||||
"verifiable_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"verified": false,
|
||||
"via": "email",
|
||||
"status": "pending",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"recovery_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"via": "email",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"metadata_public": {
|
||||
"public": "data"
|
||||
},
|
||||
"metadata_admin": {
|
||||
"admin": "data"
|
||||
},
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"credentials": {
|
||||
"password": {
|
||||
"type": "password",
|
||||
"identifiers": [
|
||||
"test"
|
||||
],
|
||||
"config": {
|
||||
"hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA"
|
||||
},
|
||||
"version": 0,
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"schema_id": "default",
|
||||
"schema_url": "file://stub/default.schema.json",
|
||||
"state": "active",
|
||||
"traits": {
|
||||
"email": "some@other-example.org"
|
||||
},
|
||||
"verifiable_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"verified": false,
|
||||
"via": "email",
|
||||
"status": "pending",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"recovery_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"via": "email",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"metadata_public": {
|
||||
"public": "data"
|
||||
},
|
||||
"metadata_admin": {
|
||||
"admin": "data"
|
||||
},
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"credentials": {
|
||||
"password": {
|
||||
"type": "password",
|
||||
"identifiers": [
|
||||
"test"
|
||||
],
|
||||
"config": {
|
||||
"hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA"
|
||||
},
|
||||
"version": 0,
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"schema_id": "default",
|
||||
"schema_url": "file://stub/default.schema.json",
|
||||
"state": "active",
|
||||
"traits": {
|
||||
"email": "some@other-example.org"
|
||||
},
|
||||
"verifiable_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@other-example.org",
|
||||
"verified": false,
|
||||
"via": "email",
|
||||
"status": "",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"recovery_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"via": "email",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"metadata_public": {
|
||||
"public": "data"
|
||||
},
|
||||
"metadata_admin": {
|
||||
"admin": "data"
|
||||
},
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"credentials": {
|
||||
"password": {
|
||||
"type": "password",
|
||||
"identifiers": [
|
||||
"test"
|
||||
],
|
||||
"config": {
|
||||
"hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA"
|
||||
},
|
||||
"version": 0,
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"schema_id": "default",
|
||||
"schema_url": "file://stub/default.schema.json",
|
||||
"state": "active",
|
||||
"traits": {
|
||||
"email": "some@example.org"
|
||||
},
|
||||
"verifiable_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"verified": false,
|
||||
"via": "email",
|
||||
"status": "pending",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"recovery_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"via": "email",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"metadata_public": {
|
||||
"public": "data"
|
||||
},
|
||||
"metadata_admin": {
|
||||
"admin": "data"
|
||||
},
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"credentials": {
|
||||
"password": {
|
||||
"type": "password",
|
||||
"identifiers": [
|
||||
"test"
|
||||
],
|
||||
"config": {
|
||||
"hashed_password": "$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA"
|
||||
},
|
||||
"version": 0,
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"schema_id": "default",
|
||||
"schema_url": "file://stub/default.schema.json",
|
||||
"state": "active",
|
||||
"traits": {
|
||||
"email": "some@example.org"
|
||||
},
|
||||
"verifiable_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"verified": false,
|
||||
"via": "email",
|
||||
"status": "pending",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"recovery_addresses": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"value": "some@example.org",
|
||||
"via": "email",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"metadata_public": {
|
||||
"public": "data"
|
||||
},
|
||||
"metadata_admin": {
|
||||
"admin": "data"
|
||||
},
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ory/herodot"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tidwall/gjson"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
|
|
@ -188,7 +190,7 @@ func (e *WebHook) ExecuteRegistrationPreHook(_ http.ResponseWriter, req *http.Re
|
|||
}
|
||||
|
||||
func (e *WebHook) ExecutePostRegistrationPrePersistHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow, id *identity.Identity) error {
|
||||
if !gjson.GetBytes(e.conf, "can_interrupt").Bool() {
|
||||
if !(gjson.GetBytes(e.conf, "can_interrupt").Bool() || gjson.GetBytes(e.conf, "response.parse").Bool()) {
|
||||
return nil
|
||||
}
|
||||
return otelx.WithSpan(req.Context(), "selfservice.hook.ExecutePostRegistrationPrePersistHook", func(ctx context.Context) error {
|
||||
|
|
@ -204,7 +206,7 @@ func (e *WebHook) ExecutePostRegistrationPrePersistHook(_ http.ResponseWriter, r
|
|||
}
|
||||
|
||||
func (e *WebHook) ExecutePostRegistrationPostPersistHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow, session *session.Session) error {
|
||||
if gjson.GetBytes(e.conf, "can_interrupt").Bool() {
|
||||
if gjson.GetBytes(e.conf, "can_interrupt").Bool() || gjson.GetBytes(e.conf, "response.parse").Bool() {
|
||||
return nil
|
||||
}
|
||||
return otelx.WithSpan(req.Context(), "selfservice.hook.ExecutePostRegistrationPostPersistHook", func(ctx context.Context) error {
|
||||
|
|
@ -288,11 +290,16 @@ func (e *WebHook) execute(ctx context.Context, data *templateContext) error {
|
|||
httpClient = e.deps.HTTPClient(ctx)
|
||||
ignoreResponse = gjson.GetBytes(e.conf, "response.ignore").Bool()
|
||||
canInterrupt = gjson.GetBytes(e.conf, "can_interrupt").Bool()
|
||||
parseResponse = gjson.GetBytes(e.conf, "response.parse").Bool()
|
||||
tracer = trace.SpanFromContext(ctx).TracerProvider().Tracer("kratos-webhooks")
|
||||
spanOpts = []trace.SpanStartOption{trace.WithAttributes(attrs...)}
|
||||
errChan = make(chan error, 1)
|
||||
)
|
||||
|
||||
if ignoreResponse && (parseResponse || canInterrupt) {
|
||||
return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("A webhook is configured to ignore the response but also to parse the response. This is not possible."))
|
||||
}
|
||||
|
||||
ctx, span := tracer.Start(ctx, "selfservice.webhook", spanOpts...)
|
||||
e.deps.Logger().WithRequest(req.Request).Info("Dispatching webhook")
|
||||
|
||||
|
|
@ -323,8 +330,8 @@ func (e *WebHook) execute(ctx context.Context, data *templateContext) error {
|
|||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
span.SetStatus(codes.Error, "HTTP status code >= 400")
|
||||
if canInterrupt {
|
||||
if err := parseWebhookResponse(resp); err != nil {
|
||||
if canInterrupt || parseResponse {
|
||||
if err := parseWebhookResponse(resp, data.Identity); err != nil {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
errChan <- err
|
||||
}
|
||||
|
|
@ -333,6 +340,13 @@ func (e *WebHook) execute(ctx context.Context, data *templateContext) error {
|
|||
return
|
||||
}
|
||||
|
||||
if parseResponse {
|
||||
if err := parseWebhookResponse(resp, data.Identity); err != nil {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
errChan <- err
|
||||
}
|
||||
}
|
||||
|
||||
errChan <- nil
|
||||
}()
|
||||
|
||||
|
|
@ -355,38 +369,91 @@ func (e *WebHook) execute(ctx context.Context, data *templateContext) error {
|
|||
return <-errChan
|
||||
}
|
||||
|
||||
func parseWebhookResponse(resp *http.Response) (err error) {
|
||||
func parseWebhookResponse(resp *http.Response, id *identity.Identity) (err error) {
|
||||
if resp == nil {
|
||||
return errors.Errorf("empty response provided from the webhook")
|
||||
}
|
||||
var hookResponse rawHookResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&hookResponse); err != nil {
|
||||
return errors.Wrap(err, "webhook response could not be unmarshalled properly from JSON")
|
||||
}
|
||||
|
||||
var validationErrs []*schema.ValidationError
|
||||
for _, msg := range hookResponse.Messages {
|
||||
messages := text.Messages{}
|
||||
for _, detail := range msg.DetailedMessages {
|
||||
var msgType text.UITextType
|
||||
if detail.Type == "error" {
|
||||
msgType = text.Error
|
||||
} else {
|
||||
msgType = text.Info
|
||||
}
|
||||
messages.Add(&text.Message{
|
||||
ID: text.ID(detail.ID),
|
||||
Text: detail.Text,
|
||||
Type: msgType,
|
||||
Context: detail.Context,
|
||||
})
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
var hookResponse struct {
|
||||
Identity *identity.Identity `json:"identity"`
|
||||
}
|
||||
validationErrs = append(validationErrs, schema.NewHookValidationError(msg.InstancePtr, "a webhook target returned an error", messages))
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&hookResponse); err != nil {
|
||||
return errors.Wrap(err, "webhook response could not be unmarshalled properly from JSON")
|
||||
}
|
||||
|
||||
if hookResponse.Identity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(hookResponse.Identity.Traits) > 0 {
|
||||
id.Traits = hookResponse.Identity.Traits
|
||||
}
|
||||
|
||||
if len(hookResponse.Identity.SchemaID) > 0 {
|
||||
id.SchemaID = hookResponse.Identity.SchemaID
|
||||
}
|
||||
|
||||
if len(hookResponse.Identity.State) > 0 {
|
||||
id.State = hookResponse.Identity.State
|
||||
}
|
||||
|
||||
if len(hookResponse.Identity.VerifiableAddresses) > 0 {
|
||||
id.VerifiableAddresses = hookResponse.Identity.VerifiableAddresses
|
||||
}
|
||||
|
||||
if len(hookResponse.Identity.VerifiableAddresses) > 0 {
|
||||
id.VerifiableAddresses = hookResponse.Identity.VerifiableAddresses
|
||||
}
|
||||
|
||||
if len(hookResponse.Identity.RecoveryAddresses) > 0 {
|
||||
id.RecoveryAddresses = hookResponse.Identity.RecoveryAddresses
|
||||
}
|
||||
|
||||
if len(hookResponse.Identity.MetadataPublic) > 0 {
|
||||
id.MetadataPublic = hookResponse.Identity.MetadataPublic
|
||||
}
|
||||
|
||||
if len(hookResponse.Identity.MetadataAdmin) > 0 {
|
||||
id.MetadataAdmin = hookResponse.Identity.MetadataAdmin
|
||||
}
|
||||
|
||||
return nil
|
||||
} else if resp.StatusCode == http.StatusNoContent {
|
||||
return nil
|
||||
} else if resp.StatusCode >= http.StatusBadRequest {
|
||||
var hookResponse rawHookResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&hookResponse); err != nil {
|
||||
return errors.Wrap(err, "webhook response could not be unmarshalled properly from JSON")
|
||||
}
|
||||
|
||||
var validationErrs []*schema.ValidationError
|
||||
for _, msg := range hookResponse.Messages {
|
||||
messages := text.Messages{}
|
||||
for _, detail := range msg.DetailedMessages {
|
||||
var msgType text.UITextType
|
||||
if detail.Type == "error" {
|
||||
msgType = text.Error
|
||||
} else {
|
||||
msgType = text.Info
|
||||
}
|
||||
messages.Add(&text.Message{
|
||||
ID: text.ID(detail.ID),
|
||||
Text: detail.Text,
|
||||
Type: msgType,
|
||||
Context: detail.Context,
|
||||
})
|
||||
}
|
||||
validationErrs = append(validationErrs, schema.NewHookValidationError(msg.InstancePtr, "a webhook target returned an error", messages))
|
||||
}
|
||||
|
||||
if len(validationErrs) == 0 {
|
||||
return errors.New("error while parsing webhook response: got no validation errors")
|
||||
}
|
||||
|
||||
return schema.NewValidationListError(validationErrs)
|
||||
}
|
||||
|
||||
if len(validationErrs) == 0 {
|
||||
return errors.New("error while parsing webhook response: got no validation errors")
|
||||
}
|
||||
|
||||
return schema.NewValidationListError(validationErrs)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ory/x/snapshotx"
|
||||
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
|
|
@ -607,6 +609,90 @@ func TestWebHooks(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
t.Run("update identity fields", func(t *testing.T) {
|
||||
run := func(t *testing.T, id identity.Identity, responseCode int, response []byte) *identity.WithCredentialsAndAdminMetadataInJSON {
|
||||
f := ®istration.Flow{ID: x.NewUUID()}
|
||||
req := &http.Request{
|
||||
Host: "www.ory.sh",
|
||||
Header: map[string][]string{},
|
||||
RequestURI: "/some_end_point",
|
||||
Method: http.MethodPost,
|
||||
URL: &url.URL{Path: "some_end_point"},
|
||||
}
|
||||
ts := newServer(webHookHttpCodeWithBodyEndPoint(t, responseCode, response))
|
||||
conf := json.RawMessage(fmt.Sprintf(`{"url": "%s", "method": "POST", "body": "%s", "response": {"parse":true}}`, ts.URL+path, "file://./stub/test_body.jsonnet"))
|
||||
wh := hook.NewWebHook(&whDeps, conf)
|
||||
in := &id
|
||||
err := wh.ExecutePostRegistrationPrePersistHook(nil, req, f, in)
|
||||
require.NoError(t, err)
|
||||
result := identity.WithCredentialsAndAdminMetadataInJSON(*in)
|
||||
return &result
|
||||
}
|
||||
|
||||
t.Run("case=update identity fields", func(t *testing.T) {
|
||||
expected := identity.Identity{
|
||||
Credentials: map[identity.CredentialsType]identity.Credentials{identity.CredentialsTypePassword: {Type: "password", Identifiers: []string{"test"}, Config: []byte(`{"hashed_password":"$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA"}`)}},
|
||||
SchemaID: "default",
|
||||
SchemaURL: "file://stub/default.schema.json",
|
||||
State: identity.StateActive,
|
||||
Traits: []byte(`{"email":"some@example.org"}`),
|
||||
VerifiableAddresses: []identity.VerifiableAddress{{
|
||||
Value: "some@example.org",
|
||||
Verified: false,
|
||||
Via: "email",
|
||||
Status: identity.VerifiableAddressStatusPending,
|
||||
}},
|
||||
RecoveryAddresses: []identity.RecoveryAddress{{
|
||||
Value: "some@example.org",
|
||||
Via: "email",
|
||||
}},
|
||||
MetadataPublic: []byte(`{"public":"data"}`),
|
||||
MetadataAdmin: []byte(`{"admin":"data"}`),
|
||||
InternalCredentials: identity.CredentialsCollection{{Type: "password", Identifiers: []string{"test"}, Config: []byte(`{}`)}},
|
||||
}
|
||||
|
||||
t.Run("case=body is empty", func(t *testing.T) {
|
||||
actual := run(t, expected, http.StatusOK, []byte(`{}`))
|
||||
snapshotx.SnapshotT(t, &actual)
|
||||
})
|
||||
|
||||
t.Run("case=identity is present but empty", func(t *testing.T) {
|
||||
actual := run(t, expected, http.StatusOK, []byte(`{"identity":{}}`))
|
||||
snapshotx.SnapshotT(t, &actual)
|
||||
})
|
||||
|
||||
t.Run("case=identity has updated traits", func(t *testing.T) {
|
||||
actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"traits":{"email":"some@other-example.org"}}}`))
|
||||
snapshotx.SnapshotT(t, &actual)
|
||||
})
|
||||
|
||||
t.Run("case=identity has updated state", func(t *testing.T) {
|
||||
actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"state":"inactive"}}`))
|
||||
snapshotx.SnapshotT(t, &actual)
|
||||
})
|
||||
|
||||
t.Run("case=identity has updated public metadata", func(t *testing.T) {
|
||||
actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"metadata_public":{"useful":"metadata"}}}`))
|
||||
snapshotx.SnapshotT(t, &actual)
|
||||
})
|
||||
|
||||
t.Run("case=identity has updated admin metadata", func(t *testing.T) {
|
||||
actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"metadata_admin":{"useful":"metadata"}}}`))
|
||||
snapshotx.SnapshotT(t, &actual)
|
||||
})
|
||||
|
||||
t.Run("case=identity has updated verified addresses", func(t *testing.T) {
|
||||
actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"traits":{"email":"some@other-example.org"},"verifiable_addresses":[{"value":"some@other-example.org","via":"email"}]}}`))
|
||||
snapshotx.SnapshotT(t, &actual)
|
||||
})
|
||||
|
||||
t.Run("case=identity has updated recovery addresses", func(t *testing.T) {
|
||||
actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"traits":{"email":"some@other-example.org"},"recovery_addresses":[{"value":"some@other-example.org","via":"email"}]}}`))
|
||||
snapshotx.SnapshotT(t, &actual)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("must error when config is erroneous", func(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Header: map[string][]string{"Some-Header": {"Some-Value"}},
|
||||
|
|
@ -624,6 +710,24 @@ func TestWebHooks(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("cannot have parse and ignore both set", func(t *testing.T) {
|
||||
ts := newServer(webHookHttpCodeEndPoint(200))
|
||||
req := &http.Request{
|
||||
Header: map[string][]string{"Some-Header": {"Some-Value"}},
|
||||
Host: "www.ory.sh",
|
||||
TLS: new(tls.ConnectionState),
|
||||
URL: &url.URL{Path: "/some_end_point"},
|
||||
|
||||
Method: http.MethodPost,
|
||||
}
|
||||
f := &login.Flow{ID: x.NewUUID()}
|
||||
conf := json.RawMessage(fmt.Sprintf(`{"url": "%s", "method": "GET", "body": "./stub/test_body.jsonnet", "response": {"ignore": true, "parse": true}}`, ts.URL+path))
|
||||
wh := hook.NewWebHook(&whDeps, conf)
|
||||
|
||||
err := wh.ExecuteLoginPreHook(nil, req, f)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("must error when template is erroneous", func(t *testing.T) {
|
||||
ts := newServer(webHookHttpCodeEndPoint(200))
|
||||
req := &http.Request{
|
||||
|
|
|
|||
|
|
@ -45,6 +45,68 @@ context("Registration success with email profile with webhooks", () => {
|
|||
expect(identity.traits.email).to.equal(email)
|
||||
})
|
||||
})
|
||||
|
||||
it("should sign up and modify the identity", () => {
|
||||
const email = gen.email()
|
||||
const password = gen.password()
|
||||
|
||||
const updatedEmail = {
|
||||
identity: {
|
||||
traits: { email: "updated-" + email },
|
||||
verifiable_addresses: [
|
||||
{ via: "email", value: "updated-" + email, verified: true },
|
||||
{ via: "email", value: "this-email-should-be-ignored" },
|
||||
{ via: "email", value: "" },
|
||||
],
|
||||
recovery_addresses: [
|
||||
{ via: "email", value: "updated-" + email },
|
||||
{ via: "email", value: "this-email-should-be-ignored" },
|
||||
{ via: "email", value: "" },
|
||||
],
|
||||
metadata_public: { some: "public fields" },
|
||||
},
|
||||
}
|
||||
cy.setPostPasswordRegistrationHooks([
|
||||
{ hook: "session" },
|
||||
{
|
||||
hook: "web_hook",
|
||||
config: {
|
||||
url:
|
||||
"http://127.0.0.1:4459/webhook/write?response=" +
|
||||
encodeURIComponent(JSON.stringify(updatedEmail)),
|
||||
method: "POST",
|
||||
body: "file://test/e2e/profiles/webhooks/webhook_body.jsonnet",
|
||||
response: { parse: true },
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
cy.get(appPrefix(app) + 'input[name="traits"]').should("not.exist")
|
||||
cy.get('input[name="traits.email"]').type(email)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
|
||||
cy.submitPasswordForm()
|
||||
|
||||
cy.getSession().should((session) => {
|
||||
const { identity } = session
|
||||
expect(identity.id).to.not.be.empty
|
||||
expect(identity.schema_id).to.equal("default")
|
||||
expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`)
|
||||
expect(identity.traits.email).to.equal("updated-" + email)
|
||||
expect(identity.metadata_public.some).to.equal("public fields")
|
||||
expect(identity.verifiable_addresses[0].verified).to.equal(true)
|
||||
expect(identity.verifiable_addresses[0].verified_at).not.to.be.empty
|
||||
expect(identity.verifiable_addresses[0].via).to.eq("email")
|
||||
expect(identity.verifiable_addresses[0].value).to.eq(
|
||||
"updated-" + email,
|
||||
)
|
||||
expect(identity.verifiable_addresses).to.have.length(1)
|
||||
|
||||
expect(identity.recovery_addresses).to.have.length(1)
|
||||
expect(identity.recovery_addresses[0].via).to.eq("email")
|
||||
expect(identity.recovery_addresses[0].value).to.eq("updated-" + email)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -209,6 +209,13 @@ Cypress.Commands.add("enableLoginForVerifiedAddressOnly", () => {
|
|||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add("setPostPasswordRegistrationHooks", (hooks) => {
|
||||
updateConfigFile((config) => {
|
||||
config.selfservice.flows.registration["after"].password = { hooks }
|
||||
return config
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add("shortLoginLifespan", ({} = {}) => {
|
||||
updateConfigFile((config) => {
|
||||
config.selfservice.flows.login.lifespan = "100ms"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Session } from "@ory/kratos-client"
|
||||
|
||||
export interface MailMessage {
|
||||
fromAddress: string
|
||||
toAddresses: Array<string>
|
||||
|
|
@ -151,6 +149,15 @@ declare global {
|
|||
returnTo?: string
|
||||
}): Chainable<void>
|
||||
|
||||
/**
|
||||
* Sets the post registration hook.
|
||||
*
|
||||
* @param hooks
|
||||
*/
|
||||
setPostPasswordRegistrationHooks(
|
||||
hooks: Array<{ hook: string; config?: any }>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Submits a verification flow via the Browser
|
||||
*
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
|
|
@ -97,15 +98,24 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
payload := struct {
|
||||
IdentityId string `json:"identity_id,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
FlowId string `json:"flow_id"`
|
||||
FlowType string `json:"flow_type"`
|
||||
IdentityId string `json:"identity_id,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
FlowId string `json:"flow_id"`
|
||||
FlowType string `json:"flow_type"`
|
||||
Context json.RawMessage `json:"ctx"`
|
||||
}{}
|
||||
|
||||
encoder := json.NewDecoder(r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.WithError(err).Warn("could not read request body")
|
||||
b := bytes.NewBufferString(fmt.Sprintf("error while reading request body: %s", err))
|
||||
_, _ = w.Write(b.Bytes())
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
if err := encoder.Decode(&payload); err != nil {
|
||||
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.WithError(err).Warn("could not unmarshal request JSON")
|
||||
b := bytes.NewBufferString(fmt.Sprintf("error while parsing request JSON: %s", err))
|
||||
|
|
@ -113,7 +123,7 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
log.WithField("payload", payload).Info("unmarshalled request")
|
||||
log.WithField("payload", string(body)).Info("unmarshalled request")
|
||||
|
||||
if !strings.Contains(payload.Email, "_blocked@ory.sh") || payload.FlowType == "api" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
|
@ -129,7 +139,7 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
msg := errorMessage{InstancePtr: "#/traits/email", Messages: []detailedMessage{detail}}
|
||||
resp := rawHookResponse{Messages: []errorMessage{msg}}
|
||||
err := json.NewEncoder(w).Encode(&resp)
|
||||
err = json.NewEncoder(w).Encode(&resp)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
buff := bytes.NewBufferString(err.Error())
|
||||
|
|
@ -138,11 +148,22 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func writeWebhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{"application": "webhooks", "path": r.URL.Path, "response": r.URL.Query().Get("response")}).Info("sending response")
|
||||
_, _ = w.Write([]byte(r.URL.Query().Get("response")))
|
||||
}
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/health", healthCheck)
|
||||
mux.HandleFunc("/webhook", accessLog(headerAuth(webhookHandler)))
|
||||
mux.HandleFunc("/webhook/write", accessLog(writeWebhookHandler))
|
||||
|
||||
s := http.Server{
|
||||
Addr: ":4459",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"@types/node": "^16.9.6",
|
||||
"@types/yamljs": "^0.2.31",
|
||||
"chrome-remote-interface": "0.31.2",
|
||||
"cypress": "^10.3.1",
|
||||
"cypress": "^11.2.0",
|
||||
"dayjs": "^1.10.4",
|
||||
"got": "^11.8.2",
|
||||
"otplib": "^12.0.1",
|
||||
|
|
@ -754,9 +754,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/cypress": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.3.1.tgz",
|
||||
"integrity": "sha512-As9HrExjAgpgjCnbiQCuPdw5sWKx5HUJcK2EOKziu642akwufr/GUeqL5UnCPYXTyyibvEdWT/pSC2qnGW/e5w==",
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-11.2.0.tgz",
|
||||
"integrity": "sha512-u61UGwtu7lpsNWLUma/FKNOsrjcI6wleNmda/TyKHe0dOBcVjbCPlp1N6uwFZ0doXev7f/91YDpU9bqDCFeBLA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
|
|
@ -779,7 +779,7 @@
|
|||
"dayjs": "^1.10.4",
|
||||
"debug": "^4.3.2",
|
||||
"enquirer": "^2.3.6",
|
||||
"eventemitter2": "^6.4.3",
|
||||
"eventemitter2": "6.4.7",
|
||||
"execa": "4.1.0",
|
||||
"executable": "^4.1.1",
|
||||
"extract-zip": "2.0.1",
|
||||
|
|
@ -952,9 +952,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eventemitter2": {
|
||||
"version": "6.4.5",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.5.tgz",
|
||||
"integrity": "sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw==",
|
||||
"version": "6.4.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz",
|
||||
"integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/execa": {
|
||||
|
|
@ -2960,9 +2960,9 @@
|
|||
}
|
||||
},
|
||||
"cypress": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.3.1.tgz",
|
||||
"integrity": "sha512-As9HrExjAgpgjCnbiQCuPdw5sWKx5HUJcK2EOKziu642akwufr/GUeqL5UnCPYXTyyibvEdWT/pSC2qnGW/e5w==",
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-11.2.0.tgz",
|
||||
"integrity": "sha512-u61UGwtu7lpsNWLUma/FKNOsrjcI6wleNmda/TyKHe0dOBcVjbCPlp1N6uwFZ0doXev7f/91YDpU9bqDCFeBLA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@cypress/request": "^2.88.10",
|
||||
|
|
@ -2984,7 +2984,7 @@
|
|||
"dayjs": "^1.10.4",
|
||||
"debug": "^4.3.2",
|
||||
"enquirer": "^2.3.6",
|
||||
"eventemitter2": "^6.4.3",
|
||||
"eventemitter2": "6.4.7",
|
||||
"execa": "4.1.0",
|
||||
"executable": "^4.1.1",
|
||||
"extract-zip": "2.0.1",
|
||||
|
|
@ -3117,9 +3117,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"eventemitter2": {
|
||||
"version": "6.4.5",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.5.tgz",
|
||||
"integrity": "sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw==",
|
||||
"version": "6.4.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz",
|
||||
"integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==",
|
||||
"dev": true
|
||||
},
|
||||
"execa": {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
"@types/node": "^16.9.6",
|
||||
"@types/yamljs": "^0.2.31",
|
||||
"chrome-remote-interface": "0.31.2",
|
||||
"cypress": "^10.3.1",
|
||||
"cypress": "^11.2.0",
|
||||
"dayjs": "^1.10.4",
|
||||
"got": "^11.8.2",
|
||||
"otplib": "^12.0.1",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@
|
|||
"password": {
|
||||
"identifier": true
|
||||
}
|
||||
},
|
||||
"verification": {
|
||||
"via": "email"
|
||||
},
|
||||
"recovery": {
|
||||
"via": "email"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,5 +2,7 @@ function(ctx) {
|
|||
identity_id: if std.objectHas(ctx, "identity") then ctx.identity.id else null,
|
||||
email: if std.objectHas(ctx, "identity") then ctx.identity.traits.email else null,
|
||||
flow_id: ctx.flow.id,
|
||||
flow_type: ctx.flow.type
|
||||
flow_type: ctx.flow.type,
|
||||
identity: if std.objectHas(ctx, "identity") then ctx.identity else null,
|
||||
ctx: ctx
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package x
|
||||
|
||||
// Coalesce returns the first non-empty string value
|
||||
func Coalesce[T ~string](str ...T) T {
|
||||
for _, s := range str {
|
||||
if len(s) > 0 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Loading…
Reference in New Issue