How all this works: Authorization logic

Friday 23 January 2026 06:00 CST   David Braverman
BlogEngine.NETBlogsHow this worksSoftware

In my last post on how the Daily Parker blog engine works, I talked about the fundamental abstractions that I built it around. Today I'm going to talk about the code some more, but more concretely, by explaining how the application decides who can see or do what. I'm a little proud of this design, to be honest.

Just a quick reminder of the difference between authentication and authorization: authentication confirms that you are who you say you are; authorization decides what you can do. The Daily Parker lets Microsoft handle authentication and just trusts that they're doing it correctly. (I'm planning to add other authentication services, including Google and Facebook.)

So assuming that a user is who she claims to be, what does the blog engine do with that information?

When someone attempts to do something that requires a permission, the app first asks the security middleware for the person's user ID, then uses that ID to read the person's profile, which is stored in the app's database. The profile contains a list of the user's roles and the privileges they have within each role. Role names are arbitrary; I can define them as I need them without changing the software. So far I've defined 9 roles, including "Blog post" and "Comment," which are the two visitors interact with the most even if they don't know they do. Each role has five possible privileges: List, Read, Change, Delete, and Self. For example, to write a blog entry like I'm doing now, a user has to have the Blog Post role with Change privilege. (Of course, most of the app's basic functions work fine if you don't bother authenticating. Anonymous users account for over 99% of the blog's page views, after all. But even anonymous users need to be authorized to do stuff. The application still checks authorizations at every step,)

In some cases, a user just has to be in a particular role to see certain information. For example, to see the "add blog post" icon on the navigation bar, you have to have the Blog Post role. The Blazor middleware enforces that with AuthorizeView containers.

Once you're on a page, though, authorization may get more finely-tuned. The view blog post page (on which you're probably reading this right now) makes five specific checks before revealing things like the edit button and the post metadata footer. Here's where we get to the code I'm pretty jazzed about.

At the top of the authorization chain is a call to IAuthorizationService.IsAuthorizedAsync, which simply returns true or false. It looks like this:

Task<bool> IsAuthorizedAsync(
	Project? project,
	UserProfile? user,
	IAuthorizable? item,
	string feature,
	Privileges demand,
	CancellationToken cancellationToken = default);

