Google Authentication in MAUI.NET

Google Authentication in MAUI.NET

Native Google authentification for Android and iOS 👌

Authentication with Google's OAuth 2.0 allows developers to authenticate applications on a secure web server (SSL certificate) without having to manage user data and secure it.

At the end of this post, you will be able to add Google Native Authentication as designed by Google to your Maui.NET application

Details about the usage of Google native Android and iOS libraries are also applicable to Xamarin.Forms.

Prerequisites

Classic auth

This style of authentication allows complete management of the users' data. In our case, it forces us to generate a token and manage it completely. When the token expires, in the most basic case, the user will be obliged to reconnect. In some more advanced cases, there is a refreshment token (remember token) that allows the refreshing of a token (another token to manage completely).

Security and consistency of user identification data are our responsibility.

Classic authentication

OAuth2.0

This method allows an identity provider to send an access token containing the rights of a user to a remote system. As soon as the token expires, the user can request another one from the provider without having to authenticate again.

Note Security of the identity data is managed and guaranteed by the identity provider.

Implementation

For the implementation, we will create a model class UserDTO and a service that will have three asynchronous tasks that will respectively serve to :

  1. Authenticate us with Google ;

  2. Recover logged-in user ;

  3. Disconnect the connected user ;

public interface IGoogleAuthService
{
    Task<UserDTO> AuthenticateAsync();
    Task LogoutAsync();
    Task<UserDTO> GetCurrentUserAsync();
}

Since Google authentication must be implemented in each platform (iOS, Android), see this link to understand the multiplatform implementation in MAUI.NET.

public class UserDTO
{
    public string Id { get; set; }
    public string FullName { get; set; }    
    public string Email { get; set; }
    public string UserName { get; set; }
}

We implement the first partial class.

 public partial class GoogleAuthService : IGoogleAuthService
 {

 }

For Android

We must implement the Android part of the GoogleAuthService. Just create a partial class GoogleAuthService in the Android platform of MAUI.NET and implement IGoogleAuthService.

In an attempt to launch the Google authentication popup, we will need the following objects: an instance of the current Android activity, an instance of Android.Gms.Auth.Api.SignIn.GoogleSignInOptions which is used to configure the keys and scopes we want to receive from Google, an instance of Android.Gms.Auth.Api.SignIn.GoogleSignInClient which is the client that will start the Google authentication activity.

The configurations are :

// Android platform
public GoogleAuthService()
{
    // Get current activity
    _activity = Platform.CurrentActivity;

    // Config Auth Option
    _gso = new        GoogleSignInOptions.Builder(GoogleSignInOptions.DefaultSignIn)
                    .RequestIdToken("XXXX")
                    .RequestEmail()
                    .RequestId()
                    .RequestProfile()
                    .Build();

            // Get client
     _googleSignInClient = GoogleSignIn.GetClient(_activity, _gso); 
}

The token ID is related to the signature you package. See this link to avoid the error code 10.

To retrieve the Token ID follow this link to avoid the error code 10.

Nous pouvons implémenter la méthode AuthenticateAsync():

public Task<UserDTO> AuthenticateAsync()
{ 
    _activity.StartActivityForResult(_googleSignInClient.SignInIntent, 9001);

    return null;
}

At this point we will have the authentication popup launched, we need to retrieve the data from Google. To do so, we just have to :

  1. Create a static event in the MainActivity class ;

  2. Rise static event in the MainActivity

The implementation is as follows :

public class MainActivity : MauiAppCompatActivity
{
    // Event 
    public static event EventHandler<(bool Success, GoogleSignInAccount Account)> ResultGoogleAuth;

    // Result of auth activity
    protected override async void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
    // Auth request code
    if (requestCode == 9001)
    {
        try
        {
            // Read intent result data
            var currentAccount = await GoogleSignIn.GetSignedInAccountFromIntentAsync(data);
            // Rise static event for send data
            ResultGoogleAuth?.Invoke(this, (currentAccount.Email != null, currentAccount));
        }
        catch (Exception ex)
        {
            ResultGoogleAuth?.Invoke(this, (false, null));
        }
    }
}

Let's modify the GoogleAuthService partial class as follows to take into account the static event.

// Lines added
private TaskCompletionSource<UserDTO> _taskCompletionSource;
private Task<UserDTO> GoogleAuthentication
{
    get => _taskCompletionSource.Task;
}

public GoogleAuthService()
{
    // Line added
    MainActivity.ResultGoogleAuth += MainActivity_ResultGoogleAuth;
} 

public Task<UserDTO> AuthenticateAsync()
{
    // Line added
    _taskCompletionSource = new TaskCompletionSource<UserDTO>();

    // ... OLD CODE

    // Line added
    return GoogleAuthentication;
}

We can implement the data retrieval via the static event as follows.

private void MainActivity_ResultGoogleAuth(object sender, (bool Success, GoogleSignInAccount Account) e)
{
    if (e.Success)
        // Set result of Task
        _taskCompletionSource.SetResult(new UserDTO
        {
            Email = e.Account.Email,
            FullName = e.Account.DisplayName,
            Id = e.Account.Id,
            UserName = e.Account.GivenName
        });
    else
        // Set Exception
        _taskCompletionSource.SetException(new Exception("Error"));
}

We can proceed to the implementation of the GetCurrentUserAsync() method :

public async Task<UserDTO> GetCurrentUserAsync()
{
    try
    {
        var user = await _googleSignInClient.SilentSignInAsync();
        return new UserDTO
        {
            Email = user.Email,
            FullName = $"{user.DisplayName}",
            Id = user.Id,
            UserName = user.GivenName
        };

    }
    catch (Exception)
    {
        throw new Exception("Error");
    }
}

