Gerenciador de callback de autorização de build

Este documento explica como implementar um gerenciador de callback de autorização OAuth 2.0 usando servlets Java em um exemplo de aplicativo da Web que mostra as tarefas do usuário usando a API Google Tasks. O aplicativo de exemplo primeiro solicita autorização para acessar as Tarefas do Google do usuário e depois mostra as tarefas na lista padrão.

Público-alvo

Este documento é adaptado para pessoas que conhecem a arquitetura de aplicativos da Web Java e J2EE. Recomendamos algum conhecimento do fluxo de autorização do OAuth 2.0.

Conteúdo

Para ter um exemplo totalmente funcional, várias etapas são necessárias:

Declarar mapeamentos de servlet no arquivo web.xml

Vamos usar dois servlets no aplicativo:

  • PrintTasksTitlesServlet (mapeado para /): o ponto de entrada do aplicativo que processa a autenticação do usuário e exibe as tarefas dele.
  • OAuthCodeCallbackHandlerServlet (mapeado para /oauth2callback): o callback OAuth 2.0 que processa a resposta do endpoint de autorização do OAuth.

Confira abaixo o arquivo web.xml, que mapeia esses dois servlets para URLs no nosso aplicativo:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

 <servlet>
   <servlet-name>PrintTasksTitles</servlet-name>
   <servlet-class>com.google.oauthsample.PrintTasksTitlesServlet</servlet-class>
 </servlet>

 <servlet-mapping>
   <servlet-name>PrintTasksTitles</servlet-name>
   <url-pattern>/</url-pattern>
 </servlet-mapping>

 <servlet>
   <servlet-name>OAuthCodeCallbackHandlerServlet</servlet-name>
   <servlet-class>com.google.oauthsample.OAuthCodeCallbackHandlerServlet</servlet-class>
 </servlet>

 <servlet-mapping>
   <servlet-name>OAuthCodeCallbackHandlerServlet</servlet-name>
   <url-pattern>/oauth2callback</url-pattern>
 </servlet-mapping>

</web-app>
/WEB-INF/web.xml

Autenticar os usuários no seu sistema e solicitar autorização para acessar as tarefas

O usuário entra no aplicativo pelo URL raiz '/', que é mapeado para o servlet PrintTaskListsTitlesServlet. Nesse servlet, as seguintes tarefas são realizadas:

  • Verifica se o usuário está autenticado no sistema
  • Se o usuário não estiver autenticado, ele será redirecionado para a página de autenticação.
  • Se o usuário for autenticado, vamos verificar se já temos um token de atualização no armazenamento de dados, que é processado pelo OAuthTokenDao abaixo. Se não houver um token de atualização armazenado para o usuário, isso significa que ele ainda não concedeu a autorização do aplicativo para acessar as tarefas. Nesse caso, o usuário é redirecionado para o endpoint de autorização do OAuth 2.0 do Google.
Confira abaixo uma forma de implementar isso:

package com.google.oauthsample;

import ...

/**
 * Simple sample Servlet which will display the tasks in the default task list of the user.
 */
@SuppressWarnings("serial")
public class PrintTasksTitlesServlet extends HttpServlet {

  /**
   * The OAuth Token DAO implementation, used to persist the OAuth refresh token.
   * Consider injecting it instead of using a static initialization. Also we are
   * using a simple memory implementation as a mock. Change the implementation to
   * using your database system.
   */
  public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl();

  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    // Getting the current user
    // This is using App Engine's User Service but you should replace this to
    // your own user/login implementation
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();

    // If the user is not logged-in it is redirected to the login service, then back to this page
    if (user == null) {
      resp.sendRedirect(userService.createLoginURL(getFullRequestUrl(req)));
      return;
    }

    // Checking if we already have tokens for this user in store
    AccessTokenResponse accessTokenResponse = oauthTokenDao.getKeys(user.getEmail());

    // If we don't have tokens for this user
    if (accessTokenResponse == null) {
      OAuthProperties oauthProperties = new OAuthProperties();
      // Redirect to the Google OAuth 2.0 authorization endpoint
      resp.sendRedirect(new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(),
          OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties
              .getScopesAsString()).build());
      return;
    }
  }

  /**
   * Construct the request's URL without the parameter part.
   *
   * @param req the HttpRequest object
   * @return The constructed request's URL
   */
  public static String getFullRequestUrl(HttpServletRequest req) {
    String scheme = req.getScheme() + "://";
    String serverName = req.getServerName();
    String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort();
    String contextPath = req.getContextPath();
    String servletPath = req.getServletPath();
    String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo();
    String queryString = (req.getQueryString() == null) ? "" : "?" + req.getQueryString();
    return scheme + serverName + serverPort + contextPath + servletPath + pathInfo + queryString;
  }
}
Arquivo PrintTasksTitlesServlet.java

