OAuth 2.0 e a biblioteca de cliente OAuth do Google para Java

Informações gerais

Objetivo: este documento descreve as funções genéricas do OAuth 2.0 oferecidas pela biblioteca de cliente do OAuth do Google para Java. É possível usar essas funções para autenticação e autorização de qualquer serviço da Internet.

Para instruções sobre como usar o GoogleCredential para fazer a autorização do OAuth 2.0 com os serviços do Google, consulte Como usar o OAuth 2.0 com a biblioteca de cliente das APIs do Google para Java.

Resumo:o OAuth 2.0 é uma especificação padrão para permitir que os usuários finais autorizem com segurança um aplicativo cliente a acessar recursos protegidos do lado do servidor. Além disso, a especificação do token do portador OAuth 2.0 explica como acessar esses recursos protegidos usando um token de acesso concedido durante o processo de autorização do usuário final.

Para mais detalhes, consulte a documentação do Javadoc para os seguintes pacotes:

Registro do cliente

Antes de usar a biblioteca de cliente do Google OAuth para Java, é provável que você precise registrar o aplicativo em um servidor de autorização para receber um ID e uma chave secreta do cliente. Para informações gerais sobre esse processo, consulte a especificação do registro do cliente.

Credencial e armazenamento de credenciais

Credencial é uma classe auxiliar OAuth 2.0 segura para linhas de execução que serve para acessar recursos protegidos usando um token de acesso. Ao usar um token de atualização, Credential também atualiza o token de acesso quando o token de acesso expira usando o token de atualização. Por exemplo, se você já tiver um token de acesso, poderá fazer uma solicitação desta maneira:

  public static HttpResponse executeGet(
      HttpTransport transport, JsonFactory jsonFactory, String accessToken, GenericUrl url)
      throws IOException {
    Credential credential =
        new Credential(BearerToken.authorizationHeaderAccessMethod()).setAccessToken(accessToken);
    HttpRequestFactory requestFactory = transport.createRequestFactory(credential);
    return requestFactory.buildGetRequest(url).execute();
  }

A maioria dos aplicativos precisa manter o token de acesso da credencial e o token de atualização para evitar um redirecionamento futuro para a página de autorização no navegador. A implementação de CredentialStore dessa biblioteca foi descontinuada e será removida em versões futuras. A alternativa é usar as interfaces DataStoreFactory e DataStore com StoredCredential, que são fornecidas pela biblioteca de cliente HTTP do Google para Java.

Você pode usar uma das seguintes implementações fornecidas pela biblioteca:

Usuários do Google App Engine:

O uso de AppEngineCredentialStore foi descontinuado e está sendo removido.

Recomendamos que você use o AppEngineDataStoreFactory com StoredCredential. Se você tiver credenciais armazenadas da maneira antiga, use os métodos auxiliares MigrateTo(AppEngineDataStoreFactory) ou MigrateTo(DataStore) adicionados para migrar.

Use DataStoreCredentialRefreshListener e defina-o para a credencial usando GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener).

Fluxo do código de autorização

Use o fluxo do código de autorização para permitir que o usuário final conceda ao seu aplicativo acesso aos dados protegidos. O protocolo para esse fluxo é definido na especificação de concessão de código de autorização.

Esse fluxo é implementado usando AuthorizationCodeFlow. Essas etapas são:

Como alternativa, se você não estiver usando AuthorizationCodeFlow, poderá usar as classes de nível inferior:

Fluxo do código de autorização do RecyclerView

Ela fornece classes auxiliares de webinar para simplificar significativamente o fluxo do código de autorização para casos de uso básicos. Basta fornecer subclasses concretas de AbstractAuthorizationCodeServlet e AbstractAuthorizationCodeCallbackServlet (de google-oauth-client-servlet) e adicioná-las ao seu arquivo web.xml. Observe que você ainda precisa cuidar do login do usuário do seu aplicativo da Web e extrair um ID do usuário.

Exemplo de código:

public class ServletSample extends AbstractAuthorizationCodeServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    // do stuff
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new NetHttpTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialDataStore(
            StoredCredential.getDefaultDataStore(
                new FileDataStoreFactory(new File("datastoredir"))))
        .build();
  }

  @Override
  protected String getUserId(HttpServletRequest req) throws ServletException, IOException {
    // return user ID
  }
}

public class ServletCallbackSample extends AbstractAuthorizationCodeCallbackServlet {

  @Override
  protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential)
      throws ServletException, IOException {
    resp.sendRedirect("/");
  }

  @Override
  protected void onError(
      HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse)
      throws ServletException, IOException {
    // handle error
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new NetHttpTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialDataStore(
            StoredCredential.getDefaultDataStore(
                new FileDataStoreFactory(new File("datastoredir"))))
        .build();
  }

  @Override
  protected String getUserId(HttpServletRequest req) throws ServletException, IOException {
    // return user ID
  }
}

Fluxo do código de autorização do Google App Engine

O fluxo do código de autorização no App Engine é quase idêntico ao fluxo de código de autorização do json, exceto pelo fato de que podemos aproveitar a API User Java do Google App Engine. O usuário precisa estar conectado para que a API User Java seja ativada. Para saber como redirecionar os usuários a uma página de login, caso eles ainda não estejam conectados, consulte Segurança e autenticação (em web.xml).

A principal diferença do caso do webinar é que você fornece subclasses concretas de AbstractAppEngineAuthorizationCodeServlet e AbstractAppEngineAuthorizationCodeCallbackServlet (de google-oauth-client-appengine). Elas estendem as classes de waypoint abstratas e implementam o método getUserId para você usando a API User Java. AppEngineDataStoreFactory (da biblioteca de cliente HTTP do Google para Java) é uma boa opção para manter a credencial usando a API Google App Engine Data Store.

