
Hice este pequeño ejercicio porque me pareció una manera simple y divertida de aprender a usar OAuth2 con Google y así darle sentido para poder implementarlo con cualquier otro proveedor.
Este ejercicio nació de un taller en vivo que suelo hacer con mi comunidad en Discord.
Tú también puedes unirte, te dejo el enlace aquí. Pero, si también quieres construir este login, ven conmigo y hagámoslo juntos.
1. Generando las credenciales
Vamos a APIs en la consola de Google y configuramos la pantalla de consent
para luego crear una credencial OAuth.
1.— Activa tu consent screen.
2.— Crea una credencial OAuth.
Cuando Google te presente tu
client_id
y tu client_secret
, guárdalos en un lugar seguro o descarga el archivo .json
.
👀 No olvides agregar
http://localhost:8787
a las URLs de redirección al crear tu credencial OAuth.
Puedes colocarlas como variables de entorno de una vez en un archivo .env
Y en tu archivo wrangler.toml
que se generará en el siguiente paso.
// wrangler.toml [vars] GOOGLE_CLIENT_ID="325591888601-hapgr9g06vuqms8bp50rga6r60d10mb4.apps.googleusercontent.com" GOOGLE_SECRET="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" ENV="development"
2. Creando/Clonando el proyecto
Generamos un nuevo proyecto Hono para Cloudflare o también puedes clonar el repo terminado desde el link.
npm create hono@latest my-app
Funciones reutilizables
Vamos a hacer 3 consumos a diferentes URLs de la API de Google.
La primera es una redirección para conseguir el código que podemos intercambiar por un access_token
, una vez que tengamos ese código, haremos el segundo consumo buscando el access token
.
Teniendo el access_token
ya podemos hacer el tercer consumo para conseguir los datos del usuario.
Las tres funciones que hacen cada consumo son las siguientes:
// lib/GoogleOauth2.ts export const getExtraData export const getAccessToken export const reidrectToGoogle
Siempre puedes llamarlas como tú quieras.
3. Vamos a escribir la primera función y usarla
La primera función construye un URL para redireccionar a nuestro usuario que quiere hacer login a la pantalla de consentimiento que activaste.
export function redirectToGoogle(redirect: Redirect, env: Env): Response { if (!env || !env.GOOGLE_SECRET || !env.GOOGLE_CLIENT_ID) throw new Error("Missing env object"); const obj = { client_id: env.GOOGLE_CLIENT_ID, redirect_uri: env.ENV === "production" ? prodURL : "http://localhost:8787", response_type: "code", scope: "https://www.googleapis.com/auth/userinfo.email", }; const url = "https://accounts.google.com/o/oauth2/auth?" + new URLSearchParams(obj); return redirect(url); }
Nota como se están usando dos variables de entorno para agregar todos los datos que la URL necesita. También nota como decidimos que redirect_url
se empleará si estamos en producción.
Vamos a crear, pues, nuestra Home para redireccionar a nuestro usuario.
Puedes encontrar los componentes <Home/>
y <Dash/>
en el repo.
En <Home/>
tenemos un formulario que envía un query string llamado intent
que colocamos en el botón.
// <Home /> <form> <button type="submit" name="intent" value="google-login" > <i class="fa-brands fa-google"></i>{" "} <span>Inicia sesión con Google</span> </button> </form>
En nuestro index de Hono agregamos:
// server.entry.tsx import { serveStatic } from "hono/cloudflare-workers"; import Home from "./components/Home"; import { getAccessToken, getExtraData, redirectToGoogle, } from "./lib/GoogleOauth2"; app.get("/*", serveStatic({ root: "./" })); app.get("/", async (c) => { const intent = c.req.query("intent"); if (intent === "google-login") { return redirectToGoogle( c.redirect, c.env ); } return c.html(<Home />); });
Toma nota:
- Hono nos permite devolver JSX con su método
c.html
- Estamos capturando los queryParams (searchParams)
- Si el intent es igual a
google-login
utilizamos la funciónredirectToGoogle
. - La función espera que le pasemos
redirect
yenv
, pero puedes modificarla a placer.
Nuestro usuario ahora encontraría nuestra pantalla de login y lo estaríamos redireccionando para que nos dé su permiso.
4. Recibiendo el code
Lo que sigue es la redirección que Google hará una vez que el usuario nos dé permiso de usar sus datos.
Esta redirección incluye un parámetro code
y eso es justo lo que necesitamos para ejecutar la segunda función:
if (code) { const data = await getAccessToken(code, c.env); // @TODO: check for errors // ... } if (intent === "google-login") { // ... // ...
Escribamos esta segunda función:
export const getAccessToken = async( code: string, env: Env ): ExtraDataReturn => { if (!env || !env.GOOGLE_SECRET || !env.GOOGLE_CLIENT_ID) return new Error("missing env object"); const url = "https://oauth2.googleapis.com/token?" + new URLSearchParams({ code, client_secret: env.GOOGLE_SECRET, grant_type: "authorization_code", client: env.GOOGLE_CLIENT_ID, redirect_uri: env.ENV === "production" ? prodURL : "http://localhost:8787", scope: "https://www.googleapis.com/auth/userinfo.email", }); return fetch(url, { method: "post", headers: { "contant-type": "application/json" }, }) .then((r) => r.json()) .catch((e) => ({ ok: false, error: e })) as ResultResponse; };
¿Qué pasa en esta función?
Recibimos el string del código que Google nos proporcionó después de que el usuario nos diera permiso. En la variable code
junto con el objeto de variables de entorno (recuerda que estas variables deben vivir en wrangles.toml
). Construimos una nueva url y le hacemos post, devolvemos la respuesta.
La respuesta es un objeto con el access_token
dentro.
if (code) { const data = await getAccessToken(code, c.env); // check errors @todo const extra = (await getExtraData(data.access_token)); // @TODO: guarda/actualiza tu usuario en DB }
¡Genial! Tenemos progreso, ahora vamos por la tercera.
5. Consiguiendo el email y foto del usuario
Ahora que tenemos el access_token
, pues es momento de emplearlo.
Escribimos nuestra tercera y última función para conseguir el correo y la imagen del usuario:
export const getExtraData = (access_token: string) => { const url = "https://www.googleapis.com/oauth2/v2/userinfo"; return fetch(url, { headers: { Authorization: "Bearer " + access_token, }, }) .then((r) => r.json()) .catch((e) => ({ ok: false, error: e })); };
Observa como esta función es la más simple, pues ya no necesitamos incluir nuestras credenciales, basta con el token que ya trae consigo toda la información sobre los permisos. Devolvemos la respuesta.
Nuestro if
queda de la siguiente manera:
// server.entry.jsx if (code) { const data = await getAccessToken(code, c.env); // check errors @todo const extra = await getExtraData(data.access_token) // @TODO: save/update your user in DB setCookie(c, "userId", extra.email as string); return c.html(<Dash email={extra.email} picture={extra.picture} />); }
Aquí aprovechamos para colocar la cookie de sesión, pues tenemos todo lo necesario y podemos dejar entrar al usuario a su dashboard, por eso lo redireccionamos también. (en mi ejemplo no hay redirección, solo devuelvo un componente).
¡Bien, ya estamos por terminar! No podemos detenernos ahora. 💪🏼
6. Checando la cookie al iniciar y cerrando sesión
Para terminar, vamos a colocar una pequeña validación. Si la cookie ya está presente, en vez de devolver <Home/>
devolveremos <Dash/>
en nuestra ruta, antes de todo.
//server.entry.tsx import { getCookie, setCookie } from "hono/cookie"; const cookie = getCookie(c, "userId"); if (cookie) { // look up to DB return c.html(<Dash email={cookie} />); }
Y si encontramos un intent
igual a logout
borramos la cookie y devolvemos <Home/>
if (intent === "logout") { setCookie(c, "userId", ""); // Podrías redireccionar, yo estoy usando una sola ruta return c.html(<Home />); }
Recordemos que este intent
viene del formulario en <Dash/>
:
// <Dash/> <form> <button name="intent" value="logout" > Cerrar sesión </button> </form>
¡Genial, nuestro flujo está completo! 🤯
Recuerda que te dejo todo el código en el repo de Github. Espero esto te sea útil, si es así, por favor considera suscribirte 😉
Abrazo. Bliss. 🤓

Zustand llegó para destronar a Redux
Checa este otro Post