Observação: a implementação acima usa algumas bibliotecas do App Engine, que são usadas para simplificar. Se você estiver desenvolvendo para outra plataforma, implemente novamente a interface UserService, que processa a autenticação do usuário.

O aplicativo usa um DAO para persistir e acessar os tokens de autorização do usuário. Confira abaixo a interface OAuthTokenDao e uma implementação simulada (na memória) OAuthTokenDaoMemoryImpl usadas neste exemplo:

package com.google.oauthsample;

import com.google.api.client.auth.oauth2.draft10.AccessTokenResponse;

/**
 * Allows easy storage and access of authorization tokens.
 */
public interface OAuthTokenDao {

  /**
   * Stores the given AccessTokenResponse using the {@code username}, the OAuth
   * {@code clientID} and the tokens scopes as keys.
   *
   * @param tokens The AccessTokenResponse to store
   * @param userName The userName associated wit the token
   */
  public void saveKeys(AccessTokenResponse tokens, String userName);

  /**
   * Returns the AccessTokenResponse stored for the given username, clientId and
   * scopes. Returns {@code null} if there is no AccessTokenResponse for this
   * user and scopes.
   *
   * @param userName The username of which to get the stored AccessTokenResponse
   * @return The AccessTokenResponse of the given username
   */
  public AccessTokenResponse getKeys(String userName);
}
Arquivo OAuthTokenDao.java
package com.google.oauthsample;

import com.google.api.client.auth.oauth2.draft10.AccessTokenResponse;
...

/**
 * Quick and Dirty memory implementation of {@link OAuthTokenDao} based on
 * HashMaps.
 */
public class OAuthTokenDaoMemoryImpl implements OAuthTokenDao {

  /** Object where all the Tokens will be stored */
  private static Map<String, AccessTokenResponse> tokenPersistance = new HashMap<String, AccessTokenResponse>();

  public void saveKeys(AccessTokenResponse tokens, String userName) {
    tokenPersistance.put(userName, tokens);
  }

  public AccessTokenResponse getKeys(String userName) {
    return tokenPersistance.get(userName);
  }
}
Arquivo OAuthTokenDaoMemoryImpl.java

Além disso, as credenciais do OAuth 2.0 para o aplicativo são armazenadas em um arquivo de propriedades. Como alternativa, você pode simplesmente ter esses valores como uma constante em uma das suas classes Java, mas aqui está a classe OAuthProperties e o arquivo oauth.properties que está sendo usado no exemplo:

package com.google.oauthsample;

import ...

/**
 * Object representation of an OAuth properties file.
 */
public class OAuthProperties {

  public static final String DEFAULT_OAUTH_PROPERTIES_FILE_NAME = "oauth.properties";

  /** The OAuth 2.0 Client ID */
  private String clientId;

  /** The OAuth 2.0 Client Secret */
  private String clientSecret;

  /** The Google APIs scopes to access */
  private String scopes;

  /**
   * Instantiates a new OauthProperties object reading its values from the
   * {@code OAUTH_PROPERTIES_FILE_NAME} properties file.
   *
   * @throws IOException IF there is an issue reading the {@code propertiesFile}
   * @throws OauthPropertiesFormatException If the given {@code propertiesFile}
   *           is not of the right format (does not contains the keys {@code
   *           clientId}, {@code clientSecret} and {@code scopes})
   */
  public OAuthProperties() throws IOException {
    this(OAuthProperties.class.getResourceAsStream(DEFAULT_OAUTH_PROPERTIES_FILE_NAME));
  }

  /**
   * Instantiates a new OauthProperties object reading its values from the given
   * properties file.
   *
   * @param propertiesFile the InputStream to read an OAuth Properties file. The
   *          file should contain the keys {@code clientId}, {@code
   *          clientSecret} and {@code scopes}
   * @throws IOException IF there is an issue reading the {@code propertiesFile}
   * @throws OAuthPropertiesFormatException If the given {@code propertiesFile}
   *           is not of the right format (does not contains the keys {@code
   *           clientId}, {@code clientSecret} and {@code scopes})
   */
  public OAuthProperties(InputStream propertiesFile) throws IOException {
    Properties oauthProperties = new Properties();
    oauthProperties.load(propertiesFile);
    clientId = oauthProperties.getProperty("clientId");
    clientSecret = oauthProperties.getProperty("clientSecret");
    scopes = oauthProperties.getProperty("scopes");
    if ((clientId == null) || (clientSecret == null) || (scopes == null)) {
      throw new OAuthPropertiesFormatException();
    }
  }

