Autenticación en servicios OAuth2

Diagrama de la lógica del token de autenticación
Figura 1: Procedimiento para obtener un token de autenticación válido del Administrador de cuentas de Android.

Para acceder de forma segura a un servicio en línea, los usuarios deben autenticarse; es decir, deben proporcionar pruebas de su identidad. Para una aplicación que accede a un servicio de terceros, el problema de seguridad es aún más complicado. No solo el usuario debe estar autenticado para acceder al servicio, sino que la aplicación también debe estar autorizada para actuar en nombre del usuario.

El protocolo OAuth2 es el método estándar de la industria para abordar la autenticación en servicios de terceros. OAuth2 proporciona un valor único, denominado token de autenticación, que representa la identidad del usuario y la autorización de la aplicación para actuar en nombre del usuario. En esta lección, se muestra cómo conectarse a un servidor de Google compatible con OAuth2. Aunque los servicios de Google se usan como ejemplo, las técnicas demostradas funcionarán en cualquier servicio que admita el protocolo OAuth2 de forma correcta.

El uso de OAuth2 es útil para lo siguiente:

  • Obtener permiso del usuario para acceder a un servicio en línea con su cuenta
  • Lograr la autenticación para un servicio en línea en nombre del usuario
  • Control de errores de autenticación

Recopila información

Para comenzar a usar OAuth2, debes conocer algunos aspectos específicos de la API sobre el servicio al que intentas acceder:

  • La URL del servicio al que deseas acceder.
  • El alcance de Auth, que es una cadena que define el tipo específico de acceso que solicita tu app. Por ejemplo, el alcance de autenticación para el acceso de solo lectura a Google Tasks es View your tasks, mientras que el alcance de autenticación para el acceso de lectura y escritura a Google Tasks es Manage your tasks.
  • Un ID de cliente y un secreto de cliente, que son strings que permiten que el servicio identifique tu app. Debes obtener estas strings directamente del propietario del servicio. Google tiene un sistema de autoservicio para obtener IDs de cliente y secretos.

Cómo solicitar el permiso de Internet

En el caso de las apps orientadas a Android 6.0 (nivel de API 23) y versiones posteriores, el método getAuthToken() no requiere ningún permiso. Sin embargo, para realizar operaciones en el token, debes agregar el permiso INTERNET al archivo de manifiesto, como se muestra en el siguiente fragmento de código:

<manifest ... >
    <uses-permission android:name="android.permission.INTERNET" />
    ...
</manifest>

Cómo solicitar un token de autenticación

Para obtener el token, llama a AccountManager.getAuthToken().

Precaución: Debido a que algunas operaciones de la cuenta pueden implicar comunicación de red, la mayoría de los métodos AccountManager son asíncronos. Esto significa que, en lugar de hacer todo el trabajo de autenticación en una función, debes implementarla como una serie de devoluciones de llamada.

En el siguiente fragmento, se muestra cómo trabajar con una serie de devoluciones de llamada para obtener el token:

Kotlin

val am: AccountManager = AccountManager.get(this)
val options = Bundle()

am.getAuthToken(
        myAccount_,                     // Account retrieved using getAccountsByType()
        "Manage your tasks",            // Auth scope
        options,                        // Authenticator-specific options
        this,                           // Your activity
        OnTokenAcquired(),              // Callback called when a token is successfully acquired
        Handler(OnError())              // Callback called if an error occurs
)

Java

AccountManager am = AccountManager.get(this);
Bundle options = new Bundle();

am.getAuthToken(
    myAccount_,                     // Account retrieved using getAccountsByType()
    "Manage your tasks",            // Auth scope
    options,                        // Authenticator-specific options
    this,                           // Your activity
    new OnTokenAcquired(),          // Callback called when a token is successfully acquired
    new Handler(new OnError()));    // Callback called if an error occurs

En este ejemplo, OnTokenAcquired es una clase que implementa AccountManagerCallback. AccountManager llama a run() en OnTokenAcquired con un AccountManagerFuture que contiene un Bundle. Si la llamada se realizó correctamente, el token estará dentro de Bundle.

A continuación, te indicamos cómo puedes obtener el token de Bundle:

Kotlin

private class OnTokenAcquired : AccountManagerCallback<Bundle> {

    override fun run(result: AccountManagerFuture<Bundle>) {
        // Get the result of the operation from the AccountManagerFuture.
        val bundle: Bundle = result.getResult()

        // The token is a named value in the bundle. The name of the value
        // is stored in the constant AccountManager.KEY_AUTHTOKEN.
        val token: String = bundle.getString(AccountManager.KEY_AUTHTOKEN)
    }
}

Java

private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
    @Override
    public void run(AccountManagerFuture<Bundle> result) {
        // Get the result of the operation from the AccountManagerFuture.
        Bundle bundle = result.getResult();

        // The token is a named value in the bundle. The name of the value
        // is stored in the constant AccountManager.KEY_AUTHTOKEN.
        String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
        ...
    }
}

Si todo resulta bien, Bundle contiene un token válido en la clave KEY_AUTHTOKEN y está todo listo.

