Implementar login con LinkedIn en React y OpenID Connect

Tiempo de lectura: 6 minutos

Hoy vamos a ver cómo implementar login mediante LinkedIn mediante OpenID connect usando React.

Campo de arroz - Pexels

Lo primero que tienes que hacer es registrar tu aplicación en Linkedin para obtener el client ID y el private ID:

Una vez creado, vamos a crear un componente llamado LinkedInOauth.tsx

Uso TypeScript:

import { LinkedinClientId, LinkedinClientIdSecret } from '@/util/Codes';
import React from 'react';

const CLIENT_ID = "client_id_linkedin";
const CLIENT_SECRET = "client_id_secret_linkedin;
const REDIRECT_URL = encodeURIComponent('http://localhost:3000/login');
const SCOPES = 'profile%20email%20openid';
const STATE = "random_value";
const linkedinOAuthURL = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URL}&scope=${SCOPES}`;

const LinkedInOAuth = () => {
    const handleLogin = async (code) => {
        // Exchange the code for an access token
        const data = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
            method: 'POST',
            body: new URLSearchParams({
                grant_type: 'authorization_code',
                code,
                redirect_uri: REDIRECT_URL,
                client_id: CLIENT_ID,
                client_secret: CLIENT_SECRET
            })
        }).then((response) => response.json());

        const accessToken = data.access_token;

        // Fetch the user's LinkedIn profile
        const userProfile: any = await fetch(
            'https://api.linkedin.com/v2/me?projection=(id,firstName,lastName)',
            {
                headers: {
                    Authorization: `Bearer ${accessToken}`
                }
            }
        );

        // Handle the user profile data (e.g., store it in your database and log the user in)
        console.log(
            `Welcome, ${userProfile.data.firstName.localized.en_US} ${userProfile.data.lastName.localized.en_US}!`
        );
    };

    const handleLinkedInCallback = () => {
        const queryString = window.location.search;
        const urlParams = new URLSearchParams(queryString);
        const code = urlParams.get('code');
        if (code) handleLogin(code);
    };

    React.useEffect(() => {
        handleLinkedInCallback();
    }, []);

    return (
        <div>
            <a href={linkedinOAuthURL}>Sign in with LinkedIn</a>
        </div>
    );
};

export default LinkedInOAuth;

Debes completar la parte de const CLIENT_ID = «CLIENT_ID»; añadiendo el código de cliente LinkedIn.
Tambien podemos añadir const CLIENT_SECRET para poder verificar el token (aunque recomiendo verificarlo en el servidor). añadiendo el código de cliente secret LinkedIn

La variable const state = ‘random_string_for_csrf_protection’; se puede generar de forma aleatoria. Si queremos utilizarla para evitar ataques de CSFR (Cross-Site Request Forgery) lo explicaré a continuación.

En const redirectUri indicaremos cual es nuestra URL de redirección permitida en LinkedIn.

El código de handleLogin tendremos que completarlo con nuestro back para poder verificar el token linkedin obtenido. Aunque incluyo uno para hacerlo en cliente, no es nada recomendable.

En este código incluyo la verificación de Cross-Site:

import React, { useEffect } from 'react';

const CLIENT_ID = "client_id_linkedin";
const CLIENT_SECRET = "client_id_secret_linkedin;
const REDIRECT_URL = 'http://localhost:3000/login';
const SCOPES = 'profile%20email%20openid';

// Función para generar un string aleatorio (para el estado de CSRF)
const generateRandomState = (length = 16) => {
  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  return Array.from({ length }, () => characters.charAt(Math.floor(Math.random() * characters.length))).join('');
};

const LinkedInOAuth = () => {
  // Generar la URL de autenticación de LinkedIn con el estado CSRF
  const createLinkedInAuthURL = () => {
    const state = generateRandomState();
    localStorage.setItem('linkedin_oauth_state', state);

    return `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(
      REDIRECT_URL
    )}&scope=${SCOPES}&state=${state}`;
  };

  const handleLogin = async (code) => {
    // Intercambiar el código de autorización por un token de acceso
    const tokenResponse = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
      method: 'POST',
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code,
        redirect_uri: REDIRECT_URL,
        client_id: CLIENT_ID,
        client_secret: CLIENT_SECRET,
      }),
    }).then((response) => response.json());

    const accessToken = tokenResponse.access_token;

    // Obtener el perfil del usuario de LinkedIn
    const userProfile = await fetch(
      'https://api.linkedin.com/v2/me?projection=(id,firstName,lastName)',
      {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      }
    ).then((res) => res.json());

    // Procesar los datos del perfil del usuario
    console.log(
      `Welcome, ${userProfile.firstName.localized.en_US} ${userProfile.lastName.localized.en_US}!`
    );
  };

  const handleLinkedInCallback = () => {
    const queryString = window.location.search;
    const urlParams = new URLSearchParams(queryString);
    const code = urlParams.get('code');
    const stateFromURL = urlParams.get('state');
    const storedState = localStorage.getItem('linkedin_oauth_state');

    // Verificar si el estado en la URL coincide con el almacenado (para evitar ataques CSRF)
    if (stateFromURL !== storedState) {
      console.error('CSRF protection failed: State mismatch.');
      return;
    }

    if (code) {
      handleLogin(code);
    }
  };

  useEffect(() => {
    handleLinkedInCallback();
  }, []);

  return (
    <div>
      <a href={createLinkedInAuthURL()}>Sign in with LinkedIn</a>
    </div>
  );
};

export default LinkedInOAuth;

Explicación del código:

Redirigir al usuario a LinkedIn:

  • La función linkedInAuthUrl construye la URL de autorización con los parámetros necesarios (client_id, redirect_uri, scope, etc.).
  • La función handleLogin simplemente redirige al usuario a esa URL de LinkedIn.

Capturar el código de autorización:

  • Usamos useEffect para detectar si en la URL hay un código de autorización (code) cuando LinkedIn redirige de vuelta a tu aplicación.
  • Una vez que tienes el code, llamas a la función fetchAccessToken para enviarlo al backend.

Intercambiar el código por un token de acceso:

  • En fetchAccessToken, haces una solicitud al backend para obtener el token de acceso a través del endpoint /auth/linkedin/token, que maneja la lógica del intercambio de código en el backend.
  • Si el intercambio es exitoso, se almacena el token de acceso en el estado (setAccessToken).

Y para utilizarlo haremos:

<LinkedInOauth/>

Distintos Scopes disponibles en Linkedin:

LinkedIn ofrece varios scopes que determinan los tipos de datos y permisos que tu aplicación puede solicitar del usuario. A continuación te dejo una lista de los scopes más comunes que puedes usar con LinkedIn OAuth 2.0:

r_liteprofile (Perfil básico)

  • Acceso al perfil básico del usuario.
  • Este es el perfil reducido que contiene campos como el nombre, apellido, la foto de perfil, y el ID de LinkedIn.
  • Datos disponibles:
    • First name
    • Last name
    • Profile picture (URL)
    • ID de LinkedIn
    Ejemplo de uso:
   scope=r_liteprofile

r_emailaddress (Correo electrónico)

  • Permite obtener la dirección de correo electrónico principal asociada con la cuenta de LinkedIn del usuario.
  • Datos disponibles:
    • Dirección de correo electrónico del usuario.
    Ejemplo de uso:
   scope=r_emailaddress

w_member_social (Publicar actualizaciones en nombre del usuario)

  • Permite a tu aplicación publicar contenido (artículos, actualizaciones) en el perfil del usuario.
  • Con este permiso, tu aplicación puede compartir contenido en la cuenta de LinkedIn del usuario de manera programática. Ejemplo de uso:
   scope=w_member_social

Scopes Adicionales:

r_fullprofile (Perfil completo) (Deprecado y muy limitado)

  • Este permiso permitía obtener datos más detallados del perfil del usuario, pero ya no está disponible para la mayoría de las aplicaciones.
  • Ahora solo algunas aplicaciones verificadas por LinkedIn pueden solicitar acceso a perfiles completos.

rw_organization_admin (Administración de organizaciones)

  • Permite gestionar las páginas de empresas y organizaciones en LinkedIn a las que el usuario tiene permisos administrativos.
  • Datos disponibles:
    • Publicar contenido en nombre de una página de empresa.
    • Ver los miembros de una organización.
    Ejemplo de uso:
   scope=rw_organization_admin

r_organization_social (Acceso a datos de organización)

  • Permite acceder a la información y contenido social de una organización.
  • Datos disponibles:
    • Obtener el contenido publicado por la organización en LinkedIn.
    Ejemplo de uso:
   scope=r_organization_social

Usos Comunes de Scopes:

Para un login básico con LinkedIn, se suelen utilizar:

scope=r_liteprofile r_emailaddress

Si tu aplicación también necesita publicar actualizaciones o contenido en LinkedIn en nombre del usuario, puedes añadir:

scope=r_liteprofile r_emailaddress w_member_social

Notas Importantes:

  • Privacidad y revisión de LinkedIn: Si solicitas permisos avanzados (como publicar en nombre de un usuario o acceder a datos de una organización), tu aplicación podría necesitar pasar por un proceso de revisión y aprobación por parte de LinkedIn.
  • Consentimiento del usuario: El usuario siempre debe aprobar los permisos que solicitas en el proceso de autenticación.

Extra: Protección contra ataques CSRF.

Para generar un valor aleatorio para state y protegerte contra ataques CSRF (Cross-Site Request Forgery), puedes crear una cadena aleatoria utilizando funciones de JavaScript. Esto se hace comúnmente en el frontend y se almacena temporalmente, por ejemplo, en el localStorage o en una cookie para verificarlo más adelante cuando LinkedIn redirija de vuelta a tu aplicación.

Generar cadena aleatoria:

const generateRandomString = (length) => {
  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  const charactersLength = characters.length;
  for (let i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
  }
  return result;
};

// Generar el 'state' dinámicamente
const state = generateRandomString(16); // 16 es el tamaño recomendado

Generar y almacenar el state antes de redirigir a LinkedIn:

  • Puedes almacenar el valor de state en localStorage para que luego puedas verificarlo cuando LinkedIn redirija de vuelta a tu app.
const handleLogin = () => {
  const state = generateRandomString(16);
  localStorage.setItem('linkedin_oauth_state', state); // Almacenar el state en localStorage

  const clientId = 'TU_CLIENT_ID';
  const redirectUri = encodeURIComponent('http://localhost:3000/auth/linkedin/callback');
  const scope = 'r_liteprofile r_emailaddress';

  window.location.href = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&state=${state}&scope=${scope}`;
};
  1. Verificar el state al recibir la redirección:
  • Cuando LinkedIn redirija de vuelta a tu aplicación con el state como parámetro, debes compararlo con el valor almacenado en localStorage para asegurarte de que coincidan.