  /**
   * @return the clientId
   */
  public String getClientId() {
    return clientId;
  }

  /**
   * @return the clientSecret
   */
  public String getClientSecret() {
    return clientSecret;
  }

  /**
   * @return the scopes
   */
  public String getScopesAsString() {
    return scopes;
  }

  /**
   * Thrown when the OAuth properties file was not at the right format, i.e not
   * having the right properties names.
   */
  @SuppressWarnings("serial")
  public class OAuthPropertiesFormatException extends RuntimeException {
  }
}
Arquivo OAuthProperties.java

Confira abaixo o arquivo oauth.properties, que contém as credenciais do OAuth 2.0 do seu aplicativo. Você precisa mudar os valores abaixo por conta própria.

# Client ID and secret. They can be found in the APIs console.
clientId=1234567890.apps.googleusercontent.com
clientSecret=aBcDeFgHiJkLmNoPqRsTuVwXyZ
# API scopes. Space separated.
scopes=https://www.googleapis.com/auth/tasks
Arquivo oauth.properties

O ID e a chave secreta do cliente OAuth 2.0 identificam seu aplicativo e permitem que a API Tasks aplique filtros e regras de cota definidos para ele. O ID do cliente e o segredo podem ser encontrados no Console de APIs do Google. No console, você vai precisar:

  • Crie ou selecione um projeto.
  • Ative a API Tasks definindo o status dela como ATIVADO na lista de serviços.
  • Em Acesso à API, crie um ID do cliente do OAuth 2.0, se ainda não tiver sido criado.
  • Verifique se o URL do gerenciador de callback de código OAuth 2.0 do projeto está registrado/na lista de permissões nos URIs de redirecionamento. Por exemplo, neste projeto de exemplo, você precisa registrar https://www.example.com/oauth2callback se o aplicativo da Web for veiculado no domínio https://www.example.com.

URI de redirecionamento no console de APIs
URI de redirecionamento no console de APIs

Detectar o código de autorização do endpoint de autorização do Google

No caso em que o usuário ainda não autorizou o aplicativo a acessar as tarefas e, portanto, foi redirecionado para o endpoint de autorização OAuth 2.0 do Google, uma caixa de diálogo de autorização do Google é mostrada ao usuário solicitando que ele conceda ao aplicativo acesso às tarefas:

Caixa de diálogo de autorização do Google
Caixa de diálogo de autorização do Google

Depois de conceder ou negar o acesso, o usuário será redirecionado de volta para o gerenciador de callback de código OAuth 2.0 especificado como um redirecionamento/callback ao criar o URL de autorização do Google:

new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(),
      OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties
          .getScopesAsString()).build()

O gerenciador de callback de código OAuth 2.0 (OAuthCodeCallbackHandlerServlet) processa o redirecionamento do endpoint OAuth 2.0 do Google. Há dois casos a serem processados:

  • O usuário concedeu acesso: analisa a solicitação para receber o código OAuth 2.0 dos parâmetros do URL
  • O usuário negou o acesso: mostra uma mensagem ao usuário

package com.google.oauthsample;

import ...

/**
 * Servlet handling the OAuth callback from the authentication service. We are
 * retrieving the OAuth code, then exchanging it for a refresh and an access
 * token and saving it.
 */
@SuppressWarnings("serial")
public class OAuthCodeCallbackHandlerServlet extends HttpServlet {

  /** The name of the Oauth code URL parameter */
  public static final String CODE_URL_PARAM_NAME = "code";

  /** The name of the OAuth error URL parameter */
  public static final String ERROR_URL_PARAM_NAME = "error";

  /** The URL suffix of the servlet */
  public static final String URL_MAPPING = "/oauth2callback";

  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    // Getting the "error" URL parameter
    String[] error = req.getParameterValues(ERROR_URL_PARAM_NAME);

