cover

Usando OAuth2 con Google y Hono en 6 pasos

author photo

Héctorbliss

@hectorbliss


robot logo saying hello

No te quedes atrás: Actualízate

Suscríbete para recibir información sobre nuevos frameworks, updates, eventos, tips, hacks y más.

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. OAuth credential 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:

  1. Hono nos permite devolver JSX con su método c.html
  2. Estamos capturando los queryParams (searchParams)
  3. Si el intent es igual a google-login utilizamos la función redirectToGoogle.
  4. La función espera que le pasemos redirect y env, 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.

Fixtergeek login screen demo

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. 🤓

banner

¿Quieres mantenerte al día sobre los próximos cursos y eventos?

Suscríbete a nuestro newsletter

Jamás te enviaremos spam, nunca compartiremos tus datos y puedes cancelar tu suscripción en cualquier momento 😉

robot logo saying hello
facebook icontwitter iconlinkedin iconinstagram iconyoutube icon

© 2016 - 2023 Fixtergeek