Authentication and authorization#
The Datasync Community Toolkit is a set of open-source libraries for building client-server application where the application data is available offline. Unlike, for example, Google Firebase or AWS AppSync (which are two competitors in this space), the Datasync Community Toolkit allows you to connect to any database, use any authentication, and provides robust authorization rules. You can also run the service anywhere - on your local machine, in a container, or on any cloud provider. Each side of the application (client and server) is implemented using .NET - ASP.NET Core Web APIs on the server side, and any .NET client technology (including Avalonia, MAUI, Uno Platform, WinUI3, and WPF) on the client side.
In the last tutorial, I introduced the basic functionality of an online datasync client - that is, a client that accesses the data on the service remotely without any datasync functionality. Today, I’m going to be implementing authentication in both the server and client. Both sides of the client-server relationship must agree on authentication, so it’s only natural that you have to configure authentication in both places.
I’m going to be using Microsoft Entra ID for this, configured in a manner that allows you to use your outlook.com address. I’m going to be using MSAL throughout to make things easier. This is not a blog post on how to implement authentication generally, and - as you will see - you can use any authentication mechanism as long as the client and server agree. You can use the same client and server from the last tutorial for this tutorial.
Authentication is handled by passing a Json Web Token (JWT) from the client to the server. The JWT is generated by an OAuth2 or OIDC service that both the server and the client agree on. While I am using [Microsoft Entra] for this functionality in this tutorial, you can use other identity providers such as Google, Facebook, Auth0, or custom identity services based on OpenID Dict or [Duende IdentityServer]. If you are not using Microsoft Entra, you will need to follow the identity service instructions for configuring ASP.NET Core and for configuring the client application so it can receive a token.
Add authentication to the server app#
You should always start by adding authorization to the server application. For Microsoft Entra, this involves three steps:
- Create an app registration
- Update the server application to support Microsoft Entra.
- Add the client ID from the app registration to app settings as a secret.
The full instructions for this process are included, but you can find the original reference in the official Microsoft Entra documentation
1. Create an App Registration#
- Sign in to the Azure portal. Select the correct tenant and subscription if you have access to multiple environments.
- Search for and select Microsoft Entra ID.
-
Under Manage, select App registrations > New registration:
- Name: Enter a name for the application. e.g. “Todo-ServerApp”. Users of your app will see this name.
- Supported account types: Select Accounts in any organizational directory (Any Microsoft Entra directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox).
-
Select Register.
- Under Manage, select Expose an API > Add a scope.
- Access the default application ID URI by selecting Save and continue.
-
Enter the following details in the form:
- Scope name:
access_as_user
- Who can consent?: Admins and users
- Admin consent display name:
Access app
- Admin consent description:
Access app description
- User consent display name:
Access app
- User consent description:
Access app description
- State:
Enabled
- Scope name:
-
Select Add scope to complete the process.
Note that value of the scope (similar to api://client-id/access_as_user
) - you will need this when configuring the client. Finally, select Overview and note the Application (client) ID in the Essentials section. You’ll need this when configuring the backend service.
2. Add Microsoft Entra authentication to the server app.#
Use the NuGet package manager to install the Microsoft.Identity.Web
package in your server project. Add the following lines at the top of the Program.cs
:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;
Then add the following code to the services definition section of the Program.cs
file:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration);
Add the following lines to the HTTP pipeline:
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Note the addition of UseAuthentication()
and UseAuthorization()
. I’ve included the other lines as a reference to show where in the pipeline they belong.
Finally, decorate any controllers with either [AllowAnonymous]
or [Authorize]
to ensure that the authorization information is passed to any access control provider you have. For this sample, I’m going to require authorization:
[Authorize]
[Route("tables/todoitem")]
public class TodoItemController(IRepository<TodoItem> repository) : TableController<TodoItem>(repository)
{
}
3. Add the client ID to app settings.#
Finally, you need to copy the client ID for the app registration you created and put it in an app setting as a secret. If you are running the server locally, you can use user secrets:
- Open the solution in Visual Studio.
- Right click on the ServerApp, then select Manage User Secrets.
- Fill in the JSON details like this:
{ "AzureAd": { "ClientId": "your-client-id-here" } }
When deploying in Azure App Service, you can create an app setting named AzureAd__ClientId
instead. Do not put this value in your appsettings.json
file - it’s not quite a secret, but you don’t want to check it into a GitHub repository. If you want to read more about configuring a Web API to use Microsoft Entra ID, read the official documentation.
If you use Postman, Insomnia, or another HTTP client, you can perform a HTTP GET /tables/todoitem
on the running server. You will receive a 401 Unauthorized
- this indicates that your authorization code is actually blocking a request.
Adding authentication to the client app#
You may remember that I created a WPF client application in the previous tutorial. I’m going to use the same application and just add authentication to it. Setting up the client is more complex than the server side. There are also different mechanisms for creating the required registration for each platform. I’m covering WPF here, but you should look at the MSAL tutorials for MAUI, WinUI3, or whatever client platform you are using. The thing you need to understand is how to get a token that you can then send to the backend to authorize the request. For this, there is also three steps:
- Create an app registration for the client app.
- Configure the app registration within the client app.
- Write code to authenticate the user.
1. Create an app registration for the client app.#
- Back in the Microsoft Entra ID page on the Azure portal, select App registrations > New registration.
-
In the Register an application page, fill in the form:
- Name: Enter
Todo-WPF-ClientApp
(to distinguish from the one used by the backend service). - Supported account types: Select Accounts in any organizational directory (Any Microsoft Entra directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox).
- Redirect URI: Select Public client (mobile & desktop), and enter the URL
http://localhost
.
- Name: Enter
-
Select Register.
- Select API permissions > Add a permission > My APIs, then select the app registration you created earlier for your backend service. In some circumstances, you may find the app registration under “APIs my organization uses” instead.
- Under Select permissions, select
access_as_user
, then select Add permissions. - Select Authentication > Mobile and desktop applications.
-
Check the following boxes:
- next to
https://login.microsoftonline.com/common/oauth2/nativeclient
. - next to
msal{client-id}://auth
- the client-id will be your client ID.
- next to
-
Select Save at the bottom of the page.
- Finally, select Overview and make a note of the Application (client) ID. You’ll need this along with the scope (from earlier) to configure your client app.
I’ve defined three redirect URLs here:
http://localhost
is used by WPF applications.https://login.microsoftonline.com/common/oauth2/nativeclient
is used by WinUI applications.msal{client-id}://auth
is used by MAUI applications.
2. Configure the client app#
The application needs to know three bits of information:
- The service URI (normally
https://localhost:7181
for running locally). - The Application (client) ID for the client registration (the one you just did).
- The scope for the web API.
Create a Constants.cs
static class. This can be used in your OnlineTodoService.cs
file as well as other parts of the application to access relevant information. Here is an example:
namespace ClientApp;
internal static class Constants
{
/// <summary>
/// The base URI for the Datasync service.
/// </summary>
public static string ServiceUri = "https://localhost:7181";
/// <summary>
/// The application (client) ID for the native app within Microsoft Entra ID
/// </summary>
public static string ApplicationId = "<your-client-application-id>";
/// <summary>
/// The list of scopes to request
/// </summary>
public static string[] Scopes = ["api://<your-server-application-id>/access_as_user"];
}
Update OnlineTodoService
The original OnlineTodoService.cs
class hard-coded the baseUrl
. Ensure you update this file so that it uses the URL from Constants.cs
.
3. Write code to authenticate the user.#
MSAL uses a PublicClientApplication
to handle authentication. It’s based on your native client application ID. Install Microsoft.Identity.Client
and Microsoft.Identity.Client.Desktop
from NuGet, the latter being specific to desktop apps. There are slightly different versions for WinUI3 and MAUI, but the essence remains the same. I’m using the CommunityToolkit.Mvvm
library for dependency injection. In the app, I’ve injected the IPublicClientApplication
interface as a singleton.
Services = new ServiceCollection()
.AddSingleton<IPublicClientApplication>((_) => CreateIdentityClient())
.AddSingleton<ITodoService, OnlineTodoService>()
.AddTransient<TodoListViewModel>()
.AddScoped<IAlertService, AlertService>()
.AddScoped<IAppInitializer, AppInitializer>()
.BuildServiceProvider();
The CreateIdentityClient()
method is also in App.xaml.cs
and looks like this:
public IPublicClientApplication CreateIdentityClient()
{
var client = PublicClientApplicationBuilder.Create(Constants.ApplicationId)
.WithAuthority(AzureCloudInstance.AzurePublic, "common")
.WithRedirectUri("http://localhost")
.WithWindowsEmbeddedBrowserSupport()
.Build();
return client;
}
Next, move your attention to the OnlineTodoService
class. You need something that you can call that returns an authentication token:
public IPublicClientApplication IdentityClient { get; }
public async Task<AuthenticationToken> GetAuthenticationToken(CancellationToken cancellationToken = default)
{
var accounts = await IdentityClient.GetAccountsAsync();
AuthenticationResult? result = null;
try
{
result = await IdentityClient.AcquireTokenSilent(Constants.Scopes, accounts.FirstOrDefault()).ExecuteAsync(cancellationToken);
}
catch (MsalUiRequiredException)
{
result = await IdentityClient.AcquireTokenInteractive(Constants.Scopes).WithUseEmbeddedWebView(true).ExecuteAsync(cancellationToken);
}
catch (Exception ex)
{
Debug.WriteLine($"Error: Authentication failed: {ex.Message}");
}
return new AuthenticationToken
{
DisplayName = result?.Account?.Username ?? "",
ExpiresOn = result?.ExpiresOn ?? DateTimeOffset.MinValue,
Token = result?.AccessToken ?? "",
UserId = result?.Account?.Username ?? ""
};
}
The IdentityClient
here is retrieved via dependency injection. The AuthenticationToken
is a part of the CommunityToolkit.Datasync.Client
library that we are already using. Again, this is pure MSAL - nothing to do with the Community Toolkit Datasync library - you are retrieving a token to use by the library. Your task in this method is “do whatever is necessary to get an access token”.
The final step is to add an authentication handler to the HTTP pipeline for the client. The Datasync Community Toolkit provides a GenericAuthenticationProvider
class that does JWT authentication. The authentication handler automatically calls the provided method to get an authentication token whenever it needs one. It transparently handles cases when the token needs to be refreshed, which allows you to do silent authentication when you have a refresh token.
public OnlineTodoService(IPublicClientApplication identityClient)
{
var clientOptions = new HttpClientOptions()
{
Endpoint = new Uri(Constants.ServiceUri),
HttpPipeline = [ new GenericAuthenticationProvider(GetAuthenticationToken) ]
};
client = new(clientOptions);
IdentityClient = identityClient;
}
The GenericAuthenticationProvider
is a DelegatingHandler
that you can use with any HttpClient
for authentication. It adds the token from the AuthenticationToken
returned from the GetAuthenticationToken
method as an authorization header to each HTTP request going through the configured client. The authentication provider also intelligently caches the token so that it is reused until just before it expires, at which point another token is requested (and MSAL silently fulfills that request).
Run the application, click Refresh and see the authentication happen!
If you want to use the same authentication with a regular HTTP client, use the following:
var clientFactory = new HttpClientFactory(clientOptions);
var httpClient = clientFactory.CreateClient();
The HttpClientFactory
class is provided inside the CommunityToolkit.Datasync.Client.Http
namespace. Doing this allows you to use generic HTTP calls to call your non-datasync web APIs using the same authentication, logging, etc.
Wrap-up#
The main problem with authentication in datasync is the same as authentication in a Web API world. You have to get that going before you can configure the datasync library to use it. Once you have configured authentication to work, it’s as simple as an additional single line in the client options. You can also use the same mechanism in your own HTTP clients. This makes building authenticated clients for other purposes (like calling a non-datasync web API) simple as well.
In the next tutorial, we’ll take a final look at the online client by investigating the pipeline and how it can be used for API keys, logging, and more.