We can finally proceed to the method LogoutAsync()

public Task LogoutAsync() => _googleSignInClient.SignOutAsync();

For iOS

We must implement the iOS part of the GoogleAuthService. Just create a partial class GoogleAuthService in the iOS platform of MAUI.NET and implement IGoogleAuthService.

Note that even if a library exists, the Google authentication for iOS goes through the browser.

The required configurations are as follows :

public GoogleAuthService()
{
    Google.SignIn.SignIn.SharedInstance.Scopes = new string[] { "https://www.googleapis.com/auth/userinfo.email" };
    Google.SignIn.SignIn.SharedInstance.ClientId = GoogleClientID;
}

The iOS configurations go through static variables. Then we can implement the method(Asynchronous task) Task<UserDTO> AuthenticateAsync(). In the latter, we will put an event listener to receive the authenticated user and we will call the method that will open a browser page. Since we are going to remove a view on top of another one, to keep the link, we will have to specify the PresentingViewController of the Google browser.

Our Task will be :

public async Task<UserDTO> AuthenticateAsync()
{
    //Event Handler that will get result of authentication
    Google.SignIn.SignIn.SharedInstance.SignedIn += SharedInstance_SignedIn;
    //Method that will prepare PresentigViewController of Authentication Browser
    PreparePresentedViewController();
    //Methode that launch Browser with Google auth        
    Google.SignIn.SignIn.SharedInstance.SignInUser();
    // we'll modify this line later
    return null;
}

The next method to prepare is PreparePresentedViewController()

private void PreparePresentedViewController()
{
    var window = UIApplication.SharedApplication.KeyWindow;

    var viewController = window.RootViewController;

    while(viewController.PresentingViewController != null)
        viewController = viewController.PresentingViewController; ;

    SignIn.SharedInstance.PresentingViewController = viewController;
}

We can retrieve the identity information provided by Google. For this, we will implement the SharedInstance_SignedIn listener as follows :

 private void SharedInstance_SignedIn(object sender, Google.SignIn.SignInDelegateEventArgs arg)
{
    //Check if we don't have any error after login
    if (arg.Error != null)
        throw new Exception($"Error - {arg.Error.LocalizedDescription} - {Convert.ToInt32(arg.Error.Code)}");
    //Initilaiza token value
    var token = "";
    //Get token from authentication
    SignIn.SharedInstance.CurrentUser.Authentication.GetTokens((Authentication auth, NSError error) =>
    {
        if (error == null)
            token = auth.IdToken;
        else
        {
            throw new Exception($"Cannot get token id ERR -> {error.Code}  - {error.LocalizedDescription}");
        }
    });

    // Final User connected
    var user = new UserDTO
    {
        Id = token,
        Email = arg.User.Profile.Email,
        FullName = arg.User.Profile.Name,
        UserName = arg.User.Profile.Email,
    };           
}

Since our The AuthenticateAsync method is an asynchronous task that must return the authenticated user, we must modify the class as follows:

//Task that we'll use for return data
private TaskCompletionSource<UserDTO> _taskCompletionSource;

hen we modify the AuthenticateAsync task as follows :

public async Task<UserDTO> AuthenticateAsync()
{
    _taskCompletionSource = new TaskCompletionSource<UserDTO>();
        // Old code between
        //....
        // Old code between 
    return await _taskCompletionSource.Task;
}

And finally, we modify our event listener as follows :

private void SharedInstance_SignedIn(object sender, Google.SignIn.SignInDelegateEventArgs arg)
{
    if (arg.Error != null)
    {
        _taskCompletionSource.TrySetException(new Exception($"Error - {arg.Error.LocalizedDescription} - {Convert.ToInt32(arg.Error.Code)}"));
        return;
    }

    var token = "";
                SignIn.SharedInstance.CurrentUser.Authentication.GetTokens((Authentication auth, NSError error) =>
    {
        if (error == null)
            token = auth.IdToken;
        else
        {
            _taskCompletionSource.TrySetException(new Exception($"Cannot get token id ERR -> {error.Code}  - {error.LocalizedDescription}"));
            return;
        }
    });

    _taskCompletionSource.TrySetResult(new UserDTO
    {
        Id = token,
        Email = arg.User.Profile.Email,
        FullName = arg.User.Profile.Name,
        UserName = arg.User.Profile.Email,
    });
}

We can tackle the asynchronous task GetCurrentUserAsync.

public async Task<UserDTO> GetCurrentUserAsync()
{
    //Check if we have previous SignIn
    if(SignIn.SharedInstance.HasPreviousSignIn)
        SignIn.SharedInstance.RestorePreviousSignIn();

    // Get current user
    var currentUser = SignIn.SharedInstance.CurrentUser;

    //Check if user exist
    if (currentUser == null)
        throw new Exception("User not found");

    //Return current user
    return new UserDTO
    {
        Email = currentUser.Profile.Email,
        FullName = currentUser.Profile.Name,
        Id = currentUser.UserId,
        UserName = currentUser.Profile.Name
    };
}

We can implement LogoutAsync :

public Task LogoutAsync() => Task.FromResult(() => SignIn.SharedInstance.SignOutUser());

Let's finish the iOS setup :

  1. Override the OpenUrl method of AppDelegate class is as follows:
public override bool OpenUrl(UIApplication application, NSUrl url, NSDictionary options)
{
    SignIn.SharedInstance.HandleUrl(url);
    return true;
}
  1. Configure Info.plist file to add the client ID provided by Google, more details in this link

  2. Add a plist file in the iOS platform named Entitlements.plist more details in this lien.

Final Result

Resources