Authentication and authorization using Microsoft identity platform in Blazor web app
First thing you will need to do is go to https://portal.azure.com/#allservices and under services choose App registration and register new app.
Once you create it application (client) ID and Directory(tenant) ID will be generated for you. You will need to add section like this to your app.settings file:
"AzureAd": { "Instance": "https://login.microsoftonline.com/", "Domain": "yourdomain.com", "TenantId": "your-tenant-id", "ClientId": "your-client-id", "CallbackPath": "/signin-oidc" }
You will probably need to setup you login redirect url and logout url and possibly some other initial configuration and probably easiest way is go to Integration assistant that will walk you through setup of app. Here you can choose type of the app (Web app in this case) and assistant will suggest actions you might need to do for successfull app registration.

While you already in azure portal go to App roles from the left side to create app roles. Click Create app role to create first role. Display name is friendly name of role but what is important in code is Value property. This Value you will reference in code.

If you are creating app for single tenant scenario (for authorization in you company) you may want to add users to specific roles. What was confusing for me is what you will need to go somewhere else to add them. On the left side menu , under manage section you will not find such option. You will need to go to “Enterprise applications
” service and in the search box search for your app. After you click it you will see under Manage section see “Users and groups” .

You are now ready to go back to Visual Studio and install Microsoft.Identity.Web
and Microsoft.Identity.Web.UI
packages.
In your Startup file add only this one line:
_ = services.AddMicrosoftIdentityWebAppAuthentication(Configuration);
Also, inside your AddControllersWithViews
add Authorize
filter where you will define what roles user needs to have to be able to login to app. To do that create new Policy.
_ = services.AddControllersWithViews(options => options.Filters.Add(new AuthorizeFilter(PolicyBuilder.AllRolesPolicy()))).AddMicrosoftIdentityUI();
PolicyBuilder is jsut custom made class that holds creation off all policies my app uses.
public class PolicyBuilder
{
public static AuthorizationPolicy WriterPolicy()
{
return new AuthorizationPolicyBuilder().RequireAuthenticatedUser()
.RequireRole(RoleLevel.ADMIN, RoleLevel.WRITER)
.Build();
}
public static AuthorizationPolicy AllRolesPolicy()
{
return new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser().RequireRole(RoleLevel.ADMIN, RoleLevel.READER, RoleLevel.WRITER)
.Build();
}
}
public class RoleLevel
{
public const string ADMIN = "Admin";
public const string READER = "Reader";
public const string WRITER = "Writer";
}
So now, if your app uses some internal controllers you can protect them using Authorize attribute like this:
[Route("api/something/{id}/images")]
[Authorize(Policy = Policies.HAS_WRITE_PERMISSIONS)]
[HttpPost]
public async Task UploadImage(int id, [FromForm] IFormFile myFile)
To protect views from rendering to not authenticated users you can use <AuthorizeView>
For example, here I am using <AuthorizeView>
to not render SaveButton
(which is used across app inside forms)
<AuthorizeView Policy="@Policies.HAS_WRITE_PERMISSIONS">
<Authorized>
<DxButton SizeMode="SizeMode.Large" SubmitFormOnClick="true" Text="@(IsSaving ? "Saving...":"Save" )" Enabled="!IsSaving && !Disabled"></DxButton>
</Authorized>
<NotAuthorized>
<br />
<i> You are not authorized to make any changes</i>
</NotAuthorized>
</AuthorizeView>
@code{
[Parameter]
public bool IsSaving { get; set; }
[Parameter]
public bool Disabled { get; set; }
}
In my app I have ADMIN, READ, and WRITE roles and I didn’t want to create specific views just for readers and forms that will make destructive operations for writers. App is simple, it doesn’t communicate to server using WEB API (because it is Blazor server, and it is some internal company app that will be used by only few users).
So I decided to just hide some views like SaveButton. However this doesn’t prevent clever user from inserting button element (for example using Chrome Developer tools) inside form and submitting it so I added some server validation also. Because all my destructive operations were already wrapped using some decorator method I added that logic inside that decorator. All destructive methods, like inserting, updating and deleting already were going through it so this was easies solution. In my case, simplicity was top requirement. Your scenario might be different.
public class DestructiveOperationDecorator : IDestructiveOperationDecorator
{
private readonly IIdentityContext _identityContext;
private readonly ILogger<DestructiveOperationDecorator> _logger;
public IToastService ToastService { get; }
public DestructiveOperationDecorator(IToastService toastService, IIdentityContext identityContext, ILogger<DestructiveOperationDecorator> logger)
{
ToastService = toastService;
_identityContext = identityContext;
_logger = logger;
}
public async Task ExecuteIfAuthorizedWithToastNotification(Func<Task> func)
{
if (await _identityContext.HasWritePermissions() == false)
{
ToastService.ShowError("You are not authorized to make any changes");
return;
}
try
{
await func();
ToastService.ShowSuccess("Successfully saved");
}
catch (WEBUIException e)
{
ToastService.ShowError(e.Message);
_logger.LogError(e, e.Message);
}
catch (Exception e)
{
ToastService.ShowError("Error happened");
_logger.LogError(e, e.Message);
}
}
Here, before executing any actions I am cheching if user is authenticated to do that. Once again, not authenticated user should not come this far, but for security measure this code is here. For disclaimer porpuses, once again, you scenarion might be different, you might need to create some API and protected it and make web app communicate with it.
IdentityContext is just a simply class that uses AuthenticationStateProvider to access information about user:
public class IdentityContext : IIdentityContext
{
private readonly AuthenticationStateProvider _authenticationStateProvider;
public IdentityContext(AuthenticationStateProvider authenticationStateProvider)
{
_authenticationStateProvider = authenticationStateProvider;
}
public async Task<bool> HasWritePermissions()
{
var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
var hasWritePermissions = user.IsInRole(RoleLevel.ADMIN) || user.IsInRole(RoleLevel.WRITER);
return hasWritePermissions;
}
}