Repository: ory/kratos

Issue:
`/sessions` endpoint returns sessions with complete identities even when configured AAL for whoami isn't reached
### Preflight checklist

- [X] I could not find a solution in the existing issues, docs, nor discussions.
- [X] I agree to follow this project's [Code of Conduct](https://github.com/ory/kratos/blob/master/CODE_OF_CONDUCT.md).
- [X] I have read and am following this repository's [Contribution Guidelines](https://github.com/ory/kratos/blob/master/CONTRIBUTING.md).
- [X] I have joined the [Ory Community Slack](https://slack.ory.sh).
- [X] I am signed up to the [Ory Security Patch Newsletter](https://ory.us10.list-manage.com/subscribe?u=ffb1a878e4ec6c0ed312a3480&id=f605a41b53).

### Ory Network Project

_No response_

### Describe the bug

When `required_aal` for whoami is set as `highest_available`, Kratos will avoid leaking identity information without a valid AAL

https://github.com/ory/kratos/blob/0c5ea9bf735a67ef35011ba41d7f98afc6f8e118/selfservice/flow/login/hook.go#L230-L231
https://github.com/ory/kratos/blob/0c5ea9bf735a67ef35011ba41d7f98afc6f8e118/session/handler.go#L229-L236

Unfortunately, this check is only performed on `/sessions/whoami`, allowing other `/sessions` requests without a higher AAL - leaking the user's identity.

### Reproducing the bug

1. Setup Kratos with whoami `required_aal` set to `highest_available`, and an identity that supports AAL2
2. Login with AAL1
3. Using the AAL1 session, request `/sessions`
4. Previous sessions will have an identity field

### Relevant log output

_No response_

### Relevant configuration

```yml
session:
  whoami:
    required_aal: highest_available
```


### Version

master, but could be reproduced on 1.0.0

### On which operating system are you observing this issue?

None

### In which environment are you deploying?

None

### Additional Context

_No response_

Discussion:
@alnr: This is by design, since `/sessions` is an admin API. Please reopen if you think I'm mistaken.@K3das: It is not an admin API, and I don't believe I could reopen this issue myself.

https://www.ory.sh/docs/kratos/reference/api#tag/frontend/operation/listMySessions@aeneasr: Great find! PRs welcomed :)

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a22d2f137..8aeb4aafd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -120,6 +120,7 @@ Closes https://github.com/ory-corp/cloud/issues/7176
     identifier of the original identity was not updated.
 
 * Cancel conditional passkey before trying again ([#4247](https://github.com/ory/kratos/issues/4247)) ([d9f6f75](https://github.com/ory/kratos/commit/d9f6f75b6a43aad996f6390f73616a2cf596c6e4))
+* Count MFA addresses in CountActiveMultiFactorCredentials for code method ([9860c9a](https://github.com/ory/kratos/commit/9860c9a4faa5bd5d725c742c4d4ce9473baa0963)), closes [ory/network#409](https://github.com/ory/network/issues/409)
 * Do not roll back transaction on partial identity insert error ([#4211](https://github.com/ory/kratos/issues/4211)) ([82660f0](https://github.com/ory/kratos/commit/82660f04e2f33d0aa86fccee42c90773a901d400))
 * Don't show oidc subject in login hints ([#4264](https://github.com/ory/kratos/issues/4264)) ([b95fd3f](https://github.com/ory/kratos/commit/b95fd3fa723521807824cad84e4a9ce812172311))
 * Duplicate autocomplete trigger ([6bbf915](https://github.com/ory/kratos/commit/6bbf91593a37e4973a86f610290ebab44df8dc81))
@@ -161,6 +162,7 @@ Closes https://github.com/ory-corp/cloud/issues/7176
     
     ```
 
+* Incorrect if switch in previous sceen case in two step registration ([f8ee403](https://github.com/ory/kratos/commit/f8ee40396a36a2e7a348c9cf983dec7db13814c5)), closes [#374](https://github.com/ory/kratos/issues/374)
 * Incorrect query plan ([#4218](https://github.com/ory/kratos/issues/4218)) ([7d0e78a](https://github.com/ory/kratos/commit/7d0e78a4f6631b0662beee3b8e9dd0d774b875ea))
 * Order-by clause and span names ([#4200](https://github.com/ory/kratos/issues/4200)) ([b6278af](https://github.com/ory/kratos/commit/b6278af5c7ed7fb845a71ad0e64f8b87402a8f4b))
 * Pass on correct context during verification ([#4151](https://github.com/ory/kratos/issues/4151)) ([7e0b500](https://github.com/ory/kratos/commit/7e0b500aada9c1931c759a43db7360e85afb57e3))
@@ -236,6 +238,7 @@ Closes https://github.com/ory-corp/cloud/issues/7176
 
 * Add a policy callback to customize OIDC credential linking ([#4302](https://github.com/ory/kratos/issues/4302)) ([e2f878a](https://github.com/ory/kratos/commit/e2f878a3ed25b4617bf6bd43b6e147d87d3b8ca2))
 * Add attributes to webhook events for better debugging ([#4206](https://github.com/ory/kratos/issues/4206)) ([00da05d](https://github.com/ory/kratos/commit/00da05da9f77bbfb68b364b3ba2a5d0a2d9e4f15))
+* Add captcha group to first-step registration ([eca4ae9](https://github.com/ory/kratos/commit/eca4ae9dcce37d03bbd1bf5f0cd492466c02acde))
 * Add context param to policy ([#4315](https://github.com/ory/kratos/issues/4315)) ([261596b](https://github.com/ory/kratos/commit/261596b7261c315b7d8291e886023c34fc9135c5))
 * Add explicit config flag for secure cookies ([#4180](https://github.com/ory/kratos/issues/4180)) ([2aabe12](https://github.com/ory/kratos/commit/2aabe12e5329acc807c495445999e5591bdf982b)):
 
@@ -270,6 +273,10 @@ Closes https://github.com/ory-corp/cloud/issues/7176
     
     See https://github.com/ory-corp/cloud/issues/7350
 
+* Add new Division ui node attributes ([235af52](https://github.com/ory/kratos/commit/235af527dea47b87ad0f18ff04f9b807e4639ae3)):
+
+    Division nodes may be used to hook dynamic scripts and are not actively used in the Ory Kratos open source.
+
 * Add oid as subject source for microsoft ([#4171](https://github.com/ory/kratos/issues/4171)) ([77beb4d](https://github.com/ory/kratos/commit/77beb4de5209cee0bea4b63dfec21d656cf64473)), closes [#4170](https://github.com/ory/kratos/issues/4170):
 
     In the case of Microsoft, using `sub` as an identifier can lead to problems. Because the use of OIDC at Microsoft is based on an app registration, the content of `sub` changes with every new app registration. `Sub` is therefore not uniquely related to the user. It is therefore not possible to transfer users from one app registration to another without further problems.
@@ -357,6 +364,11 @@ Closes https://github.com/ory-corp/cloud/issues/7176
     correctly sometimes.
 
 * Use one transaction for `/admin/recovery/code` ([#4225](https://github.com/ory/kratos/issues/4225)) ([3e87e0c](https://github.com/ory/kratos/commit/3e87e0c4559736f9476eba943bac8d67cde91aad))
+* Webhook header allowlist configuration option ([#4309](https://github.com/ory/kratos/issues/4309)) ([871f5aa](https://github.com/ory/kratos/commit/871f5aab6d7b2a655ebcd6f0f90e79635ffc85f6)), closes [#4290](https://github.com/ory/kratos/issues/4290):
+
+    Adds a `clients.web_hook.header_allowlist` configuration option for
+    configuring the webhook header allowlist.
+
 
 ### Tests
 
diff --git a/driver/config/.snapshots/TestDefaultWebhookHeaderAllowlist.json b/driver/config/.snapshots/TestDefaultWebhookHeaderAllowlist.json
new file mode 100644
index 000000000..6ec4b1e3d
--- /dev/null
+++ b/driver/config/.snapshots/TestDefaultWebhookHeaderAllowlist.json
@@ -0,0 +1,19 @@
+[
+  "Accept",
+  "Accept-Encoding",
+  "Accept-Language",
+  "Content-Length",
+  "Content-Type",
+  "Origin",
+  "Priority",
+  "Referer",
+  "Sec-Ch-Ua",
+  "Sec-Ch-Ua-Mobile",
+  "Sec-Ch-Ua-Platform",
+  "Sec-Fetch-Dest",
+  "Sec-Fetch-Mode",
+  "Sec-Fetch-Site",
+  "Sec-Fetch-User",
+  "True-Client-Ip",
+  "User-Agent"
+]
diff --git a/driver/config/config.go b/driver/config/config.go
index 366f9e371..f859393e0 100644
--- a/driver/config/config.go
+++ b/driver/config/config.go
@@ -208,6 +208,7 @@ const (
 	ViperKeyOAuth2ProviderOverrideReturnTo                   = "oauth2_provider.override_return_to"
 	ViperKeyClientHTTPNoPrivateIPRanges                      = "clients.http.disallow_private_ip_ranges"
 	ViperKeyClientHTTPPrivateIPExceptionURLs                 = "clients.http.private_ip_exception_urls"
+	ViperKeyWebhookHeaderAllowlist                           = "clients.web_hook.header_allowlist"
 	ViperKeyPreviewDefaultReadConsistencyLevel               = "preview.default_read_consistency_level"
 	ViperKeyVersion                                          = "version"
 	ViperKeyPasswordMigrationHook                            = "selfservice.methods.password.config.migrate_hook"
@@ -954,6 +955,28 @@ func (p *Config) SelfAdminURL(ctx context.Context) *url.URL {
 	return p.baseURL(ctx, ViperKeyAdminBaseURL, ViperKeyAdminHost, ViperKeyAdminPort, 4434)
 }
 
+func (p *Config) WebhookHeaderAllowlist(ctx context.Context) []string {
+	return p.GetProvider(ctx).StringsF(ViperKeyWebhookHeaderAllowlist, []string{
+		"Accept",
+		"Accept-Encoding",
+		"Accept-Language",
+		"Content-Length",
+		"Content-Type",
+		"Origin",
+		"Priority",
+		"Referer",
+		"Sec-Ch-Ua",
+		"Sec-Ch-Ua-Mobile",
+		"Sec-Ch-Ua-Platform",
+		"Sec-Fetch-Dest",
+		"Sec-Fetch-Mode",
+		"Sec-Fetch-Site",
+		"Sec-Fetch-User",
+		"True-Client-Ip",
+		"User-Agent",
+	})
+}
+
 func (p *Config) OAuth2ProviderHeader(ctx context.Context) http.Header {
 	hh := map[string]string{}
 	if err := p.GetProvider(ctx).Unmarshal(ViperKeyOAuth2ProviderHeader, &hh); err != nil {
diff --git a/embedx/config.schema.json b/embedx/config.schema.json
index 24e16743d..545c10567 100644
--- a/embedx/config.schema.json
+++ b/embedx/config.schema.json
@@ -43,7 +43,10 @@
       "description": "Ory Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.sh/kratos/docs/concepts/browser-redirect-flow-completion).",
       "type": "string",
       "format": "uri-reference",
-      "examples": ["https://my-app.com/dashboard", "/dashboard"]
+      "examples": [
+        "https://my-app.com/dashboard",
+        "/dashboard"
+      ]
     },
     "selfServiceSessionRevokerHook": {
       "type": "object",
@@ -53,7 +56,9 @@
         }
       },
       "additionalProperties": false,
-      "required": ["hook"]
+      "required": [
+        "hook"
+      ]
     },
     "selfServiceSessionIssuerHook": {
       "type": "object",
@@ -63,7 +68,9 @@
         }
       },
       "additionalProperties": false,
-      "required": ["hook"]
+      "required": [
+        "hook"
+      ]
     },
     "selfServiceRequireVerifiedAddressHook": {
       "type": "object",
@@ -73,7 +80,9 @@
         }
       },
       "additionalProperties": false,
-      "required": ["hook"]
+      "required": [
+        "hook"
+      ]
     },
     "selfServiceVerificationHook": {
       "type": "object",
@@ -83,7 +92,9 @@
         }
       },
       "additionalProperties": false,
-      "required": ["hook"]
+      "required": [
+        "hook"
+      ]
     },
     "selfServiceShowVerificationUIHook": {
       "type": "object",
@@ -93,7 +104,9 @@
         }
       },
       "additionalProperties": false,
-      "required": ["hook"]
+      "required": [
+        "hook"
+      ]
     },
     "b2bSSOHook": {
       "type": "object",
@@ -107,7 +120,10 @@
         }
       },
       "additionalProperties": false,
-      "required": ["hook", "config"]
+      "required": [
+        "hook",
+        "config"
+      ]
     },
     "webHookAuthBasicAuthProperties": {
       "properties": {
@@ -127,11 +143,17 @@
             }
           },
           "additionalProperties": false,
-          "required": ["user", "password"]
+          "required": [
+            "user",
+            "password"
+          ]
         }
       },
       "additionalProperties": false,
-      "required": ["type", "config"]
+      "required": [
+        "type",
+        "config"
+      ]
     },
     "httpRequestConfig": {
       "type": "object",
@@ -139,7 +161,9 @@
         "url": {
           "title": "HTTP address of API endpoint",
           "description": "This URL will be used to send the emails to.",
-          "examples": ["https://example.com/api/v1/email"],
+          "examples": [
+            "https://example.com/api/v1/email"
+          ],
           "type": "string",
           "pattern": "^https?://"
         },
@@ -204,15 +228,25 @@
             "in": {
               "type": "string",
               "description": "How the api key should be transferred",
-              "enum": ["header", "cookie"]
+              "enum": [
+                "header",
+                "cookie"
+              ]
             }
           },
           "additionalProperties": false,
-          "required": ["name", "value", "in"]
+          "required": [
+            "name",
+            "value",
+            "in"
+          ]
         }
       },
       "additionalProperties": false,
-      "required": ["type", "config"]
+      "required": [
+        "type",
+        "config"
+      ]
     },
     "selfServiceWebHook": {
       "type": "object",
@@ -255,7 +289,10 @@
                     "const": true
                   }
                 },
-                "required": ["ignore", "parse"]
+                "required": [
+                  "ignore",
+                  "parse"
+                ]
               }
             },
             "url": {
@@ -328,30 +365,46 @@
                   "response": {
                     "properties": {
                       "ignore": {
-                        "enum": [true]
+                        "enum": [
+                          true
+                        ]
                       }
                     },
-                    "required": ["ignore"]
+                    "required": [
+                      "ignore"
+                    ]
                   }
                 },
-                "required": ["response"]
+                "required": [
+                  "response"
+                ]
               }
             },
             {
               "properties": {
                 "can_interrupt": {
-                  "enum": [false]
+                  "enum": [
+                    false
+                  ]
                 }
               },
-              "require": ["can_interrupt"]
+              "require": [
+                "can_interrupt"
+              ]
             }
           ],
           "additionalProperties": false,
-          "required": ["url", "method"]
+          "required": [
+            "url",
+            "method"
+          ]
         }
       },
       "additionalProperties": false,
-      "required": ["hook", "config"]
+      "required": [
+        "hook",
+        "config"
+      ]
     },
     "OIDCClaims": {
       "title": "OpenID Connect claims",
@@ -384,7 +437,9 @@
               "essential": true
             },
             "acr": {
-              "values": ["urn:mace:incommon:iap:silver"]
+              "values": [
+                "urn:mace:incommon:iap:silver"
+              ]
             }
           }
         }
@@ -432,7 +487,9 @@
       "properties": {
         "id": {
           "type": "string",
-          "examples": ["google"]
+          "examples": [
+            "google"
+          ]
         },
         "provider": {
           "title": "Provider",
@@ -463,7 +520,9 @@
             "x",
             "fedcm-test"
           ],
-          "examples": ["google"]
+          "examples": [
+            "google"
+          ]
         },
         "label": {
           "title": "Optional string which will be used when generating labels for UI buttons.",
@@ -478,17 +537,23 @@
         "issuer_url": {
           "type": "string",
           "format": "uri",
-          "examples": ["https://accounts.google.com"]
+          "examples": [
+            "https://accounts.google.com"
+          ]
         },
         "auth_url": {
           "type": "string",
           "format": "uri",
-          "examples": ["https://accounts.google.com/o/oauth2/v2/auth"]
+          "examples": [
+            "https://accounts.google.com/o/oauth2/v2/auth"
+          ]
         },
         "token_url": {
           "type": "string",
           "format": "uri",
-          "examples": ["https://www.googleapis.com/oauth2/v4/token"]
+          "examples": [
+            "https://www.googleapis.com/oauth2/v4/token"
+          ]
         },
         "mapper_url": {
           "title": "Jsonnet Mapper URL",
@@ -505,7 +570,10 @@
           "type": "array",
           "items": {
             "type": "string",
-            "examples": ["offline_access", "profile"]
+            "examples": [
+              "offline_access",
+              "profile"
+            ]
           }
         },
         "microsoft_tenant": {
@@ -524,21 +592,31 @@
           "title": "Microsoft subject source",
           "description": "Controls which source the subject identifier is taken from by microsoft provider. If set to `userinfo` (the default) then the identifier is taken from the `sub` field of OIDC ID token or data received from `/userinfo` standard OIDC endpoint. If set to `me` then the `id` field of data structure received from `https://graph.microsoft.com/v1.0/me` is taken as an identifier. If the value is `oid` then the the oid (Object ID) is taken to identify users across different services.",
           "type": "string",
-          "enum": ["userinfo", "me", "oid"],
+          "enum": [
+            "userinfo",
+            "me",
+            "oid"
+          ],
           "default": "userinfo",
-          "examples": ["userinfo"]
+          "examples": [
+            "userinfo"
+          ]
         },
         "apple_team_id": {
           "title": "Apple Developer Team ID",
           "description": "Apple Developer Team ID needed for generating a JWT token for client secret",
           "type": "string",
-          "examples": ["KP76DQS54M"]
+          "examples": [
+            "KP76DQS54M"
+          ]
         },
         "apple_private_key_id": {
           "title": "Apple Private Key Identifier",
           "description": "Sign In with Apple Private Key Identifier needed for generating a JWT token for client secret",
           "type": "string",
-          "examples": ["UX56C66723"]
+          "examples": [
+            "UX56C66723"
+          ]
         },
         "apple_private_key": {
           "title": "Apple Private Key",
@@ -555,29 +633,43 @@
           "title": "Organization ID",
           "description": "The ID of the organization that this provider belongs to. Only effective in the Ory Network.",
           "type": "string",
-          "examples": ["12345678-1234-1234-1234-123456789012"]
+          "examples": [
+            "12345678-1234-1234-1234-123456789012"
+          ]
         },
         "additional_id_token_audiences": {
           "title": "Additional client ids allowed when using ID token submission",
           "type": "array",
           "items": {
             "type": "string",
-            "examples": ["12345678-1234-1234-1234-123456789012"]
+            "examples": [
+              "12345678-1234-1234-1234-123456789012"
+            ]
           }
         },
         "claims_source": {
           "title": "Claims source",
           "description": "Can be either `userinfo` (calls the userinfo endpoint to get the claims) or `id_token` (takes the claims from the id token). It defaults to `id_token`",
           "type": "string",
-          "enum": ["id_token", "userinfo"],
+          "enum": [
+            "id_token",
+            "userinfo"
+          ],
           "default": "id_token",
-          "examples": ["id_token", "userinfo"]
+          "examples": [
+            "id_token",
+            "userinfo"
+          ]
         },
         "pkce": {
           "title": "Proof Key for Code Exchange",
           "description": "PKCE controls if the OpenID Connect OAuth2 flow should use PKCE (Proof Key for Code Exchange). IMPORTANT: If you set this to `force`, you must whitelist a different return URL for your OAuth2 client in the provider's configuration. Instead of <base-url>/self-service/methods/oidc/callback/<provider>, you must use <base-url>/self-service/methods/oidc/callback",
           "type": "string",
-          "enum": ["auto", "never", "force"],
+          "enum": [
+            "auto",
+            "never",
+            "force"
+          ],
           "default": "auto"
         },
         "fedcm_config_url": {
@@ -585,17 +677,26 @@
           "description": "The URL where the FedCM IdP configuration is located for the provider. This is only effective in the Ory Network.",
           "type": "string",
           "format": "uri",
-          "examples": ["https://example.com/config.json"]
+          "examples": [
+            "https://example.com/config.json"
+          ]
         },
         "net_id_token_origin_header": {
           "title": "NetID Token Origin Header",
           "description": "Contains the orgin header to be used when exchanging a NetID FedCM token for an ID token",
           "type": "string",
-          "examples": ["https://example.com"]
+          "examples": [
+            "https://example.com"
+          ]
         }
       },
       "additionalProperties": false,
-      "required": ["id", "provider", "client_id", "mapper_url"],
+      "required": [
+        "id",
+        "provider",
+        "client_id",
+        "mapper_url"
+      ],
       "allOf": [
         {
           "if": {
@@ -604,17 +705,23 @@
                 "const": "microsoft"
               }
             },
-            "required": ["provider"]
+            "required": [
+              "provider"
+            ]
           },
           "then": {
-            "required": ["microsoft_tenant"]
+            "required": [
+              "microsoft_tenant"
+            ]
           },
           "else": {
             "not": {
               "properties": {
                 "microsoft_tenant": {}
               },
-              "required": ["microsoft_tenant"]
+              "required": [
+                "microsoft_tenant"
+              ]
             }
           }
         },
@@ -625,7 +732,9 @@
                 "const": "apple"
               }
             },
-            "required": ["provider"]
+            "required": [
+              "provider"
+            ]
           },
           "then": {
             "not": {
@@ -635,7 +744,9 @@
                   "minLength": 1
                 }
               },
-              "required": ["client_secret"]
+              "required": [
+                "client_secret"
+              ]
             },
             "required": [
               "apple_private_key_id",
@@ -644,7 +755,9 @@
             ]
           },
           "else": {
-            "required": ["client_secret"],
+            "required": [
+              "client_secret"
+            ],
             "allOf": [
               {
                 "not": {
@@ -654,7 +767,9 @@
                       "minLength": 1
                     }
                   },
-                  "required": ["apple_team_id"]
+                  "required": [
+                    "apple_team_id"
+                  ]
                 }
               },
               {
@@ -665,7 +780,9 @@
                       "minLength": 1
                     }
                   },
-                  "required": ["apple_private_key_id"]
+                  "required": [
+                    "apple_private_key_id"
+                  ]
                 }
               },
               {
@@ -676,7 +793,9 @@
                       "minLength": 1
                     }
                   },
