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. Você pode usar essas funções para autenticação e autorização em qualquer serviço da Internet.

Para ver instruções sobre como usar o GoogleCredential para fazer a autorização OAuth 2.0 com os Serviços do Google, consulte Como usar o OAuth 2.0 com a Biblioteca de Cliente da API do Google para Java (em inglês).

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.

Veja mais detalhes na documentação do Javadoc para os seguintes pacotes:

Registro de cliente

Antes de usar a biblioteca de cliente OAuth do Google para Java, é provável que você precise registrar seu aplicativo com 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 sobre o registro do cliente.

Credenciais e armazenamento de credenciais

A credencial é uma classe auxiliar OAuth 2.0 segura para linhas de execução 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 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 o token de atualização da credencial para evitar um redirecionamento futuro para a página de autorização no navegador. A implementação do CredentialStore nesta biblioteca foi descontinuada e precisa ser removida em versões futuras. A alternativa é usar as interfaces de 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:

AppEngineCredentialStore foi descontinuado e está sendo removido.

Recomendamos que você use o AppEngineDataStoreFactory com o StoredCredential. Se você tiver credenciais armazenadas da maneira antiga, poderá usar 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 do código de autorização para permitir que o usuário final conceda ao seu aplicativo acesso aos dados protegidos dele. O protocolo desse fluxo é especificado na especificação da 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 o AuthorizationCodeFlow, use as classes de nível inferior:

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

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 (de google-oauth-client- servlet) e adicioná-las ao seu arquivo web.xml. Observe que você ainda precisa cuidar do login do usuário no 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 servlet, exceto pelo fato de podermos aproveitar a API Users Java do Google App Engine. O usuário precisa fazer login para ativar a API de usuários do Java. Para informações sobre como redirecionar usuários para uma página de login caso ainda não tenham feito login, consulte Segurança e autenticação (em web.xml).

A principal diferença do caso do servlet é que você fornece subclasses concretas de AbstractAppEngineAuthorizationCodeServlet e AbstractAppEngineAuthorizationCodeCallbackServlet (de google-oauth-client-appengine). Elas estendem as classes abstratas de servlet e implementam o método getUserId para você usando a API de usuários do Java. AppEngineDataStoreFactory (da biblioteca de cliente HTTP do Google para Java é uma boa opção para persistir 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 da 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 clientes baseados no 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 onde o usuário final pode conceder ao seu aplicativo acesso aos dados protegidos dele.
  • 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 do OAuth 2.0, quando o servidor é chamado para acessar um recurso protegido com um token de acesso expirado, o servidor geralmente responde com um código de status HTTP 401 Unauthorized, como o seguinte:

   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 saber mais, consulte a documentação do provedor OAuth 2.0.

Outra abordagem é verificar o parâmetro expires_in na resposta do token de acesso. Especifica o ciclo de vida em segundos do token de acesso concedido, que geralmente é 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, em geral, recomendamos aguardar um código de status 401 Unauthorized em vez de presumir que o token expirou com base no tempo decorrido. Você também pode tentar atualizar um token de acesso pouco antes de ele expirar. Se o servidor de token não estiver disponível, continue usando o token de acesso até receber um 401. Essa é a estratégia usada por padrão em Credencial.

Outra opção é pegar um novo token de acesso antes de cada solicitação, mas isso exige uma solicitação HTTP extra para o servidor de token todas as vezes, portanto, é 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 armazenamento seguro e persistente para minimizar as solicitações de um aplicativo de novos tokens de acesso. 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 explicitamente o token, 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, você precisa remover o token de acesso do armazenamento. No Android, por exemplo, você precisa chamar AccountManager.invalidateAuthToken.