AuthSub en las bibliotecas cliente de los protocolos de datos de Google

Advertencia: Esta página trata sobre las APIs anteriores de Google, las APIs de Google Data, y solo es pertinente para las APIs que se enumeran en el directorio de las APIs de Google Data, muchas de las cuales se reemplazaron por APIs más nuevas. Para obtener información sobre una API nueva específica, consulta su documentación. Para obtener información sobre cómo autorizar solicitudes con una API más reciente, consulta Autenticación y autorización de Cuentas de Google.

En este documento, se describe cómo usar las bibliotecas cliente de la API de Google Data para conectarse a la autenticación de AuthSub para aplicaciones web de Google.

La interfaz de AuthSub permite que una aplicación basada en la Web acceda a un servicio de Google en nombre de un usuario. Para mantener un alto nivel de seguridad, la interfaz de AuthSub permite que la aplicación obtenga un token de autenticación sin tener que controlar la información de acceso a la cuenta del usuario.

Las bibliotecas cliente de las APIs de datos de Google proporcionan métodos para ayudarte a usar AuthSub en tu aplicación web. Específicamente, existen métodos para construir la URL de la solicitud, adquirir un token de autenticación de un solo uso, intercambiar el token de un solo uso por un token de sesión y firmar la solicitud.

Nota: La biblioteca cliente de JavaScript tiene su propia versión de AuthSub, llamada AuthSubJS. Para obtener información sobre cómo usar AuthSubJS en tus aplicaciones de JavaScript, consulta Using "AuthSub" Authentication with the JavaScript Client Library.

Público

Este documento está dirigido a los programadores que desean que sus aplicaciones basadas en la Web accedan a los servicios de Google en nombre de los usuarios con las bibliotecas cliente de las APIs de Google Data.

En este documento, se supone que conoces la interfaz de AuthSub y el proceso general para incorporar AuthSub en tu aplicación web. Para obtener una descripción completa del protocolo de AuthSub, consulta Autenticación de AuthSub para aplicaciones web.

Usa AuthSub y las APIs de Google Data sin las bibliotecas cliente

Si quieres que tu cliente de aplicación web interactúe con un servicio de datos de Google usando AuthSub como sistema de autenticación, todo lo que necesitas saber se encuentra en Autenticación de AuthSub para aplicaciones web. No es necesario que uses las bibliotecas cliente de las APIs de Google Data si no quieres.

A continuación, se incluye un esquema de cómo tu aplicación podría autenticar a un usuario con AuthSub:

Tu aplicación crea la URL de AuthSub adecuada y, luego, envía al usuario a esa URL para que pueda acceder. El sistema de AuthSub envía al usuario de vuelta a la URL de tu sitio que especificaste y devuelve un token de un solo uso. Tu aplicación puede intercambiar ese token por un token de sesión. Luego, tu aplicación envía el token en el encabezado de autorización con cada solicitud que la aplicación envía al servicio.

Las bibliotecas cliente de las APIs de Google Data simplifican este proceso de autorización, ya que controlan varios detalles por ti. En este documento, se explica cómo hacerlo.

Trabajar con AuthSub y las APIs de Google Data: ejemplos de bibliotecas cliente

En esta sección, se muestra un ejemplo del uso de los métodos de la biblioteca cliente de las APIs de Google Data para seguir los pasos que se describen en la sección "Trabajo con AuthSub" de la documentación de AuthSub.

En este ejemplo, integramos la interfaz de AuthSub en una aplicación web que interactúa con el Calendario de Google (aunque no necesitas saber nada sobre el Calendario de Google para seguir el ejemplo). En el ejemplo, se supone que la aplicación web está alojada en example.com.

Decide qué tipo de token usar (session=0 o session=1).

Puedes usar tokens de un solo uso (session=0) o tokens de sesión (session=1). En este documento, se usarán tokens de sesión, ya que son más útiles en aplicaciones que realizarán varias solicitudes a la API. Como se explica en la documentación de AuthSub, si decides usar tokens de sesión en tu aplicación web, deberás administrar el almacenamiento de tokens por tu cuenta. En este documento, no se aborda la administración de tokens. También ten en cuenta que los tokens solicitados con session=0 no se pueden intercambiar (actualizar) más adelante por un token de sesión de larga duración.

