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

Visão geral

Finalidade: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 em qualquer serviço da Internet.

Para instruções sobre como usar o GoogleCredential para autorizar o 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 de portador do 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 OAuth do Google para Java, provavelmente você precisará registrar seu 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 de clientes.

Armazenamento de credenciais e credenciais

Credential é uma classe auxiliar do OAuth 2.0 que oferece proteção contra conflitos de threads para acessar recursos protegidos usando um token de acesso. Ao usar um token de atualização, o Credential também atualiza o token de acesso quando ele expira. Por exemplo, se você já tiver um token de acesso, faça uma solicitação da seguinte 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 e de atualização da credencial para evitar um redirecionamento futuro para a página de autorização no navegador. A implementação CredentialStore nesta 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 (links em inglês).

É possível usar uma das seguintes implementações fornecidas pela biblioteca:

Usuários do Google App Engine:

O 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 adicionados migrateTo(AppEngineDataStoreFactory) ou migrateTo(DataStore) 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 de código de autorização para permitir que o usuário final conceda ao seu aplicativo acesso aos dados protegidos. O protocolo desse fluxo é definido na especificação da concessão de código de autorização.

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

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

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

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

Código de exemplo:

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 do código de autorização do servlet, exceto que podemos aproveitar a API Users Java do Google App Engine. O usuário precisa estar conectado para que a API Users Java seja ativada. Para informações sobre como redirecionar usuários a uma página de login caso ainda não tenham feito isso, consulte Segurança e autenticação (em web.xml).

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

Código de exemplo:

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 da linha de comando

Exemplo de código simplificado retirado de dailymotion-cmdline-sample (link em inglês):

/** 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 em 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 ele 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 com o 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 do OAuth 2.0, quando o servidor é chamado para acessar um recurso protegido com um token de acesso expirado, ele 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 mais detalhes, consulte a documentação do provedor OAuth 2.0.

Uma abordagem alternativa é 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 é de uma hora. No entanto, o token de acesso pode não expirar 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 do vencimento. 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, mas isso requer uma solicitação HTTP extra para o servidor de token todas as vezes, portanto, provavelmente é uma escolha 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 do aplicativo. No entanto, para aplicativos instalados, o armazenamento seguro é um problema difícil.

Um token de acesso pode se tornar inválido por motivos diferentes da expiração, por exemplo, se o usuário tiver revogado o token explicitamente. Portanto, verifique se o código de processamento de erros é robusto. Depois de detectar que um token não é mais válido, por exemplo, se ele tiver expirado ou sido revogado, remova o token de acesso do armazenamento. No Android, por exemplo, é necessário chamar AccountManager.invalidateAuthToken.