Controlador de devolución de llamada de autorización de compilación

En este documento, se explica cómo implementar un controlador de devolución de llamada de autorización de OAuth 2.0 con servlets de Java a través de una aplicación web de muestra que mostrará las tareas del usuario con la API de Google Tasks. La aplicación de ejemplo primero solicitará autorización para acceder a Google Tasks del usuario y, luego, mostrará las tareas del usuario en la lista de tareas predeterminadas.

Público

Este documento está dirigido a personas familiarizadas con la arquitectura de aplicaciones web Java y J2EE. Se recomienda tener conocimientos sobre el flujo de autorización de OAuth 2.0.

Contenido

Para que la muestra funcione correctamente, se necesitan varios pasos, como se indica a continuación:

Declara las asignaciones de servlet en el archivo web.xml

Utilizaremos 2 servlets en nuestra aplicación:

  • PrintTasksTitlesServlet (asignado a /): Es el punto de entrada de la aplicación que manejará la autenticación del usuario y mostrará sus tareas.
  • OAuthCodeCallbackHandlerServlet (asignado a /oauth2callback): La devolución de llamada de OAuth 2.0 que controla la respuesta del extremo de autorización de OAuth.

A continuación, se muestra el archivo web.xml que asigna estos 2 servlets a URL en nuestra aplicación:

<?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>
Archivo /WEB-INF/web.xml

Autentica a los usuarios de tu sistema y solicita autorización para acceder a sus tareas

El usuario ingresa a la aplicación a través de la URL raíz “/”, que se asigna al servlet PrintTaskListsTitlesServlet. En ese servlet, se realizan las siguientes tareas:

  • Comprueba si el usuario está autenticado en el sistema.
  • Si el usuario no está autenticado, se lo redirecciona a la página de autenticación.
  • Si el usuario está autenticado, verificamos si ya tenemos un token de actualización en nuestro almacenamiento de datos, que se controla mediante OAuthTokenDao a continuación. Si no hay un token de actualización almacenado para el usuario, significa que este aún no otorgó autorización a la aplicación para acceder a sus tareas. En ese caso, se redirecciona al usuario al extremo de autorización de OAuth 2.0 de Google.
A continuación, se muestra una manera de implementarlo:

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;
  }
}
Archivo PrintTasksTitles.java

Nota: En la implementación anterior, se usan algunas bibliotecas de App Engine, que se usan con el objetivo de simplificarlas. Si estás desarrollando para otra plataforma, puedes volver a implementar la interfaz UserService que se encarga de la autenticación de usuarios.

La aplicación usa un DAO para conservar los tokens de autorización del usuario y acceder a ellos. A continuación, se muestra la interfaz, OAuthTokenDao, y una implementación simulada (en la memoria), OAuthTokenDaoMemoryImpl, que se usan en esta muestra:

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);
}
Archivo 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 tokenPersistance = new HashMap();

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

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

Además, las credenciales de OAuth 2.0 para la aplicación se almacenan en un archivo de propiedades. Como alternativa, puedes tenerlas como una constante en alguna parte de una de las clases Java, aunque aquí se muestran la clase OAuthProperties y el archivo oauth.properties que se usa en la muestra:

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 {
  }
}
Archivo OAuthProperties.java

A continuación, se muestra el archivo oauth.properties que contiene las credenciales de OAuth 2.0 de tu aplicación. Debes cambiar los siguientes valores por tu cuenta.

# 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
Archivo oauth.properties

El secreto de cliente y el ID de cliente de OAuth 2.0 identifican tu aplicación y permiten que la API de Tasks aplique filtros y reglas de cuotas definidos para tu aplicación. Puedes encontrar el ID de cliente y el secreto en la Consola de APIs de Google. Cuando estés en la consola, deberás hacer lo siguiente:

  • Crea o selecciona un proyecto.
  • Para habilitar la API de Tasks, cambia el estado de la API a ACTIVADO en la lista de servicios.
  • En Acceso a la API, crea un ID de cliente de OAuth 2.0 si aún no tienes uno.
  • Asegúrate de que la URL del controlador de devolución de llamada del código OAuth 2.0 del proyecto esté registrada o incluida en la lista blanca en los URI de redireccionamiento. Por ejemplo, en este proyecto de muestra, tendrías que registrar https://www.example.com/oauth2callback si tu aplicación web se entrega desde el dominio https://www.example.com.

