Cómo implementar un flujo de trabajo OAuth2 en Node.js

Cómo implementar un flujo de trabajo OAuth2 en JavaScript y Node.js configurando una conexión OAuth a la API de Github.

Primeros pasos

Para este tutorial, vamos a utilizar el marco JavaScript de pila completa de CheatCode, Joystick. Joystick reúne un marco de interfaz de usuario de front-end con un back-end de Node.js para crear aplicaciones.

Para comenzar, querremos instalar Joystick a través de NPM. Asegúrese de estar usando Node.js 16+ antes de instalar para garantizar la compatibilidad (lea este tutorial primero si necesita aprender a instalar Node.js o ejecutar varias versiones en su computadora):

Terminal

npm i -g @joystick.js/cli

Esto instalará Joystick globalmente en su computadora. Una vez instalado, vamos a crear un nuevo proyecto:

Terminal

joystick create app

Después de unos segundos, verá un mensaje desconectado de 04 en su nuevo proyecto y ejecute 16 . Antes de ejecutar 29 , necesitamos agregar una dependencia:36 .

Terminal

cd app && npm i node-fetch

Con eso instalado, continúe e inicie su aplicación:

Terminal

joystick start

Después de esto, su aplicación debería estar ejecutándose y estamos listos para comenzar.

Advertencia justa

Si bien OAuth2 en sí mismo es un estándar para implementar patrones de autenticación, la implementación de ese estándar no siempre es consistente. Elegimos Github como nuestra API de ejemplo porque su implementación de OAuth está bien hecha y bien documentada. Este no es siempre el caso para la API que elijas .

El punto es:mire los pasos que cubrimos aquí como una aproximación de lo que una implementación de OAuth2 debería parecerse a una API. A veces tienes suerte, a veces terminas con una denuncia por ruido de la policía. Algunas inconsistencias comunes a tener en cuenta:

  1. Parámetros no documentados o mal documentados que deben pasarse en HTTP 49 , consulta 56 , o 67 .
  2. Tipos de respuesta no documentados o mal documentados que deben pasarse en HTTP 72 . Por ejemplo, algunas API pueden requerir el 80 el encabezado se establece en 99 para obtener una respuesta en formato JSON.
  3. Código de ejemplo incorrecto en la documentación.
  4. Códigos de error incorrectos cuando se pasan parámetros incorrectos (consulte los elementos anteriores).

Si bien esto no es todo encontrará, estos son generalmente los que harán perder su tiempo y energía. Si está seguro de que está siguiendo perfectamente la documentación de su API y aún tiene problemas:revise la lista anterior y juegue con lo que está pasando (incluso si no está documentado por la API en cuestión, por muy frustrante que sea) .

Obtener credenciales de la API de Github

Para comenzar, necesitaremos registrar nuestra aplicación con Github y obtener credenciales de seguridad. Este es un patrón común con todas las implementaciones de OAuth2 . En particular, necesitarás dos cosas:un 108 y un 112 .

El 121 le dice a la API quién o qué aplicación está tratando de obtener permiso para autenticarse en nombre de un usuario mientras que el 135 autoriza la conexión demostrando la propiedad de la aplicación especificada por el 140 (esto es público, así que técnicamente cualquiera puede pasarlo a una API, mientras que el 154 es, como su nombre lo indica, secreto ).

Si aún no tiene una cuenta de Github, diríjase a este enlace y cree una cuenta.

Una vez que haya iniciado sesión, en la esquina superior derecha del sitio, haga clic en el ícono circular con su avatar y una flecha hacia abajo al lado. En el menú que aparece, seleccione "Configuración".

A continuación, cerca de la parte inferior del menú de la izquierda en esa página, busque y haga clic en la opción "Configuración del desarrollador". En la página siguiente, en el menú de la izquierda, busque y haga clic en la opción "Aplicaciones OAuth".

Si es la primera vez que registra una aplicación OAuth con Github, debería ver un botón verde que le indica "Registrar una nueva aplicación". Haga clic en eso para iniciar el proceso de obtención de su 165 y 175 .