-                  "required": ["apple_private_key"]
+                  "required": [
+                    "apple_private_key"
+                  ]
                 }
               }
             ]
@@ -862,7 +981,10 @@
       "title": "Required Authenticator Assurance Level",
       "description": "Sets what Authenticator Assurance Level (used for 2FA) is required to access this feature. If set to `highest_available` then this endpoint requires the highest AAL the identity has set up. If set to `aal1` then the identity can access this feature without 2FA.",
       "type": "string",
-      "enum": ["aal1", "highest_available"],
+      "enum": [
+        "aal1",
+        "highest_available"
+      ],
       "default": "highest_available"
     },
     "selfServiceAfterSettings": {
@@ -1058,7 +1180,9 @@
         "path": {
           "title": "Path to PEM-encoded Fle",
           "type": "string",
-          "examples": ["path/to/file.pem"]
+          "examples": [
+            "path/to/file.pem"
+          ]
         },
         "base64": {
           "title": "Base64 Encoded Inline",
@@ -1106,7 +1230,9 @@
               "$ref": "#/definitions/emailCourierTemplate"
             }
           },
-          "required": ["email"]
+          "required": [
+            "email"
+          ]
         },
         "valid": {
           "additionalProperties": false,
@@ -1119,7 +1245,9 @@
               "$ref": "#/definitions/smsCourierTemplate"
             }
           },