URI de redireccionamiento en la Consola de APIs
URI de redireccionamiento en la Consola de APIs

Detecta el código de autorización del extremo de autorización de Google.

En el caso de que el usuario aún no haya autorizado a la aplicación para acceder a sus tareas y, por lo tanto, se haya redirigido al extremo de autorización de OAuth 2.0 de Google, se le mostrará un diálogo de autorización de Google en el que se le solicitará que otorgue a tu aplicación acceso a sus tareas:

Diálogo de autorización de Google
Diálogo de autorización de Google

Después de conceder o denegar el acceso, se redireccionará al usuario al controlador de devolución de llamada de código OAuth 2.0, que se especificó como redireccionamiento o devolución de llamada al construir la URL de autorización de Google:

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

El controlador de devolución de llamada de código de OAuth 2.0, OAuthCodeCallbackHandlerServlet, controla el redireccionamiento desde el extremo de Google OAuth 2.0. Hay 2 casos para manejar:

  • El usuario otorgó acceso: Analiza la solicitud para obtener el código de OAuth 2.0 de los parámetros de URL.
  • El usuario denegó el acceso: muestra un mensaje al usuario.

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;
  }
}
Archivo OAuthCodeCallbackHandler.java

Intercambia el código de autorización por un token de actualización y acceso

Luego, el OAuthCodeCallbackHandlerServlet intercambia el código de Auth 2.0 por un token de actualización y de acceso, lo conserva en el almacén de datos y redirecciona al usuario de vuelta a la URL de OAuthCodeCallbackHandlerServlet:

El código que se agrega al siguiente archivo aparece destacado en la sintaxis. El código existente aparece inhabilitado.

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";
/** Es la URL a la que se redireccionará al usuario después de procesar la devolución de llamada. Considera * guardar esto en una cookie antes de redireccionar a los usuarios a la URL de autorización de Google * si tienes varias URL posibles a las que redireccionar a las personas. */ String final estática pública REDIRECT_URL = "/"; /** La implementación del DAO del token de OAuth. Procura inyectarla en lugar de usar * una inicialización estática. Además, usamos una implementación de memoria simple * como modelo. Cambia la implementación para usar tu sistema de base de datos.
  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;
  }
* * @param code El código obtenido del servicio de autorización * @param currentUrl La URL de la devolución de llamada * @param oauthProperties El objeto que contiene la configuración OAuth * @return El objeto que contiene un token de acceso y actualización * @throws IO
Archivo OAuthCodeCallbackHandler.java

Nota: En la implementación anterior, se usan algunas bibliotecas de App Engine, que se usan con el objetivo de simplificarlas. Si estás desarrollando para otra plataforma, puedes volver a implementar la interfaz UserService que se encarga de la autenticación de usuarios.

Leer las tareas del usuario y mostrarlas

El usuario otorgó a la aplicación acceso a sus tareas. La aplicación tiene un token de actualización que se guarda en el almacén de datos al que se puede acceder a través de OAuthTokenDao. El servlet PrintTaskListsTitlesServlet ahora puede usar estos tokens para acceder a las tareas del usuario y mostrarlas:

El código que se agrega al siguiente archivo aparece destacado en la sintaxis. El código existente aparece inhabilitado.

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;
    }
// Imprimir los títulos de listas de tareas del usuario en la respuesta resp.setContentType("text/plain"); resp.getWriter().append("Task Tasks Items for user " + 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 la lista de tareas de Google Tasks para usar la lista de usuarios de Google Tasks. * * @param accessTokenResponse El objeto AccessTokenResponse de OAuth 2.0 * que contiene el token de acceso y un token de actualización. * @param genera el escritor del flujo de salida, donde se deben ejecutar los títulos de las listas de tareas. * @return. Una lista de los títulos de tareas de los usuarios en la lista de tareas predeterminada.
Archivo PrintTasksTitles.java

El usuario se mostrará con sus tareas:

Las tareas del usuario
Las tareas del usuario

Aplicación de ejemplo

Puedes descargar el código de la aplicación de ejemplo aquí. No dudes en consultarlo.