Client Authentication
As noted above, the Silverlight Business Application template utilizes a WCF RIA Services backend that implements an AuthenticationService using ASP.NET web form authentication. The template also supported The TimeEntryRia sample application has extended the solution to use accounts stored in a custom database rather than use the default ASP.NET authentication schema - a common practice with enterprise applications.
As WCF RIA Services is no longer available, and the general approach to web services have moved on, there are a number of alternate approaches to implementing services and integrating authentication.
Note
There are lots of authentication options out there - you can learn more about some of them from the resources below:
In the sample migration, ASP.NET Core Web APIs are used and secured using an app level client credential using IdentityServer4. The implementation of these server-side services is beyond the scope of this article, however the source can be reviewed in the sample project. This and the following tasks will walk through the client-side implementation of authentication, with the intent to show how the baseline Silverlight capability can be replicated.
IdentityServer4 Client-side service overview
As discussed earlier, the UWP implementation of the TimeEntryUno application will use IdentityServer4 to secure access to the data service APIs via a client access token. This token is then used to access the data services, such as the authentication service that validates user logins.
Tip
The IdentityServer4 server-side implementation used in the sample mirrors the QuickStart tutorial shown below. :
The code to retrieve the access token is encapsulated within the a class IdentityServerClient and uses the HttpClient class as well as the IdentityModel NuGet package.
Install the nuget packages
To install the, IdentityModel NuGet package, right-click the solution, and select Manage NuGet packages for solution...
In the Manage Packages for Solution UI, select the Browse tab, search for IdentityModel and select it in the search results.
On the right-side of the Manage Packages for Solution UI, ensure the UWP and WASM projects are selected, and then click Install.
Repeat the above process, adding System.Text.Json.
Implement the IdentityServerClient class
In the Shared project, create a new folder and name it WebServices.
Within the WebServices folder, add a new class and name it IdentityServerClient.
Replace the using statements with the following:
using IdentityModel.Client; using Microsoft.Extensions.Logging; using System; using System.Net.Http; using System.Threading.Tasks; using Uno.Extensions;
Add the following member variables:
private static HttpClient _client; private string _identityServerBaseAddress; private string _clientId; private string _clientSecret; private string _scope;
To create the static instance of the HttpClient, add following static constructor:
static IdentityServerClient() { _client = new HttpClient(); }
Uno allows you to reuse views and business logic across platforms. Sometimes though you may want to write different code per platform. You may need to access platform-specific native APIs and 3rd-party libraries, or want your app to look and behave differently depending on the platform. In this case, when targeting WASM, the application needs to use an alternate HttpHandler when running under WASM, so we have conditional code that runs only on WASM that instantiates Uno.UI.Wasm.WasmHttpHandler(). All other platforms (in this app we only have UWP), use an instance of HttpClientHandler.
Tip
You can learn more about platform-specific C# and XAML here:
To supply the required parameters to the IdentityServerClient class, add the following constructor:
public IdentityServerClient(string identityServerBaseAddress, string clientId, string clientSecret, string scope) { _identityServerBaseAddress = identityServerBaseAddress; _clientId = clientId; _clientSecret = clientSecret; _scope = scope; }
Here is an example of the constructor in use:
_identityServerClient = new IdentityServerClient( identityServerBaseAddress: "https://localhost:5001", clientId: "TimeEntryUno", clientSecret: "A2W7aQVFQWRX", scope: "TimeEntryApi");
The ClientId, ClientSecret and Scope values are defined in the configuration of the IdentityServer4 instance that is used.
In order to retrieve an access token from the IdentityServer4 API, add the following method:
public async Task<string> GetAccessTokenAsync() { var discoveryResponse = await _client.GetDiscoveryDocumentAsync(address: _identityServerBaseAddress); if (discoveryResponse.IsError) { this.Log().LogError(discoveryResponse.Error); throw new Exception(discoveryResponse.Error); } var tokenResponse = await _client.RequestClientCredentialsTokenAsync( new ClientCredentialsTokenRequest { Address = discoveryResponse.TokenEndpoint, ClientId = _clientId, ClientSecret = _clientSecret, Scope = _scope }); if (tokenResponse.IsError) { this.Log().LogError(tokenResponse.Error); throw new Exception(tokenResponse.Error); } return tokenResponse.AccessToken; }
The IdentityModel NuGet package includes an extension method GetDiscoveryDocumentAsync that works with the HttpClient instance constructed earlier. This method sends a discovery document request to the specified IdentityServer4 and returns a DiscoveryDocumentResponse.
If there is an error, it is logged and an exception is thrown (production implementations may retry, etc.), otherwise the RequestClientCredentialsTokenAsync extension method uses a ClientCredentialsTokenRequest constructed with the retrieved DiscoveryDocumentResponse.TokenEndpoint, ClientId, ClientSecret and Scope, to retrieve a TokenResponse. If an error occurs, it is logged and an exception thrown - again, production apps my choose to retry, etc., otherwise the access token is returned.
At this point the IdentityServerClient class implements the bare minimum necessary to retrieve an access token. It does not include retry logic or any code to manage token expiration, key rotation etc.
In order to authentication in the UWP project, the following capabilities must be added to the Package.appxmanifest:
- EnterpriseAuthentication
- PrivateNetwork
- Shared User Certificates
The WebAssembly linker can be overly aggressive when it comes to trimming the linked code-base to minimize the application size. To ensure the code for the IdentityModel and System.Text.Json packages are not removed, open the LinkerConfig.xml file in the WASM project.
Update the LinkerConfig.xml file to match the following:
<linker> <assembly fullname="TimeEntryUno.Wasm" /> <assembly fullname="Uno.UI" /> <assembly fullname="System.Text.Json" /> <assembly fullname="IdentityModel" /> <assembly fullname="System.Core"> <!-- This is required by JSon.NET and any expression.Compile caller --> <type fullname="System.Linq.Expressions*" /> </assembly> </linker>
To simplify the use of the IdentityServerClient class, it can be encapsulated into a singleton service. The next task will show the implementation used in the TimeEntryUno app.