Decide si registrar tu aplicación web (secure=0 o secure=1)

AuthSub se puede usar en tres modos diferentes: sin registrar, registrado y registrado con seguridad mejorada. En el resto de este documento, nos referiremos a la última opción como AuthSub seguro. Si bien el modo no registrado/registrado es más fácil de configurar que AuthSub seguro, Google te recomienda que uses tokens seguros para mejorar la seguridad.

Cómo registrarse

Si eliges Registration for Web-Based Applications, tu aplicación tendrá los siguientes beneficios:

  1. Un nivel de seguridad más alto
  2. Google debe confiar en la app (no se muestra ninguna advertencia al usuario en la página de autorización de Google).

AuthSub registrado y seguro

Si decides usar AuthSub seguro, deberás crear un par de claves privadas y certificados públicos RSA autofirmados, además de registrar tu aplicación web. Consulta Cómo generar claves y certificados para usar con el modo registrado (a continuación) para ver ejemplos de cómo crear certificados X.509.

Determina el alcance de tu acceso a los datos

Cada servicio de Google define un valor de scope que determina (y posiblemente limita) el acceso de un token a los datos del usuario. Consulta las Preguntas frecuentes para obtener la lista de valores de scope disponibles.

Como decidimos interactuar con la API de Google Calendar, scope debería ser http://www.google.com/calendar/feeds/.

Nota: Siempre establece el valor del alcance en la URL más amplia posible, a menos que necesites una restricción más precisa. Por ejemplo, un alcance más limitado, como scope=http://www.google.com/calendar/feeds/default/allcalendars/full, restringirá el acceso del token solo al feed allcalendars/full. Usar scope=http://www.google.com/calendar/feeds/ permitirá el acceso a todos los feeds del Calendario: http://www.google.com/calendar/feeds/*.

Tokens con varios permisos

Para crear tokens que accedan a varias APIs de datos de Google, separa cada alcance con un espacio codificado como URL. En el siguiente ejemplo, se crea un token que tendrá acceso a los datos de los Contactos de Google y del Calendario de Google de un usuario.

scope=http://www.google.com/calendar/feeds/%20http://www.google.com/m8/feeds/

Solicita un token de autenticación de un solo uso

Para adquirir un token de AuthSub para un usuario y un servicio determinados, tu aplicación debe redireccionar al usuario a la URL AuthSubRequest, que le solicita que acceda a su Cuenta de Google. (Para obtener más información sobre la URL de AuthSubRequest, consulta la Autenticación de AuthSub para aplicaciones web completa).

Para construir la URL de AuthSubRequest en tu aplicación, usa lo siguiente para cada biblioteca cliente:

Java

import com.google.gdata.client.*;

String nextUrl = "http://www.example.com/RetrieveToken.jsp";
String scope = "http://www.google.com/calendar/feeds/";
boolean secure = false;  // set secure=true to request secure AuthSub tokens
boolean session = true;
String authSubUrl = AuthSubUtil.getRequestUrl(nextUrl, scope, secure, session);

Si deseas autenticar a los usuarios en tu dominio de G Suite, haz lo siguiente:

import com.google.gdata.client.*;

String hostedDomain = "example.com";
String nextUrl = "http://www.example.com/RetrieveToken.jsp";
String scope = "http://www.google.com/calendar/feeds/";
boolean secure = false;  // set secure=true to request AuthSub tokens
boolean session = true;
String authSubUrl = AuthSubUtil.getRequestUrl(hostedDomain, nextUrl, scope, secure, session);

.NET

using Google.GData.Client;

String nextUrl = "http://www.example.com/RetrieveToken.aspx";
String scope = "http://www.google.com/calendar/feeds/";
bool secure = false; // set secure=true to request secure AuthSub tokens
bool session = true;
String authSubUrl = AuthSubUtil.getRequestUrl(nextUrl, scope, secure, session);

Si deseas autenticar a los usuarios en tu dominio de G Suite, haz lo siguiente:

using Google.GData.Client;

String hostedDomain = "example.com";
String nextUrl = "http://www.example.com/RetrieveToken.aspx";
String scope = "http://www.google.com/calendar/feeds/";
bool secure = false; // set secure=true to request secure AuthSub tokens
bool session = true;
String authSubUrl = AuthSubUtil.getRequestUrl(hostedDomain, nextUrl, scope, secure, session);