-          "required": ["email"]
+          "required": [
+            "email"
+          ]
         }
       }
     },
@@ -1190,7 +1318,9 @@
     "selfservice": {
       "type": "object",
       "additionalProperties": false,
-      "required": ["default_browser_return_url"],
+      "required": [
+        "default_browser_return_url"
+      ],
       "properties": {
         "default_browser_return_url": {
           "$ref": "#/definitions/defaultReturnTo"
@@ -1225,20 +1355,30 @@
                   "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).",
                   "type": "string",
                   "format": "uri-reference",
-                  "examples": ["https://my-app.com/user/settings"],
+                  "examples": [
+                    "https://my-app.com/user/settings"
+                  ],
                   "default": "https://www.ory.sh/kratos/docs/fallback/settings"
                 },
                 "lifespan": {
                   "type": "string",
                   "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                   "default": "1h",
-                  "examples": ["1h", "1m", "1s"]
+                  "examples": [
+                    "1h",
+                    "1m",
+                    "1s"
+                  ]
                 },
                 "privileged_session_max_age": {
                   "type": "string",
                   "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                   "default": "1h",
-                  "examples": ["1h", "1m", "1s"]
+                  "examples": [
+                    "1h",
+                    "1m",
+                    "1s"
+                  ]
                 },
                 "required_aal": {
                   "$ref": "#/definitions/featureRequiredAal"
@@ -1287,14 +1427,20 @@
                   "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).",
                   "type": "string",
                   "format": "uri-reference",
-                  "examples": ["https://my-app.com/signup"],
+                  "examples": [
+                    "https://my-app.com/signup"
+                  ],
                   "default": "https://www.ory.sh/kratos/docs/fallback/registration"
                 },
                 "lifespan": {
                   "type": "string",
                   "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                   "default": "1h",
-                  "examples": ["1h", "1m", "1s"]
+                  "examples": [
+                    "1h",
+                    "1m",
+                    "1s"
+                  ]
                 },
                 "before": {
                   "$ref": "#/definitions/selfServiceBeforeRegistration"
@@ -1319,20 +1465,29 @@
                   "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).",
                   "type": "string",
                   "format": "uri-reference",
-                  "examples": ["https://my-app.com/login"],
+                  "examples": [
+                    "https://my-app.com/login"
+                  ],
                   "default": "https://www.ory.sh/kratos/docs/fallback/login"
                 },
                 "lifespan": {
                   "type": "string",
                   "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                   "default": "1h",
-                  "examples": ["1h", "1m", "1s"]
+                  "examples": [
+                    "1h",
+                    "1m",
+                    "1s"
+                  ]
                 },
                 "style": {
                   "title": "Login Flow Style",
                   "description": "The style of the login flow. If set to `unified` the login flow will be a one-step process. If set to `identifier_first` (experimental!) the login flow will first ask for the identifier and then the credentials.",
                   "type": "string",
-                  "enum": ["unified", "identifier_first"],
+                  "enum": [
+                    "unified",
+                    "identifier_first"
+                  ],
                   "default": "unified"
                 },
                 "before": {
@@ -1359,7 +1514,9 @@
                   "description": "URL where the Ory Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).",
                   "type": "string",
                   "format": "uri-reference",
-                  "examples": ["https://my-app.com/verify"],
+                  "examples": [
+                    "https://my-app.com/verify"
+                  ],
                   "default": "https://www.ory.sh/kratos/docs/fallback/verification"
                 },
                 "after": {
@@ -1371,7 +1528,11 @@
                   "type": "string",
                   "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                   "default": "1h",
-                  "examples": ["1h", "1m", "1s"]
+                  "examples": [
+                    "1h",
+                    "1m",
+                    "1s"
+                  ]
                 },
                 "before": {
                   "$ref": "#/definitions/selfServiceBeforeVerification"
@@ -1380,7 +1541,10 @@
                   "title": "Verification Strategy",
                   "description": "The strategy to use for verification requests",
                   "type": "string",
-                  "enum": ["link", "code"],
+                  "enum": [
+                    "link",
+                    "code"
+                  ],
                   "default": "code"
                 },
                 "notify_unknown_recipients": {
@@ -1407,7 +1571,9 @@
                   "description": "URL where the Ory Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).",
                   "type": "string",
                   "format": "uri-reference",
-                  "examples": ["https://my-app.com/verify"],
+                  "examples": [
+                    "https://my-app.com/verify"
+                  ],
                   "default": "https://www.ory.sh/kratos/docs/fallback/recovery"
                 },
                 "after": {
@@ -1419,7 +1585,11 @@
                   "type": "string",
                   "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                   "default": "1h",
-                  "examples": ["1h", "1m", "1s"]
+                  "examples": [
+                    "1h",
+                    "1m",
+                    "1s"
+                  ]
                 },
                 "before": {
                   "$ref": "#/definitions/selfServiceBeforeRecovery"
@@ -1428,7 +1598,10 @@
                   "title": "Recovery Strategy",
                   "description": "The strategy to use for recovery requests",
                   "type": "string",
-                  "enum": ["link", "code"],
+                  "enum": [
+                    "link",
+                    "code"
+                  ],
                   "default": "code"
                 },
                 "notify_unknown_recipients": {
@@ -1448,7 +1621,9 @@
                   "description": "URL where the Ory Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).",
                   "type": "string",
                   "format": "uri-reference",
-                  "examples": ["https://my-app.com/kratos-error"],
+                  "examples": [
+                    "https://my-app.com/kratos-error"
+                  ],
                   "default": "https://www.ory.sh/kratos/docs/fallback/error"
                 }
               }
@@ -1477,19 +1652,25 @@
                             "type": "string",
                             "description": "The ID of the organization.",
                             "format": "uuid",
-                            "examples": ["00000000-0000-0000-0000-000000000000"]
+                            "examples": [
+                              "00000000-0000-0000-0000-000000000000"
+                            ]
                           },
                           "label": {
                             "type": "string",
                             "description": "The label of the organization.",
-                            "examples": ["ACME SSO"]
+                            "examples": [
+                              "ACME SSO"
+                            ]
                           },
                           "domains": {
                             "type": "array",
                             "items": {
                               "type": "string",
                               "format": "hostname",
-                              "examples": ["my-app.com"],
+                              "examples": [
+                                "my-app.com"
+                              ],
                               "description": "If this domain matches the email's domain, this provider is shown."
                             }
                           }
