Correct way to use Web Authenticator in .NET MAUI for Google login on Android

7.1k Views Asked by At

I have followed the official documentation of the web authenticator for .NET Maui here. It explains what to do efficiently, but they miss some crucial information regarding the callback URL. According to the article, the URL should be myapp://. I assume it could be anything like thisapp://.

The problem with myapp:// is that the WebAuthenticatorActivity does not like this. I get the following error when using myapp:// as the callback URL:

You must subclass the WebAuthenticatorCallbackActivity and create an IntentFilter for it which matches your callbackUrl.

At the bottom I will post the code.

I have also had a look at Dan Siegal's helper library and his video on the MAUI web authenticator, but he could not get the app working in the video. I have also viewed his sample on GitHub and he uses "myapp" as the callback URL. The problem with only "myapp" is that the Uri class does not like it when you specify the callback URL for the web authenticator. Here is the error I get:

Invalid URI: The format of the URI could not be determined.

So basically I can't use "myapp://" or "myapp" and decided to do a combination of the two. For the Callback URL, I used "myapp://" and for the WebAuthenticatorActivity I used "myapp". This actually works, but there is still one problem. On the Google console it expects a redirect URL. I have tried "myapp://" or "myapp" but it does not allow this:

Enter image description here

After searching the Internet, I found an article that mentions that I should use: https://mysite/signin-google. I have set up the API correctly according to the documentation and by using Dan Siegal's library.

So if I use "myapp://" as the callback URL for the web authenticator and "myapp" for the WebAuthenticatorActivity and https://mysite/signin-google on Google console, all is working until the point where Google tries to redirect back to my app from the browser. This is the error I get:

This mysite page can't by found. No webpage was found for the web address: https://mysite/signin-google

It comes down to:

What value should I use for:

  1. The callback URL of the web authenticator
  2. The WebAuthenticatorActivity
  3. The Google Console redirect URL

Here is my code:

File MauiProgram.cs

using CommunityToolkit.Maui;
using FertilizerFarm.Helpers;
using FertilizerFarm.Services;
using FertilizerFarm.View;
using Refit;
using Telerik.Maui.Controls.Compatibility;

namespace FertilizerFarm;
public static class Constants
{

    public const string BaseUrl = "https://mysite/mobileauth/";
    public const string CallbackScheme = "myapp://";
}
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
             .UseTelerik()
            .UseMauiApp<App>()
            .UseMauiCommunityToolkit()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                fonts.AddFont("telerikfontexamples.ttf", "TelerikFontExamples");
            });
        builder.Services.AddSingleton<IConnectivity>(Connectivity.Current)
        .AddSingleton(WebAuthenticator.Default)
        .AddSingleton(SecureStorage.Default)
        .AddRefitClient<IUserProfileService>()
        .AddSingleton<AuthService>()
        .AddSingleton<JWTService>()
        .AddTransient<LoginViewModel>()
        .AddTransient<Login>()

        return builder.Build();
    }
    public static IServiceCollection AddRefitClient<T>(this IServiceCollection services)
      where T : class
    {
        services.AddSingleton(sp =>
        {
            var settings = new RefitSettings
            {
                AuthorizationHeaderValueGetter = () => sp.GetRequiredService<ISecureStorage>().GetAsync("access_token")
            };
            return RestService.For<T>(Constants.BaseUrl, settings);
        });
        return services;
    }
}

File WebAuthenticatorCallbackActivity.cs

using Android.App;
using Android.Content;
using Android.Content.PM;

namespace FertilizerFarm;

[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true)]
[IntentFilter(new[] { Intent.ActionView },
                  Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
                  DataScheme = CALLBACK_SCHEME)]
public class WebAuthenticatorActivity : Microsoft.Maui.Authentication.WebAuthenticatorCallbackActivity
{
    const string CALLBACK_SCHEME = "myapp";
}

File LoginViewModel.cs

[RelayCommand]
    async Task SocialLogin()
    {
        try
        {
            var result = await _webAuthenticator.AuthenticateAsync(new WebAuthenticatorOptions
            {
                CallbackUrl = new Uri($"{Constants.CallbackScheme}"),
                Url = new Uri(Constants.BaseUrl + "google")
            });

            await _storage.SetAsync("access_token", result.AccessToken);

            using var response = await _userProfile.GetProfileClaims();
            if (response.IsSuccessStatusCode)
            {
                Claims.Clear();
                var claims = response.Content.Select(x => $"{x.Key}: {x.Value}");
                foreach (var claim in claims)
                    Claims.Add(claim);
            }
        }
        catch (Exception ex)
        {
        }
    }
2

There are 2 best solutions below

2
On

After conducting extensive research on this topic, I am pleased to share my findings and experiences with you regarding the implementation of Google OAuth 2.0 authentication in both desktop and mobile applications.

