How-To: Get Started with Authentication
Uno.Extensions.Authentication
provides you with a consistent way to add authentication to your application. It is recommended to use one of the built-in IAuthenticationService
implementations. This tutorial will use the custom authorization to validate user credentials.
Step-by-step
Important
This guide assumes you used the template wizard or dotnet new unoapp
to create your solution. If not, it is recommended that you follow the Creating an application with Uno.Extensions documentation to create an application from the template.
1. Basic Credential Checking
Add
Authentication
to the<UnoFeatures>
property in the Class Library (.csproj) file.<UnoFeatures> Material; + Authentication; Toolkit; MVUX; </UnoFeatures>
Append
UseAuthentication
to theIHostBuilder
instance. TheLogin
callback is used to verify the credentials. If the user is authenticated, the callback needs to return a non-empty dictionary of key-value pairs (this would typically contain tokens such as an access token and/or refresh token).private IHost Host { get; set; } protected override void OnLaunched(LaunchActivatedEventArgs args) { var builder = this.CreateBuilder(args) .Configure(host => { host .UseAuthentication(auth => auth.AddCustom(custom => custom.Login( async (sp, dispatcher, tokenCache, credentials, cancellationToken) => { var isValid = credentials.TryGetValue("Username", out var username) && username == "Bob"; return isValid ? credentials : default; }) )); }); ... }
Tip
On Apple platforms (iOS, Mac Catalyst) the Uno storage extension, used by the authentication extension, use the OS Key Chain service to store secrets. This requires your application to have the proper entitlements to work properly.
Update
MainPage
to accept input viaTextBox
with a binding expression to connect to theUsername
property on the view model. TheButton
is also bound to theAuthenticate
method.<TextBox Text="{Binding Username, Mode=TwoWay}" /> <Button Content="Login" Click="{x:Bind ViewModel.Authenticate}" />
Update
MainViewModel
to accept anIAuthenticationService
instance. Note that theLoginAsync
method requires anIDispatcher
instance to be supplied, so this is added as a dependency of theMainViewModel
too.public string? Username { get; set; } private readonly IAuthenticationService _auth; private readonly IDispatcher _dispatcher; public MainViewModel( IDispatcher dispatcher, INavigator navigator, IAuthenticationService auth) { _auth = auth; _dispatcher = dispatcher; _navigator = navigator; } public async Task Authenticate() { if (await _auth.LoginAsync(_dispatcher, new Dictionary<string, string> { { "Username", Username ?? string.Empty } }, CancellationToken.None)) { await _navigator.NavigateViewModelAsync<SecondViewModel>(this); } }
Update the
Start
method inShellViewModel
to invoke Refresh, which will determine if there are valid credentials. If this returns true, can navigate directly toSecondViewModel
, otherwise to theMainViewModel
.public async Task Start() { if (await _auth.RefreshAsync(CancellationToken.None)) { await Navigator.NavigateViewModelAsync<SecondViewModel>(this); } else { await Navigator.NavigateViewModelAsync<MainViewModel>(this); } }
Update the "Second" route in
App.xaml.cs
to specify that it depends on the "Main" route. This will make sure that even if the app navigates directly to the SecondPage, the MainPage will be added to the back stack.routes .Register( new RouteMap("", View: views.FindByViewModel<ShellViewModel>() , Nested: new RouteMap[] { new RouteMap("Main", View: views.FindByViewModel<MainViewModel>()), new RouteMap("Second", View: views.FindByViewModel<SecondViewModel>(), DependsOn:"Main"), }));
Update
SecondPage
XAML to include a Button for logging out of the application. This will invoke theLogout
method on theSecondViewModel
.At this point the application can be run and the user can enter a username and click the Login button. If the name is "Bob" they will be navigated to the
SecondPage
. If the application is restarted the application will automatically navigate to theSecondPage
, since the user is still logged in.The user is likely to want to logout of the application, the
LogoutAsync
method has to be called on theIAuthenticationService
.<Button Content="Logout" Click="{x:Bind ViewModel.Logout}" />
Add the
Logout
method to theSecondViewModel
. In this case theSecondViewModel
has been changed to a record, with properties for theDispatcher
and theIAuthenticationService
.public record SecondViewModel(IDispatcher Dispatcher, IAuthenticationService Auth) { public async Task Logout() { await Auth.LogoutAsync(Dispatcher, CancellationToken.None); } }
From this walk through, you can see how the IAuthenticationService can be used to authenticate a user using a very simple check on the username. The Login, Refresh, and Logout methods can all be implemented in order to change the behavior of the application.
2. Invoking an Authentication Service
Add
Http
to the<UnoFeatures>
property in the Class Library (.csproj) file. This will bring the references we need to accessRefit
.<UnoFeatures> Material; Authentication; + Http; Toolkit; MVUX; </UnoFeatures>
Add POCO objects to working with dummyjson.com
[Headers("Content-Type: application/json")] public interface IDummyJsonEndpoint { [Post("/auth/login")] Task<AuthResponse> Login(Credentials credentials, CancellationToken ct); } public class Credentials { [JsonPropertyName("username")] public string? Username { get; init; } [JsonPropertyName("password")] public string? Password { get; init; } } public class AuthResponse { [JsonPropertyName("token")] public string? Token { get; set; } }
Add configuration for Refit endpoints
private IHost Host { get; set; } protected override void OnLaunched(LaunchActivatedEventArgs args) { var builder = this.CreateBuilder(args) .Configure(host => { host .UseHttp((context, services) => http.AddRefitClient<IDummyJsonEndpoint>(context) ); }); ... }
Update
appsettings.json
to include a section that specifies the base Url for the Refit service. Note that the section name needs to match the interface (dropping the leading I) name. In this case, the interface name isIDummyJsonEndpoint
, so the configuration section isDummyJsonEndpoint
{ "AppConfig": { "Title": "AuthSample" }, "LocalizationConfiguration": { "Cultures": [ "en" ] }, "DummyJsonEndpoint": { "Url": "https://dummyjson.com", "UseNativeHandler": true } }
Add the
UseAuthentication
method to theInitializeHost
method, this time using theAddCustom
overload that accepts a type parameter. Instead of anIServiceProvider
being passed into theLogin
callback, an instance of theIDummyJsonEndpoint
will be provided.private IHost Host { get; set; } protected override void OnLaunched(LaunchActivatedEventArgs args) { var builder = this.CreateBuilder(args) .Configure(host => { host .UseAuthentication(auth => auth.AddCustom<IDummyJsonEndpoint>(custom => custom.Login( async (authService, dispatcher, tokenCache, credentials, cancellationToken) => { var name = credentials.FirstOrDefault(x => x.Key == "Username").Value; var password = credentials.FirstOrDefault(x => x.Key == "Password").Value; var creds = new Credentials { Username = name, Password = password }; var authResponse = await authService.Login(creds, cancellationToken); if (authResponse?.Token is not null) { credentials["AccessToken"] = authResponse.Token; return credentials; } return default; }) )); }); ... }
In this case, the Username and Password are extracted out of the credentials dictionary and added to an instance of the Credentials class (which we added earlier, along with the IDummyJsonEndpoint interface), which is passed to the Login method.
Update the MainPage to include a TextBox for entering the password:
<TextBox Text="{Binding Username, Mode=TwoWay}" /> <TextBox Text="{Binding Password, Mode=TwoWay}" /> <Button Content="Login" Click="{x:Bind ViewModel.Authenticate}" />
Update the Authenticate method on the MainViewModel to pass both username and password to the LoginAsync method
public async Task Authenticate() { if (await _auth.LoginAsync(_dispatcher, new Dictionary<string, string> { { nameof(Username), Username ?? string.Empty }, { nameof(Password), Password ?? string.Empty } }, CancellationToken.None)) { await _navigator.NavigateViewModelAsync<SecondViewModel>(this); } }
With this done, the application has changed from self-validating the username entered by the user, to using a back-end service to perform the validation.