Enforcing OIDC for some users with Ory Kratos Webhooks
Use webhooks to enforce authentication requirements for different users of your platform.
Ory Kratos is an identity provider and authentication service for use in your own platforms and apps. Out of the box it has support for username and password, social login services, and any OIDC compliant identity provider. This provides great flexiblity, you can mix and match IdPs for your users in any combination that makes sense.
You might want to force some users or user groups to use a specific OIDC provider. For example, if your app has a blend of staff accounts and general public, you probably want to force them to use your Single Sign-On provider, while letting your public users sign in with password or social sign in. If you are using Ory Network, then they cover this requirement with their B2B feature. However, is only available on their Growth and Enterprise plans. If you are self hosting or using the Production plan then you have to do more work yourself.
Flow Interrupting, Post-Login Webhook
To enforce login requirements we can implement a flow interrupting, post-login webhook. The webhook is called after the login flow has completed, but before the session is issued to the user. This allows us to decide whether the session should be issued.
Post Login Webhook Payload
Ory's docs are infuriating, and they don't actually list anywhere the data that can be sent to the webhook. You can discover this yourself, by using a jsonnet snipped that sends the full context:
function(ctx) {
ctx: ctx
}
You can send the full ctx to your webhook, but you probably shouldn't
If you do this, your webhook will receive:
- The current login flow
- The complete Identity for the current user
- The user's Session
- Request headers
- Request cookies
The full payload looks like this:
{
"flow": {
// the full Kratos Login Flow
},
"identity": {
"created_at": "2025-11-26T00:43:13.323402Z",
"id": "0aa9a049-6826-4035-9f05-32137b0ca01b",
"metadata_public": {},
"organization_id": null,
"recovery_addresses": [
{
"created_at": "2025-11-26T00:43:13.323586Z",
"id": "ea073e9e-fcd2-4833-82f0-da524ed1174e",
"updated_at": "2025-11-26T00:43:13.323586Z",
"value": "user@example.com",
"via": "email"
}
],
"schema_id": "default",
"schema_url": "http://127.0.0.1:4433/schemas/ZGVmYXVsdA",
"state": "active",
"state_changed_at": "2025-11-26T00:43:13.322923Z",
"traits": {
"email": "user@example.com",
"name": {
"first": "Reba",
"last": "Zachariah"
}
},
"updated_at": "2025-11-26T00:43:13.323402Z",
"verifiable_addresses": [
{
"created_at": "2025-11-26T00:43:13.323497Z",
"id": "20072ac4-ca70-4529-87a7-87346beefb27",
"status": "completed",
"updated_at": "2025-11-26T00:43:13.323497Z",
"value": "user@example.com",
"verified": true,
"verified_at": "2025-11-26T00:43:13.322Z",
"via": "email"
}
]
},
"request_cookies": {
// key value pairs of all the request cookies from the client
},
"request_headers": {
// key value pairs of all the request headers from the client
},
"request_method": "POST",
"request_url": "http://127.0.0.1:4433/self-service/login?flow=9082155b-8c66-4a9b-a0ca-381adf850ecf",
"session": {
"active": true,
"authenticated_at": "2026-08-12T02:28:58.592473547Z",
"authentication_methods": [
{
"aal": "aal1",
"completed_at": "2026-08-12T02:28:58.592404588Z",
"method": "password"
}
],
"authenticator_assurance_level": "aal1",
"devices": [
{
"id": "00000000-0000-0000-0000-000000000000",
"ip_address": "127.0.0.1:58308",
"location": "",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"
}
],
"expires_at": "2026-09-09T02:28:58.592473547Z",
"id": "00000000-0000-0000-0000-000000000000",
"identity": {
"created_at": "2025-11-26T00:43:13.323402Z",
"id": "0aa9a049-6826-4035-9f05-32137b0ca01b",
"metadata_public": {},
"organization_id": null,
"recovery_addresses": [
{
"created_at": "2025-11-26T00:43:13.323586Z",
"id": "ea073e9e-fcd2-4833-82f0-da524ed1174e",
"updated_at": "2025-11-26T00:43:13.323586Z",
"value": "user@example.com",
"via": "email"
}
],
"schema_id": "default",
"schema_url": "http://127.0.0.1:4433/schemas/ZGVmYXVsdA",
"state": "active",
"state_changed_at": "2025-11-26T00:43:13.322923Z",
"traits": {
"email": "user@example.com",
"name": {
"first": "Reba",
"last": "Zachariah"
}
},
"updated_at": "2025-11-26T00:43:13.323402Z",
"verifiable_addresses": [
{
"created_at": "2025-11-26T00:43:13.323497Z",
"id": "20072ac4-ca70-4529-87a7-87346beefb27",
"status": "completed",
"updated_at": "2025-11-26T00:43:13.323497Z",
"value": "user@example.com",
"verified": true,
"verified_at": "2025-11-26T00:43:13.322Z",
"via": "email"
}
]
},
"issued_at": "2026-08-12T02:28:58.592473547Z"
}
}The full data available to the post login webhook
You probably don't want to send all of this to your webhook, though, so construct a jsonnet snippet that makes sense,
Check The User's Session
The webhook itself is quite simple. It should look in the session object's authentication_methods list. If a user has logged in with a password, you'll see something like this:
{
"aal": "aal1",
"completed_at": "2026-08-12T02:28:58.592404588Z",
"method": "password"
}Example password authentication method
If the user has logged in with OIDC, you'll see this:
{
"aal": "aal1",
"completed_at": "2026-08-12T02:28:58.592404588Z",
"method": "oidc",
"provider": "provider_id"
}Example OIDC authentication method
Note that this array may have more than one element, and if the user has 2FA then you'll see that as an additional method.
Allow or Deny the Login
You can use this information to decide whether to allow or deny the login.
if (allowed(authentication_methods)) {
// an empty object for Kratos to parse, but not making any changes
return {};
}
else {
// return a 403 to prevent the login
res.status(403);
}Configure Kratos
Once your webhook is done, you need to tell Kratos to use it by adding to the Kratos config file:
flows:
login:
after:
hooks:
- hook: web_hook
config:
url: http://example.com/webhook
body: base64://ZnVuY3Rpb24oY3R4KSB7CiAgICBpZGVudGl0eTogY3R4LmlkZW50aXR5LAogICAgc2Vzc2lvbjogY3R4LnNlc3Npb24KfQo=
method: POST
emit_analytics_event: true
response:
ignore: false
parse: true
auth:
type: api_key
config:
name: Authorization
value: KRATOS_WEBHOOKS
in: headerExample Kratos config
Conclusion
You can use anything about the identity or session to allow or deny the login. For example, specific email domains require a specific OIDC provider. You could enforce 2FA for some users but not others.
Webhook delivery needs to be reliable. As this is a flow-interrupting webhook, if Kratos can't reach the webhook then the user can't log in. You probably also need to implement something similar to enforce the same rules across registration and recovery flows to ensure consistent application of rules.
Have fun!