Desktop Apps:

  1. When it comes to implementing Google OAuth 2.0 authentication in desktop applications, I would highly recommend referring to the example provided in this link: https://dev.to/jaymalli_programmer/google-oauth-20-authorization-service-implementation-in-net-maui-okl. I have personally tested this implementation, and I can confirm its functionality.

    • Step 1: Create authorization service & add below code.

      public static class Auth
      {
          public static string ConstructGoogleSignInUrl()
          {
              // Specify the necessary parameters for the Google Sign-In URL
              const string clientId = "your_app_clientId";
              const string responseType = "code";
              const string accessType = "offline";
              const string redirect_uri = "your_app_redirect_uri";
      
              // Construct the Google Sign-In URL
              return "https://accounts.google.com/o/oauth2/v2/auth" +
                              $"?client_id={Uri.EscapeDataString(clientId)}" +
                              $"&redirect_uri={Uri.EscapeDataString(redirect_uri)}" +
                              $"&response_type={Uri.EscapeDataString(responseType)}" +
                              $"&scope={Uri.EscapeDataString(scope)}" +
                              $"&access_type={Uri.EscapeDataString(accessType)}" +
                              "&include_granted_scopes=true" +
                              "&prompt=consent";
          }
      }
      
    • Step 2: Load webview with the google login screen on button click.

      private void OnGetFilesClicked(object sender, EventArgs e)
      {
          WebView _signInWebView = new WebView
          {
              Source = Auth.ConstructGoogleSignInUrl(),
              VerticalOptions = LayoutOptions.Fill,
          };
      }
      
      ContentPage signInContentPage = new ContentPage
      {
          Content = grid,
      };
      
      try
      {
        Application.Current.MainPage.Navigation.PushModalAsync(signInContentPage);
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex.Message);
      }
      
  • Step 3: The Google Login screen is visible after clicked on button. Fill the user email id & password, Click "Allow" button on oauth consent scrren.

    Google login screen

    You will get the code in the redirect URL if user authorized successfully. Url

  • Step 4: Get access token from the code which was provided by URL after authorizing a user.

    public static (string, string) ExchangeCodeForAccessToken(string code)
    {
        // Configure the necessary parameters for the token exchange
        const string clientId = "your_app_client_id";
        const string clientSecret = "your_app_client_secret";
        const string redirectUri = "your_app_redirect_URI";
    
        // Create an instance of HttpClient
        using (HttpClient client = new HttpClient())
        {
            // Construct the token exchange URL
            const string tokenUrl = "https://oauth2.googleapis.com/token";
    
            // Create a FormUrlEncodedContent object with the required parameters
            FormUrlEncodedContent content = new FormUrlEncodedContent(new Dictionary<string, string>
            {
               { "code", code },
               { "client_id", clientId },
               { "client_secret", clientSecret },
               { "redirect_uri", redirectUri },
               { "grant_type", "authorization_code" }
            });
    
            // Send a POST request to the token endpoint to exchange the code for an access token
            HttpResponseMessage response = client.PostAsync(tokenUrl, content).Result;
    
            // Check if the request was successful
            if (response.IsSuccessStatusCode)
            {
                // Read the response content
                string responseContent = response.Content.ReadAsStringAsync().Result;
    
                // Parse the JSON response to extract the access token
                JObject json = JObject.Parse(responseContent);
                string accessToken = json.GetValue("access_token").ToString();
                string refreshToken = json.GetValue("refresh_token").ToString();
                return (accessToken, refreshToken);
            }
            else
            {
                // Exception:  "Token exchange request failed with status code: {response.StatusCode}"
            }
        }
        return (null, null);
    }
    

    Get the access_token & refresh_token which is used to create credentials for access services API such as Drive , Gmail API etc.

    _signInWebView.Navigating += (sender, e) =>
        {
            string code = Auth.OnWebViewNavigating(e, signInContentPage);
            if (e.Url.StartsWith("http://localhost") && code != null)
            {
                (string access_token, string refresh_token) = Auth.ExchangeCodeForAccessToken(code);
            }
    
        };
    
  • This code is not mine, was taken from Jay Malli and posted on 18 Oct 2023.