En esta página, deberá proporcionar tres cosas:

  1. Un nombre para su aplicación OAuth. Esto es lo que Github mostrará a los usuarios cuando confirmen su acceso a su cuenta.
  2. Una URL de página de inicio para su aplicación (puede ser solo una URL ficticia para realizar pruebas).
  3. Una "URL de devolución de llamada de autorización" que es donde Github enviará un 187 especial en respuesta a la aprobación de un usuario para otorgar permiso a nuestra aplicación para acceder a su cuenta.

Para el #3, en este tutorial, queremos ingresar 198 (esto es diferente de lo que verá en la captura de pantalla anterior, pero es equivalente en términos de intención). 200 es donde la aplicación que creamos usando el marco Joystick de CheatCode se ejecutará de forma predeterminada. El 215 parte es la ruta/ruta que conectaremos a continuación donde esperamos que Github nos envíe una autorización 225 que podemos cambiar por un 237 para la cuenta del usuario.

Después de completar esto, haga clic en "Registrar aplicación" para crear su aplicación OAuth. En la siguiente pantalla, querrá ubicar el "ID de cliente" y hacer clic en el botón "Generar un nuevo secreto de cliente" cerca del medio de la página.

Nota :cuando generas tu 248 Github intencionalmente solo te lo mostrará en la pantalla una vez . Se recomienda que respaldes esto y tu 257 en un administrador de contraseñas u otro administrador de secretos. Si lo pierde, deberá generar un nuevo secreto y eliminar el anterior para evitar un posible problema de seguridad.

Mantenga esta página activa o copie el 263 y 276 para usar en el siguiente paso.

Agregar nuestras credenciales a nuestro archivo de configuración

Antes de profundizar en el código, a continuación, debemos copiar nuestro 280 y 294 en el archivo de configuración de nuestra aplicación. En una aplicación Joystick, esto se crea automáticamente para nosotros cuando ejecutamos 307 .

Abre el 316 archivo en la raíz de su aplicación:

/configuración-desarrollo.json

{
  "config": {
    "databases": [ ... ],
    "i18n": {
      "defaultLanguage": "en-US"
    },
    "middleware": {},
    "email": { ... }
  },
  "global": {},
  "public": {
    "github": {
      "client_id": "dc47b6a0a67b904c58c7"
    }
  },
  "private": {
    "github": {
      "client_id": "dc47b6a0a67b904c58c7",
      "client_secret": "<Client Secret Here>",
      "redirect_uri": "http://localhost:2600/oauth/github"
    }
  }
}

Queremos centrarnos en dos lugares:el 324 y 330 objetos ya presentes en el archivo. Dentro de ambos, queremos anidar un 342 objeto que contendrá nuestras credenciales.

Presta atención aquí :solo queremos almacenar el 352 bajo el 362 objeto mientras queremos almacenar tanto el 373 y 380 bajo el 399 objeto. También queremos agregar el 400 escribimos en Github (el 416 uno).

Una vez que haya configurado estos, estamos listos para profundizar en el código.

Cableado de la solicitud de autorización del cliente

Para comenzar, agregaremos una página simple en nuestra interfaz de usuario donde podemos acceder al botón "Conectar a Github" en el que nuestros usuarios pueden hacer clic para inicializar una solicitud de OAuth. Para construirlo, vamos a reutilizar el 428 ruta que se define automáticamente para nosotros cuando generamos una aplicación con 438 . Muy rápido, si abrimos 441 en la raíz del proyecto, podemos ver cómo Joystick lo representa:

/index.servidor.js

import node from "@joystick.js/node";
import api from "./api";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

En una aplicación Joystick, las rutas se definen a través de una instancia Express.js que se configura automáticamente a través del 452 función importada del 462 paquete. A esa función se le pasa un objeto con un 471 opción establecida en un objeto donde se definen todas las rutas para nuestra aplicación.

Aquí, el 486 la ruta de índice (o ruta "raíz") usa el 499 función definida por Joystick en HTTP 507 objeto que obtenemos de Express.js. Esa función está diseñada para representar un componente de Joystick creado con la biblioteca de interfaz de usuario de Joystick 514 .