PHP

require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Gdata_AuthSub');

$nextUrl = 'http://www.example.com/RetrieveToken.php';
$scope = 'http://www.google.com/calendar/feeds/';
$secure = 0;  // set $secure=1 to request secure AuthSub tokens
$session = 1;
$authSubUrl = Zend_Gdata_AuthSub::getAuthSubTokenUri($nextUrl, $scope, $secure, $session);

Si deseas autenticar a los usuarios en tu dominio de G Suite, haz lo siguiente:

require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Gdata_AuthSub');

$hostedDomain = 'example.com';
$nextUrl = 'http://www.example.com/RetrieveToken.php';
$scope = 'http://www.google.com/calendar/feeds/';
$secure = 0;  // set $secure=1 to request secure AuthSub tokens
$session = 1;
$authSubUrl = Zend_Gdata_AuthSub::getAuthSubTokenUri($nextUrl, $scope, $secure, $session) . '&hd=' . $hostedDomain;

Python

import gdata.auth

next = 'http://www.example.com/RetrieveToken.pyc'
scope = 'http://www.google.com/calendar/feeds/'
secure = False  # set secure=True to request secure AuthSub tokens
session = True
auth_sub_url = gdata.auth.GenerateAuthSubRequestUrl(next, scope, secure=secure, session=session)

Si deseas autenticar a los usuarios en tu dominio de G Suite, haz lo siguiente:

import gdata.auth

hosted_domain = 'example.com'
next = 'http://www.example.com/RetrieveToken.pyc'
scope = 'http://www.google.com/calendar/feeds/'
secure = False  # set secure=True to request secure AuthSub tokens
session = True
auth_sub_url = gdata.auth.GenerateAuthSubRequestUrl(next, scope, secure=secure, session=session, domain=hosted_domain)

Después de construir la URL "next", tu aplicación puede usarla de varias maneras para enviar al usuario al controlador AuthSubRequest. El enfoque más común es mostrar una página que le indique al usuario que debe seguir un vínculo para autorizar que tu aplicación acceda a su Cuenta de Google y, luego, adjuntar la URL de la solicitud al vínculo. Por ejemplo, podrías generar la siguiente cadena en tu app web:

String authorizationUrl =
        "<p>MyApp needs access to your Google Calendar account to read your Calendar feed. " +
        "To authorize MyApp to access your account, <a href=\"" + authSubUrl + "\">log in to your account</a>.</p>";

El usuario sigue el vínculo a la página de AuthSub en Google y accede. Luego, el sistema de AuthSub redirecciona al usuario a tu aplicación con la URL "next" que proporcionaste.

Cómo extraer el token de un solo uso

Cuando Google redirecciona a tu aplicación, el token se adjunta a la URL "next" como un parámetro de consulta. En el caso de los ejemplos anteriores, después de que el usuario accede, Google redirecciona a una URL como http://www.example.com/RetrieveToken?token=DQAADKEDE. Tu aplicación debe extraer el valor del token de su parámetro de búsqueda de URL.

Si tu aplicación configuró una cookie de autenticación en el navegador del usuario antes de enviarlo al sistema de AuthSub, cuando Google redireccione a la URL "next", tu aplicación podrá leer la cookie de autenticación para reconocer qué usuario llegó a esa URL. Puedes usar una cookie de este tipo para asociar un ID de usuario en tu aplicación con el token de AuthSub recuperado de Google.

Las bibliotecas cliente proporcionan métodos convenientes para extraer el token de un solo uso:

Java

String singleUseToken = AuthSubUtil.getTokenFromReply(httpServletRequest.getQueryString());

.NET

String singleUseToken = Request.QueryString["token"];
// or
String singleUseToken = AuthSubUtil.getTokenFromReply(new Uri(Request.QueryString));

PHP

$singleUseToken = $_GET['token'];

Python

current_url = 'http://' + req.hostname + req.unparsed_uri
# Unlike the other calls, extract_auth_sub_token_from_url() will create an AuthSubToken or SecureAuthSubToken object.
# Use str(single_use_token) to return the token's string value.
single_use_token = gdata.auth.extract_auth_sub_token_from_url(current_url)

