Skip to content
Andrés Matte edited this page Feb 25, 2016 · 4 revisions

Un authenticator es básicamente una componente que nos ofrece android para manejar las cuentas de nuestra aplicación de manera profesional, elegante y con una gran cobertura de los posibles casos que se puedan presentar. Su implementación, aunque sea stub, es obligatoria si se quiere implementar un sync adapter. Para esta parte supondremos que tenemos una API desarrollada que permite enviar un usuario y contraseña y retorna el token necesario para que podamos interactuar con ella.

Beneficios

En este caso estamos obligados a implementar un authenticator si es que queremos hacer un sync adapter. Pero si no estuvieramos obligados aún sería extremadamente útil implementar esta componente para manejar las cuentas. Se podría pensar que basta con hacer un log-in que utilice la API para obtener el token y guardar este en la base de datos. De esta forma nos ahorramos estudiar e implementar esta componente. La verdad es que eso puede funcionar pero deja una gran gama de casos posibles sin cubrir.

Imaginemos que el usuario cambia su contraseña en otro cliente y quiere que esto se vea reflejado en la aplicación. Con la implementación anteriormente indicada no podemos enterarnos de esto. Para darnos cuenta tendriamos que implementar nuestro propio sistema que maneje este caso. O si el usuario quiere que al loguearse en una aplicación se loguee automáticamente en todas las otras aplicaciones relacionadas (como las de Google). El authenticator maneja todos estos casos simplificando la tarea del desarrollador, por lo que su uso es absolutamente recomendable.

En resumen, podemos obtener los siguientes beneficios:

  1. Forma estandar de autenticar a los usuarios.

  2. Simplifica autenticación para el desarrollador.

  3. Maneja casos de acceso denegados.

  4. Compartir cuentas entre aplicaciones.

  5. Nos permite implementar un SyncAdapter.

  6. Además, tu aplicación se mete en terreno de gigantes ;).

Qué haremos

Para implementar este componente abordaremos los siguientes temas:

  1. Definiciones básicas

  2. Flujo general

  3. Crear authenticator

  4. Crear la actividad de log-in

  5. Crear el authenticator service

  6. Cómo usarlo

  7. Permisos

Definiciones básicas

Token

Es una clave de acceso temporal que el servidor le entrega a un cliente. El usuario se identifica, por lo general con usuario y contraseña, y el servidor le retorna esta clave que debe adjuntar en todos los requests que haga. Puede ser limitado y expirar pasada cierta cantidad de tiempo.

AccountManager

Esta clase es como el maestro de la orquesta. Básicamente se encarga de la gestión de todas las cuentas en el dispositivo y sabe a quien llamar en cada caso que se presente. Esta clase la provee android por lo que no necesitamos implementarla.

AccountAuthenticator

Cada empresa o conjunto de aplicaciones tiene distintas maneras de autenticar a los usuarios, por lo tanto android ofrece AccountAuthenticator para personalizar este proceso. Cada grupo de aplicaciones, que también puede ser solo una, (por ejemplo Facebook, Whatsapp, o Google) tiene su propio AccountAuthenticator. Esta clase sabe que actividad mostrar para que el usuario ingrese sus credenciales y donde encontrar algún token retornado por el servidor previamente.

AccountAuthenticatorActivity

Actividad llamada por AccountAuthenticator para que el usuario ingrese a su cuenta o se registre. Esta debe interactuar con el servidor para obtener el token y retornarlo al AccountAuthenticator.

Flujo general

El flujo no es muy complejo. Primero se le dice a AccountManager que me dé el token de cierta cuenta. Luego, este le pregunta al AccountAuthenticator relevante si tiene algún token. En caso de no existir hace que se abra la actividad de registro/logueo, obtiene el token retornado por el servidor y se retorna al AccountManager. El token se guarda para uso futuro y se retorna al que lo pidió por primera vez a través de un callback.

A continuación se presenta un gráfico que estuvo alguna vez en la documentación de google y que puede ayudar a clarificar el flujo. Se irán explicando los conceptos principales a medida que vayamos avanzando en la implementación.

Flujo Atuhenticator

Crear Authenticator

El AccountAuthenticator es el encargado de realizar todas las operaciones importantes relacionadas con la cuenta: obtener el token, mostrar pantalla de logueo y comunicarse con el servidor. Para crear un nuestro propio AccountAuthenticator tenemos que extender la clase abstracta AbstractAccountAuthenticator e implementar algunos métodos, donde los más importantes son addAccount() y getAuthToken().