Aquí, podemos ver el 521 camino que se está pasando. Abramos ese archivo ahora y modifíquelo para mostrar nuestro botón "Conectar a Github".

/ui/pages/index/index.js

import ui from "@joystick.js/ui";

const Index = ui.component({
  events: {
    'click .login-with-github': (event) => {
      location.href = `https://github.com/login/oauth/authorize?client_id=${joystick.settings.public.github.client_id}&scope=repo user`;
    },
  },
  css: `
    div {
      padding: 40px;
    }

    .login-with-github {
      background: #333;
      padding: 15px 20px;
      border-radius: 3px;
      border: none;
      font-size: 15px;
      color: #fff;
    }

    .login-with-github {
      cursor: pointer;
    }

    .login-with-github:active {
      position: relative;
      top: 1px;
    }
  `,
  render: () => {
    return `
      <div>
        <button class="login-with-github">Connect to Github</button>
      </div>
    `;
  },
});

export default Index;

Aquí, hemos sobrescrito el contenido existente de nuestro 531 archivo con el componente que renderizará nuestro botón. En Joystick, los componentes se definen llamando al 545 función importada del 551 paquete y pasó un objeto de opciones para describir el comportamiento y la apariencia del componente.

Aquí, abajo en el 561 función, devolvemos una cadena de HTML que queremos que Joystick represente en el navegador para nosotros. En esa cadena, tenemos un simple 574 elemento con un nombre de clase 586 . Si nos fijamos en la opción de arriba 590 , 603 , podemos ver que se aplican algunos estilos a nuestro componente, agregando un poco de relleno a la página y estilizando nuestro botón.

La parte importante aquí está en el 610 objeto. Aquí, definimos un detector de eventos para un 628 evento en un elemento con la clase 633 . Cuando ese evento se detecta en el navegador, la función que hemos asignado a 647 aquí será llamado.

En el interior, nuestro objetivo es redirigir al usuario a la URL de Github para iniciar una solicitud de autorización de OAuth. Para hacerlo, configuramos el 657 global valor en el navegador a una cadena que contiene la URL junto con algunos parámetros de consulta:

  1. 662 aquí se le asigna el valor de 679 que configuramos en nuestro 682 archivo anterior.
  2. 699 establecido igual a dos "ámbitos" que otorgan permisos específicos al 700 obtenemos de Github para este usuario. Aquí, estamos usando el 715 y 720 (separados por espacios según la documentación de Github) alcances para darnos acceso a los repositorios de usuarios en Github y su perfil de usuario completo. Una lista completa de alcances para solicitar está disponible aquí.

Si guardamos estos cambios con nuestra aplicación en ejecución, Joystick se actualizará automáticamente en el navegador. Suponiendo que nuestras credenciales sean correctas, deberíamos ser redirigidos a Github y ver algo como esto:

A continuación, antes de hacer clic en el botón "Autorizar", debemos conectar el punto final al que Github redirigirá al usuario (la "URL de devolución de llamada de autorización" que configuramos en 732 anterior).

Manejo del intercambio de fichas

El paso final para que todo funcione es realizar un intercambio de tokens con Github. Para aprobar nuestra solicitud y finalizar nuestra conexión, Github necesita verificar la solicitud para conectarse con nuestro servidor. Para hacerlo, cuando el usuario haga clic en "Autorizar" en la interfaz de usuario que acabamos de ver en Github, enviará una solicitud a la "URL de devolución de llamada de autorización" que especificamos al configurar nuestra aplicación, pasando un 741 valor en los parámetros de consulta de la URL de solicitud que podemos "intercambiar" por un 755 permanente para nuestro usuario.

Para comenzar, lo primero que debemos hacer es conectar esa URL/ruta en nuestro 764 archivo:

/index.servidor.js

import node from "@joystick.js/node";
import api from "./api";
import github from "./api/oauth/github";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/oauth/github": async (req, res) => {
      await github({ req });
      res.status(200).redirect('/');
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