Si usas AuthSub seguro, asegúrate de configurar tu clave privada RSA para que se cree un SecureAuthSubToken:

f = open('/path/to/yourRSAPrivateKey.pem')
rsa_key = f.read()
f.close()
current_url = 'http://' + req.hostname + req.unparsed_uri
# Unlike the other calls, extract_auth_sub_token_from_url() will create an AuthSubToken or SecureAuthSubToken object.
# Use str(single_use_token) to return the token's string value.
single_use_token = gdata.auth.extract_auth_sub_token_from_url(current_url, rsa_key=rsa_key)

Cómo solicitar un token de sesión

El token que recuperas de la URL siempre es de un solo uso. El siguiente paso es actualizar ese token para obtener un token de sesión de larga duración con la URL de AuthSubSessionToken, como se describe en la documentación completa de Autenticación de AuthSub para aplicaciones web. Si usas AuthSub seguro, deberás configurar tu clave privada RSA antes de realizar el intercambio. A continuación, se incluyen algunos ejemplos de cómo usar cada una de las bibliotecas cliente:

Java

import com.google.gdata.client.*;
import com.google.gdata.client.calendar.*;

String sessionToken = AuthSubUtil.exchangeForSessionToken(singleUseToken, null);

CalendarService calendarService = new CalendarService("google-ExampleApp-v1.0");
calendarService.setAuthSubToken(sessionToken, null);

// ready to interact with Calendar feeds

Para AuthSub seguro, pasa tu clave privada de RSA a exchangeForSessionToken en lugar de pasar null:

import com.google.gdata.client.*;
import com.google.gdata.client.calendar.*;

java.security.PrivateKey privateKey =
    AuthSubUtil.getPrivateKeyFromKeystore("AuthSubExample.jks", "privKeyPa$$word", "AuthSubExample", "privKeyPa$$word");
String sessionToken = AuthSubUtil.exchangeForSessionToken(singleUseToken, privateKey);

CalendarService calendarService = new CalendarService("google-ExampleApp-v1.0");
calendarService.setAuthSubToken(sessionToken, privateKey);

// ready to interact with Calendar feeds

.NET

using Google.GData.Client;
using Google.GData.Calendar;

String sessionToken = AuthSubUtil.exchangeForSessionToken(singleUseToken, null).ToString();

GAuthSubRequestFactory authFactory = new GAuthSubRequestFactory("cl", "google-ExampleApp-v1.0");
authFactory.Token = (String) sessionToken;

CalendarService calendarService = new CalendarService(authFactory.ApplicationName);
calendarService.RequestFactory = authFactory;

// ready to interact with Calendar feeds

Para AuthSub seguro, pasa tu clave privada de RSA a exchangeForSessionToken en lugar de pasar null:

using Google.GData.Client;
using Google.GData.Calendar;

protected AsymmetricAlgorithm getRsaKey()
{
  X509Certificate2 cert = new X509Certificate2("C:/MyAspSite/test_cert.pfx", "privKeyPa$$word");
  RSACryptoServiceProvider privateKey = cert.PrivateKey as RSACryptoServiceProvider;
  return privateKey;
}

AsymmetricAlgorithm rsaKey = getRsaKey();
String sessionToken = AuthSubUtil.exchangeForSessionToken(singleUseToken, rsaKey).ToString();

GAuthSubRequestFactory authFactory = new GAuthSubRequestFactory("cl", "google-ExampleApp-v1.0");
authFactory.Token = (String) sessionToken;
authFactory.PrivateKey = rsaKey;

CalendarService calendarService = new CalendarService(authFactory.ApplicationName);
calendarService.RequestFactory = authFactory;

// ready to interact with Calendar feeds

PHP

require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Gdata_AuthSub');
Zend_Loader::loadClass('Zend_Gdata_Calendar');

$sessionToken = Zend_Gdata_AuthSub::getAuthSubSessionToken($singleUseToken);

// Create a Calendar service object and set the session token for subsequent requests
$calendarService = new Zend_Gdata_Calendar(null, 'google-ExampleApp-v1.0');
$calendarService->setAuthSubToken($sessionToken);

// ready to interact with Calendar feeds