Para implementar los métodos del authenticator se debe seguir un patrón relativamente estandar. Para cada uno de los métodos se debe devolver una de las siguientes opciones:

  • Si los argumentos que se entregan al método son suficientes para llevar a cabo la operación entonces se debe realizar la acción y devolver un Bundle con los resultados.

  • Si el authenticator necesita información del usuario y este no la tiene, entonces se creará un Intent para iniciar una actividad que le pedirá los datos al usuario. Este Intent tiene que ser retornado en un Bundle con el key KEY_INTENT. Como la actividad tiene que devolver al authenticator el resultado de la petición de datos, entonces se debe incluir el AccountAuthenticatorResponse en el intent con el key KEY_ACCOUNT_MANAGER_RESPONSE. Luego, la actividad tiene que llamar onResult(Bundle) si tuvo éxito, o onError(int, String) si hubo algún error. Esto se evita al hacer que la actividad extienda de AccountAuthenticatorActivity.

  • En caso de haber algún error se debe retornar un Bundle que contenga un par de valores con un código de error como key (int) y con un String que explique el error como valor.

En el caso nuestro, haremos una implementación relativamente sencilla de estos métodos. El esqueleto es como sigue:

    import android.accounts.AbstractAccountAuthenticator;
    import android.accounts.Account;
    import android.accounts.AccountAuthenticatorResponse;
    import android.accounts.AccountManager;
    import android.accounts.NetworkErrorException;
    import android.content.Context;
    import android.content.Intent;
    import android.os.Bundle;
    import android.text.TextUtils;

    import com.example.andres.myapplication.Activities.AuthenticatorActivity;

    /*
        Clase que maneja la autenticacion y realiza la gran mayoria de las operaciones importantes de una cuenta.
     */
    public class AccountAuthenticator extends AbstractAccountAuthenticator {

        private Context mContext;

        public AccountAuthenticator(Context context) {
            super(context);
            mContext = context;
        }

        @Override
        public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
            return null;
        }

        /*
          Llamado cuando el usuario quiere loguearse y anadir un nuevo usuario.
          @return bundle con intent para iniciar AuthenticatorActivity.
         */
        @Override
        public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {

            ...

        }

        @Override
        public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
            return null;
        }

        /*
            Obtiene el token de una cuenta. Si falla, se avisa que se debe llamar a AuthenticatorActivity.
            @return Si resulta, bundle con informacion de cuenta y token.
                    Si falla, bundle con informacion de cuenta y activity.
         */
        @Override
        public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {

            ...

        }

        @Override
        public String getAuthTokenLabel(String authTokenType) {
            return null;
        }

        @Override
        public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
            return null;
        }

        @Override
        public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
            return null;
        }
    }

addAcount

Este método se llama cuando un usuario se loguea y queremos añadir una nueva cuenta al dispositivo. Se puede llamar a través de la aplicación, para lo cual necesitamos algunos permisos que se detallarán después. También es el método que se llama cuando se ingresa a configuración, apretamos Agregar Cuenta y seleccionamos nuestro Authenticator.

Agregar cuenta configuracion Android

        /*
          Llamado cuando el usuario quiere loguearse y anadir un nuevo usuario.
          @return bundle con intent para iniciar AuthenticatorActivity.
         */
        @Override
        public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {

            final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
            intent.putExtra(AuthenticatorActivity.ARG_ACCOUNT_TYPE, accountType);
            intent.putExtra(AuthenticatorActivity.ARG_AUTH_TYPE, authTokenType);
            intent.putExtra(AuthenticatorActivity.ARG_IS_ADDING_NEW_ACCOUNT, true);
            intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);

            final Bundle bundle = new Bundle();
            bundle.putParcelable(AccountManager.KEY_INTENT, intent);

            return bundle;
        }

getAuthToken

