Search code examples
azure-active-directoryauthorization.net-core-3.1microsoft-identity-platform.net-core-authorization

Microsoft Identity Platform Azure AD App Role not working in NET Core 3.1 Razor Pages


I've been following a recent MS project example here: https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/5-WebApp-AuthZ/5-1-Roles

I couldn't find any examples for a Razor project, only MVC so have ported across everything I believe is needed to support authorization using Azure AD App Roles and using the same AuthorizationPolicyBuilder library as found in the project example.

I created an App Role in Azure AD by updating the App Manifest file and assigning the user to the required role for this WebApp. I believe I've covered the required steps in Azure given I can clearly see the required Role 'ViewLogs' is included in the ID Token returned from Azure following a successful login.

enter image description here

Issue is a page protected by the authrization policy for 'ViewLogs' gives me an Access Denied message every time I try to load the page. I'm not sure how I can diagnose this to understand why it's not letting me in.

Startup.cs

public void ConfigureServices(IServiceCollection services)
    {
        // Added to original .net core template.
        // ASP.NET Core apps access the HttpContext through the IHttpContextAccessor interface and 
        // its default implementation HttpContextAccessor. It's only necessary to use IHttpContextAccessor 
        // when you need access to the HttpContext inside a service.
        // Example usage - we're using this to retrieve the details of the currrently logged in user in page model actions.
        services.AddHttpContextAccessor();

        // DO NOT DELETE (for now...)
        // This 'Microsoft.AspNetCore.Authentication.AzureAD.UI' library was originally used for Azure Ad authentication 
        // before we implemented the newer Microsoft.Identity.Web and Microsoft.Identity.Web.UI NuGet packages. 
        // Note after implememting the newer library for authetication, we had to modify the _LoginPartial.cshtml file.
        //services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
        //    .AddAzureAD(options => Configuration.Bind("AzureAd", options));

        ///////////////////////////////////

        // Add services required for using options.
        // e.g used for calling Graph Api from WebOptions class, from config file.
        services.AddOptions();

        // Add service for MS Graph API Service Client.
        //services.AddTransient<OidcConnectEvents>();

        // Sign-in users with the Microsoft identity platform
        services.AddSignIn(Configuration);

        // Token acquisition service based on MSAL.NET
        // and chosen token cache implementation
        services.AddWebAppCallsProtectedWebApi(Configuration, new string[] { Constants.ScopeUserRead })
            .AddInMemoryTokenCaches();

        // Add the MS Graph SDK Client as a service for Dependancy Injection.
        services.AddGraphService(Configuration);

        // The following lines code instruct the asp.net core middleware to use the data in the "roles" claim in the Authorize attribute and User.IsInrole()
        // See https://learn.microsoft.com/aspnet/core/security/authorization/roles?view=aspnetcore-2.2 for more info.
        services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            // The claim in the Jwt token where App roles are available.
            options.TokenValidationParameters.RoleClaimType = "roles";
        });

        // Adding authorization policies that enforce authorization using Azure AD roles. Polices defined in seperate classes.
        services.AddAuthorization(options =>
        {
            options.AddPolicy(AuthorizationPolicies.AssignmentToUserReaderRoleRequired, policy => policy.RequireRole(AppRole.UserReaders));
            options.AddPolicy(AuthorizationPolicies.AssignmentToViewLogsRoleRequired, policy => policy.RequireRole(AppRole.ViewLogs));
        });

        services.AddRazorPages().AddMvcOptions(options =>
        {
            var policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();
            options.Filters.Add(new AuthorizeFilter(policy));
        }).AddMicrosoftIdentityUI();

        // Adds the service for creating the Jwt Token used for calling microservices.
        // Note we are using our independant bearer token issuer service here, NOT Azure AD
        services.AddScoped<JwtService>(); 
    }

AppRole Class.cs

/// <summary>
/// Contains a list of all the Azure AD app roles this app depends on and works with.
/// </summary>
public static class AppRole
{
    /// <summary>
    /// User readers can read basic profiles of all users in the directory.
    /// </summary>
    public const string UserReaders = "UserReaders";

    /// <summary>
    /// View Logs can review system logs and run all log queries.
    /// </summary>
    public const string ViewLogs = "ViewLogs";
}

/// <summary>
/// Wrapper class the contain all the authorization policies available in this application.
/// </summary>
public static class AuthorizationPolicies
{
    public const string AssignmentToUserReaderRoleRequired = "AssignmentToUserReaderRoleRequired";
    public const string AssignmentToViewLogsRoleRequired = "AssignmentToViewLogsRoleRequired";
}

Page I want to visit:

//[Authorize]
//[AllowAnonymous]
[Authorize(Policy = AuthorizationPolicies.AssignmentToViewLogsRoleRequired)] // Not working
public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;
    public readonly JwtService _jwtService;

    public IndexModel(ILogger<IndexModel> logger, JwtService jwtService)
    {
        _logger = logger;
        _jwtService = jwtService;
    }
    
    public void OnGet()
    {
        var username = HttpContext.User.Identity.Name;
        var forename = HttpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value;
        var surname = HttpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value;
        _logger.LogInformation("" + username + " requested the Index page");
    }

    public JsonResult OnGetJwtToken()
    {
        var token = _jwtService.GenerateSecurityToken();
        return new JsonResult(token);
    }
}

Thanks in advance...


Solution

  • I found a snippet from another thread which gave me the clues to find a solution. It seems that there are some subtle differences between Razor and MVC with Controllers when implementing authorization with polices, using claims from the ID Token returned from Azure following a successful login. I discovered i couldn't use the same Authorization policy with "RequireRole" syntax. But using "RequireClaim" seems to have fixed the issue.

    Modified code in my Startup.cs file:

    // Adding authorization policies that enforce authorization using Azure AD roles. Polices defined in seperate classes.
            services.AddAuthorization(options =>
            {
                // This line may not work for razor at all, haven't tried it but is what was used in MVC from the MS Project example. 
                options.AddPolicy(AuthorizationPolicies.AssignmentToUserReaderRoleRequired, policy => policy.RequireRole(AppRole.UserReaders));
                
                // NOTE BELOW - I had to change the syntax from RequireRole to RequireClaim
                options.AddPolicy(AuthorizationPolicies.AssignmentToViewLogsRoleRequired, policy => policy.RequireClaim(ClaimTypes.Role, AppRole.ViewLogs));
            });
    

    The rest of the code and processes used have not changed from my original question in getting this to finally work. Hopefully this helps others who stumble across a similar issue...