Para AuthSub seguro, el intercambio requiere que primero configures un Zend_Gdata_HttpClient y establezcas tu clave privada RSA con setAuthSubPrivateKeyFile():

require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Gdata_AuthSub');
Zend_Loader::loadClass('Zend_Gdata_Calendar');

$client = new Zend_Gdata_HttpClient();
$client->setAuthSubPrivateKeyFile('/path/to/myrsakey.pem', null, true);
$sessionToken = Zend_Gdata_AuthSub::getAuthSubSessionToken($singleUseToken, $client);

$calendarService = new Zend_Gdata_Calendar($client, 'google-ExampleApp-v1.0');
$calendarService->setAuthSubToken($sessionToken);

// ready to interact with Calendar feeds

Python

import gdata.calendar
import gdata.calendar.service

calendar_service = gdata.calendar.service.CalendarService()
calendar_service.UpgradeToSessionToken(single_use_token)  # calls gdata.service.SetAuthSubToken() for you

# ready to interact with Calendar feeds

Nota: El proceso es el mismo para AuthSub seguro, siempre y cuando hayas usado gdata.auth.extract_auth_sub_token_from_url(url, rsa_key=rsa_key) para extraer el token de un solo uso.

Nota: Cuando usas AuthSub seguro, tu clave privada no se envía a través de la red. Las bibliotecas cliente envían la firma única que se genera cuando se firma la solicitud con tu clave, no la clave en sí.

Cómo usar el token de sesión

Puedes usar el token de sesión para autenticar solicitudes al servidor colocando el token en el encabezado de autorización, como se describe en la documentación de AuthSub.

Después de configurar tu token de sesión, puedes usar las llamadas estándar de la biblioteca cliente de las APIs de Google Data para interactuar con el servicio sin tener que preocuparte por el token. Para obtener más detalles, consulta la documentación de la biblioteca cliente y la guía para desarrolladores de las APIs de Google Data para el servicio y el lenguaje con los que interactúas.

Cómo recuperar información sobre un token de sesión

Si deseas probar que tu cliente y el servidor coinciden en los parámetros del token, puedes pasar el token al controlador AuthSubTokenInfo, que devuelve un conjunto de pares nombre-valor que contienen información sobre el token.

Java

Map<String, String> tokenInfo = AuthSubUtil.getTokenInfo(sessionToken, null);

Si usas AuthSub seguro, pasa tu clave privada RSA:

Map<String, String> tokenInfo = AuthSubUtil.getTokenInfo(sessionToken, privateKey);

.NET

Dictionary<String, String> tokenInfo = AuthSubUtil.GetTokenInfo(sessionToken, null);

Si usas AuthSub seguro, pasa tu clave privada RSA:

Dictionary<String, String> tokenInfo = AuthSubUtil.GetTokenInfo(sessionToken, privateKey);

PHP

$tokenInfo = Zend_Gdata_AuthSub::getAuthSubTokenInfo($sessionToken);

Si usas AuthSub seguro, pasa tu Zend_Gdata_HttpClient para que la solicitud se firme con tu clave privada RSA:

$tokenInfo = Zend_Gdata_AuthSub::getAuthSubTokenInfo($sessionToken, $client);

Python

token_info = calendar_service.AuthSubTokenInfo()

Revoca un token de sesión

Los tokens de sesión de AuthSub no vencen. Tu cliente puede almacenar el token de sesión durante el tiempo que sea necesario.

Por lo tanto, cuando tu cliente termine de usar el token de sesión, puede revocarlo con el controlador AuthSubRevokeToken, como se describe en la documentación de AuthSub.

Por ejemplo, si deseas administrar tokens de una manera tradicional similar a una sesión, tu cliente puede obtener un token al comienzo de la sesión de un usuario y revocarlo al final de la sesión del usuario.

Para revocar un token, usa lo siguiente en cada biblioteca cliente:

Java

AuthSubUtil.revokeToken(sessionToken, null);

Si usas AuthSub seguro, pasa tu clave privada RSA:

AuthSubUtil.revokeToken(sessionToken, privateKey);

.NET

AuthSubUtil.revokeToken(sessionToken, null);

Si usas AuthSub seguro, pasa tu clave privada RSA:

AuthSubUtil.revokeToken(sessionToken, privateKey);

PHP