Es el método descrito en la Figura [fig:flujo_authenticator]. Obtiene un token guardado de algún log-in anterior. Si no existe aún, el usuario deberá loguearse. Para lograr eso tenemos que llamar al método AccountManager.peekAuthToken(). Si no hay un token retornamos lo mismo que para addAcount. De esta forma se lanzará la actividad para que el usuario se loguee.

        /*
            Obtiene el token de una cuenta. Si falla, se avisa que se debe llamar a AuthenticatorActivity.
            @return Si resulta, bundle con informacion de cuenta y token.
                    Si falla, bundle con informacion de cuenta y activity.
         */
        @Override
        public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {

            // Extrae username y pass del account manager
            final AccountManager am = AccountManager.get(mContext);

            // Pide el authtoken
            String authToken = am.peekAuthToken(account, authTokenType);

            // Si authToken esta vacio (no hay token guardado), se intenta autenticar en servidor
            if (TextUtils.isEmpty(authToken)){
                final String password = am.getPassword(account);
                if (password != null) {
                    // Se autentica en el servidor
                    authToken = authenticateInServer(account);
                }
            }
            // Si obtenemos un authToken, lo retornamos
            if (!TextUtils.isEmpty(authToken)) {

                final Bundle result = new Bundle();
                result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
                result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
                result.putString(AccountManager.KEY_AUTHTOKEN, authToken);

                return result;
            }
            // Si llegamos aca aun no podemos obtener el token.
            // Necesitamos pedirle de nuevo que se loguee.
            final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
            intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
            intent.putExtra(AuthenticatorActivity.ARG_ACCOUNT_TYPE, account.type);
            intent.putExtra(AuthenticatorActivity.ARG_AUTH_TYPE, authTokenType);

            final Bundle bundle = new Bundle();
            bundle.putParcelable(AccountManager.KEY_INTENT, intent);
            return bundle;
        }

Advertencia

Si por alguna razón el token que se tiene ya no es válido, este se debe invalidar con el método AccountManager.invalidateAuthToken(). Luego, se debe pedir al usuario que se loguee nuevamente para que obtenga un token válido.

Crear la actividad de log-in

En esta actividad el usuario ingresará sus credenciales y obtendrá el token que debemos guardar para enviarlo al authenticator asociado. Un punto importante es que esta actividad va a extender de AccountAuthenticatorActivity. Al extender de esta clase se obtiene el método setAccountAuthenticatorResult(). Además, al extender de esta clase se sobreescribe el método finish() de la actividad, que es llamado siempre cuando la actividad termina. Ahora este método llamará automáticamente a onResult() si es que se invocó a setAuthenticatorResult() o a onError() si es que no se invocó (ver últimas lineas del código de finishLogin() más abajo).

Se creó un método submit() que se llama al apretar el botón de log-in.

    public void submit() {

        // Se obtiene el usuario y contrasena ingresados
        final String userName = ((TextView) findViewById(R.id.account_name)).getText().toString();
        final String userPass = ((TextView) findViewById(R.id.account_password)).getText().toString();

        // Se loguea de forma asincronica para no entorpecer el UI thread
        new AsyncTask<Void, Void, Intent>() {
            @Override
            protected Intent doInBackground(Void... params) {

                // Se loguea en el servidor y retorna token
                String authtoken = logIn(userName, userPass);

                // Informacion necesaria para enviar al authenticator
                final Intent res = new Intent();
                res.putExtra(AccountManager.KEY_ACCOUNT_NAME, userName);
                res.putExtra(AccountManager.KEY_ACCOUNT_TYPE, "com.example.andres.myapplication");
                res.putExtra(AccountManager.KEY_AUTHTOKEN, authtoken);
                res.putExtra(PARAM_USER_PASS, userPass);

                return res;
            }
            @Override
            protected void onPostExecute(Intent intent) {

                finishLogin(intent);
            }
        }.execute();
    }

También se crea el método finishLogin(), llamado al finalizar el método submit(). Este se encarga de crear la cuenta nueva si es que no existe y de enviar la información al authenticator.

    private void finishLogin(Intent intent) {

        String accountName = intent.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
        String accountPassword = intent.getStringExtra(PARAM_USER_PASS);
        final Account account = new Account(accountName, intent.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE));

        // Si es que se esta anadiendo una nueva cuenta
        if (getIntent().getBooleanExtra(ARG_IS_ADDING_NEW_ACCOUNT, false)) {

            String authtoken = intent.getStringExtra(AccountManager.KEY_AUTHTOKEN);
            // Pueden haber muchos tipos de cuenta. En este caso solo tenemos una que llame 'normal'
            String authtokenType = "normal";
            // Creando cuenta en el dispositivo y seteando el token que obtuvimos.
            mAccountManager.addAccountExplicitly(account, accountPassword, null);

            // Ojo: hay que setear el token explicitamente si la cuenta no existe, no basta con mandarlo al authenticator
            mAccountManager.setAuthToken(account, authtokenType, authtoken);
        }

        // Si no se esta anadiendo cuenta, el token antiguo estaba invalidado.
        // Seteamos contrasena nueva por si la cambio.
        else {
            // Solo seteamos contrasena
            // Aca no es necesario setear el token explicitamente, basta con enviarlo al Authenticator
            mAccountManager.setPassword(account, accountPassword);
        }
        // Setea el resultado para que lo reciba el Authenticator
        setAccountAuthenticatorResult(intent.getExtras());
        setResult(RESULT_OK, intent);

        // Cerramos la actividad
        finish();
    }

