Azure AD B2C and MVVM in Xamarin.Forms

Logan Ramos 1 Reputation point
2021-01-28T02:19:00.71+00:00

I've followed this tutorial https://learn.microsoft.com/en-us/xamarin/xamarin-forms/data-cloud/authentication/azure-ad-b2c but it doesn't seem to work with a MVVM pattern. I have my login view model with the following code

using Microsoft.Identity.Client;  
using MobileApp.Views;  
using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;  
using Xamarin.Forms;  
  
namespace MobileApp.ViewModels  
{  
    public class LoginViewModel : BaseViewModel  
    {  
        public Command LoginCommand { get; }  
  
        private bool _loginButtonShown = false;  
  
        public bool LoginButtonShown  
        {  
            get => _loginButtonShown;  
            set => SetProperty(ref _loginButtonShown, value);  
        }  
  
        public LoginViewModel()  
        {  
            LoginCommand = new Command(OnLoginClicked);  
        }  
  
        private async void OnLoginClicked(object obj)  
        {  
            AuthenticationResult result;  
            try  
            {  
                result = await App.AuthenticationClient  
                    .AcquireTokenInteractive(Constants.Scopes)  
                    .WithPrompt(Prompt.SelectAccount)  
                    .WithParentActivityOrWindow(App.UIParent)  
                    .ExecuteAsync();  
                // Ideally want to pass the reuslt token to the next page  
                await Shell.Current.GoToAsync($"//{nameof(AboutPage)}");  
            }  
            catch (MsalException ex)  
            {  
                if (ex.Message != null && ex.Message.Contains("AADB2C90118"))  
                {  
                    result = await OnForgotPassword();  
                    // We would ideally want to pass the result token here  
                    await Shell.Current.GoToAsync($"//{nameof(AboutPage)}");  
                }  
                else if (ex.ErrorCode != "authentication_canceled")  
                {  
                    // We should change this as we don't want to reference the view from our viewmodel  
                    await App.Current.MainPage.DisplayAlert("An error has occurred", "Exception message: " + ex.Message, "Dismiss");  
                }  
            }  
        }  
  
        private async Task<AuthenticationResult> OnForgotPassword()  
        {  
            try  
            {  
                return await App.AuthenticationClient  
                    .AcquireTokenInteractive(Constants.Scopes)  
                    .WithPrompt(Prompt.SelectAccount)  
                    .WithParentActivityOrWindow(App.UIParent)  
                    .WithB2CAuthority(Constants.AuthorityPasswordReset)  
                    .ExecuteAsync();  
            }  
            catch (MsalException)  
            {  
                // Do nothing - ErrorCode will be displayed in OnLoginButtonClicked  
                return null;  
            }  
        }  
  
        public async void OnAppearing()  
        {  
            IsBusy = true;  
            LoginButtonShown = !IsBusy;  
            try  
            {  
                IEnumerable<IAccount> accounts = await App.AuthenticationClient.GetAccountsAsync();  
                // Attempt to get the user token from the secure store  
                AuthenticationResult result = await App.AuthenticationClient.AcquireTokenSilent(Constants.Scopes, accounts.FirstOrDefault()).ExecuteAsync();  
                await Shell.Current.GoToAsync($"//{nameof(AboutPage)}");  
            }   
            catch  
            {  
                // User isn't logged in so lets show the login button  
                IsBusy = false;  
                LoginButtonShown = !IsBusy;  
            }  
        }  
    }  
}  

In the view itself I just bind to this viewmodel. However, after the users logins nothing happens. The forgot password feature also does not work

Xamarin
Xamarin
A Microsoft open-source app platform for building Android and iOS apps with .NET and C#.
5,301 questions
Microsoft Entra External ID
Microsoft Entra External ID
A modern identity solution for securing access to customer, citizen and partner-facing apps and services. It is the converged platform of Azure AD External Identities B2B and B2C. Replaces Azure Active Directory External Identities.
2,678 questions
{count} votes