Algunos cambios menores a lo que vimos antes. Aquí, estamos agregando nuestra ruta 779 exactamente de la misma manera que aprendimos sobre 783 más temprano. Dentro, añadimos el 797 palabra clave a la función que se llamará cuando se cargue nuestra ruta, anticipando una llamada a una función 806 que devolverá una promesa de JavaScript de que podemos 815 antes de responder a la solicitud de la ruta.

Una vez que se completa esa función, queremos responder a la solicitud de Github con un estado de 827 y llama al 831 para redirigir al usuario a la página de nuestra aplicación donde originó la solicitud (nuestro 844 ruta de índice).

A continuación, conectemos esa función que anticipábamos que estaría disponible en 858 en nuestro proyecto:

/api/oauth/github.js

/* eslint-disable consistent-return */

import fetch from 'node-fetch';
import { URL, URLSearchParams } from 'url';

const getReposFromGithub = (username = '', access_token = '') => {
  return fetch(`https://api.github.com/user/repos`, {
    headers: {
      Accept: 'application/json',
      Authorization: `token ${access_token}`,
    },
  }).then(async (response) => {
    const data = await response.json();
    return data;
  }).catch((error) => {
    console.warn(error);
    throw new Error(error);
  });
};

const getUserFromGithub = (access_token = '') => {
  return fetch('https://api.github.com/user', {
    headers: {
      Accept: 'application/json',
      Authorization: `token ${access_token}`,
    },
  }).then(async (response) => {
    const data = await response.json();
    return data;
  }).catch((error) => {
    console.warn(error);
    throw new Error(error);
  });
};

const getAccessTokenFromGithub = (code = '') => {
  try {
    const url = new URL('https://github.com/login/oauth/access_token');
    const searchParams = new URLSearchParams({
      client_id: joystick.settings.private.github.client_id,
      client_secret: joystick.settings.private.github.client_secret,
      code,
      redirect_uri: joystick.settings.private.github.redirect_uri,
    });

    url.search = searchParams.toString();

    return fetch(url, {
      method: 'POST',
      headers: {
        Accept: 'application/json'
      },
    }).then(async (response) => {
      const data = await response.json();
      return data;
    }).catch((error) => {
      console.warn(error);
      throw new Error(error);
    });
  } catch (exception) {
    throw new Error(`[github.getAccessTokenFromGithub] ${exception.message}`);
  }
};

const validateOptions = (options) => {
  try {
    if (!options) throw new Error('options object is required.');
    if (!options.req) throw new Error('options.req is required.');
  } catch (exception) {
    throw new Error(`[github.validateOptions] ${exception.message}`);
  }
};

const github = async (options, { resolve, reject }) => {
  try {
    validateOptions(options);
    const { access_token } = await getAccessTokenFromGithub(options?.req?.query?.code);
    const user = await getUserFromGithub(access_token);
    const repos = await getReposFromGithub(user?.login, access_token);

    // NOTE: Set this information on a user in your database or store elsewhere for reuse.
    console.log({
      access_token,
      user,
      repos,
    });

    resolve();
  } catch (exception) {
    reject(`[github] ${exception.message}`);
  }
};

export default (options) =>
  new Promise((resolve, reject) => {
    github(options, { resolve, reject });
  });

Para hacer que todo sea más fácil de entender, aquí estamos haciendo un volcado de código completo y luego recorriéndolo paso a paso. En este archivo, usamos un patrón conocido como patrón de acción (algo que se me ocurrió hace unos años para organizar código algorítmico o de varios pasos en una aplicación).

La construcción básica de un patrón de acción es que tenemos una sola función principal (aquí, definida como 865 ) que llama a otras funciones en secuencia. Cada función en esa secuencia realiza una sola tarea y, si es necesario, devuelve un valor para entregar a las otras funciones en la secuencia.

Cada función se define como una función de flecha con JavaScript 877 bloque inmediatamente dentro de su cuerpo. En el 882 block, ejecutamos el código de la función y en el 891 llamamos al 906 pasando una cadena estandarizada con nuestro error.

