Login en google desde Xamarin Forms

Tras muchos intentos he conseguido implementar la autenticación de google en una app de Xamarin.Forms. Es mucha la documentación que hay para ofrecer esta funcionalidad en una aplicación web, en una aplicación nativa para Android e iOS, pero poco es lo que he encontrado asociado a Xamarin.Forms. Por tanto, he considerado interesante escribir este post.

He divido la explicación en varios puntos

  • Configuración: tareas a realizar antes de ponernos a escribir líneas de código
  • Realizando autenticación: Implementación del grueso de las funcionalidades en el proyecto común de Xamarin.Forms
  • Implementación en Android: Tareas a realizar en el proyecto de dicha plataforma
  • Implementación en iOS: Tareas a realizar en el proyecto de dicha plataforma

 

Configuración

La primera configuración que hay que realizar es la de la consola de desarrollador de google

  • Ir a la consola del api e iniciar sesión
  • En el panel de control, añadir un nuevo proyecto (o seleccionar uno existente)  
  • Desde el apartado de credenciales en la pestaña pantalla de consentimiento de OAuth especificar el nombre, icono e email de asistencia.
  • En la pestaña de credenciales, crear unos credenciales seleccionando Id de cliente de OAUTH. Será necesario crear unos para Android y otros para iOS 

Para Android hay que especificar un nombre (se puede indicar el que queramos), el nombre del paquete (se puede encontrar en el AndroidManifest.xml) y la huella digital del certificado SHA-1 que se usará para firmar el apk. Para obtenerlo he tenido que lanzar el siguiente comando en mi Mac

keytool -list -v -keystore {YOUR_KEY_STORE_PATH} –alias androiddebugkey -storepass android -keypass android Me ha servidor de ayuda esta consulta en StackOverflow

Es necesario tener en cuenta, que se necesitaran tantos Ids de cliente OAuth como firmas distintas utilicemos en nuestra aplicación Android. Es posible que se use una por entorno (desarrollo, pre-producción, producción)

Para iOS hay que especificar el Id del conjunto, que no es otra cosa que el bundleId, el cual se puede encontrar en el info.plist de la aplicación iOS

  • Una vez creados los credenciales, se genera un Id de cliente por cada uno 

 

Realizando autenticación

El primer paso a realizar en el proyecto común, es añadir los paquetes nuget que vamos a utilizar. Estos son Xamarin.Auth, para facilitarnos la tarea de autenticación. Y Newtonsoft.Json, para realizar algunas llamadas a un API REST

 

Una vez instalados los paquetes nuget que se van a necesitar en la solución, ya se puede iniciar el proceso de autenticación en google. Dado que google utiliza OAuth2, se utilizará la clase OAuth2Authenticator del paquete nuget Xamarin.Auth. Para crear un objeto de este tipo hay que especificar los siguientes parámetros:

  • ClientId: Es el identificador creado en la consola de google. Recuerda que había uno por plataforma e incluso, uno por entorno.
  • ClientSecret: No es necesario para la autenticación en google
  • Scope: Se utiliza para indicar el alcance que tendrá la autenticación. Esto es, a qué información se va a tener acceso. Podemos encontrar más información sobre los scopes en la documentación de Google.
  • AuthorizeUrl: URL de google requerida en la autenticación. Existe una web con la documentación de google asociada a este parámetro.
  • RedirectUrl: URL a la que te va a  redirigir google una vez hecha la autenticación de forma correcta. En este caso, al estar realizando la autenticación en una app, no es necesario que la URL exista, simplemente se utilizará para que Xamarin.OAuth capture la redirección a esa web y devuelva el control a nuestra app.
  • AccessTokenUrl: URL de google requerida en la autenticación. Existe una web con la documentación de google asociada a este parámetro.
  • GetUserNameAsyncFunc: No se dan detalles, porque en esta autenticación se será necesario utilizarlo, por lo que se informará a null.
  • IsUsingNativeUI: Es necesario marcarlo a true para cumplir con la nueva política de Google.