Exemplo de código:

public class AppEngineSample extends AbstractAppEngineAuthorizationCodeServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    // do stuff
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new UrlFetchTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialStore(
            StoredCredential.getDefaultDataStore(AppEngineDataStoreFactory.getDefaultInstance()))
        .build();
  }
}

public class AppEngineCallbackSample extends AbstractAppEngineAuthorizationCodeCallbackServlet {

  @Override
  protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential)
      throws ServletException, IOException {
    resp.sendRedirect("/");
  }

  @Override
  protected void onError(
      HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse)
      throws ServletException, IOException {
    // handle error
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new UrlFetchTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialStore(
            StoredCredential.getDefaultDataStore(AppEngineDataStoreFactory.getDefaultInstance()))
        .build();
  }
}

Fluxo do código de autorização de linha de comando

Exemplo de código simplificado retirado de dailymotion-cmdline-sample:

/** Authorizes the installed application to access user's protected data. */
private static Credential authorize() throws Exception {
  OAuth2ClientCredentials.errorIfNotSpecified();
  // set up authorization code flow
  AuthorizationCodeFlow flow = new AuthorizationCodeFlow.Builder(BearerToken
      .authorizationHeaderAccessMethod(),
      HTTP_TRANSPORT,
      JSON_FACTORY,
      new GenericUrl(TOKEN_SERVER_URL),
      new ClientParametersAuthentication(
          OAuth2ClientCredentials.API_KEY, OAuth2ClientCredentials.API_SECRET),
      OAuth2ClientCredentials.API_KEY,
      AUTHORIZATION_SERVER_URL).setScopes(Arrays.asList(SCOPE))
      .setDataStoreFactory(DATA_STORE_FACTORY).build();
  // authorize
  LocalServerReceiver receiver = new LocalServerReceiver.Builder().setHost(
      OAuth2ClientCredentials.DOMAIN).setPort(OAuth2ClientCredentials.PORT).build();
  return new AuthorizationCodeInstalledApp(flow, receiver).authorize("user");
}

private static void run(HttpRequestFactory requestFactory) throws IOException {
  DailyMotionUrl url = new DailyMotionUrl("https://api.dailymotion.com/videos/favorites");
  url.setFields("id,tags,title,url");

  HttpRequest request = requestFactory.buildGetRequest(url);
  VideoFeed videoFeed = request.execute().parseAs(VideoFeed.class);
  ...
}

public static void main(String[] args) {
  ...
  DATA_STORE_FACTORY = new FileDataStoreFactory(DATA_STORE_DIR);
  final Credential credential = authorize();
  HttpRequestFactory requestFactory =
      HTTP_TRANSPORT.createRequestFactory(new HttpRequestInitializer() {
        @Override
        public void initialize(HttpRequest request) throws IOException {
          credential.initialize(request);
          request.setParser(new JsonObjectParser(JSON_FACTORY));
        }
      });
  run(requestFactory);
  ...
}

Fluxo de cliente baseado em navegador

Estas são as etapas típicas do fluxo de cliente baseado no navegador especificado na especificação de concessão implícita:

  • Usando BrowserClientRequestUrl, redirecione o navegador do usuário final para a página de autorização em que o usuário final pode conceder ao seu aplicativo acesso aos dados protegidos.
  • Use um aplicativo JavaScript para processar o token de acesso encontrado no fragmento de URL no URI de redirecionamento registrado no servidor de autorização.

Exemplo de uso para um aplicativo da Web:

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
  String url = new BrowserClientRequestUrl(
      "https://server.example.com/authorize", "s6BhdRkqt3").setState("xyz")
      .setRedirectUri("https://client.example.com/cb").build();
  response.sendRedirect(url);
}

Como detectar um token de acesso expirado

De acordo com a especificação do portador OAuth 2.0, quando o servidor é chamado para acessar um recurso protegido com um token de acesso expirado, o servidor normalmente responde com um código de status HTTP 401 Unauthorized como este:

   HTTP/1.1 401 Unauthorized
   WWW-Authenticate: Bearer realm="example",
                     error="invalid_token",
                     error_description="The access token expired"

No entanto, parece haver muita flexibilidade na especificação. Para detalhes, consulte a documentação do provedor de OAuth 2.0.

Outra abordagem possível é verificar o parâmetro expires_in na resposta do token de acesso. Isso especifica a vida útil em segundos do token de acesso concedido, que normalmente é uma hora. No entanto, o token de acesso pode não expirar realmente no final desse período e o servidor pode continuar permitindo o acesso. É por isso que normalmente recomendamos aguardar um código de status 401 Unauthorized, em vez de presumir que o token expirou com base no tempo decorrido. Como alternativa, tente atualizar um token de acesso pouco antes de ele expirar e, se o servidor de token estiver indisponível, continue usando o token de acesso até receber um 401. Essa é a estratégia usada por padrão em Credential.

Outra opção é pegar um novo token de acesso antes de cada solicitação. Porém, isso exige sempre uma solicitação HTTP extra para o servidor de token, então é provável que essa seja uma opção ruim em termos de velocidade e uso da rede. O ideal é armazenar o token de acesso em um armazenamento seguro e permanente para minimizar as solicitações de novos tokens de acesso a um aplicativo. No entanto, para aplicativos instalados, o armazenamento seguro é um problema difícil.

Um token de acesso pode se tornar inválido por outros motivos além da expiração. Por exemplo, se o usuário revogou o token explicitamente, verifique se o código de tratamento de erros é robusto. Depois de detectar que um token não é mais válido (por exemplo, se ele expirou ou foi revogado), remova o token de acesso do armazenamento. No Android, por exemplo, é preciso chamar AccountManager.invalidateAuthToken.