La idea en juego aquí es darle a nuestro código algo de estructura y mantener las cosas organizadas mientras que los errores son más fáciles de rastrear (si ocurre un error dentro de una función, el 917 parte nos dice dónde ocurrió exactamente el error).

Aquí, debido a que esta es una acción de "Promesa", envolvemos el 923 principal funcione con una Promesa de JavaScript en la parte inferior de nuestro archivo y exporte eso función. De vuelta en nuestro 937 archivo, es por eso que podemos usar el 948 patrón.

Para nuestra "acción", tenemos tres pasos:

  1. Cambia el 959 que obtenemos de Github para un 967 permanente .
  2. Obtener el usuario asociado con ese 973 de la API de Github.
  3. Obtenga los repositorios para el usuario asociado con ese 985 de la API de Github.

La idea aquí es mostrar el proceso de obtener un token y luego realizar solicitudes de API con esa ficha Entonces está claro, esto se mantiene genérico para que pueda aplicar este patrón/inicio de sesión a cualquier API de OAuth.

/api/oauth/github.js

const getAccessTokenFromGithub = (code = '') => {
  try {
    const url = new URL('https://github.com/login/oauth/access_token');
    const searchParams = new URLSearchParams({
      client_id: joystick.settings.private.github.client_id,
      client_secret: joystick.settings.private.github.client_secret,
      code,
      redirect_uri: joystick.settings.private.github.redirect_uri,
    });

    url.search = searchParams.toString();

    return fetch(url, {
      method: 'POST',
      headers: {
        Accept: 'application/json'
      },
    }).then(async (response) => {
      const data = await response.json();
      return data;
    }).catch((error) => {
      console.warn(error);
      throw new Error(error);
    });
  } catch (exception) {
    throw new Error(`[github.getAccessTokenFromGithub] ${exception.message}`);
  }
};

Centrándose en el primer paso de la secuencia 995 , aquí, necesitamos realizar una solicitud de regreso al 1005 endpoint en la API de Github para obtener un 1016 permanente .

Para hacerlo, queremos realizar un HTTP 1020 solicitud (según los documentos de Github y el estándar para las implementaciones de OAuth), pasando los parámetros necesarios para la solicitud (nuevamente, según Github pero similar para todas las solicitudes de OAuth2).

Para hacer eso, importamos el 1032 y 1043 clases de Node.js 1059 paquete (no tenemos que instalar este paquete, está disponible automáticamente en una aplicación Node.js).

Primero, necesitamos crear un nuevo objeto URL para el 1061 punto final en Github con 1071 pasando esa URL. A continuación, debemos generar los parámetros de búsqueda para nuestra solicitud 1084 y entonces usamos el 1095 clase, pasando un objeto con todos los parámetros de consulta que queremos agregar a nuestra URL.

Aquí, necesitamos cuatro:1106 , 1114 , 1129 y 1132 . Usando estos cuatro parámetros, Github podrá autenticar nuestra solicitud de un 1140 y devolver uno que podamos usar.

Para nuestro 1157 , 1164 y 1174 , los extraemos del 1188 objeto que definimos anteriormente en el tutorial. El 1194 es el código que recuperamos del 1205 valor que nos pasa Github (en una aplicación Express.js, cualquier parámetro de consulta pasado a nuestro servidor se establece en el objeto 1217 en el 1227 entrante objeto deseado).

Con eso, antes de realizar nuestra solicitud, agregamos nuestros parámetros de búsqueda a nuestra URL configurando el 1233 valor igual al resultado de llamar a 1246 en nuestro 1251 variable. Esto generará una cadena que parece 1261 .

Finalmente, con esto arriba importamos 1276 del 1288 paquete que instalamos anteriormente. Lo llamamos, pasando nuestro 1290 objeto que acabamos de generar, seguido de un objeto de opciones con un 1305 valor establecido en 1319 (lo que significa que queremos que la solicitud se realice como HTTP 1325 solicitud) y un 1330 objeto. En ese 1343 objeto, pasamos el estándar 1358 encabezado para decirle a la API de Github el tipo MIME que aceptaremos para su respuesta a nuestra solicitud (en este caso, 1363 ). Si omitimos esto, Github devolverá la respuesta usando el 1376 predeterminado tipo MIME.

