Hoy vamos a ver cómo implementar login mediante LinkedIn mediante OpenID connect usando React.
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ónfetchAccessToken
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
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.
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.
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.
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}`; };
- 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 enlocalStorage
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:
generateRandomString(length)
:
- Genera una cadena aleatoria de la longitud especificada (
length
). Puedes cambiar el tamaño dependiendo de cuán seguro quieras que sea elstate
, aunque 16 caracteres es suficiente para la mayoría de los casos.
- Almacenar el
state
:
- Antes de redirigir al usuario a LinkedIn, generas y almacenas el
state
enlocalStorage
. También lo envías como parte de la URL de autorización.
- 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.
Ingeniero en Informática, Investigador, me encanta crear cosas o arreglarlas y darles una nueva vida. Escritor y poeta. Más de 20 APPs publicadas y un libro en Amazon.