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-steps
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 instructions for creating an application from the template.
1. Basic Credential Checking
Install
Uno.Extensions.Authenticationinto all projectsAppend
UseAuthenticationto theIHostBuilderinstance. TheLogincallback 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; } 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; }) )); }); ...Update
MainPageto accept input viaTextBoxwith a binding expression to connect to theUsernameproperty on the view model. TheButtonis also bound to theAuthenticatemethod.<TextBox Text="{Binding Username, Mode=TwoWay}" /> <Button Content="Login" Click="{x:Bind ViewModel.Authenticate}" />Update
MainViewModelto accept anIAuthenticationServiceinstance. Note that theLoginAsyncmethod requires anIDispatcherinstance to be supplied, so this is added as a dependency of theMainViewModeltoo.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
Startmethod inShellViewModelto 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.host.csto 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 backstack.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
SecondPageXAML to include a Button for logging out of the application. This will invoke theLogoutmethod 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 the SecondPage, since the user is still logged in.
The user is likely to want to logout of the application, the
LogoutAsyncmethod has to be called on theIAuthenticationService.<Button Content="Logout" Click="{x:Bind ViewModel.Logout}" />Add the
Logoutmethod to theSecondViewModel. In this case theSecondViewModelhas been changed to a record, with properties for theDispatcherand 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 method can all be implemented in order to change the behavior of the application.
2. Invoking an Authentication Service
Add
Uno.Extensions.Http.Refitpackage reference to Extensions.propsAdd 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; } protected override void OnLaunched(LaunchActivatedEventArgs args) { var builder = this.CreateBuilder(args) .Configure(host => { host .UseHttp((context, services) => http.AddRefitClient<IDummyJsonEndpoint>(context) ); }); ...Update
appsettings.jsonto 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
UseAuthenticationmethod to theInitializeHostmethod, this time using theAddCustomoverload that accepts a type parameter. Instead of anIServiceProviderbeing passed into theLogincallback, and instance of theIDummyJsonEndpointwill be provided.private IHost Host { get; } 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.