Implementación de Google OAuth para utilizar la API de Google en Cloudflare Workers

Compartir esta publicación

Share on facebook
Share on linkedin
Share on twitter
Share on email

Recientemente tuve la oportunidad de construir una pequeña aplicación que necesitaba autenticar y autorizar a un usuario utilizando el mecanismo de inicio de sesión de Google, y solicita en su nombre datos de una API de Google.

Elegí implementar esto como un Cloudflare Worker como un servicio de computación sin servidor aprovechando Cloudflare key-value storage (KV) para el almacenamiento de sesiones. Las herramientas de Cloudflare (wrangler) han evolucionado mucho desde mi primer intento con Cloudflare Workers, así que pensé que ya era hora de volver a intentarlo.

Como cualquier buen ingeniero de software lo haría, empecé a buscar cualquier repositorio que pudiera utilizar una plantilla para cablear Google OAuth fácilmente. Pero no encontré nada que funcionara bien con Cloudflare Web worker, o que tuviera alguna documentación/pruebas adecuadas, o que tuviera una calidad decente. Así que en esta entrada del blog (y en el repositorio GitHub complementario jazcarate/cloudflare-worker-google-oauth), quiero documentar, explicar y repasar algunas decisiones interesantes para que alguien como yo pueda tener esto para impulsar su desarrollo.

Dicho esto, siéntete libre de agenciarte cualquiera o todo el código del repo.

Resultado

En primer lugar, esto es lo que vamos a desarrollar: Una aplicación que puede mostrar y filtrar los archivos de Drive de un usuario; y proporcionar un enlace a ellos.

Screenshot 2021 07 13 at 00.19.08

Elijo como excusa la API de listados de Drive de Google. Todo lo que veremos a partir de ahora se puede cambiar fácilmente para utilizar cualquiera de las mil APIs de Google, ya que todas requieren más o menos la misma autenticación y configuración.

Estructura y sistemas

Para desacoplar la lógica de fuentes externas (como Google), el proyecto se entiende mejor como un punto de entrada delgado index.ts, la lógica de negocio central en el método de manejo (handler.ts), y cada dependencia externa en la carpeta lib/.

La interfaz principal de handler.ts es una función que inyecta todos los sistemas

export default function (
  kv: KVSystem,
  google: GoogleSystem,
  env: EnvSystem,
  crypto: CryptoSystem,
): (event: FetchEvent) => Promise<Response> {
  const { remove, get, save } = kv
  const { tokenExchange, removeToken, listDriveFiles } = google
  const { isLocal, now } = env
  const { generateAuth } = crypto
  //...

Esto no sólo ayuda a separar las preocupaciones, sino que nos permite simular dependencias externas en las pruebas.

Solicitud inicial

Una vez que tenemos todos los sistemas inicializados, hacemos coincidir si la petición es un callback /auth. Volveremos a esta sección más adelante. Si la petición no es un callback, entonces compruebo si el usuario está autentificado. Hay varias formas en las que un usuario podría no estarlo.

Como si no presentaran ninguna cookie.

const cookies = request.headers.get('Cookie')
if (!cookies) return login(env, google, url)

O si tienen cookies, pero no la que nos interesa (auth)

const auth = findCookie(AUTH_COOKIE, cookies)
if (!auth) return login(env, google, url)

O el KV no tiene una entrada para esa cookie de autenticación (ya sea porque ha expirado, o que el cliente es malicioso y está tratando de adivinar)

const token = await get(auth)
if (!token) return login(env, google, url)

Autenticado

Después de este punto, sé que el usuario está autenticado. Esto significa que tengo acceso a su token para consultar la API de Google.

Así que ahora tengo que elegir uno de los tres caminos:

  1. f si es a la raíz (/)
  2. Si la petición es a /logout
  3. o cualquier otro path.

Hay que tener en cuenta que esto es sólo el pathname, no incluye parámetros de búsqueda como ?q=foo; así que la URL /?q=foo .nombre de la ruta es sólo '/'

switch (url.pathname) {
    case '/':       // ...
    case '/logout': // ...
    default:        // ...
}

Lista de archivos

Si la petición es a la raíz, entonces consulto la API de Google con el token Autorización como el API espera

async function listDriveFiles(
  accessToken: string,
  query: string | null,
): Promise<DriveFiles> {
  const url = new URL('https://www.googleapis.com/drive/v2/files')
  if (query) url.searchParams.append('q', `title contains '${query}'`)

  const response = await fetch(url.toString(), {
    headers: { Authorization: `Bearer ${accessToken}` },
  })
  /// ...
}

Si el fetch devuelve algo, compruebo el cuerpo para ver si hay errores y entrar en pánico si es necesario

const resp = await response.json()
if (resp.error) throw new Error(JSON.stringify(resp.error))

Una vez que Google responde a una lista de archivos, entonces es sólo cuestión de renderizarlos. Decidí mantener el renderizado simple, y alinear todo el HTML; pero esto puede ser fácilmente adaptado para devolver un JSON, o usar un motor de plantillas apropiado. Dejaré esto como un ejercicio para el lector.

Cierre de sesión

En el caso de cierre de sesión, revocamos el token con google, eliminamos el auth del KV y respondemos al cliente con una cookie eliminada.

event.waitUntil(Promise.allSettled([removeToken(token), remove(auth)]))
        
return new Response('Loged out', {
    headers: setCookie('deleted', EXPIRED),
})

Aprovecho el ciclo de vida de waitUntil para responder al cliente inmediatamente, y llamar a google y eliminar el KV en segundo plano. Y uso allSettled ya que no me importa especialmente si el KV no pudo ser eliminado, o si Google tuvo problemas para eliminar el token ya que no hay mucho más que pueda hacer.

No se ha encontrado

En caso contrario, simplemente devuelvo un estado 404 No encontradoódigo>.

return new Response('Not found', { status: 404 })

Llamada de retorno

Volviendo a la petición /auth; esta no necesita autenticación (ya que estamos en proceso de creación). Así que sólo compruebo el contrato que Documentos de inicio de sesión de Google. El parámetro de consulta no tiene errores

const error = url.searchParams.get('error')
if (error !== null)
    return new Response(`Google OAuth error: [${error}]`, { status: 400 })

Y tiene un código

const code = url.searchParams.get('code')
if (code === null)
    return new Response(`Bad auth callback (no 'code')`, { status: 400 })

Si es así, podemos cambiar el código por un token adecuado a través de otra API de Google

const response = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
    },
    body,
})