@@ -1529,14 +1710,20 @@
                     "base_url": {
                       "title": "Override the base URL which should be used as the base for recovery and verification links.",
                       "type": "string",
-                      "examples": ["https://my-app.com"]
+                      "examples": [
+                        "https://my-app.com"
+                      ]
                     },
                     "lifespan": {
                       "title": "How long a link is valid for",
                       "type": "string",
                       "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                       "default": "1h",
-                      "examples": ["1h", "1m", "1s"]
+                      "examples": [
+                        "1h",
+                        "1m",
+                        "1s"
+                      ]
                     }
                   }
                 }
@@ -1604,13 +1791,16 @@
                       "type": "string",
                       "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                       "default": "1h",
-                      "examples": ["1h", "1m", "1s"]
+                      "examples": [
+                        "1h",
+                        "1m",
+                        "1s"
+                      ]
                     },
                     "missing_credential_fallback_enabled": {
                       "type": "boolean",
                       "title": "Enable Code OTP as a Fallback",
                       "description": "Enabling this allows users to sign in with the code method, even if their identity schema or their credentials are not set up to use the code method. If enabled, a verified address (such as an email) will be used to send the code to the user. Use with caution and only if actually needed.",
-
                       "default": false
                     }
                   }
@@ -1789,13 +1979,17 @@
                           "type": "string",
                           "title": "Relying Party Display Name",
                           "description": "An name to help the user identify this RP.",