Crear el authenticator service

Ahora tenemos que hacer que nuestro Authenticator esté disponible para todas las apps que quieran utilizarlo, como por ejemplo las configuraciones del teléfono. También necesitamos que obviamente esté corriendo sin que tengamos la aplicación abierta, por lo tanto lo más sensato es usar Servicios.

Servicios

Un servicio es un componente de una aplicación hecho para correr operaciones de larga duración en el background. No posee interfaz gráfica y necesita que otra componente de la aplicación la inicie. Luego, y pese a que la aplicación que la inició se cierre, el servicio seguirá corriendo hasta que termine con su tarea. Puede usarse para tocar música, para interactuar con un content provider o para realizar operaciones con un servidor.

En este caso usaremos un servicio Bound. Este tipo de servicios dan la opción de enlazarse con una componente de la aplicación a través del método bindService(). Este tipo de servicios ofrece una interfaz cliente-servidor que permite a los componentes interactuar con el servicio, enviar requests, obtener resultados, etc. Un bound service corre solo mientras la componente de la aplicación esté enlazado a el.

Para crear un bound service tenemos que implementar onBind(), un callback que retorna un IBinder. Este objeto define la interfaz de comunicación con el servicio. La implementación que haremos será muy sencilla, por lo que si quieres averiguar más sobre servicios puedes mirar la documentación[^6] .

    public class AuthenticatorService extends Service {
        private AccountAuthenticator mAuthenticator;

        @Override
        public void onCreate() {
            // Create a new authenticator object
            mAuthenticator = new AccountAuthenticator(this);
        }

        @Override
        public IBinder onBind(Intent intent) {
            return mAuthenticator.getIBinder();
        }
    }

XML y manifest

En el manifest debemos agregar nuestro servicio:

    <service android:name=".Services.AuthenticatorService">
        <intent-filter>
             <action android:name="android.accounts.AccountAuthenticator" />
        </intent-filter>
        <meta-data android:name="android.accounts.AccountAuthenticator"
                   android:resource="@xml/authenticator" />
    </service>

Ahora, si es que aún no tienes una carpeta con nombre xml dentro de res creala. Dentro de ella crearemos un archivo xml con el nombre authenticator.xml. Este archivo nos permitirá definir algunos atributos.

    <?xml version="1.0" encoding="utf-8"?>
    <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
        android:accountType="com.example.andres.myapplication"
        android:icon="@drawable/ic_launcher"
        android:smallIcon="@drawable/ic_launcher"
        android:label="@string/app_name"
    />

Algunos de los atributos importantes que se pueden poner en el archivo son:

  • accountType: nombre para identificar nuestro tipo de cuenta. Cuando alguna aplicación quiera autenticarse con nuestra cuenta debe usar ese accountType.

  • icon y smallIcon: iconos a ser mostrados en la configuración del celular, en la zona de cuentas.

  • label: nombre de nuestro tipo de cuenta en la configuración del celular.

  • accountPreferences: también se puede definir un archivo de preferencias que se desplegará al apretar nuestra cuenta en configuraciones del celular. En este caso no lo definimos.

Cómo usarlo

El uso más básico es preguntarle a AccountManager por las cuentas del tipo que nos interesa y elegir la primera, si es que sabemos que solo habrá una. En caso de poder haber más de una (como las cuentas de Google) sería conveniente poner esas cuentas en una lista y que el usuario decidiera cual utilizar. En la actividad principal de la aplicación de ejemplo se utiliza el siguiente código:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        accountManager = (AccountManager) getSystemService(ACCOUNT_SERVICE);

        Account[] accounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
        if (accounts.length == 0){
            // Tambien se puede llamar a metodo accountManager.addAcount(...)
            Intent intent = new Intent(this, AuthenticatorActivity.class);
            intent.putExtra(AuthenticatorActivity.ARG_IS_ADDING_NEW_ACCOUNT, true);
            startActivity(intent);
        }
        else{
            mAccount = accounts[0];
            accountManager.getAuthToken(mAccount, "normal", null, this, mGetAuthTokenCallback, null);
            ...
        }
        ....
    }

El código de `mGetAuthTokenCallback` es:


    private AccountManagerCallback<Bundle> mGetAuthTokenCallback =
            new AccountManagerCallback<Bundle>() {
                    @Override
                    public void run(final AccountManagerFuture<Bundle> arg0) {
                        try {
                            token = (String) arg0.getResult().get(AccountManager.KEY_AUTHTOKEN);
                        } catch (Exception e) {
                            // handle error
                        }
                    }
                };

Siguiente: Clase SyncAdapter.