y vuelve a comprobar si hay errores

const resp = await response.json()
if (resp.error) throw new Error(resp.error)

Con un access_token válido de Google en la mano, generamos una cadena aleatoria para la autenticación de la aplicación

const newAuth = generateAuth()

Y almacenar en el KV la forma de traducir de newAuth a access_token

await save(
    newAuth,
    tokenResponse.access_token,
    //...
)

Por último, enviamos al cliente de vuelta a su lugar de origen, con su nueva cookie de autenticación. Guardamos la URL original en el parámetro state que Google OAuth nos permite enviar.

return redirect(
    '/' + decodeURIComponent(url.searchParams.get('state') || ''),
    setCookie(newAuth, new Date(expiration)),
)

Otros sistemas

Los sistemas de la carpeta lib/ son bastante sencillos, y pueden dividirse en dos subgrupos conceptualmente:

Dependencias mejoradas de Cloudflare

Como esta aplicación se ejecuta en Cloudflare Workers (tanto el entorno real en la nube como el entorno de desarrollo generado por npm run dev), algunas variables se inyectan en el ámbito global. Estas variables se escriben en el archivo bindings.d.ts; y son generadas por los pasos 2 y 3 de README.md#Setup wrangler.

  • El módulo KV utiliza la variable global authTokens que Cloudflare Worker inyecta en el worker. Puede encontrar más información sobre cómo funciona la KV aquí.
  • El módulo Env mantiene el entorno inyectable. Aunque podríamos usar las variables globales (CLIENT_ID y CLIENT_SECRET) inyectadas al web worker, este enfoque me permite probar el handler sin tener que volver a cablear las variables globales; eso es un dolor.

Utilidades

Y algunos otros sistemas que no dependen de Cloudflare Worker, sino más bien de funciones de utilidad, agrupados por su dominio específico.

  • El módulo http tiene algunas utilidades para analizar el formato de la cabecera Cookies y construir una respuesta 302 Redirect.
  • El módulo de Google tiene una API de tipos que sólo utiliza fetch.
  • El módulo Crypto es una pequeña utilidad para utilizar la API crypto para generar una cadena aleatoria segura.

Si todo esto es demasiado complicado; puedes probar nuestra solución de gestión de identidades – VYou que fue desarrollada por desarrolladores de software de Apiumhub.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Suscríbete a nuestro boletín de noticias

Recibe actualizaciones de los últimos descubrimientos tecnológicos

Acerca de Apiumhub

Apiumhub reúne a una comunidad de desarrolladores y arquitectos de software para ayudarte a transformar tu idea en un producto potente y escalable. Nuestro Tech Hub se especializa en Arquitectura de Software, Desarrollo Web & Desarrollo de Aplicaciones Móviles. Aquí compartimos con usted consejos de la industria & mejores prácticas, basadas en nuestra experiencia.

Posts populares

¿Tienes un proyecto desafiante?

Podemos trabajar juntos