    // Checking if there was an error such as the user denied access
    if (error != null && error.length > 0) {
      resp.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "There was an error: \""+error[0]+"\".");
      return;
    }
    // Getting the "code" URL parameter
    String[] code = req.getParameterValues(CODE_URL_PARAM_NAME);

    // Checking conditions on the "code" URL parameter
    if (code == null || code.length == 0) {
      resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "The \"code\" URL parameter is missing");
      return;
    }
  }

  /**
   * Construct the OAuth code callback handler URL.
   *
   * @param req the HttpRequest object
   * @return The constructed request's URL
   */
  public static String getOAuthCodeCallbackHandlerUrl(HttpServletRequest req) {
    String scheme = req.getScheme() + "://";
    String serverName = req.getServerName();
    String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort();
    String contextPath = req.getContextPath();
    String servletPath = URL_MAPPING;
    String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo();
    return scheme + serverName + serverPort + contextPath + servletPath + pathInfo;
  }
}
Arquivo OAuthCodeCallbackHandlerServlet.java

Trocar o código de autorização por um token de atualização e de acesso

Em seguida, o OAuthCodeCallbackHandlerServlet troca o código Auth 2.0 por tokens de atualização e de acesso, o persistindo no repositório de dados e redirecionando o usuário de volta ao URL PrintTaskListsTitlesServlet:

O código adicionado ao arquivo abaixo tem a sintaxe destacada, e o código já existente está esmaecido.

package com.google.oauthsample;

import ...

/**
 * Servlet handling the OAuth callback from the authentication service. We are
 * retrieving the OAuth code, then exchanging it for a refresh and an access
 * token and saving it.
 */
@SuppressWarnings("serial")
public class OAuthCodeCallbackHandlerServlet extends HttpServlet {

  /** The name of the Oauth code URL parameter */
  public static final String CODE_URL_PARAM_NAME = "code";

  /** The name of the OAuth error URL parameter */
  public static final String ERROR_URL_PARAM_NAME = "error";

  /** The URL suffix of the servlet */
  public static final String URL_MAPPING = "/oauth2callback";
/** O URL para redirecionar o usuário depois de processar o callback. Considere * salvar isso em um cookie antes de redirecionar os usuários para o URL de autorização * do Google se você tiver vários URLs possíveis para redirecionar as pessoas. */ public static final String REDIRECT_URL = "/"; /** A implementação do DAO do token OAuth. Considere injetá-lo em vez de usar * uma inicialização estática. Também estamos usando uma implementação de memória simples * como um modelo. Mude a implementação para usar seu sistema de banco de dados. */ public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl();
  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    // Getting the "error" URL parameter
    String[] error = req.getParameterValues(ERROR_URL_PARAM_NAME);

    // Checking if there was an error such as the user denied access
    if (error != null && error.length > 0) {
      resp.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "There was an error: \""+error[0]+"\".");
      return;
    }

    // Getting the "code" URL parameter
    String[] code = req.getParameterValues(CODE_URL_PARAM_NAME);

    // Checking conditions on the "code" URL parameter
    if (code == null || code.length == 0) {
      resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "The \"code\" URL parameter is missing");
      return;
    }
// Cria o URL de solicitação de entrada String requestUrl = getOAuthCodeCallbackHandlerUrl(req); // Troca o código por tokens OAuth AccessTokenResponse accessTokenResponse = exchangeCodeForAccessAndRefreshTokens(code[0], requestUrl); // Recupera o usuário atual // Isso usa o serviço de usuário do App Engine, mas você precisa substituí-lo pela // sua própria implementação de usuário/login UserService userService = UserServiceFactory.getUserService(); String email = userService.getCurrentUser().getEmail(); // Salva os tokens oauthTokenDao.saveKeys(accessTokenResponse, email); resp.sendRedirect(REDIRECT_URL); }
  /**
   * Construct the OAuth code callback handler URL.
   *
   * @param req the HttpRequest object
   * @return The constructed request's URL
   */
  public static String getOAuthCodeCallbackHandlerUrl(HttpServletRequest req) {
    String scheme = req.getScheme() + "://";
    String serverName = req.getServerName();
    String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort();
    String contextPath = req.getContextPath();
    String servletPath = URL_MAPPING;
    String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo();
    return scheme + serverName + serverPort + contextPath + servletPath + pathInfo;
  }