Una vez que sabemos de qué manera hay que realizar la autenticación, solo falta escribir un poco de código. Como en cada plataforma (Android e iOS, en el ejemplo) se tienen parámetros diferentes, lo que he hecho es, en la librería común crear una interfaz IGoogleAuthService que será implementada en cada una de las plataformas. Está interfaz solo tiene un método Authenticate al que se le pasa la interfaz IGoogleAuthenticationDelegate. Esta interfaz, será la view/viewModel desde el que se lanza la autenticación, que implementará 3 métodos que se lanzarán cuando dicha autenticación sea completada, fallida o cancelada.

public interface IGoogleAuthService
{
    void Autheticate(IGoogleAuthenticationDelegate googleAuthenticationDelegate);
}

public interface IGoogleAuthenticationDelegate
{
    void OnAuthenticationCompleted(GoogleOAuthToken token);
    void OnAuthenticationFailed(string message, Exception exception);
    void OnAuthenticationCanceled();
}

 

Además de estas interfaces, en la librería común he creado la clase GoogleAuthenticator, que se encarga de crear un objeto OAuth2Authenticator, que se utilizará en las implementaciones de  IGoogleAuthService en cada plataforma, y de notificar a un IGoogleAuthenticationDelegate el resultado de ésta. Asímismo, lanza el evento AuthenticationDone cuando el usuario termina su autenticación (tanto si es correcta, incorrecta o fallida)

public class GoogleAuthenticator
{
    private const string AuthorizeUrl = "https://accounts.google.com/o/oauth2/v2/auth";
    private const string AccessTokenUrl = "https://www.googleapis.com/oauth2/v4/token";
    private const bool IsUsingNativeUI = true;

    private OAuth2Authenticator _auth;
    private IGoogleAuthenticationDelegate _authenticationDelegate;

    public delegate void AuthenticationDoneHandler();
    public event AuthenticationDoneHandler AuthenticationDone;

    public GoogleAuthenticator(string clientId, string scope, string redirectUrl, IGoogleAuthenticationDelegate authenticationDelegate)
    {
        _authenticationDelegate = authenticationDelegate;

        _auth = new OAuth2Authenticator(clientId, string.Empty, scope,
                                        new Uri(AuthorizeUrl),
                                        new Uri(redirectUrl),
                                        new Uri(AccessTokenUrl),
                                        null, IsUsingNativeUI);

        _auth.Completed += OnAuthenticationCompleted;
        _auth.Error += OnAuthenticationFailed;
    }

    public OAuth2Authenticator GetAuthenticator()
    {
        return _auth;
    }

    public void OnPageLoading(Uri uri)
    {
        _auth.OnPageLoading(uri);
    }

    private void OnAuthenticationCompleted(object sender, AuthenticatorCompletedEventArgs e)
    {
        if (e.IsAuthenticated)
        {
            var token = new GoogleOAuthToken
            {
                TokenType = e.Account.Properties["token_type"],
                AccessToken = e.Account.Properties["access_token"]
            };
            _authenticationDelegate.OnAuthenticationCompleted(token);
        }
        else
        {
            _authenticationDelegate.OnAuthenticationCanceled();
        }

        if (AuthenticationDone != null)
            AuthenticationDone();
    }

    private void OnAuthenticationFailed(object sender, AuthenticatorErrorEventArgs e)
    {
        _authenticationDelegate.OnAuthenticationFailed(e.Message, e.Exception);
        if (AuthenticationDone != null)
            AuthenticationDone();
    }
}

public static class GoogleAuthenticatorHelper
{
    public static GoogleAuthenticator Auth;
}

 

Una vez realiza la autenticación, se podría obtener datos del usuario, siempre que estén dentro del scope con el que se realiza la autenticación. Para ello, en este proyecto, he creado una clase de utilidad (GoogleAccountInfoService), que devuelve el email del usuario autenticado (en este ejemplo solo se obtiene el email, pero hay muchos más datos que se podrían obtener, en función del scope utilizado)

