-
Notifications
You must be signed in to change notification settings - Fork 3
Authenticator
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.
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:
-
Forma estandar de autenticar a los usuarios.
-
Simplifica autenticación para el desarrollador.
-
Maneja casos de acceso denegados.
-
Compartir cuentas entre aplicaciones.
-
Nos permite implementar un SyncAdapter.
-
Además, tu aplicación se mete en terreno de gigantes ;).
Para implementar este componente abordaremos los siguientes temas:
-
Definiciones básicas
-
Flujo general
-
Crear authenticator
-
Crear la actividad de log-in
-
Crear el authenticator service
-
Cómo usarlo
-
Permisos
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.
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.
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.
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
.
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.
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 elAccountAuthenticatorResponse
en el intent con el keyKEY_ACCOUNT_MANAGER_RESPONSE
. Luego, la actividad tiene que llamaronResult(Bundle)
si tuvo éxito, oonError(int, String)
si hubo algún error. Esto se evita al hacer que la actividad extienda deAccountAuthenticatorActivity
. -
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;
}
}
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.
/*
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;
}
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;
}
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.
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();
}
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.
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();
}
}
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.
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.