/** * Troca o código fornecido por uma troca e um token de atualização. * * @param code O código recebido do serviço de autorização * @param currentUrl O URL do callback * @param oauthProperties O objeto que contém a configuração do OAuth * @return O objeto que contém um token de acesso e de atualização * @throws IOException */ public AccessTokenResponse exchangeCodeForAccessAndRefreshTokens(String code, String currentUrl) throws IOException { HttpTransport httpTransport = new NetHttpTransport(); JacksonFactory jsonFactory = new JacksonFactory(); // Carrega o arquivo de configuração do OAuth OAuthProperties oauthProperties = new OAuthProperties(); return new GoogleAuthorizationCodeGrant(httpTransport, jsonFactory, oauthProperties .getClientId(), oauthProperties.getClientSecret(), code, currentUrl).execute(); } }
Arquivo OAuthCodeCallbackHandlerServlet.java

Observação: a implementação acima usa algumas bibliotecas do App Engine, que são usadas para simplificar. Se você estiver desenvolvendo para outra plataforma, implemente novamente a interface UserService, que processa a autenticação do usuário.

Ler e mostrar as tarefas do usuário

O usuário concedeu ao aplicativo acesso às tarefas. O aplicativo tem um token de atualização salvo no repositório de dados acessível pelo OAuthTokenDao. O servlet PrintTaskListsTitlesServlet agora pode usar esses tokens para acessar e mostrar as tarefas do usuário:

O código adicionado ao arquivo abaixo tem a sintaxe destacada, e o código já existente está esmaecido.

package com.google.oauthsample;

import ...

/**
 * Simple sample Servlet which will display the tasks in the default task list of the user.
 */
@SuppressWarnings("serial")
public class PrintTasksTitlesServlet extends HttpServlet {

  /**
   * The OAuth Token DAO implementation, used to persist the OAuth refresh token.
   * Consider injecting it instead of using a static initialization. Also we are
   * using a simple memory implementation as a mock. Change the implementation to
   * using your database system.
   */
  public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl();

  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    // Getting the current user
    // This is using App Engine's User Service but you should replace this to
    // your own user/login implementation
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();

    // If the user is not logged-in it is redirected to the login service, then back to this page
    if (user == null) {
      resp.sendRedirect(userService.createLoginURL(getFullRequestUrl(req)));
      return;
    }

    // Checking if we already have tokens for this user in store
    AccessTokenResponse accessTokenResponse = oauthTokenDao.getKeys(user.getEmail());

    // If we don't have tokens for this user
    if (accessTokenResponse == null) {
      OAuthProperties oauthProperties = new OAuthProperties();
      // Redirect to the Google OAuth 2.0 authorization endpoint
      resp.sendRedirect(new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(),
          OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties
              .getScopesAsString()).build());
      return;
    }
// Mostra os títulos das listas de tarefas do usuário na resposta resp.setContentType("text/plain"); resp.getWriter().append("Títulos de listas de tarefas para o usuário " + user.getEmail() + ":\n\n"); printTasksTitles(accessTokenResponse, resp.getWriter());
  }

  /**
   * Construct the request's URL without the parameter part.
   *
   * @param req the HttpRequest object
   * @return The constructed request's URL
   */
  public static String getFullRequestUrl(HttpServletRequest req) {
    String scheme = req.getScheme() + "://";
    String serverName = req.getServerName();
    String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort();
    String contextPath = req.getContextPath();
    String servletPath = req.getServletPath();
    String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo();
    String queryString = (req.getQueryString() == null) ? "" : "?" + req.getQueryString();
    return scheme + serverName + serverPort + contextPath + servletPath + pathInfo + queryString;
  }
/** * Usa a API Google Tasks para extrair uma lista de tarefas dos usuários na lista * padrão. * * @param accessTokenResponse O objeto AccessTokenResponse do OAuth 2.0 * que contém o token de acesso e um token de atualização. * @param output o gravador de stream de saída em que gravar os títulos das listas de tarefas * @return Uma lista dos títulos das tarefas dos usuários na lista de tarefas padrão. * @throws IOException */ public void printTasksTitles(AccessTokenResponse accessTokenResponse, Writer output) throws IOException { // Initializing the Tasks service HttpTransport transport = new NetHttpTransport(); JsonFactory jsonFactory = new JacksonFactory(); OAuthProperties oauthProperties = new OAuthProperties(); GoogleAccessProtectedResource accessProtectedResource = new GoogleAccessProtectedResource( accessTokenResponse.accessToken, transport, jsonFactory, oauthProperties.getClientId(), oauthProperties.getClientSecret(), accessTokenResponse.refreshToken); Tasks service = new Tasks(transport, accessProtectedResource, jsonFactory); // Using the initialized Tasks API service to query the list of tasks lists com.google.api.services.tasks.model.Tasks tasks = service.tasks.list("@default").execute(); for (Task task : tasks.items) { output.append(task.title + "\n"); } } }
Arquivo PrintTasksTitlesServlet.java

O usuário vai aparecer com as tarefas:

As tarefas do usuário
As tarefas do usuário

Exemplo de aplicativo

O código para este aplicativo de exemplo pode ser baixado aqui. Confira.