public class GoogleAccountInfoService
{
    public async Task GetEmailAsync(string tokenType, string accessToken)
    {
        var httpClient = new HttpClient();
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(tokenType, accessToken);
        var json = await httpClient.GetStringAsync("https://www.googleapis.com/userinfo/email?alt=json");
        var email = JsonConvert.DeserializeObject(json);
        return email.Data.Email;
    }
}

public class GoogleEmail
{
    public GoogleEmailData Data { get; set; }
}

public class GoogleEmailData
{
    public string Email { get; set; }
}

 

Implementación en Android

En el proyecto Android he tenido que crear dos clases. La primera implementa la interfaz IGoogleAuthService. Para completar esta implementación, basta con utilizar la clase GoogleAuthenticator del proyecto común, pasándole el clientId y redirectUrl propios de Andorid.

[assembly: Dependency(typeof(GoogleAuthService))]
namespace OAuth.Droid.Services
{
    public class GoogleAuthService : IGoogleAuthService
    {
        internal static MainActivity MainActivity { get; set; }

        public GoogleAuthService()
        {

        }

        public void Autheticate(IGoogleAuthenticationDelegate googleAuthenticationDelegate)
        {
            GoogleAuthenticatorHelper.Auth = new GoogleAuthenticator(
               YOUR_CLIENT_ID,
               YOUR_SCOPE,
               YOUR_REDIRECT_URL,
                googleAuthenticationDelegate);

            // Display the activity handling the authentication
            var authenticator = GoogleAuthenticatorHelper.Auth.GetAuthenticator();
            var intent = authenticator.GetUI(MainActivity);
            MainActivity.StartActivity(intent);
        }
    }
}

 

Como se aprecia en el código anterior, lo que se hace es abrir un browser en el que se realiza la autenticación, para controlar el resultado de ésta, es necesario crear un Interceptor, tal y como se muestra a continuación. Este paso es necesario para conseguir ser notificados de si la autenticación se ha realizado correcta o incorrectamente, o si ha sido cancelada.

[Activity(Label = "GoogleAuthInterceptor")]
[
    IntentFilter
    (
        actions: new[] { Intent.ActionView },
        Categories = new[]
        {
                Intent.CategoryDefault,
                Intent.CategoryBrowsable
        },
        DataSchemes = new[]
        {
            // First part of the redirect url (Package name)
            "YOUR_REDIRECT_URL_FIRST_PART"
        },
        DataPaths = new[]
        {
            // Second part of the redirect url (Path)
            "YOUR_REDIRECT_URL_SECOND_PART"
        }
    )
]
public class GoogleAuthInterceptor : Activity
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        Android.Net.Uri uri_android = Intent.Data;

        // Convert iOS NSUrl to C#/netxf/BCL System.Uri - common API
        Uri uri_netfx = new Uri(uri_android.ToString());

        // Send the URI to the Authenticator for continuation
        GoogleAuthenticatorHelper.Auth?.OnPageLoading(uri_netfx);

        Finish();
    }
}

 

Implementación en iOS

Las modificaciones en el proyecto iOS son incluso más sencillas que en Android.

Lo primero que hay que hacer es modificar info.plist para interceptar las llamadas desde safari. Al igual que en android, la autenticación en google se hace desde el browser de sistema operativo, Safari en este caso. Para interceptar safari cuando la autenticación se ha realizado, y devolver el control a nuestra app, es necesario añadir la siguiente información en el info.plist

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>YOUR_PROJECT_NAME</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>BUNDLE_ID</string>
        </array>
        <key>CFBundleURLTypes</key>
        <string>Viewer</string>
    </dict>
</array>

 

Una vez modificado este archivo, solo falta implementar el método OpenUrl en el AppDelegate. Este método es el que será llamada cuando safari navegue a la página a la que se realiza la redirección una vez que el usuario ha sido autenticado.

public override bool OpenUrl(
    UIApplication application,
    NSUrl url,
    string sourceApplication,
    NSObject annotation)
{
    // Convert iOS NSUrl to C#/netxf/BCL System.Uri
    var uri_netfx = new Uri(url.AbsoluteString);

    GoogleAuthenticatorHelper.Auth?.OnPageLoading(uri_netfx);

    return true;
}

 

