Azure Active Directory: Implementando SSO en Android

,

Azure Active Directory (también AAD o Azure AD) es una plataforma de Microsoft que permite administrar identidades y, entre otras cosas, proporciona a las aplicaciones mecanismos de autenticación y autorización.

Aunque el abanico de posibilidades es amplio, nos centraremos en el uso de Azure Active Directory para su configuración en aplicaciones móviles Android. En este sentido, Azure AD ofrece una API para agregar inicio de sesión único (SSO), permitiendo trabajar con cuentas y credenciales existentes del usuario (como iniciar sesión con su cuenta de Microsoft).

En este post veremos cómo realizar la integración de la librería MSAL (Microsoft Authentication Library) en un proyecto Android, configurarla e incluir las funcionalidades de inicio y cierre de sesión usando la API de AAD.

Qué veremos en esta guía:

  • Integración e implementación de Azure en Android
  • Configuración del panel Adobe Active Directory
  • Uso de cuentas únicas
  • Uso de cuentas múltiples

Integración e implementación de Azure en Android

Integración

En primer lugar tendremos que incluir el repositorio Maven en el fichero build.gradle global del proyecto.

allprojects {
    repositories {
        jcenter()
        google()
        ...
        maven { url "https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1" }
    }
}

A continuación, nos vamos al fichero build.gradle a nivel de app e incluimos la librería.

dependencies {
    ...
    implementation 'com.microsoft.identity.client:msal:2.0.4'
}

Implementación

Antes de empezar con la propia integración de la API, conviene saber y ver cuáles son los requisitos previos para poder hacerla funcionar:

  1. Creación de una cuenta Azure para acceder al panel de administración de desarrolladores
  2. Configurar un proyecto en la cuenta Azure Active Directory
  3. Configurar las aplicaciones Android que integren la librería MSAL

Creación de cuenta Azure y proyecto en Azure Active Director

Creamos una cuenta en el portal de Azure (gratuita o mediante un plan de pago), el cual nos dará acceso al panel completo de herramientas de Azure Active Directory. Una vez logueados con la cuenta, accedemos a la sección correspondiente a Azure Active Directory.

Dentro de la sección AAD podremos configurar una serie de funcionalidades, así como ver la actividad generada en nuestros proyectos creados. Vamos a crear un proyecto en AAD donde podremos configurar nuestra aplicación y configurar aspectos relacionados con la autenticación del usuario.

Desde el menú lateral accedemos a la sección “Registros de aplicaciones” y pulsamos sobre “Nuevo registro”. Nos pedirá información relacionada sobre la nueva aplicación y el método de autenticación.

Configuramos el nombre de la aplicación y quienes estarán autorizados a iniciar sesión:

  • Cuentas en cualquier directorio organizativo. Todos los usuarios con una cuenta profesional o educativa de Microsoft pueden usar la aplicación o API, incluidos los centros educativos y las empresas que utilicen Office 365.
  • Cuentas en cualquier directorio organizativo y cuentas de Microsoft personales. La aplicación o API pueden usarla todos los usuarios con una cuenta profesional o educativa, o bien con una cuenta Microsoft personal. También se incluyen los centros educativos y empresas que usan Office 365, así como las cuentas personales que se usen para iniciar sesión en servicios como Xbox y Skype.
  • Solo cuentas personales de Microsoft. Cuentas personales que se usan para iniciar sesión en servicios como Xbox y Skype.

Una vez registrado el proyecto de aplicación, accederemos a un nuevo panel con la configuración específica al proyecto. Ahí podremos realizar personalizaciones al inicio de sesión (logo personalizado, URL del sitio web, etc). Al margen de las personalizaciones, vamos a configurar la aplicación Android para soportar el nuevo proyecto creado en Azure Active Directory.

Accedemos a la sección “Autenticación” del menú lateral y agregamos una plataforma, seleccionaremos “Android”. Nos pedirá la configuración específica al proyecto Android:

  • Nombre del paquete: corresponde al packageId. Ejemplo: es.sdos.aad.sample
  • Hash firma: corresponde al hash SHA1 de la firma que usamos para generar el APK en formato Base64.

Tendremos que registrar tantas aplicaciones Android como variantes existentes en el proyecto. Por ejemplo, si tenemos variantes de preproducción o debug deberemos registrarla de la misma manera que hemos hecho anteriormente.

Obtener hash de la firma

