Autenticar para os serviços OAuth2

Diagrama da lógica do token de autenticação
Figura 1. Procedimento para ter um token de autenticação válido do gerente de contas do Android.

Para acessar um serviço on-line com segurança, os usuários precisam passar por autenticação. Eles têm que fornecer uma comprovação de identidade. Para apps que acessam serviços de terceiros, o problema de segurança é ainda mais complicado. O usuário precisa não só ser autenticado para acessar o serviço, como também o app precisa estar autorizado a agir em nome do usuário.

A maneira padrão do setor de lidar com autenticação para serviços de terceiros é o protocolo OAuth2. O OAuth2 fornece um valor único, chamado de token de autenticação, que representa tanto a identidade do usuário quanto a autorização do aplicativo para agir em nome do usuário. Esta lição mostra como se conectar a um servidor do Google compatível com o OAuth2. Embora os serviços do Google sejam usados como exemplo, as técnicas demonstradas funcionarão em qualquer serviço que ofereça compatibilidade com o protocolo OAuth2.

Usar o OAuth2 é bom para:

  • receber permissão do usuário para acessar um serviço on-line usando a conta dele;
  • autenticar um serviço on-line em nome do usuário;
  • lidar com erros de autenticação.

Coletar informações

Para começar a usar o OAuth2, você precisa saber algumas informações sobre a API que está tentando acessar:

  • O URL do serviço que você quer acessar.
  • O escopo de autenticação, que é uma string que define o tipo específico de acesso que seu app está pedindo. Por exemplo, o escopo de autenticação para acesso somente leitura ao Google Tarefas é View your tasks, enquanto o escopo de autenticação para acesso de leitura/gravação ao Google Tarefas é Manage your tasks.
  • Um ID do cliente e uma chave secreta do cliente, que são strings que identificam seu app para o serviço. É necessário receber essas strings diretamente do proprietário do serviço. O Google tem um sistema de autoatendimento para receber IDs e chaves secretas do cliente. O artigo Autorização ou uso de APIs REST explica como usar esse sistema para receber esses valores e usá-los com a API Google Tasks.

Solicitar a permissão de acesso à Internet

Para apps voltados ao Android 6.0 (API de nível 23) e versões mais recentes, o método getAuthToken() não requer nenhuma permissão. Para realizar operações no token, no entanto, você precisa adicionar a permissão INTERNET ao seu arquivo de manifesto, como mostrado no seguinte snippet de código:

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

Solicitar um token de autenticação

Para receber o token, chame AccountManager.getAuthToken().

Cuidado: como algumas operações de conta podem envolver a comunicação de rede, a maioria dos métodos AccountManager são assíncronos. Isso significa que, em vez de fazer toda sua autenticação funcionar em uma função, você precisa implementá-la como uma série de callbacks.

O snippet a seguir mostra como trabalhar com uma série de callbacks para receber o 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
    

Neste exemplo, OnTokenAcquired é uma classe que implementa o AccountManagerCallback. AccountManager chama run() em OnTokenAcquired com um AccountManagerFuture que contém um Bundle. Se a chamada é concluída, o token está dentro do Bundle.

Veja como conseguir o token do 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);
            ...
        }
    }
    

Se tudo correr bem, o Bundle terá um token válido na chave KEY_AUTHTOKEN e estará tudo pronto para você continuar. No entanto, nem sempre é tão simples…

Solicitar um token de autenticação… de novo

Sua primeira solicitação de token de autenticação pode falhar por vários motivos:

  • Um erro no dispositivo ou na rede causado a falha do AccountManager.
  • O usuário decidiu não conceder ao seu app acesso à conta.
  • As credenciais da conta armazenadas não são suficientes para ter acesso a ela.
  • O token de autenticação armazenado em cache expirou.

Os aplicativos podem lidar com os dois primeiros casos de forma trivial, geralmente exibindo uma mensagem de erro para o usuário. Se a rede estiver inativa ou o usuário decidir não conceder acesso, não haverá muita coisa que seu aplicativo possa fazer a respeito. Os dois últimos casos são um pouco mais complicados, porque espera-se que os aplicativos com um bom comportamento lidem com essas falhas automaticamente.

O terceiro caso de falha (credenciais insuficientes) é comunicado por meio do Bundle que você recebe no seu AccountManagerCallback (OnTokenAcquired do exemplo anterior). Se o Bundle inclui um Intent na chave KEY_INTENT, o autenticador está dizendo que precisa interagir diretamente com o usuário antes que ele possa fornecer um token válido.

Pode haver muitas razões para o autenticador retornar um Intent. Talvez seja a primeira vez que o usuário fez login nessa conta. Talvez a conta do usuário tenha expirado e ele precise fazer login novamente, ou as credenciais armazenadas estejam incorretas. Talvez a conta exija uma autenticação de dois fatores ou precise ativar a câmera para fazer uma verificação de retina. Não importa qual seja o motivo. Se você quiser um token válido, será necessário disparar o Intent para recebê-lo.

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;
            }
        }
    }
    

Observe que o exemplo usa startActivityForResult(), então você pode capturar o resultado do Intent por meio da implementação de onActivityResult() na sua atividade. Isso é importante. Se você não capturar o resultado do Intent de resposta do autenticador, será impossível dizer se o usuário foi autenticado ou não. Se o resultado for RESULT_OK, o autenticador atualizou as credenciais armazenadas de modo que elas sejam suficientes para o nível de acesso que você solicitou. Chame AccountManager.getAuthToken() novamente para solicitar o novo token de autenticação.

O último caso, em que o token expirou, não é uma falha do AccountManager. A única maneira de descobrir se um token expirou ou não é entrar em contato com o servidor, e seria um desperdício para o AccountManager ficar sempre on-line para verificar o estado de todos os tokens. Portanto, essa é uma falha que só pode ser detectada quando um aplicativo como o seu tenta usar o token de autenticação para acessar um serviço on-line.

Conectar ao serviço on-line

O exemplo abaixo mostra como se conectar a um servidor do Google. Como o Google usa o protocolo OAuth2 padrão do setor para autenticar solicitações, as técnicas discutidas aqui são amplamente aplicáveis. No entanto, lembre-se de que cada servidor é diferente. Talvez você precise fazer pequenas modificações a essas instruções para considerar sua situação específica.

As APIs Google exigem que você forneça quatro valores a cada solicitação: a chave de API, o ID do cliente, a chave secreta do cliente e a chave de autenticação. Os três primeiros vêm do site Console de APIs do Google. O último é o valor de string que você recebeu chamando AccountManager.getAuthToken(). Você os transmite para o servidor do Google como parte de uma solicitação 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);
    

Se a solicitação retornar um código de erro HTTP 401, seu token foi negado. Conforme mencionado na última seção, o motivo mais comum para isso é que o token expirou. A solução é simples: chame AccountManager.invalidateAuthToken() e repita o procedimento de aquisição do token mais uma vez.

Como os tokens expirados são uma ocorrência comum e corrigi-los é muito fácil, vários aplicativos simplesmente supõem que o token expirou antes mesmo de solicitá-lo. Se a renovar um token for uma operação de baixo custo para seu servidor, você pode preferir chamar AccountManager.invalidateAuthToken() antes da primeira chamada para AccountManager.getAuthToken() e se livrar da necessidade de solicitar um token de autenticação duas vezes.