$wasRevoked = Zend_Gdata_AuthSub::AuthSubRevokeToken($sessionToken);

Si usas AuthSub seguro, pasa tu Zend_Gdata_HttpClient para que la solicitud se firme con tu clave privada RSA:

$wasRevoked = Zend_Gdata_AuthSub::AuthSubRevokeToken($sessionToken, $client);

Python

calendar_service.RevokeAuthSubToken()

Recursos y muestras adicionales

Volver al principio

Cómo generar una clave privada y un certificado público autofirmados para usarlos con AuthSub seguro

La clave privada se usa para generar una firma, que se debe incluir en cada solicitud. Google usa la clave pública incorporada en el certificado para verificar la firma. La clave pública debe ser una clave RSA de 1,024 bits codificada en un certificado X.509 en formato PEM. El certificado se debe enviar a Google en el momento del registro.

En las siguientes secciones, se proporcionan ejemplos de cómo generar claves y certificados con dos herramientas específicas: la utilidad OpenSSL y la utilidad keytool de Java.

Estos ejemplos no son específicos de las APIs de Google Data. Puedes usar las mismas utilidades para generar claves para cualquier propósito.

En los ejemplos, se supone que tu empresa se llama My_Company, se encuentra en Mountain View, California, EE.UU., y tiene el nombre de dominio example.com.

Genera claves con OpenSSL

Para crear un par de claves RSA y el certificado correspondiente, puedes usar el siguiente comando:

# Generate the RSA keys and certificate
openssl req -x509 -nodes -days 365 -newkey rsa:1024 -sha1 -subj \
  '/C=US/ST=CA/L=Mountain View/CN=www.example.com' -keyout \
  myrsakey.pem -out /tmp/myrsacert.pem

Advertencia: Si incluyes el parámetro -nodes, se creará una clave privada sin contraseña para protegerla. Sin embargo, debes considerar omitir este parámetro para mayor seguridad.

El parámetro -sha1 especifica que la clave se usará para generar firmas SHA1.

El parámetro -subj especifica la identidad de la aplicación que representa el certificado.

El parámetro -keyout especifica el archivo que contendrá las claves. Este archivo contiene información sensible y debe protegerse, y no compartirse con nadie.

El parámetro -out especifica el archivo que contendrá el certificado en formato PEM (que se puede enviar a Google durante el registro).

Genera claves para el cliente de .NET

El framework de .NET no comprende las claves ni los certificados almacenados en formato PEM. Por lo tanto, se necesita un paso adicional una vez que hayas creado el archivo .pem:

openssl pkcs12 -export -in test_cert.pem -inkey myrsacert.pem -out myrsacert.pfx -name "Testing Certificate"

En este paso, se genera un archivo PFX a partir de tu clave privada y certificado. Este archivo se puede importar a la biblioteca cliente de .NET para firmar digitalmente las solicitudes realizadas a las APIs de Google Data.

Cómo generar claves para el cliente de Java

El cliente de Java acepta claves privadas en formato PKCS#8. Después de generar una clave o un certificado con las instrucciones anteriores, crea un archivo .pk8 a partir del archivo .pem que generaste:

openssl pkcs8 -in myrsakey.pem -topk8 -nocrypt -out myrsakey.pk8

Como alternativa, puedes usar el almacén de claves de Java y la utilidad keytool para crear un par de claves RSA y el certificado correspondiente. Usa el siguiente comando:

# Generate the RSA keys and certificate
keytool -genkey -v -alias Example -keystore ./Example.jks\
  -keyalg RSA -sigalg SHA1withRSA\
  -dname "CN=www.example.com, OU=Engineering, O=My_Company, L=Mountain  View, ST=CA, C=US"\
  -storepass changeme -keypass changeme

Advertencia: "changeme" no es una buena contraseña; este es solo un ejemplo.

El parámetro -dname especifica la identidad de la aplicación que representa el certificado. El parámetro -storepass especifica la contraseña para proteger el almacén de claves. El parámetro -keypass especifica la contraseña para proteger la clave privada.

Para escribir el certificado en un archivo que se pueda usar en la herramienta ManageDomains, usa el siguiente comando:

# Output the public certificate to a file
keytool -export -rfc -keystore ./Example.jks -storepass changeme \
  -alias Example -file mycert.pem

Volver al principio