-                          "examples": ["Ory Foundation"]
+                          "examples": [
+                            "Ory Foundation"
+                          ]
                         },
                         "id": {
                           "type": "string",
                           "title": "Relying Party Identifier",
                           "description": "The id must be a subset of the domain currently in the browser.",
-                          "examples": ["ory.sh"]
+                          "examples": [
+                            "ory.sh"
+                          ]
                         },
                         "origin": {
                           "type": "string",
@@ -1803,7 +1997,9 @@
                           "description": "An explicit RP origin. If left empty, this defaults to `id`, prepended with the current protocol schema (HTTP or HTTPS).",
                           "format": "uri",
                           "deprecationMessage": "This field is deprecated. Use `origins` instead.",
-                          "examples": ["https://www.ory.sh"]
+                          "examples": [
+                            "https://www.ory.sh"
+                          ]
                         },
                         "origins": {
                           "type": "array",
@@ -1824,13 +2020,18 @@
                           "description": "An icon to help the user identify this RP.",
                           "format": "uri",
                           "deprecationMessage": "This field is deprecated and ignored due to security considerations.",
-                          "examples": ["https://www.ory.sh/an-icon.png"]
+                          "examples": [
+                            "https://www.ory.sh/an-icon.png"
+                          ]
                         }
                       },
                       "type": "object",
                       "oneOf": [
                         {
-                          "required": ["id", "display_name"],
+                          "required": [
+                            "id",
+                            "display_name"
+                          ],
                           "properties": {
                             "origin": {
                               "not": {}
@@ -1841,7 +2042,11 @@
                           }
                         },
                         {
-                          "required": ["id", "display_name", "origin"],
+                          "required": [
+                            "id",
+                            "display_name",
+                            "origin"
+                          ],
                           "properties": {
                             "origin": {
                               "type": "string"
@@ -1852,7 +2057,11 @@
                           }
                         },
                         {
-                          "required": ["id", "display_name", "origins"],
+                          "required": [
+                            "id",
+                            "display_name",
+                            "origins"
+                          ],
                           "properties": {
                             "origin": {
                               "not": {}
@@ -1877,10 +2086,14 @@
                     "const": true
                   }
                 },
-                "required": ["enabled"]
+                "required": [
+                  "enabled"
+                ]
               },
               "then": {
-                "required": ["config"]
+                "required": [
+                  "config"
+                ]
               }
             },
             "passkey": {
@@ -1903,13 +2116,17 @@
                           "type": "string",
                           "title": "Relying Party Display Name",
                           "description": "A name to help the user identify this RP.",
-                          "examples": ["Ory Foundation"]
+                          "examples": [
+                            "Ory Foundation"
+                          ]
                         },
                         "id": {
                           "type": "string",
                           "title": "Relying Party Identifier",
                           "description": "The id must be a subset of the domain currently in the browser.",
-                          "examples": ["ory.sh"]
+                          "examples": [
+                            "ory.sh"
+                          ]
                         },
                         "origins": {
                           "type": "array",
@@ -1925,7 +2142,10 @@
                         }
                       },
                       "type": "object",
-                      "required": ["display_name", "id"]
+                      "required": [
+                        "display_name",
+                        "id"
+                      ]
                     }
                   },
                   "additionalProperties": false
@@ -1937,10 +2157,14 @@
                     "const": true
                   }
                 },
-                "required": ["enabled"]
+                "required": [
+                  "enabled"
+                ]
               },
               "then": {
-                "required": ["config"]
+                "required": [
+                  "config"
+                ]
               }
             },
             "oidc": {
@@ -1963,7 +2187,9 @@
                       "title": "Base URL for OAuth2 Redirect URIs",
                       "description": "Can be used to modify the base URL for OAuth2 Redirect URLs. If unset, the Public Base URL will be used.",
                       "format": "uri",
-                      "examples": ["https://auth.myexample.org/"]
+                      "examples": [
+                        "https://auth.myexample.org/"
+                      ]
                     },
                     "providers": {
                       "title": "OpenID Connect and OAuth2 Providers",
@@ -2071,7 +2297,9 @@
                       "$ref": "#/definitions/smsCourierTemplate"
                     }
                   },
-                  "required": ["email"]
+                  "required": [
+                    "email"
+                  ]
                 }
               }
             },
@@ -2090,7 +2318,9 @@
                       "$ref": "#/definitions/smsCourierTemplate"
                     }
                   },
-                  "required": ["email"]
+                  "required": [
+                    "email"
+                  ]
                 }
               }
             }
@@ -2100,13 +2330,18 @@
           "type": "string",
           "title": "Override message templates",
           "description": "You can override certain or all message templates by pointing this key to the path where the templates are located.",
-          "examples": ["/conf/courier-templates"]
+          "examples": [
+            "/conf/courier-templates"
+          ]
         },
         "message_retries": {
           "description": "Defines the maximum number of times the sending of a message is retried after it failed before it is marked as abandoned",
           "type": "integer",
           "default": 5,
-          "examples": [10, 60]
+          "examples": [
+            10,
+            60
+          ]
         },
         "worker": {
           "description": "Configures the dispatch worker.",
@@ -2129,7 +2364,10 @@
           "title": "Delivery Strategy",
           "description": "Defines how emails will be sent, either through SMTP (default) or HTTP.",
           "type": "string",
-          "enum": ["smtp", "http"],
+          "enum": [
+            "smtp",
+            "http"
+          ],
           "default": "smtp"
         },
         "http": {
@@ -2186,7 +2424,9 @@
               "title": "SMTP Sender Name",
               "description": "The recipient of an email will see this as the sender name.",
               "type": "string",
-              "examples": ["Bob"]
+              "examples": [
+                "Bob"
+              ]
             },
             "headers": {
               "title": "SMTP Headers",
@@ -2223,19 +2463,26 @@
                 "title": "Channel id",
                 "description": "The channel id. Corresponds to the .via property of the identity schema for recovery, verification, etc. Currently only sms is supported.",
                 "maxLength": 32,
-                "enum": ["sms"]
+                "enum": [
+                  "sms"
+                ]
               },
               "type": {
                 "type": "string",
                 "title": "Channel type",
                 "description": "The channel type. Currently only http is supported.",
-                "enum": ["http"]
+                "enum": [
+                  "http"
+                ]
               },
               "request_config": {
                 "$ref": "#/definitions/httpRequestConfig"
               }
             },