Mobile Apps:

  1. If you aspire to implement OAuth 2.0 Google Authentication https://developers.google.com/identity/protocols/oauth2/native-app in your mobile app, be prepared for a more complex process. In this case, you will need to create a backend server that can communicate with Google's APIs. You can refer to a comprehensive example of how this can be achieved by examining the code in the following GitHub repository: https://github.com/dotnet/maui/blob/main/src/Essentials/samples/Sample.Server.WebAuthenticator/Controllers/MobileAuthController.cs. This example demonstrates how to set up the necessary server-side components to handle authentication and redirection in a .NET MAUI mobile application, ensuring that all the required information is processed correctly.

    • Step 1: Create an ASP.NET Core Web Application

    • Step 2: Configure Google Authentication

  2. Update appsettings.json:

    Add your Google Client ID and Secret:

    {
        "GoogleClientId": "<Your Google Client ID>",
        "GoogleClientSecret": "<Your Google Client Secret>",
        // ... other settings
    }
    
  3. Modify Startup.cs for Google Authentication:

    In the ConfigureServices method, set up the authentication services:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddAuthentication(o =>
            {
                o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            })
            .AddCookie()
            .AddGoogle(g =>
            {
                g.ClientId = Configuration["GoogleClientId"];
                g.ClientSecret = Configuration["GoogleClientSecret"];
                g.SaveTokens = true;
            });
    }
    
  • Step 3: Configure Authentication and Authorization

    1. Setup Authentication Middleware:

      In the Configure method of Startup.cs, add the authentication middleware:

      public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
      {
          if (env.IsDevelopment())
          {
              app.UseDeveloperExceptionPage();
          }
      
          app.UseRouting();
      
          app.UseAuthentication();
          app.UseAuthorization();
      
          app.UseEndpoints(endpoints =>
          {
              endpoints.MapControllers();
          });
      }
      
    2. Create the AuthController: Implement the AuthController to handle the authentication flow:

      using Microsoft.AspNetCore.Authentication;
      using Microsoft.AspNetCore.Mvc;
      using System.Linq;
      using System.Threading.Tasks;
      
      namespace Sample.Server.WebAuthenticator
      {
          [Route("mobileauth")]
          [ApiController]
          public class AuthController : ControllerBase
          {
              const string callbackScheme = "myapp";
      
              [HttpGet("{scheme}")]
              public async Task Get([FromRoute] string scheme)
              {
                  var auth = await Request.HttpContext.AuthenticateAsync(scheme);
      
                  if (!auth.Succeeded
                      || auth?.Principal == null
                      || !auth.Principal.Identities.Any(id => id.IsAuthenticated)
                      || string.IsNullOrEmpty(auth.Properties.GetTokenValue("access_token")))
                  {
                      // Not authenticated, challenge
                      await Request.HttpContext.ChallengeAsync(scheme);
                  }
                  else
                  {
                      // Process the token and build the redirect URL
                      // [Insert token processing and redirect logic here]
                  }
              }
          }
      }
      
  • Step 4: Deployment for OAuth 2.0

  • Public Server Deployment: Google OAuth requires a public and secure URL for the redirect URI in a production environment. Deploy your application to a cloud service like Azure to get a public URL and HTTPS, which is necessary for OAuth to work properly in production. OAuth 2.0 with Google requires a publicly accessible redirect URI for security reasons. localhost refers to the local machine, and it's not accessible from the internet. Google needs a URL that it can reach to send the authentication response.

  • Step 6: To access the WebAuthenticator functionality the following platform-specific setup is required.

    Android requires an Intent Filter setup to handle your callback URI. This is accomplished by inheriting from the WebAuthenticatorCallbackActivity class:

    using Android.App;
    using Android.Content.PM;
    
    namespace YourNameSpace;
    
    [Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true)]
    [IntentFilter(new[] { Android.Content.Intent.ActionView },
                  Categories = new[] { Android.Content.Intent.CategoryDefault, Android.Content.Intent.CategoryBrowsable },
                  DataScheme = CALLBACK_SCHEME)]
    public class WebAuthenticationCallbackActivity : Microsoft.Maui.Authentication.WebAuthenticatorCallbackActivity
    {
        const string CALLBACK_SCHEME = "myapp";
    }
    

    If your project's Target Android version is set to Android 11 (R API 30) or higher, you must update your Android Manifest with queries that use Android's package visibility requirements. In the Platforms/Android/AndroidManifest.xml file, add the following queries/intent nodes in the manifest node:

    <queries>
        <intent>
            <action android:name="android.support.customtabs.action.CustomTabsService" />
        </intent>
    </queries>
    
    

Using WebAuthenticator

The API consists mainly of a single method, AuthenticateAsync, which takes two parameters:

  1. The URL used to start the web browser flow.

  2. The URI the flow is expected to ultimately call back to, that is registered to your app.

    The result is a `WebAuthenticatorResult, which includes any query parameters parsed from the callback URI:

    try
    {
        WebAuthenticatorResult authResult = await WebAuthenticator.Default.AuthenticateAsync(
            new Uri("<Deployed_webAPI_url>"),
            new Uri("myapp://"));
    
        string accessToken = authResult?.AccessToken;
    
        // Do something with the token
    }
    catch (TaskCanceledException e)
    {
        // Use stopped auth
    }
    

    The WebAuthenticator API takes care of launching the url in the browser and waiting until the callback is received: WebAuthenticatorFlow

  • This code is not mine and was taken from the official documentation of .NET MAUI.
4
On

myapp:// is the callback between the App and API

Google has no knowledge of the App and communicates only with the API

Google needs a redirect URL to find its way back to the to the API

This should be something like https://localhost:{PORT}/signin-google

These are the pages that helped me figure it out:

https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?view=aspnetcore-6.0

https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/communication/authentication?tabs=ios