Es posible que tu primera solicitud de un token de autenticación falle por varios motivos:

  • Se produjo un error en el dispositivo o la red hizo que AccountManager fallara.
  • El usuario decidió no permitir que tu app accediera a la cuenta.
  • Las credenciales almacenadas de la cuenta no son suficientes para obtener acceso a la cuenta.
  • El token de autenticación en caché caducó.

Las aplicaciones pueden manejar los dos primeros casos de manera trivial, en general con solo mostrar un mensaje de error al usuario. Si la red no funciona o el usuario decidió no otorgar acceso, tu aplicación no puede hacer mucho al respecto. Los últimos dos casos son un poco más complicados, ya que se espera que las aplicaciones con buen comportamiento manejen automáticamente estas fallas.

El tercer caso de error, por tener credenciales insuficientes, se comunica a través del Bundle que recibes en tu AccountManagerCallback (OnTokenAcquired del ejemplo anterior). Si el Bundle incluye un Intent en la clave KEY_INTENT, el autenticador te indica que debes interactuar directamente con el usuario para que pueda darte un token válido.

Puede haber muchos motivos para que el autenticador muestre un Intent. Puede ser la primera vez que el usuario accede a esta cuenta. Es posible que la cuenta del usuario haya vencido y deba acceder de nuevo, o tal vez sus credenciales almacenadas sean incorrectas. Es posible que la cuenta requiera autenticación de dos factores o que se deba activar la cámara para realizar un escaneo de retina. No importa cuál sea el motivo. Si quieres un token válido, tendrás que activar el Intent para obtenerlo.

Kotlin

private inner class OnTokenAcquired : AccountManagerCallback<Bundle> {

    override fun run(result: AccountManagerFuture<Bundle>) {
        val launch: Intent? = result.getResult().get(AccountManager.KEY_INTENT) as? Intent
        if (launch != null) {
            startActivityForResult(launch, 0)
        }
    }
}

Java

private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
    @Override
    public void run(AccountManagerFuture<Bundle> result) {
        ...
        Intent launch = (Intent) result.getResult().get(AccountManager.KEY_INTENT);
        if (launch != null) {
            startActivityForResult(launch, 0);
            return;
        }
    }
}

Ten en cuenta que, en el ejemplo, se usa startActivityForResult() de modo que puedas capturar el resultado de Intent mediante la implementación de onActivityResult() en tu propia actividad. Esto es importante: si no capturas el resultado del Intent de la respuesta del autenticador, es imposible saber si el usuario se autenticó correctamente.

Si el resultado es RESULT_OK, significa que el autenticador actualizó las credenciales almacenadas a fin de que sean suficientes para el nivel de acceso que solicitaste, y debes llamar a AccountManager.getAuthToken() nuevamente para solicitar el token de autenticación nuevo.

El último caso, en el que caducó el token, en realidad no es una falla de AccountManager. La única forma de descubrir si un token está vencido es comunicarse con el servidor, y para AccountManager sería costoso y una pérdida de tiempo conectarse continuamente a fin de verificar el estado de todos sus tokens. Por lo tanto, este es un error que solo se puede detectar cuando una aplicación como la tuya intenta usar el token de autenticación para acceder a un servicio en línea.

Cómo conectarse con el servicio en línea

En el siguiente ejemplo, se muestra cómo realizar la conexión con un servidor de Google. Dado que Google usa el protocolo OAuth2 estándar de la industria para autenticar las solicitudes, las técnicas que se analizan aquí se pueden aplicar ampliamente. Sin embargo, ten en cuenta que cada servidor es diferente. Es posible que necesites hacer pequeños ajustes en estas instrucciones para justificar tu situación específica.

Las APIs de Google requieren que proporciones cuatro valores con cada solicitud: la clave de API, el ID de cliente, el secreto de cliente y la clave de autenticación. Las tres primeras provienen del sitio web de la Consola de API de Google. El último es el valor de string que obtuviste cuando llamas a AccountManager.getAuthToken(). Debes pasarlos al servidor de Google como parte de una solicitud HTTP.

Kotlin

val url = URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=$your_api_key")
val conn = url.openConnection() as HttpURLConnection
conn.apply {
    addRequestProperty("client_id", your client id)
    addRequestProperty("client_secret", your client secret)
    setRequestProperty("Authorization", "OAuth $token")
}

Java

URL url = new URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=" + your_api_key);
URLConnection conn = (HttpURLConnection) url.openConnection();
conn.addRequestProperty("client_id", your client id);
conn.addRequestProperty("client_secret", your client secret);
conn.setRequestProperty("Authorization", "OAuth " + token);

Si la solicitud muestra un código de error de HTTP 401, significa que se rechazó el token. Como se mencionó en la última sección, el motivo más común es que el token venció. La solución es simple: llama a AccountManager.invalidateAuthToken() y repite el proceso de adquisición de tokens una vez más.

Debido a que los tokens vencidos son muy comunes y solucionarlos es muy sencillo, muchas aplicaciones suponen que el token expiró incluso antes de solicitarlo. Si renovar un token es una operación económica para tu servidor, te recomendamos llamar a AccountManager.invalidateAuthToken() antes de la primera llamada a AccountManager.getAuthToken() y no tener que solicitar un token de autenticación dos veces.