We have been fortunate enough to get some great contributions to DasBlog, one of the most important contributions was the integration of authentication and authorization, it was a lot of work and one that I would have struggled to complete alone. It has made everything about this project easier from securing Controllers to deciding which parts of a View to display.

However, there is one reoccurring pattern that has emerged that seems to be the result of constantly checking whether I am authorized or not. It goes something like this, you want to show say a login link when you are not logged in, and logout link when you are actually logged in, here is an example:

@using Microsoft.AspNetCore.Identity
@using DasBlog.Web.Identity
@inject SignInManager<DasBlogUser> SignInManager
@inject UserManager<DasBlogUser> UserManager

@if (SignInManager.IsSignedIn(User))
{
    <form asp-area="" asp-controller="account" asp-action="logout" method="post" id="logoutForm" class="navbar-right">
        <ul class="nav navbar-nav navbar-right">
            <li>
                Hello @UserManager.GetUserName(User)!
            </li>
            <li>
                <button type="submit" class="btn btn-link navbar-btn navbar-link">Log off</button>
            </li>
        </ul>
    </form>
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li><a asp-controller="account" asp-action="login">Log in</a></li>
    </ul>
}

I find these patterns all over the place in all kinds of code and while it is not a big deal, I thought there might be a better way to accomplish this kind of thing using TagHelpers and this is what I came up with. A TagHelper that inherits from IAuthorizeData and uses the PolicyEvaluator to determine from the HttpContext whether we are logged in or not. The implementation of the IAuthorizeData interface was just in case I later decide to make decision about tracking policies and roles.

The important part of this code is asking if we are authorized (authorizeResult.Succeeded) and then using that to determine if we need to suppress the output, which completely removes the associated html code.

[HtmlTargetElement(Attributes = "dasblog-authorized")]
[HtmlTargetElement(Attributes = "dasblog-authorized,dasblog-roles")]
public class AuthorizationRoleTagHelper : TagHelper, IAuthorizeData
{
    private readonly IPolicyEvaluator _policyEvaluator;
    
    private readonly IAuthorizationPolicyProvider _authPolicyProvider;
    
    private readonly IHttpContextAccessor _httpContextAccessor;
    
    [HtmlAttributeName("dasblog-roles")]
    public string Roles { get; set; }
    
    public string Policy { get; set; }
    
    public string AuthenticationSchemes { get; set; }
    
    public AuthorizationRoleTagHelper(IHttpContextAccessor httpContextAccessor, IAuthorizationPolicyProvider policyProvider, IPolicyEvaluator policyEvaluator)
    {
        _httpContextAccessor = httpContextAccessor;
        _authPolicyProvider = policyProvider;
        _policyEvaluator = policyEvaluator;
    }
    
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var policy = await AuthorizationPolicy.CombineAsync(_authPolicyProvider, new[] { this });
        var authenticateResult = await _policyEvaluator.AuthenticateAsync(policy, _httpContextAccessor.HttpContext);
        var authorizeResult = await _policyEvaluator.AuthorizeAsync(policy, authenticateResult, _httpContextAccessor.HttpContext, null);
        if (!authorizeResult.Succeeded)
        {
            output.SuppressOutput();
        }
    }
}

To follow this pattern to its logical conclusion I need to also explicitly know when we are not authorized and this tag helper helps in that example:

[HtmlTargetElement(Attributes = "dasblog-unauthorized")]
public class UnAuthorizedRoleTagHelper : TagHelper, IAuthorizeData
{
    private readonly IPolicyEvaluator _policyEvaluator;
    
    private readonly IAuthorizationPolicyProvider _authPolicyProvider;
    
    private readonly IHttpContextAccessor _httpContextAccessor;
    
    public UnAuthorizedRoleTagHelper(IHttpContextAccessor httpContextAccessor, IAuthorizationPolicyProvider policyProvider, IPolicyEvaluator policyEvaluator)
    {
        _httpContextAccessor = httpContextAccessor;
        _authPolicyProvider = policyProvider;
        _policyEvaluator = policyEvaluator;
    }
    
    public string Policy { get; set; }
    
    public string Roles { get; set; }
    
    public string AuthenticationSchemes { get; set; }
    
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var policy = await AuthorizationPolicy.CombineAsync(_authPolicyProvider, new[] { this });
        var authenticateResult = await _policyEvaluator.AuthenticateAsync(policy, _httpContextAccessor.HttpContext);
        var authorizeResult = await _policyEvaluator.AuthorizeAsync(policy, authenticateResult, _httpContextAccessor.HttpContext, null);
    
        if (authorizeResult.Succeeded)
        {
            output.SuppressOutput();
        }
    }
}

This leads to a much simpler and designer friendly Razor Page that allows developers and designers to include or exclude forms, divs (or any html) based on including a simple property, in this case, dasblog-authorized or dasblog-unauthorized:

@using Microsoft.AspNetCore.Identity
@using DasBlog.Web.Identity
@inject SignInManager<DasBlogUser> SignInManager
@inject UserManager<DasBlogUser> UserManager

<form dasblog-authorized asp-area="" asp-controller="account" asp-action="logout" method="post" id="logoutForm" class="navbar-right">
    <ul class="nav navbar-nav navbar-right">
        <li>
            Hello @UserManager.GetUserName(User)!
        </li>

        <li>
            <button type="submit" class="btn btn-link navbar-btn navbar-link">Log off</button>
        </li>
    </ul>
</form>

<ul dasblog-unauthorized class="nav navbar-nav navbar-right">
    <li><a asp-controller="account" asp-action="login">Log in</a></li>
</ul>

I do love Razor pages but dislike seeing intrusive if statements embedded throughout the html, I think this can be a good alternative in some circumstances.