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

Si desean 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. El usuario no solo debe estar autenticado para acceder al servicio, sino que la app también debe estar autorizada para actuar en nombre del usuario.

El protocolo OAuth2 es la manera estándar de la industria para lograr la autenticación de servicios de terceros. OAuth2 proporciona un valor único, denominado token de autenticación, que representa tanto la identidad del usuario como la autorización de la app para actuar en nombre de este. En esta lección, se muestra la conexión con 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 correctamente el protocolo OAuth2.

El uso de OAuth2 es útil para lo siguiente:

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

Cómo recopilar información

Para comenzar a usar OAuth2, debes saber algunas cuestiones sobre la API a la que estás intentando acceder:

  • La URL del servicio al que deseas acceder.
  • El alcance de autenticación, que es una string 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 ID y secretos de clientes. En el artículo Cómo autorizar y usar las API de REST, se explica cómo usar este sistema a fin de obtener estos valores para usarlos con la API de Google Tasks.

Cómo solicitar el permiso de Internet

Para las apps que se orientan a Android 6.0 (API nivel 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 de 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 involucrar comunicaciones 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, deberás implementarlo 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 se encuentra 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 funciona según lo esperado, el Bundle contendrá un token válido en la clave KEY_AUTHTOKEN y podrás continuar. Sin embargo, en ocasiones puede haber algún problema…

Cómo solicitar un token de autenticación… otra vez

Es posible que la primera solicitud de un token de autenticación falle debido a varias razones:

  • 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 apps pueden manejar los dos primeros casos de manera simple, en general con solo mostrar un mensaje de error al usuario. Si la red no funciona o el usuario decidió no otorgar acceso, tu app no podrá hacer mucho al respecto. Los últimos dos casos son un poco más complicados, ya que se espera que las apps con buen comportamiento manejen automáticamente esas fallas.

El tercer caso de falla, por credenciales insuficientes, se comunica mediante Bundle que recibes en tu AccountManagerCallback (OnTokenAcquired del ejemplo anterior). Si Bundle incluye una Intent en la clave KEY_INTENT, el autenticador te indica que debes interactuar directamente con el usuario para que te pueda dar 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. Tal vez la cuenta del usuario haya caducado y deba acceder nuevamente, o quizás las 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 del Intent mediante la implementación de onActivityResult() en tu propia actividad. Este paso es muy importante. Si no capturas el resultado del Intent de la respuesta del autenticador, será imposible saber si el usuario se autenticó correctamente o no. Si el resultado es RESULT_OK, 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() otra vez para solicitar el nuevo token de autenticación.

El último caso, en el que caducó el token, en realidad, no es un error de AccountManager. La única manera de descubrir si un token caducó o no 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. Este error solo se puede detectar cuando una app 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. Debido a que Google usa el protocolo OAuth2 estándar de la industria para autenticar las solicitudes, las técnicas discutidas aquí son ampliamente aplicables. Sin embargo, debes tener en cuenta que cada servidor es diferente. Es posible que debas hacer pequeños ajustes en estas instrucciones para justificar tu situación específica.

Para las API de Google, debes proporcionar cuatro valores con cada solicitud: la clave de API, el ID de cliente, el secreto de cliente y la clave de autenticación. Los tres primeros se toman del sitio web de la Consola de API de Google. El último es el valor de string que obtuviste mediante un llamado 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 HTTP 401, significa que se rechazó el token. Como se mencionó en la última sección, el motivo más común de esto es que el token caducó. La solución es simple: llama a AccountManager.invalidateAuthToken() y repite los pasos para la adquisición del token.

Debido a que los tokens caducados son muy comunes y arreglarlos es tan fácil, muchas apps suponen que el token caducó aún antes de pedirlo. Si la renovación de un token es una operación económica para tu servidor, tal vez prefieras llamar a AccountManager.invalidateAuthToken() antes de la primera llamada a AccountManager.getAuthToken(), con lo que no deberías solicitar un token de autenticación dos veces.