1 answer

Sort by: Most helpful
  1. lee mcragger 1 Reputation point
    2021-03-21T01:05:50.46+00:00

    @Logan Ramos
    I'm one step ahead of you, so I can help with this... using the token with my webapi however is where I'm stumped (but maybe wont apply to your project so this may be the answer to your problems)...
    Anyways, if you haven't already solved this, here's what worked for me to use the token in other MVVM classes.

    You need to create a token class to model the token you are receiving, and then store it to you local token database upon receiving it back from your login. Then you can just call "Token token = App.TokenDatabase.GetToken();" to get it anywhere in your code.

    Things you'll need to add (which can be done differently, but this is what i have currently)...
    My token class looks like this:

    using System;

    namespace yourapp.Models
    {
    public class Token
    {
    public int Id { get; set; }
    public string access_token { get; set; }
    public string error_description { get; set; }
    public DateTime expire_date { get; set; }
    public int expires_in { get; set; }

        public Token() { }  
    }  
    

    }

    In your App.xaml.cs you'll need to add: (outside of App() method)

        static TokenDatabaseController tokenDatabase;  
    
        public static TokenDatabaseController TokenDatabase  
        {  
            get  
            {  
                if (tokenDatabase == null)  
                {  
                    tokenDatabase = new TokenDatabaseController();  
                }  
                return tokenDatabase;  
            }  
        }  
    

    Create a TokenDatabaseController class, and put this code there (how you interface your sqlite or other database connection is up to you)

    using SQLite;
    using yourapp.Models;
    using Xamarin.Forms;

    namespace yourapp.Data //in a data folder, this is optional - namespaces are what you want them to be
    {
    public class TokenDatabaseController
    {
    static object locker = new object();

        private readonly SQLiteConnection _connection;  
    
        public TokenDatabaseController()  
        {  
            _connection = DependencyService.Get<ISQLite>().GetConnection();  
            _connection.CreateTable<Token>();  
        }  
    
        public Token GetToken()  
        {  
            lock (locker)  
            {  
                if (_connection.Table<Token>().Count() == 0)  
                {  
                    return null;  
                }  
                else  
                {  
                    return _connection.Table<Token>().First();  
                }  
            }  
        }  
    
        public int SaveToken(Token Token)  
        {  
            lock (locker)  
            {  
                if (Token.Id != 0)  
                {  
                    _connection.Update(Token);  
                    return Token.Id;  
                }  
                else  
                {  
                    return _connection.Insert(Token);  
                }  
            }  
        }  
    
        public int DeleteToken(int id)  
        {  
            lock (locker)  
            {  
                return _connection.Delete<Token>(id);  
            }  
        }  
    }  
    

    }

    Finally on your login page code, heres the code from my button push:

        async void SignIn_Clicked(object sender, EventArgs e)  
        {  
            AuthenticationResult result;  
    
            try  
            {  
    
                result = await App.AuthenticationClient  
                    .AcquireTokenInteractive(Constants.Scopes)  
                    .WithPrompt(Prompt.ForceLogin)  
                    .WithParentActivityOrWindow(App.UIParent)  
                    .ExecuteAsync();  
                 
    
                Token token = new Token() { access_token = result.IdToken }; //constructs a token based off the one sent back in result  
                App.TokenDatabase.SaveToken(token); // saves that token to the database for calling later in the code - you can delete it on logout if you want to, but this will get you started at least.  
    
    
                await Shell.Current.GoToAsync($"//{nameof(nextPage)}");  
    
    
            }  
            catch (MsalClientException)  
            {  
    
            }  
        }  
    

    Hope this helps!
    Sorry the formatting is a bit off, this is my first time trying to help out someone.
    If you need how my sqlite interfacing let me know. I also havent figured out the forgot password issue you are having...
    -Lee

    0 comments No comments