Dependiendo de la configuración Gradle de nuestro proyecto podemos obtener el hash de la firma .jks o .keystore de una manera u otra.

  • Si tenemos configurado signingConfigs en el fichero Gradle. Podemos ejecutar directamente la tarea signingReport desde Android Studio (pestaña “Gradle” -> Tasks -> android -> signingReport). También podemos ejecutarlo desde el terminal:
gradlew signingReport

El comando nos generará las claves SHA1 correspondientes a las distintas firmas configuradas.
En caso de no tener configurado signingConfigs en el proyecto, tendremos que obtener el SHA1 desde el terminal usando la herramienta keytool:

keytool -list -v -keystore "" -alias  -storepass  -keypass 

Ejemplo: keytool -list -v -keystore debug.keystore -alias androiddebugkey -storepass android -keypass android

Copiamos el valor correspondiente al hash en SHA1 y lo pasamos a un conversor en Base64. El valor obtenido es el que tendremos que usar en la configuración de Azure Active Directory.

Configuración del proyecto Android

Una vez configurada nuestra aplicación en el panel de Azure Active Directory, tendremos que llevar la configuración al proyecto Android.

Abrimos el fichero de manifiesto de Android (AndroidManifest.xml) y añadimos la siguiente actividad dentro de la etiqueta:


     
           
           
           
           
     

Sustituiremos  por el mismo hash obtenido en el paso anterior en la configuración de Azure Active Directory.

No olvides incluir / al comienzo del hash, ya que es requisito indispensable para la librería de Azure. Ejemplo:


     
           
           
           
           
     

En el caso de que queramos configurar múltiples hash de firma para funcionar en distintas variantes, lo ideal sería mover el hash introducido en el fichero AndroidManifest.xml al fichero strings.xml, donde cada variante use su propio fichero strings.xml. Por ejemplo, en el caso de usar distintas firmas por variante de compilación:

  • app/src/release/res/values/strings.xml
  • app/src/debug/res/values/strings.xml
  • app/src/pre/res/values/strings.xml

En segundo lugar, hay que crear un fichero azure_auth_config.json dentro de la carpeta raw del paquete res. Este fichero será un JSON que obtendremos desde el panel de Azure Active Directory, junto a la opción “Ver” de la aplicación Android que hemos agregado. El fichero JSON a copiar será el que viene indicado en el apartado “Configuración de MSAL”.