-            "required": ["id", "request_config"],
+            "required": [
+              "id",
+              "request_config"
+            ],
             "additionalProperties": false
           }
         }
@@ -2286,7 +2533,10 @@
           "type": "string",
           "title": "Default Read Consistency Level",
           "description": "The default consistency level to use when reading from the database. Defaults to `strong` to not break existing API contracts. Only set this to `eventual` if you can accept that other read APIs will suddenly return eventually consistent results. It is only effective in Ory Network.",
-          "enum": ["strong", "eventual"],
+          "enum": [
+            "strong",
+            "eventual"
+          ],
           "default": "strong"
         }
       }
@@ -2314,7 +2564,9 @@
               "description": "The URL where the admin endpoint is exposed at.",
               "type": "string",
               "format": "uri",
-              "examples": ["https://kratos.private-network:4434/"]
+              "examples": [
+                "https://kratos.private-network:4434/"
+              ]
             },
             "host": {
               "title": "Admin Host",
@@ -2328,7 +2580,9 @@
               "type": "integer",
               "minimum": 1,
               "maximum": 65535,
-              "examples": [4434],
+              "examples": [
+                4434
+              ],
               "default": 4434
             },
             "socket": {
@@ -2387,7 +2641,9 @@
                     ]
                   },
                   "uniqueItems": true,
-                  "default": ["*"],
+                  "default": [
+                    "*"
+                  ],
                   "examples": [
                     [
                       "https://example.com",
@@ -2399,7 +2655,13 @@
                 "allowed_methods": {
                   "type": "array",
                   "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.",
-                  "default": ["POST", "GET", "PUT", "PATCH", "DELETE"],
+                  "default": [
+                    "POST",
+                    "GET",
+                    "PUT",
+                    "PATCH",
+                    "DELETE"
+                  ],
                   "items": {
                     "type": "string",
                     "enum": [
@@ -2433,7 +2695,9 @@
                 "exposed_headers": {
                   "type": "array",
                   "description": "Sets which headers are safe to expose to the API of a CORS API specification.",
-                  "default": ["Content-Type"],
+                  "default": [
+                    "Content-Type"
+                  ],
                   "items": {
                     "type": "string"
                   }
@@ -2476,7 +2740,9 @@
               "type": "integer",
               "minimum": 1,
               "maximum": 65535,
-              "examples": [4433],
+              "examples": [
+                4433
+              ],
               "default": 4433
             },
             "socket": {
@@ -2526,7 +2792,10 @@
         "format": {
           "description": "The log format can either be text or JSON.",
           "type": "string",
-          "enum": ["json", "text"]
+          "enum": [
+            "json",
+            "text"
+          ]
         }
       },
       "additionalProperties": false
@@ -2567,7 +2836,9 @@
               "id": {
                 "title": "The schema's ID.",
                 "type": "string",
-                "examples": ["employee"]
+                "examples": [
+                  "employee"
+                ]
               },
               "url": {
                 "type": "string",
@@ -2581,11 +2852,16 @@
                 ]
               }
             },
-            "required": ["id", "url"]
+            "required": [
+              "id",
+              "url"
+            ]
           }
         }
       },
-      "required": ["schemas"],
+      "required": [
+        "schemas"
+      ],
       "additionalProperties": false
     },
     "secrets": {
@@ -2634,7 +2910,10 @@
           "description": "One of the values: argon2, bcrypt.\nAny other hashes will be migrated to the set algorithm once an identity authenticates using their password.",
           "type": "string",
           "default": "bcrypt",
-          "enum": ["argon2", "bcrypt"]
+          "enum": [
+            "argon2",
+            "bcrypt"
+          ]
         },
         "argon2": {
           "title": "Configuration for the Argon2id hasher.",
@@ -2690,7 +2969,9 @@
           "title": "Configuration for the Bcrypt hasher. Minimum is 4 when --dev flag is used and 12 otherwise.",
           "type": "object",
           "additionalProperties": false,
-          "required": ["cost"],
+          "required": [
+            "cost"
+          ],
           "properties": {
             "cost": {
               "type": "integer",
@@ -2712,7 +2993,11 @@
           "description": "One of the values: noop, aes, xchacha20-poly1305",
           "type": "string",
           "default": "noop",
-          "enum": ["noop", "aes", "xchacha20-poly1305"]
+          "enum": [
+            "noop",
+            "aes",
+            "xchacha20-poly1305"
+          ]
         }
       }
     },
@@ -2741,7 +3026,11 @@
           "title": "HTTP Cookie Same Site Configuration",
           "description": "Sets the session and CSRF cookie SameSite.",
           "type": "string",
-          "enum": ["Strict", "Lax", "None"],
+          "enum": [
+            "Strict",
+            "Lax",
+            "None"
+          ],
           "default": "Lax"
         }
       },
@@ -2771,7 +3060,9 @@
                   "patternProperties": {
                     "[a-zA-Z0-9-_.]+": {
                       "type": "object",
-                      "required": ["jwks_url"],
+                      "required": [
+                        "jwks_url"
+                      ],
                       "properties": {
                         "ttl": {
                           "type": "string",
@@ -2804,7 +3095,11 @@
           "type": "string",
           "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
           "default": "24h",
-          "examples": ["1h", "1m", "1s"]
+          "examples": [
+            "1h",
+            "1m",
+            "1s"
+          ]
         },
         "cookie": {
           "type": "object",
@@ -2840,7 +3135,11 @@
               "title": "Session Cookie SameSite Configuration",
               "description": "Sets the session cookie SameSite. Overrides `cookies.same_site`.",
               "type": "string",
-              "enum": ["Strict", "Lax", "None"]
+              "enum": [
+                "Strict",
+                "Lax",
+                "None"
+              ]
             }
           },
           "additionalProperties": false
@@ -2850,7 +3149,11 @@
           "description": "Sets when a session can be extended. Settings this value to `24h` will prevent the session from being extended before until 24 hours before it expires. This setting prevents excessive writes to the database. We highly recommend setting this value.",
           "type": "string",
           "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
-          "examples": ["1h", "1m", "1s"]
+          "examples": [
+            "1h",
+            "1m",
+            "1s"
+          ]
         }
       }
     },
@@ -2874,7 +3177,9 @@
       "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.",
       "type": "string",
       "pattern": "^(v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|$",
-      "examples": ["v0.5.0-alpha.1"]
+      "examples": [
+        "v0.5.0-alpha.1"
+      ]
     },
     "dev": {
       "type": "boolean"
@@ -2898,7 +3203,9 @@
       "type": "integer",
       "minimum": 0,
       "maximum": 65535,
-      "examples": [4434],
+      "examples": [
+        4434
+      ],
       "default": 0
     },
     "config": {
@@ -2934,6 +3241,40 @@
               },
               "default": []
             }