Por último, al igual que en android, es necesaria la implementación de la interfaz IGoogleAuthService. Esta implementación es muy similar a la de android, ya que para realizar la utenticación también utiliza la clase GoogleAuthenticator de la librería común. Además, nos suscribimos al evento AuthenticationDone para, una vez que la autenticación se ha realizado, cerrar el browser.

[assembly: Dependency(typeof(GoogleAuthService))]
namespace OAuth.iOS.Services
{
    public class GoogleAuthService : IGoogleAuthService
    {
        public GoogleAuthService()
        {

        }

        public void Autheticate(IGoogleAuthenticationDelegate googleAuthenticationDelegate)
        {
            GoogleAuthenticatorHelper.Auth = new GoogleAuthenticator(
               YOUR_CLIENT_ID
               YOUR_SCOPE,
               YOUR_REDIRECT_URL,
               googleAuthenticationDelegate);

            GoogleAuthenticatorHelper.Auth.AuthenticationDone += Auth_AuthenticationDone;

            // Display the activity handling the authentication
            var authenticator = GoogleAuthenticatorHelper.Auth.GetAuthenticator();

            var viewController = authenticator.GetUI();
            var currentViewController = GetCurrentViewController();
            currentViewController.PresentViewController(viewController, true, null);
        }

        private void Auth_AuthenticationDone()
        {
            CloseBrowser();
        }

        public void CloseBrowser()
        {
            var currentViewController = GetCurrentViewController();
            currentViewController.DismissViewController(true, null);
        }

        private UIViewController GetCurrentViewController()
        {
            var window = UIApplication.SharedApplication.KeyWindow;
            var vc = window.RootViewController;
            while (vc.PresentedViewController != null)
            {
                vc = vc.PresentedViewController;
            }

            return vc;
        }
    }
}

 

 

Con todo lo anterior, tendríamos un proyecto Xamarin.Forms capaz de realizar la autenticación en google.

Como es habitual, he creado un proyecto en GitHub para ofrecer más detalles de la implementación que he realizado. Espero que os sea de ayuda

https://github.com/jorgediegocrespo/XamarinGoogleAuth

 

Bibliografía

http://timothelariviere.com/2017/09/01/authenticate-users-through-google-with-xamarin-auth/

https://docs.microsoft.com/es-es/xamarin/xamarin-forms/data-cloud/authentication/oauth

https://developers.google.com/identity/protocols/OAuth2InstalledApp

Google native login with Xamarin.Forms

