Skip to main content

ADFS policies vs authorization rules - understanding the difference

While preparing our MFA rollout at ADFS level, we started making the switch from classic authorization rules to custom access control policies in ADFS. This post explains the difference and the rationale behind this switch.

A tale of two mechanisms

When you work with Active Directory Federation Services (ADFS), there are two ways to control what happens when a user tries to authenticate: authorization rules and access control policies. On the surface, they feel similar; both let you define conditions around user access. But under the hood, they represent two distinct generations of the same capability.

Understanding the difference matters especially when implementing MFA, because the mechanism you choose affects flexibility, maintainability, and how cleanly your logic can scale.

Authorization rules: the classic approach

Authorization rules are the original ADFS mechanism, introduced back when claims-based identity was first baked into the platform. They use a proprietary language called claim rule language — a terse, SQL-like syntax that you write directly in the ADFS management console under the Issuance Authorization Rules tab of a relying party trust.

A typical rule to permit all authenticated users might look like:

Example rule syntax
c:[] => issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true");
And a rule that requires group membership:
c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid", Value == "S-1-5-21-..."] => issue(Type = "..../permit", Value = "true");

Rules are powerful and expressive, but they come with trade-offs. Each rule is self-contained logic written in raw claim syntax. As your requirements grow — say, you want to combine MFA requirements with group membership and device compliance — rules tend to multiply and become hard to read, audit, or reuse across applications.

Access control policies: the modern approach

Access control policies were introduced in ADFS on Windows Server 2016 as a higher-level abstraction. Rather than writing claim rule language by hand, policies let you define reusable templates with a structured GUI or PowerShell — and then apply them to one or many relying party trusts.

Policies use a building-block model. You combine permit and deny conditions — things like group membership, network location, MFA completion, or device registration state — into a named, shareable policy object. When you need to enforce MFA for an application, you assign a policy that includes that requirement, rather than writing (and duplicating) low-level rules.

How custom policies work for MFA
A custom access control policy can express: "Permit users who are members of the MFA-Required group and have completed multi-factor authentication. Deny everyone else." This logic lives in one reusable policy object and is assigned to the relying party trust — not embedded as opaque claim syntax inside it.

Side-by-side: the key differences

Let's compare the 2 options in one view:

Dimension Authorization rules Access control policies
Syntax Claim rule language (text-based) Policy conditions (structured)
Reusability Not reusable — per-trust only Define once, assign to many trusts
MFA enforcement Possible but requires custom claim logic Built-in condition type
Readibility Low — requires expertise to parse High — readable in GUI and PowerShell
Auditing Tedious — inspect each trust individually Centralized policy inventory
Compatibility Windows Server 2012 R2 and later Windows Server 2016 and later

Why the switch matters for MFA

When implementing MFA across multiple applications, authorization rules quickly become a maintenance burden. You end up with MFA logic duplicated — and often subtly inconsistent — across every relying party trust. One trust enforces MFA for all users; another forgot to include extranet-only conditions; a third has an outdated group SID baked in from a team that no longer exists.

Custom access control policies solve this by letting you define your MFA requirement once — say, a policy called Require MFA for All Users — and assigning it uniformly. A single change to the policy propagates across every application that references it.


Important caveat: ADFS does not support using both authorization rules and access control policies on the same relying party trust simultaneously. When you assign a policy to a trust, any existing authorization rules on that trust are replaced. Plan your migration accordingly and validate each application after switching.

Conclusion

If you are starting fresh or migrating, access control policies are the clear choice for any new MFA rollout. They are more maintainable, auditable, and purpose-built for the kind of conditional access logic that modern identity requirements demand.

Authorization rules still have a place when you need very granular claim transformations that the policy model does not cover, or when you are working on older infrastructure that predates policy support. But for access control decisions, especially MFA enforcement, policies are the right abstraction.

Some takeaways:

  • Authorization rules use claim rule language and live per-trust. They are powerful but verbose and hard to reuse.
  • Access control policies are reusable objects with structured conditions. Assign once, apply everywhere.
  • Policies treat MFA enforcement as a first-class condition, no custom claim syntax required.
  • The two mechanisms are mutually exclusive on a single trust. Plan your migration carefully!
  • For any new MFA rollout using ADFS, custom access control policies are the recommended path.

Popular posts from this blog

Podman– Command execution failed with exit code 125

After updating WSL on one of the developer machines, Podman failed to work. When we took a look through Podman Desktop, we noticed that Podman had stopped running and returned the following error message: Error: Command execution failed with exit code 125 Here are the steps we tried to fix the issue: We started by running podman info to get some extra details on what could be wrong: >podman info OS: windows/amd64 provider: wsl version: 5.3.1 Cannot connect to Podman. Please verify your connection to the Linux system using `podman system connection list`, or try `podman machine init` and `podman machine start` to manage a new Linux VM Error: unable to connect to Podman socket: failed to connect: dial tcp 127.0.0.1:2655: connectex: No connection could be made because the target machine actively refused it. That makes sense as the podman VM was not running. Let’s check the VM: >podman machine list NAME         ...

Cache stampede: when our cache turned against us

While investigating some performance issues, we ran into an ASP.NET Core API that cached a fairly expensive aggregation query for 60 seconds. Under normal load, that was fine: one request rebuilds the cache, everyone else reads from it. Under peak load, dozens of requests would arrive in that same expiry window, all see a cache miss, and all fire the same expensive query in parallel. The database didn't like that. That was the moment when our caching layer stopped helping and started hurting. A burst of requests comes in at the same time, all miss the cache, and all go hammer the database or the downstream API at once. That's a cache stampede . The cache was supposed to protect our backend, and for a few hundred milliseconds it did the opposite. Why this happens IMemoryCache.GetOrCreate (and its async sibling) looks like it protects you, but it doesn't add any locking on its own. Look at the naive version: public async Task<Report> GetReportAsync(string key) ...

The role of ActivitySource in OpenTelemetry for .NET

While doing some pair programming to integrate OpenTelemetry tracing to a .NET application, we had a discussion on how to use the ActivitySource . It looks simple. You new one up, give it a name, start an activity, done. The discussion started when we added a second ActivitySource with the exact same name in a different class. This made us wonder: "Are we duplicating traces now? Is this a memory leak? Do we need a singleton?" So we decided to dig deeper. This post is what we learned… What ActivitySource actually is ActivitySource is part of System.Diagnostics , not part of the OpenTelemetry NuGet packages. Microsoft built tracing primitives directly into the BCL, and OpenTelemetry's .NET SDK simply listens to them. This is why you can add distributed tracing to a library without taking a dependency on OpenTelemetry at all. An ActivitySource is a factory for Activity objects, and an Activity is .NET's name for what OpenTelemetry calls a span.(don’t ask m...