+          },
+          "web_hook": {
+            "title": "Global web_hook HTTP client configuration",
+            "description": "Configure the global HTTP client of the web_hook action.",
+            "type": "object",
+            "properties": {
+              "header_allowlist": {
+                "title": "Allowed request headers",
+                "description": "List of request headers that are forwarded to the web hook target in canonical form.",
+                "type": "array",
+                "items": {
+                  "type": "string"
+                },
+                "default": [
+                  "Accept",
+                  "Accept-Encoding",
+                  "Accept-Language",
+                  "Content-Length",
+                  "Content-Type",
+                  "Origin",
+                  "Priority",
+                  "Referer",
+                  "Sec-Ch-Ua",
+                  "Sec-Ch-Ua-Mobile",
+                  "Sec-Ch-Ua-Platform",
+                  "Sec-Fetch-Dest",
+                  "Sec-Fetch-Mode",
+                  "Sec-Fetch-Site",
+                  "Sec-Fetch-User",
+                  "True-Client-Ip",
+                  "User-Agent"
+                ]
+              }
+            }
           }
         }
       }
@@ -3004,10 +3345,14 @@
                             "const": true
                           }
                         },
-                        "required": ["enabled"]
+                        "required": [
+                          "enabled"
+                        ]
                       }
                     },
-                    "required": ["verification"]
+                    "required": [
+                      "verification"
+                    ]
                   },
                   {
                     "properties": {
@@ -3017,21 +3362,31 @@
                             "const": true
                           }
                         },
-                        "required": ["enabled"]
+                        "required": [
+                          "enabled"
+                        ]
                       }
                     },
-                    "required": ["recovery"]
+                    "required": [
+                      "recovery"
+                    ]
                   }
                 ]
               }
             },
-            "required": ["flows"]
+            "required": [
+              "flows"
+            ]
           }
         },
-        "required": ["selfservice"]
+        "required": [
+          "selfservice"
+        ]
       },
       "then": {
-        "required": ["courier"]
+        "required": [
+          "courier"
+        ]
       }
     },
     {
@@ -3050,21 +3405,33 @@
                 ]
               }
             },
-            "required": ["algorithm"]
+            "required": [
+              "algorithm"
+            ]
           }
         },
-        "required": ["ciphers"]
+        "required": [
+          "ciphers"
+        ]
       },
       "then": {
-        "required": ["secrets"],
+        "required": [
+          "secrets"
+        ],
         "properties": {
           "secrets": {
-            "required": ["cipher"]
+            "required": [
+              "cipher"
+            ]
           }
         }
       }
     }
   ],
-  "required": ["identity", "dsn", "selfservice"],
+  "required": [
+    "identity",
+    "dsn",
+    "selfservice"
+  ],
   "additionalProperties": false
 }