useEffect(() => {
  const searchParams = new URLSearchParams(location.search);
  const code = searchParams.get('code');
  const returnedState = searchParams.get('state');
  const storedState = localStorage.getItem('linkedin_oauth_state'); // Recuperar el state almacenado

  if (returnedState !== storedState) {
    setError('CSRF protection failed: State does not match');
    return;
  }

  if (code) {
    // Proceder con el intercambio de código por token
    fetchAccessToken(code);
  }
}, [location]);

Explicación:

  1. generateRandomString(length):
  • Genera una cadena aleatoria de la longitud especificada (length). Puedes cambiar el tamaño dependiendo de cuán seguro quieras que sea el state, aunque 16 caracteres es suficiente para la mayoría de los casos.
  1. Almacenar el state:
  • Antes de redirigir al usuario a LinkedIn, generas y almacenas el state en localStorage. También lo envías como parte de la URL de autorización.
  1. Verificar el state:
  • Después de que LinkedIn redirija al usuario de vuelta, extraes el state de la URL y lo comparas con el que tienes almacenado. Si no coinciden, es probable que sea un ataque CSRF, por lo que deberías mostrar un mensaje de error y detener el proceso.

Recomendación:

Es recomendable borrar el state de localStorage después de que se haya usado para evitar que quede allí indefinidamente.

localStorage.removeItem('linkedin_oauth_state');

Con esta estrategia, proteges tu aplicación contra ataques CSRF durante el proceso de autenticación OAuth.

Deja un comentario