13 comentarios sobre “Login en google desde Xamarin Forms

  1. Primero que nada quiero felicitarte por ésta maravillosa guía.

    Descargué el fuente y cuando inicio sesión, solo se queda la pantalla de google y no me regresa la aplicación. No le hice ninguna modificación al fuente. ¿qué puede estar pasando?

    Saludos.

    Me gusta

    1. Hola José Luis. Muchas gracias por tu comentario.

      En el ejemplo de github, se hace referencia a mi configuración de la consola de desarrollador de google, la cual ya no está operativa. Tendrías que crear tu propia configuración y ponerla en la app de Xamarin Forms.

      Si revisas el post, hay sitios en los que indico que tienes que especificar datos como YOUR_CLIENT_ID, YOUR_SCOPE, YOUR_REDIRECT_UR…

      Espero haberte ayudado.
      Un saludo!!

      Me gusta

      1. Hola de nuevo Jorge, ya hice lo que me comentaste, pero a lo que yo me refiero es que cuando ejecuto la aplicación inicia sesión, pero no me redirecciona a la APP, si puedo hacer uso de la api pero se queda en el browser.

        Espero haberme explicado, muchas gracias 🙂

        Me gusta

      2. Hola de nuevo,

        Parece que tienes algo mal en tu configuración de desarrollador de google.
        En este link http://timothelariviere.com/2017/09/01/authenticate-users-through-google-with-xamarin-auth/ @tim_lariviere utiliza la siguiente configuración y la mantiene activa:
        YOUR_CLIENT_ID = «723962257721-ql0tki3si3s22l1lsovimivkmnrfm6rr.apps.googleusercontent.com»
        YOUR_CLIENT_ID = «email»
        YOUR_CLIENT_ID = «com.woodenmoose.xamarin.googleauth:/oauth2redirect»

        Acabo de probar tanto en Android como en iOS y funciona correctamente, devolviendo el control a la app tras hacer el login.
        Recuerda que tienes que modificar los archivos GoogleAuthService.cs y GoogleAuthInterceptor.cs en Android y GoogleAuthService.cs en iOS.

        En la web https://github.com/xamarin/Xamarin.Auth tienes información sobre el paquete Xamarin.Auth. Quizá te sirva de ayuda.

        Un saludo!!

        Me gusta

  2. Hola, gracias por la guía. Tengo un problema, cuando me meto con mi cuenta y tengo que volver a la aplicación, me salta un error 404 de HTTP , antes me decía que era por esta linea en GoogleAccountInfoService
    var json = await httpClient.GetStringAsync(«https://www.googleapis.com/userinfo/email?alt=json»);
    y pensé que era por el scope, lo cambie a https://www.googleapis.com/auth/userinfo.email pero me sigue dando el mismo fallo solo que ahora sin informacion.

    Me gusta

    1. Hola Kevin,
      Te pido disculpas por haber tardado tanto en contestarte. Ultimamente he estado un poco desconectado, lo siento.
      En relación a tu pregunta, te diría que la forma de autenticarse con la cuenta de google que expliqué en este post se ha quedado obsoleta. Te recomiendo que utilices la autenticación que ahora se incluye en Xamarin.Essentials.
      Un saludo!!

      Me gusta

  3. Hola Jorge, hemos estado trabajando en implementar la nueva autentificación con Xamarin Essentials usando WebAuthenticator, pero hemos tenido un problema con la implementación del mismo.

    estamos usando
    WebAuthenticatorResult authResult =
    await WebAuthenticator.AuthenticateAsync(new Uri(options.StartUrl), new Uri(Constants.RedirectUri));

    El problema es que cuando abre la ventana para ingresar los datos tanto para la session de Google como de Facebook, el usuario captura su usuario y password, y cuando termina la autentificación y regresa los datos en el redirect URI, la ventana interpreta el url (https://zz.com/authcallback) como un URL real y lo despliega en la ventana, generando un 404 Not found y el código se queda sin ejecutar ya que nunca sale del AuthenticateAsync

    tendras alguna idea o ejemplo que puedas compartir??

    rod

    Me gusta

    1. Hola Rod,
      Dado que hay más gente que está teniendo problemas similares, he estado revisando el código que escribí para este post y parece que se ha quedad un tanto obsoleto. He indagado un poco y probado algunos repos de más de la comunidad y he encontrado este post https://devblogs.microsoft.com/xamarin/authentication-xamarin-essentials-aspnet/ que plantea una solución que funciona y muy actual, basada en Xamarin.Essentials, lo que hace que sea muy fiable. Espero que te sirva de ayuda.
      Un saludo!!

      Me gusta

    2. Por otro lado, es posible que la URL que indicas tenga que existir, aunque realmente, a efectos prácticos en tu app, no se use para nada. Si tenéis alguna web corporativa, te recomiendo que sea esa la que indiques.
      Un saludo!!

      Me gusta

  4. hola Diego, gracias por el tutorial, lo realice hace como 2 meses y estuvo todo bien hasta que llego mayo del 2020, apple levanto la politica del UIWebView, desafortunadamente Xamarin.Auth usa ese componente, por lo cual no es posible subir al app store el aplicativo, y de momento los desarrolladores de la libreria no han podido dar solución, ellos mismos dicen que toca usar Xamarin.Essentilas mientras dan solucion al problema.

    Saludos desde colombia.

    Me gusta

Deja un comentario

Este sitio utiliza Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.