Una vez que se llama esto, esperamos 1384 para devolvernos una Promesa de JavaScript con la respuesta. Para obtener la respuesta como un objeto JSON, tomamos el 1398 pasado a la devolución de llamada de nuestro 1405 método y luego llamar a 1419 decirle a 1426 para formatear el cuerpo de la respuesta que recibió como datos JSON (usamos 1438 aquí para decirle a JavaScript que espere la respuesta del 1449 función).

Con ese 1458 a mano, lo devolvemos desde nuestra función. Si todo salió según lo planeado, deberíamos obtener un objeto similar a este de Github:

{
  access_token: 'gho_abc123456',
  token_type: 'bearer',
  scope: 'repo,user'
}

A continuación, si revisamos nuestro principal 1461 función para nuestra acción, podemos ver que el siguiente paso es tomar el objeto resultante que obtenemos del 1474 funcionar y desestructurarlo, arrancando el 1487 propiedad que vemos en el ejemplo de respuesta anterior.

Con esto, ahora tenemos acceso permanente a los repositorios y la cuenta de usuario de este usuario en Github (completando la parte OAuth del flujo de trabajo) hasta que revoquen el acceso.

Aunque técnicamente hemos terminado con nuestra implementación de OAuth, es útil ver el por qué detrás de lo que estamos haciendo. Ahora, con nuestro 1498 podemos realizar solicitudes a la API de Github en nombre de nuestros usuarios. Es decir, en lo que respecta a Github (y dentro de las limitaciones de los ámbitos que solicitamos), somos ese usuario hasta que el usuario diga que no lo somos y revoque nuestro acceso.

/api/oauth/github.js

const getUserFromGithub = (access_token = '') => {
  return fetch('https://api.github.com/user', {
    headers: {
      Accept: 'application/json',
      Authorization: `token ${access_token}`,
    },
  }).then(async (response) => {
    const data = await response.json();
    return data;
  }).catch((error) => {
    console.warn(error);
    throw new Error(error);
  });
};

Centrándonos en nuestra llamada a 1508 el proceso para realizar nuestra solicitud API es casi idéntico a nuestro 1510 solicitud con la adición menor de un nuevo encabezado 1525 . Este es otro encabezado HTTP estándar que nos permite pasar una cadena de autorización al servidor al que estamos haciendo nuestra solicitud (en este caso, el servidor API de Github).

En esa cadena, siguiendo las convenciones de la API de Github (esta parte será diferente para cada API; algunas requieren el 1534 patrón mientras que otros requieren el 1549 mientras que otros requieren una versión codificada en base64 de uno de esos dos u otro patrón), pasamos la palabra clave 1550 seguido de un espacio y luego el 1566 valor que recibimos del 1577 función que escribimos anteriormente.

Para manejar la respuesta, realizamos exactamente los mismos pasos que vimos anteriormente usando 1581 para formatear la respuesta como datos JSON.

¡Con eso, deberíamos esperar obtener un gran objeto que describa a nuestro usuario!

Vamos a terminar aquí. Aunque lo hacemos tener otra llamada de función a 1593 , ya aprendimos lo que debemos comprender para realizar esta solicitud.

Vuelva a bajar en nuestro 1601 principal función, tomamos el resultado de las tres llamadas y las combinamos en un objeto que registramos en nuestra consola.

¡Eso es todo! Ahora tenemos acceso OAuth2 a nuestra cuenta de usuario de Github.

Terminando

En este tutorial, aprendimos cómo implementar un flujo de trabajo de autorización de OAuth2 usando la API de Github. Aprendimos sobre la diferencia entre las diferentes implementaciones de OAuth y observamos un ejemplo de cómo inicializar una solicitud en el cliente y luego manejar un intercambio de tokens en el servidor. Finalmente, aprendimos a tomar un 1617 recuperamos de un intercambio de tokens de OAuth y lo usamos para realizar solicitudes de API en nombre del usuario.