Expression Policies
Expression policies let you write custom Python for cases where the built-in policy types are not enough.
They are commonly used to:
- branch or redirect inside authentication flows
- validate prompt data
- enforce organization-specific access rules
- make decisions based on request metadata, GeoIP, ASN, or flow context
For general policy concepts such as bindings, ordering, Any vs All, and where policies are attached, see Policy bindings and evaluation.
Expression policies execute server-side Python inside authentik. Treat the ability to create or edit them as a highly privileged permission.
Start here
- Expression reference covers return values, helper functions, variables, and flow context.
- Managing flow context keys shows how to change flow behavior.
- Switch which source is used based on email address shows a common routing pattern.
- Ensure unique email addresses and Allow only specific email domains cover prompt-data validation.
A simple example
The following expression causes an MFA validation stage to run only for one specific user:
if request.context["pending_user"].username == "marie":
return True
return False
When the policy is bound to the stage binding, the stage runs only when the expression passes.
To understand what values are available in the expression environment, see Expression reference.
Common pitfalls
Use context['pending_user'] inside flows
When a policy runs during a flow, request.user is not always the user who is currently being authenticated. Until a login or user-writing stage updates the active user, request.user may still be AnonymousUser or the previously authenticated user.
Inside authentication and enrollment flows, prefer:
pending_user = request.context.get("pending_user")
instead of relying on request.user.
Prompt data lives in context['prompt_data']
If you are validating values entered in a Prompt stage, read them from context["prompt_data"]:
email = context["prompt_data"]["email"]
Flow state can be changed through flow_plan
When an expression runs inside a flow, you can inspect and update flow state through context["flow_plan"] and context["flow_plan"].context. This is powerful, but it also means your expression can change how the rest of the flow behaves.
For examples, see Managing flow context keys and Switch which source is used based on email address.
Sample expression policies
The following examples document common patterns:
- Switch which source is used based on email address
- Managing flow context keys
- Ensure unique email addresses
- Allow only specific email domains
Depending on the flow, context["auth_method_args"] may also be populated.
Some fields from the flow-plan context are mirrored into the root policy context and updated from it, such as prompt_data, but not every flow-plan key is copied that way.
Common auth_method values include:
password: standard password loginauth_mfa: MFA login without a password stepauth_webauthn_pwl: passwordless WebAuthn or passkey logintoken: token-based authentication, such as an app passwordjwt: machine-to-machine login through an external JWTldap: LDAP bind authentication
For some authentication methods, context["auth_method_args"] contains additional structured data.
For auth_mfa, context["auth_method_args"] is shaped like:
{
"mfa_devices": [
{
"pk": 1,
"app": "otp_static",
"name": "Static Token",
"model_name": "staticdevice"
}
]
}
For token, context["auth_method_args"] depends on the code path. It may contain a token object:
{
"token": {
"pk": "f6d639aac81940f38dcfdc6e0fe2a786",
"app": "authentik_core",
"name": "test (expires=2021-08-23 15:45:54.725880+00:00)",
"model_name": "token"
}
}
or a token identifier:
{
"identifier": "service-account-token"
}
For ldap, context["auth_method_args"] contains information about the source used:
{
"source": {}
}
For simple country or ASN checks, a GeoIP policy is usually easier to manage than a custom expression. For flow manipulation, see the example pages above.