diff --git a/selfservice/hook/web_hook.go b/selfservice/hook/web_hook.go
index d756338b1..a362e8caa 100644
--- a/selfservice/hook/web_hook.go
+++ b/selfservice/hook/web_hook.go
@@ -25,6 +25,7 @@ import (
 	grpccodes "google.golang.org/grpc/codes"
 
 	"github.com/ory/herodot"
+	"github.com/ory/kratos/driver/config"
 	"github.com/ory/kratos/identity"
 	"github.com/ory/kratos/request"
 	"github.com/ory/kratos/schema"
@@ -74,6 +75,7 @@ type (
 		x.HTTPClientProvider
 		x.TracingProvider
 		jsonnetsecure.VMProvider
+		config.Provider
 	}
 
 	templateContext struct {
@@ -359,7 +361,7 @@ func (e *WebHook) execute(ctx context.Context, data *templateContext) error {
 			attribute.Bool("webhook.response.parse", parseResponse),
 		)
 
-		removeDisallowedHeaders(data)
+		removeDisallowedHeaders(data, e.deps.Config().WebhookHeaderAllowlist(ctx))
 
 		req, err := builder.BuildRequest(ctx, data)
 		if errors.Is(err, request.ErrCancel) {
@@ -429,32 +431,15 @@ func (e *WebHook) execute(ctx context.Context, data *templateContext) error {
 	return nil
 }
 
-// RequestHeaderAllowList contains the allowed request headers that are forwarded
-// to the web hook target in canonical form (textproto.CanonicalMIMEHeaderKey).
-var RequestHeaderAllowList = map[string]struct{}{
-	"Accept":             {},
-	"Accept-Encoding":    {},
-	"Accept-Language":    {},
-	"Content-Length":     {},
-	"Content-Type":       {},
-	"Origin":             {},
-	"Priority":           {},
-	"Referer":            {},
-	"Sec-Ch-Ua":          {},
-	"Sec-Ch-Ua-Mobile":   {},
-	"Sec-Ch-Ua-Platform": {},
-	"Sec-Fetch-Dest":     {},
-	"Sec-Fetch-Mode":     {},
-	"Sec-Fetch-Site":     {},
-	"Sec-Fetch-User":     {},
-	"True-Client-Ip":     {},
-	"User-Agent":         {},
-}
+func removeDisallowedHeaders(data *templateContext, headerAllowlist []string) {
+	allowedMap := make(map[string]struct{})
+	for _, header := range headerAllowlist {
+		allowedMap[header] = struct{}{}
+	}
 
-func removeDisallowedHeaders(data *templateContext) {
 	headers := maps.Clone(data.RequestHeaders)
 	maps.DeleteFunc(headers, func(key string, _ []string) bool {
-		_, found := RequestHeaderAllowList[textproto.CanonicalMIMEHeaderKey(key)]
+		_, found := allowedMap[textproto.CanonicalMIMEHeaderKey(key)]
 		return !found
 	})
 	data.RequestHeaders = headers
diff --git a/session/handler.go b/session/handler.go
index ad10e389a..7c775c995 100644
--- a/session/handler.go
+++ b/session/handler.go
@@ -827,6 +827,19 @@ func (h *Handler) listMySessions(w http.ResponseWriter, r *http.Request, _ httpr
 		return
 	}
 
+	c := h.r.Config()
+
+	var aalErr *ErrAALNotSatisfied
+	if err := h.r.SessionManager().DoesSessionSatisfy(r.Context(), s, c.SessionWhoAmIAAL(r.Context())); errors.As(err, &aalErr) {
+		h.r.Audit().WithRequest(r).WithError(err).Info("Session was found but AAL is not satisfied for calling this endpoint.")
+		h.r.Writer().WriteError(w, r, err)
+		return
+	} else if err != nil {
+		h.r.Audit().WithRequest(r).WithError(err).Info("No valid session cookie found.")
+		h.r.Writer().WriteError(w, r, herodot.ErrUnauthorized.WithWrap(err).WithReasonf("Unable to determine AAL."))
+		return
+	}
+
 	page, perPage := x.ParsePagination(r)
 	sess, total, err := h.r.SessionPersister().ListSessionsByIdentity(r.Context(), s.IdentityID, pointerx.Bool(true), page, perPage, s.ID, ExpandEverything)
 	if err != nil {



Test patch:
diff --git a/driver/config/config_test.go b/driver/config/config_test.go
index 0b65f6c14..24ce81ff7 100644
--- a/driver/config/config_test.go
+++ b/driver/config/config_test.go
@@ -460,6 +460,13 @@ func TestProviderSelfServiceLinkMethodBaseURL(t *testing.T) {
 	assert.Equal(t, "https://example.org/bar", p.SelfServiceLinkMethodBaseURL(ctx).String())
 }
 
+func TestDefaultWebhookHeaderAllowlist(t *testing.T) {
+	t.Parallel()
+	ctx := context.Background()
+	p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
+	snapshotx.SnapshotT(t, p.WebhookHeaderAllowlist(ctx))
+}
+
 func TestViperProvider_Secrets(t *testing.T) {
 	t.Parallel()
 	ctx := context.Background()
diff --git a/selfservice/hook/web_hook_integration_test.go b/selfservice/hook/web_hook_integration_test.go
index 700835309..16888917e 100644
--- a/selfservice/hook/web_hook_integration_test.go
+++ b/selfservice/hook/web_hook_integration_test.go
@@ -24,6 +24,7 @@ import (
 	"github.com/sirupsen/logrus/hooks/test"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"github.com/tidwall/gjson"
 	"github.com/tidwall/sjson"
 	"go.opentelemetry.io/otel/attribute"
 	sdktrace "go.opentelemetry.io/otel/sdk/trace"
@@ -60,14 +61,39 @@ var transientPayload = json.RawMessage(`{
 }`)
 
 func TestWebHooks(t *testing.T) {
-	_, reg := internal.NewFastRegistryWithMocks(t)
+	ctx := context.Background()
+	conf, reg := internal.NewFastRegistryWithMocks(t)
 	logger := logrusx.New("kratos", "test")
+
+	conf.Set(ctx, config.ViperKeyWebhookHeaderAllowlist, []string{
+		"Accept",
+		"Accept-Encoding",
+		"Accept-Language",
+		"Content-Length",
+		"Content-Type",
+		"Origin",
+		"Priority",
+		"Referer",
+		"Sec-Ch-Ua",
+		"Sec-Ch-Ua-Mobile",
+		"Sec-Ch-Ua-Platform",
+		"Sec-Fetch-Dest",
+		"Sec-Fetch-Mode",
+		"Sec-Fetch-Site",
+		"Sec-Fetch-User",
+		"True-Client-Ip",
+		"User-Agent",
+		"Valid-Header",
+	})
+
 	whDeps := struct {
 		x.SimpleLoggerWithClient
 		*jsonnetsecure.TestProvider
+		config.Provider
 	}{
-		x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(context.Background()), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})},
+		x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(ctx), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})},
 		jsonnetsecure.NewTestProvider(t),
+		reg,
 	}
 	type WebHookRequest struct {
 		Body    string
@@ -337,7 +363,8 @@ func TestWebHooks(t *testing.T) {
 								Header: map[string][]string{
 									"Some-Header":    {"Some-Value"},
 									"User-Agent":     {"Foo-Bar-Browser"},
-									"Invalid-Header": {"ignored"},
+									"Invalid-Header": {"should be ignored"},
+									"Valid-Header":   {"should not be ignored"},
 									"Cookie":         {"Some-Cookie-1=Some-Cookie-Value; Some-Cookie-2=Some-other-Cookie-Value", "Some-Cookie-3=Third-Cookie-Value"},
 								},
 								RequestURI: "/some_end_point",
@@ -393,6 +420,11 @@ func TestWebHooks(t *testing.T) {
 								// According to the HTTP spec any request method, but TRACE is allowed to
 								// have a body. Even this is a really bad practice for some of them, like for
 								// GET
+								assert.Zero(t, gjson.Get(whr.Body, "headers.Invalid-Header"))
+								assert.NotZero(t, gjson.Get(whr.Body, "headers.Valid-Header"))
+								whr.Body, err = sjson.Delete(whr.Body, "headers.Valid-Header")
+								assert.NoError(t, err)
+
 								assert.JSONEq(t, tc.expectedBody(req, f, s), whr.Body)
 							} else {
 								assert.Emptyf(t, whr.Body, "HTTP %s is not allowed to have a body", method)
@@ -1005,9 +1037,11 @@ func TestDisallowPrivateIPRanges(t *testing.T) {
 	whDeps := struct {
 		x.SimpleLoggerWithClient
 		*jsonnetsecure.TestProvider
+		config.Provider
 	}{
 		x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(context.Background()), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})},
 		jsonnetsecure.NewTestProvider(t),
+		reg,
 	}
 
 	req := &http.Request{
@@ -1075,9 +1109,11 @@ func TestAsyncWebhook(t *testing.T) {
 	whDeps := struct {
 		x.SimpleLoggerWithClient
 		*jsonnetsecure.TestProvider
+		config.Provider
 	}{
 		x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(context.Background()), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})},
 		jsonnetsecure.NewTestProvider(t),
+		reg,
 	}
 
 	req := &http.Request{
@@ -1155,9 +1191,11 @@ func TestWebhookEvents(t *testing.T) {
 	whDeps := struct {
 		x.SimpleLoggerWithClient
 		*jsonnetsecure.TestProvider
+		config.Provider
 	}{
 		x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(context.Background()), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})},
 		jsonnetsecure.NewTestProvider(t),
+		reg,
 	}
 
 	req := &http.Request{
diff --git a/session/handler_test.go b/session/handler_test.go
index 62385b8b3..d41f7f7cf 100644
--- a/session/handler_test.go
+++ b/session/handler_test.go
@@ -103,42 +103,49 @@ func TestSessionWhoAmI(t *testing.T) {
 		h3, _ := testhelpers.MockSessionCreateHandlerWithIdentityAndAMR(t, reg, createAAL1Identity(t, reg), []identity.CredentialsType{identity.CredentialsTypePassword})
 		r.GET("/set/aal1-aal1", h3)
 
-		run := func(t *testing.T, kind string, code int) string {
+		run := func(t *testing.T, endpoint string, kind string, code int) string {
 			client := testhelpers.NewClientWithCookies(t)
 			testhelpers.MockHydrateCookieClient(t, client, ts.URL+"/set/"+kind)
 
-			res, err := client.Get(ts.URL + RouteWhoami)
+			res, err := client.Get(ts.URL + endpoint)
 			require.NoError(t, err)
 			body := x.MustReadAll(res.Body)
 			assert.EqualValues(t, code, res.StatusCode)
 			return string(body)
 		}
 
-		t.Run("case=aal2-aal2", func(t *testing.T) {
-			conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL)
-			run(t, "aal2-aal2", http.StatusOK)
-		})
+		for k, e := range map[string]string{
+			"whoami":     RouteWhoami,
+			"collection": RouteCollection,
+		} {
+			t.Run(fmt.Sprintf("endpoint=%s", k), func(t *testing.T) {
+				t.Run("case=aal2-aal2", func(t *testing.T) {
+					conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL)
+					run(t, e, "aal2-aal2", http.StatusOK)
+				})
 
-		t.Run("case=aal2-aal2", func(t *testing.T) {
-			conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, "aal1")
-			run(t, "aal2-aal2", http.StatusOK)
-		})
+				t.Run("case=aal2-aal2", func(t *testing.T) {
+					conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, "aal1")
+					run(t, e, "aal2-aal2", http.StatusOK)
+				})
 
-		t.Run("case=aal2-aal1", func(t *testing.T) {
-			conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL)
-			body := run(t, "aal2-aal1", http.StatusForbidden)
-			assert.EqualValues(t, NewErrAALNotSatisfied("").Reason(), gjson.Get(body, "error.reason").String(), body)
-		})
+				t.Run("case=aal2-aal1", func(t *testing.T) {
+					conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL)
+					body := run(t, e, "aal2-aal1", http.StatusForbidden)
+					assert.EqualValues(t, NewErrAALNotSatisfied("").Reason(), gjson.Get(body, "error.reason").String(), body)
+				})
 
-		t.Run("case=aal2-aal1", func(t *testing.T) {
-			conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, "aal1")
-			run(t, "aal2-aal1", http.StatusOK)
-		})
+				t.Run("case=aal2-aal1", func(t *testing.T) {
+					conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, "aal1")
+					run(t, e, "aal2-aal1", http.StatusOK)
+				})
 
-		t.Run("case=aal1-aal1", func(t *testing.T) {
-			conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL)
-			run(t, "aal1-aal1", http.StatusOK)
-		})
+				t.Run("case=aal1-aal1", func(t *testing.T) {
+					conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL)
+					run(t, e, "aal1-aal1", http.StatusOK)
+				})
+			})
+		}
 	})
 
 	t.Run("case=http methods", func(t *testing.T) {