(The Async keyword and suffix just means that the code runs on its own thread so it doesn't block the rest of the application from running. Lack of async code caused most of the performance problems with the BlogEngine.NET version of this application, so it was important for me to build the entire app around asynchronous code blocks.)

The project, user, item that you want to use, role that you need to have, and privileges you require to use it comprise an AuthorizationState, which is used by this method:

public async Task<bool> IsAuthorizedAsync(
	AuthorizationState state,
	bool logResult = false,
	CancellationToken cancellationToken = default)
{
	var allowed = false; // If no strategy applies, deny by default
	var strategyName = "no strategy";
	var userName = state.User?.Id ?? "(anonymous)";

	var strategies = state.Item is null
		? DefaultAuthorizationStrategies
		: DefaultAuthorizationStrategies.Union(state.Item.AuthorizationStrategies); // General, then specific

	foreach (var strategy in strategies)
	{
		var result = strategy(state);
		switch (result)
		{
			case AuthorizationResult.Allow:
				allowed = true;
				break;

			case AuthorizationResult.Deny:
				allowed = false;
				break;

			case AuthorizationResult.None:
			default:
				continue;
		}

		strategyName = strategy.Method.Name.Humanize();
		if (!allowed) break; // Any explicit Deny takes precedence
	}

	StrategyUsed = strategyName;
	if (!logResult) return allowed;

	var description = string.Concat(state.Description, " (", strategyName, ")").Trim();
	if (allowed)
	{
		await actionLog.WriteAsync(state.Feature, state.Demand.ToString(), userName, ActionLogResult.Authorized,
			state.Item?.Id, description, cancellationToken).ConfigureAwait(false);
		logger.LogDebug("{User} authorized to {Demand} {Feature} on {Type} {Item} using {Strategy}",
			userName, state.Demand, state.Feature, state.Item?.GetType().Name, state.Item?.Id, strategyName);
	}
	else
	{
		await actionLog.WriteAsync(state.Feature, state.Demand.ToString(), userName, ActionLogResult.Unauthorized,
			state.Item?.Id, description, cancellationToken).ConfigureAwait(false);
		logger.LogInformation("{User} not authorized to {Demand} {Feature} on {Type} {Item} using {Strategy}",
			userName, state.Demand, state.Feature, state.Item?.GetType().Name, state.Item?.Id, strategyName);
	}

	return allowed;
}

Have you seen the clever bit? It's on the 4th line (beginning with var strategies =). You see, every type of item that requires authorization implements an interface called IAuthorizable that has this member:

Func<AuthorizationState, AuthorizationResult>[] AuthorizationStrategies { get; }

That's a bit of functional programming hiding out inside an object-oriented interface. It allows the objects themselves to decide how they authorize actions based on their own internal state. Each item in a class's AuthorizationStrategies collection is a function that takes an AuthorizationState and returns an AuthorizationResult which is either Allow, Deny, or None. Allow means "the action is authorized," deny means "the action is prohibited," and none means "it's not my place to decide." As you can see in the IsAuthorizedAsync code, though, actions must have an explicit Allow result to be authorized. Once an action is denied, the discussion ends; "allow" and "none" are always provisional until all of the applicable authorization strategies is checked.

For example, here's the BlogPost.FuturePost method, which is one of the checks that occurs nearly every time someone tries to read a blog post:

private static AuthorizationResult FuturePost(AuthorizationState state)
{
	if (state.Item is not BlogPost post
		 || state.Project is null
		 || post.Start <= DateTimeOffset.UtcNow) return AuthorizationResult.None;

	if (state.User is null) return AuthorizationResult.Deny;

	var user = state.User.Id;
	if (post.Owner == user) return AuthorizationResult.Allow;
	if (state.Project.Users.Contains(user)) return AuthorizationResult.Allow;

	return AuthorizationResult.Deny;
}

First, the method makes sure that the item is a blog post, that it has a project associated with it, and if it's a blog post, that its start (post) time is in the future. If the answer to any of those questions is "no," the method returns None, because it's not up to the BlogPost class to determine what to do.

Next, if there is no user—i.e., it's an anonymous request—the request is denied. This ensures that you have to be logged in to see a future post.

But we're not done, because the next three lines require either that the person requesting to see the post is the one who created it (the owner), or if not, that the person is a member of the project associated with the post. If either of those things is true, the request is tentatively allowed. If neither is true, it's denied.

Why tentatively? Remember that an "allow" result is provisional. Before the class-specific authorization strategies even start, the authorization service runs its own checks in this order:

  • Is the authorization state itself valid?
  • Was the user deleted?
  • Does the user have the requested privilege? (Sure, you're a member of the project, but you don't have BlogPost/Read privileges, so...nope.)
  • If the request is to change or delete something, is the user a member of the project?
  • Is the request just to list something that only requires authentication to list? (This solves a specific problem with anonymous users that wasn't immediately obvious to me.)
  • Is the user requesting authorization the owner of the item?
  • Is the item open for public reading? (This is the one that lets you, dear anonymous visitor, read this post.)
  • Is the item itself deleted?

Only if every one of those checks and the item's own checks ends with "Allow" does IsAuthenticatedAsync return Allow.

And because the individual classes have their own authorization strategies, I don't have to change any of the authorization code when I create new things for the project. The authorization service doesn't care what the item it's authorizing is, as long as it implements the IAuthorizable interface. So if I create a new FuelRecord class for keeping track of when I fill up my car, the FuelRecord can decide for itself who can use it, and I don't have to change any other lines of code.

By the way, there are about 150 unit tests around this, including dozens of checks against all five privilege types for each authorization state I could think of. It's tedious, but like the Inner Drive Money class, it's something I want very much not to screw up.

I mentioned in a post weeks ago that I've been thinking about this software for 10 years. This was the second-hardest problem to solve. I think I came up with a pretty cool solution. Let me know what you think in the comments.

Copyright ©2026 Inner Drive Technology. Donate!