Understanding the Authentik policy engine: policy types, Python-based expression policies with user and request context, binding policies to flows, stages, and applications, Any versus All evaluation logic, and examples of access policies based on group, working hours, and IP address.

In episode 5, you built the identity foundation: users, groups, and attributes. Now Authentik can answer the first question, who are you, through the authentication flow. But there's a second, equally important question: what are you allowed to do once you're in?
The answer lies in policies — the conditional logic that decides yes or no at almost every important point in Authentik. Here's an analogy: authentication is like the guard in the building lobby checking ID cards, while a policy is the list of rooms you may enter, complete with opening hours and special rules per floor. In this episode, you'll learn to program that room list.
Simply put, a policy is a boolean decision: pass or fail. That decision then drives Authentik's logic. There are five main places a policy can be bound (attached):
Besides policies, you can also bind a user or group directly to the same targets. This direct binding is evaluated as a simple membership check — useful when you only need an allow or deny rule without creating a new policy object.
Tip
Rule of thumb from the Authentik documentation: use a direct group binding when the decision is static (for example "only the admins group"), and use a policy when the decision depends on runtime context like network, source, prompt data, or request history.
Authentik provides several built-in policy types that can be used without writing code:
For most authorization needs, the expression policy is the primary choice — precisely because it can combine all the above conditions into a single expression.
An expression policy is a piece of Python code executed on the Authentik server side when the policy is evaluated. The expression receives a rich context, then returns True if it passes or False if it fails. The most common example: denying access if the user isn't a member of a specific group.
if ak_is_group_member(request.user, name="app-admins"):
return True
ak_message("Access denied: you are not a member of the app-admins group")
return FalseNotice two things above. First, the ak_is_group_member(user, name="...") helper checks group membership cleanly. Second, ak_message("...") sets the message shown to the user when the policy fails — don't make the user guess the reason for the denial.
Important
An expression policy is Python code running with elevated privileges inside the Authentik server. Treat the ability to create or edit one like granting administrator access. Don't copy expressions from untrusted sources.
Some of the most frequently used variables and helpers:
request.user: the user currently being evaluated against this policy.request.context: a dictionary with dynamic data from flow execution.ak_is_group_member(user, name="..."): check group membership.ak_user_has_authenticator(user): check whether the user has an MFA device.ak_message("..."): set a message visible to the user.ak_client_ip: the already-parsed client IP object, suitable for network checks.regex_match(value, pattern) and regex_replace(value, pattern, repl): string manipulation.One classic pitfall: when a policy runs inside the authentication flow, request.user isn't necessarily the user logging in — it could still be AnonymousUser until a user login stage sets it. For this case, the documentation suggests reading pending_user from the flow context (for example request.context.get("pending_user")) so the decision uses the correct identity.
Policies bound to the same target are evaluated in order from top to bottom (ascending order). This matters both when reading logs and when multiple policies produce messages for the user.
Besides order, each target binding has a Policy engine mode:
There are also several additional settings that often save you time in production:
Warning
By default, if a target has no bindings at all, Authentik considers it to pass. If you're securing sensitive applications, don't let access rules depend on the absence of a policy — and choose a fail-closed failure result for expressions that protect something important.
The policy at the start of this episode already showed the pattern: deny if not a member of the app-admins group. Binding it to an application means only members of that group can open the application.
Want a service accessible only during working hours? Just compare the hour at evaluation time.
from datetime import datetime
now = datetime.now()
return 9 <= now.hour < 17For internal services that should only be reachable from the office network, use a network comparison.
from ipaddress import ip_network
return ak_client_ip in ip_network("10.0.0.0/24")This is the most widely used pattern: bind a policy to a stage binding, so a specific stage — for example authenticator validate — only runs for some users or applications. This is exactly what you'll dissect further in episode 7 about MFA.
Never rely on guesses. Authentik provides tools to test a policy before it's used:
ak_message.Tip
Develop the habit of testing with sample users representing each role: one admin user, one regular user, and one user who should be denied. These three tests catch most logic errors.
Key points from this episode:
request.user and request.context context plus helpers like ak_is_group_member.Now you can program the "who may enter" logic. But there's still a tempting question: what if the condition reads "all admins must use MFA"? In episode 7, you'll dissect authentication stages and the Multi-Factor Authentication mechanism — from identification and password to authenticator validate — along with how to bind it to applications.