Al pegarlo dentro del fichero azure_auth_config.json creado habrá que eliminar la primera parte del JSON copiado, hasta llegar a la llave {. Tendremos un fichero similar al siguiente:

Es posible que si cambiamos ciertas configuraciones del panel de Azure Active Directory, el fichero de configuración de Android cambie. Si queremos usar distintos ficheros azure_auth_config.json por variante de compilación, podemos colocarlos en la ruta correspondiente a cada variante. Por ejemplo:

  • app/src/release/res/raw/azure_auth_config.json
  • app/src/debug/res/raw/azure_auth_config.json
  • app/src/pre/res/raw/azure_auth_config.json

Uso de cuentas

La librería MSAL para Android ofrece una API sencilla de utilidad con los habituales métodos para iniciar sesión, obtener los datos del usuario logueado, refresco de usuario, cerrar sesión y más.

MSAL distingue dos tipos de autenticación:

  • Individual o simple. Si solo queremos iniciar sesión en una cuenta al mismo tiempo.
  • Múltiple. Si queremos soportar varios usuarios.

Uso de cuenta única

Antes de comenzar la integración es necesario incluir el siguiente parámetro en el fichero JSON de configuración:

"account_mode" : "SINGLE"

Tendremos algo similar a lo siguiente:

{
    "client_id" : "your_client_id",
    "authorization_user_agent" : "DEFAULT",
    "redirect_uri" : "your_redirect_uri",
    "account_mode" : "SINGLE",
    "authorities" : ...
}

La inicialización se hace sobre la clase PublicClientApplication de la librería MSAL. Por lo general, los métodos de la librería son operaciones asíncronas, por lo que el resultado se pasará mediante un callback.

PublicClientApplication.createSingleAccountPublicClientApplication(
       getContext(),
       R.raw.azure_auth_config,
       object : IPublicClientApplication.ISingleAccountApplicationCreatedListener {
           override fun onCreated(auth: ISingleAccountPublicClientApplication?) {
               //auth?.signIn(...)
           }
           override fun onError(exception: MsalException?) {
               // Mostrar error
           }
       })

El callback cuenta con dos posibles respuestas:

  • onCreated(auth: ISingleAccountPublicClientApplication?). La inicialización se ha hecho correctamente y podremos usar el objeto ISingleAccountPublicClientApplication.
  • onError(exception: MsalException?). La librería no se pudo inicializar correctamente. El objeto devuelto contiene información del error.

Todos los métodos de sesión y autenticación pertenecen al objeto ISingleAccountPublicClientApplication, por lo que debemos mantener inicializada la librería o contar con su inicialización en el momento de realizar algún método de sesión.

Iniciar sesión

El inicio de sesión se realiza sobre la clase ISingleAccountPublicClientApplication recibida de la inicialización:

val hint = “Login with Microsoft”
val scopes = arrayOf("User.Read")
auth?.signIn(
       getActivity(),
       hint,
       scopes,
       object : AuthenticationCallback {
           override fun onSuccess(authenticationResult: IAuthenticationResult?) {
               // Guardar datos del usuario y refrescar UI
           }
 
           override fun onError(exception: MsalException?) {
               // Mostrar error
           }
 
           override fun onCancel() {
               // El usuario ha cancelado el flujo de inicio de sesión
           }
       })

El método .signIn(...) resultará en una pantalla mediante Chrome Custom Tabs que mostrará el inicio de sesión con Microsoft. Podremos configurar un array de scopes, que son los permisos que se le solicitarán al usuario y son configurables desde el panel de Azure Active Directory (pestaña “Permisos de API” del panel lateral).

El callback de respuesta podrá devolver:

  • onSuccess(authenticationResult: IAuthenticationResult?). La autenticación se ha realizado y el objeto IAuthenticationResult contiene la información del usuario para almacenar: token, usuario, email, etc.
  • onError(exception: MsalException?). Ocurrió un error en la autenticación. Podemos ver más información del error con el objeto devuelto.
  • onCancel(). La pantalla de inicio de sesión se ha cerrado por el usuario sin terminar el inicio de sesión.

Sólo podemos llamar al método .signIn(...) si el usuario no está ya logueado. Si lo volveremos a llamar entrará directamente sobre el método onError(), ya que la librería ya contiene la información del usuario logueado.

El método .signIn(...) actúa de manera similar al método .acquireToken(...) y muestra una nueva ventana interactiva para el inicio de sesión o refresco del token. Este es el método que debemos usar en los siguientes casos:

  • Primera vez que el usuario inicia sesión.
  • El usuario ha restablecido su contraseña.
  • Se ha revocado el consentimiento del usuario.
  • Se solicita un nuevo Scope al usuario.

Si el usuario ya ha iniciado sesión con anterioridad, podemos usar el método .acquireTokenSilentAsync(...)  para solicitar el token sin la intervención del usuario.

Cerrar sesión de usuario

De manera similar podemos cerrar la sesión de la cuenta Azure log en ese momento usando el método .signOut(callback: SignOutCallback) de la librería:

auth?.signOut(object : ISingleAccountPublicClientApplication.SignOutCallback {
       override fun onSignOut() {
           // Sesión cerrada correctamente
       }
 
       override fun onError(exception: MsalException) {
           // Error al cerrar sesión
       }
  })

Obtener información del usuario logueado

La información del usuario logueado puede variar y, ya que esta información proviene directamente de la cuenta Microsoft vinculada, existe un método de la librería que nos indica si la cuenta log ha cambiado. Así mismo, nos puede servir para detectar si el usuario ya no es válido o ha sido desvinculado de la aplicación.

En estos casos deberemos usar el método .getCurrentAccountAsync(callback: CurrentAccountCallback):

auth?.getCurrentAccountAsync(object : ISingleAccountPublicClientApplication.CurrentAccountCallback() {
   override fun onAccountLoaded(activeAccount: IAccount?) {
       // Guardar el usuario y refrescar la interfaz
   }
   override fun onAccountChanged(priorAccount: IAccount?, currentAccount: IAccount?) {
       // Controlar cambios del usuario. currentAccount será nulo si el usuario ya no está logueado
   }
   override fun onError(exception: MsalException) {
       // Controlar error 
   }
})

El callback nos devuelve 3 posibles respuestas:

  • onAccountLoaded(activeAccount: IAccount?). El usuario continúa logueado y con el objeto devuelto podemos actualizar la UI o datos almacenados del usuario en preferencias de la aplicación.
  • onAccountChanged(priorAccount: IAccount?, currentAccount: IAccount?). La cuenta actual ha cambiado. Debemos controlar la respuesta para controlar que el usuario deje de ser válido en UI.
  • onError(exception: MsalException). Ha ocurrido un error al cargar la información del usuario. El objeto MsalException contiene más información sobre el error.

Uso de múltiples cuentas de usuario

A diferencia del fichero de configuración cuando se configura una cuenta única, el archivo de configuración en este caso estará configurado como “MULTIPLE” ya que vamos a permitir varias cuentas de usuario al mismo tiempo:

"account_mode" : "MULTIPLE"

Tendremos algo similar a lo siguiente:

{
    "client_id" : "your_client_id",
    "authorization_user_agent" : "DEFAULT",
    "redirect_uri" : "your_redirect_uri",
    "account_mode" : "MULTIPLE",
    "authorities" : ...
}

La inicialización también cambiará ya que lo realizaremos con el método .createMultipleAccountPublicClientApplication(...) en vez de .createSingleAccountPublicClientApplication(...). Sus parámetros serán similares:

PublicClientApplication.createMultipleAccountPublicClientApplication(
      getContext(),
      R.raw.azure_auth_config,
      object : IPublicClientApplication.IMultipleAccountApplicationCreatedListener {
            override fun onCreated(auth: IMultipleAccountPublicClientApplication?) {
                // Controlar siguiente acción: cargar cuentas, realizar inicio de sesión, etc
            }
 
            override fun onError(exception: MsalException?) {
                // Mostrar error
            }
      })

El objeto IMultipleAccountPublicClientApplication devuelto a través del callback será el que necesitemos para realizar las acciones sobre la librería de Azure: inicio y cierre de sesión, obtener información de las cuentas, etc.

Obtener cuentas logs

La manera de obtener las cuentas logs será distinta al método de una única cuenta, ya que tendremos que recibir un listado de cuentas.

En este caso tendremos que usar el método .getAccounts(...) de la clase IMultipleAccountPublicClientApplication  obtenida en el paso anterior:

auth?.getAccounts(object : IPublicClientApplication.LoadAccountsCallback {
        override fun onTaskCompleted(result: MutableList?) {
            // Actualizar la información de cuentas
        }
 
        override fun onError(exception: MsalException?) {
            // Mostrar error
        }
    })

Iniciar sesión en nueva cuenta

Podemos añadir nuevas cuentas de manera interactiva usando el método .adquireToken(...):

auth?.acquireToken(
        getActivity,
        AZURE_SCOPES,
        object : AuthenticationCallback {
            override fun onSuccess(authenticationResult: IAuthenticationResult?) {
                // Usuario logueado correctamente
            }
 
            override fun onError(exception: MsalException?) {
                // Mostrar error
            }
 
            override fun onCancel() {
                // El usuario ha cerrado o cancelado la pantalla de inicio de sesión
            }
        })

Cerrar sesión en una cuenta

En este caso, si queremos cerrar sesión en unas de las cuentas donde hemos iniciado sesión, tendremos que traspasar también el objeto IAccount que queremos eliminar. Este objeto podemos obtenerlo de dos formas:

  • Mediante el método .getAccount(identifier: String, callback: GetAccountCallback) usando el identificador del usuario logueado.
  • Usando el método .getAccounts(...) explicado con anterioridad, el cual nos devolvía el listado de usuarios logueados.

Una vez tengamos identificado el objeto IAccount de la cuenta que queremos cerrar sesión, usaremos el método .deleteAccount(account: IAccount, callback):

RemoveAccountCallback):
auth?.removeAccount(
       account,
       object: IMultipleAccountPublicClientApplication.RemoveAccountCallback {
          override fun onRemoved() {
              // Usuario eliminado correctamente
          }
 
          override fun onError(exception: MsalException) {
              // Error al cerrar sesión
          }
       })

Conclusiones

La librería de autenticación SSO de Azure Active Directory ofrece una API sencilla y adaptada para proyectos con distintas necesidades de autenticación, por lo que se convierte en una alternativa real a otros sistemas de identidad como Oauth o OpenID Connect.

En general, AAD (Azure Active Directory) ofrece un conjunto de herramientas atractivas para organizaciones que pretenden unificar la autenticación y hacer uso de otras herramientas de la suite de Azure como Office 365, SharePoint o Exchange Online, de manera que todas ellas queden integradas en un sistema de autenticación único.

Android Team
ALTEN