React AWS Amplify

Apollo Client, GraphQL y Auth0: Una implementación completa

Martin Rojas
4 min de lectura
Apollo Client, GraphQL y Auth0: Una implementación completa

Integra Auth0 y Apollo Client sin esfuerzo: aprende a manejar tokens expirados, implementar enlaces de middleware y añadir encabezados para autenticación.

Este artículo es el resultado de meses probando diferentes implementaciones y descubriendo más capas al usar Auth0 y Apollo. Aunque estoy seguro de que algunos de los principios funcionarán bien con otras bibliotecas similares, no quiero atribuirme todo el crédito por este enfoque, ya que fue recopilado de múltiples foros, issues de GitHub y artículos.

Para este código, estoy utilizando la biblioteca auth0-react, que es relativamente nueva, pero esta solución también puede usarse con su SDK auth0-spa. Al intentar usar un servidor GraphQL autenticado con Apollo Client / Auth0 / React basado en los tutoriales, uno de los problemas que nunca parecía abordarse era una forma limpia de obtener los tokens y, si estos estaban expirados, actualizarlos y reintentar la consulta o mutación sin interrupciones.

La mayoría de las soluciones sugerían extraer el token del almacenamiento local (autenticación). Sin embargo, si tenías un token expirado, la única solución ofrecida era eliminarlo y cerrar la sesión del usuario. El primer avance vino de mattwilson1024 en el foro de Auth0.

AuthorizedApolloProvider.tsx

import {
	ApolloClient,
	ApolloProvider,
	createHttpLink,
	InMemoryCache,
} from '@apollo/client';
import { setContext } from '@apollo/link-context';
import React from 'react';

import { useAuth0 } from '../react-auth0-spa';

const AuthorizedApolloProvider = ({ children }) => {
	const { getTokenSilently } = useAuth0();

	const httpLink = createHttpLink({
		uri: 'http://localhost:4000/graphql', // tu URI aquí...
	});

	const authLink = setContext(async () => {
		const token = await getTokenSilently();
		return {
			headers: {
				Authorization: `Bearer ${token}`,
			},
		};
	});

	const apolloClient = new ApolloClient({
		link: authLink.concat(httpLink),
		cache: new InMemoryCache(),
		connectToDevTools: true,
	});

	return <ApolloProvider client={apolloClient}>{children}</ApolloProvider>;
};

export default AuthorizedApolloProvider;

Al crear un componente de React alrededor del proveedor de Apollo, todos los hooks y funciones de React quedan disponibles. Por lo tanto, obtener el token desde el hook de Auth0 significa que siempre será un token válido y, en los casos en que un token almacenado haya expirado, la biblioteca de Auth0 será la responsable de refrescarlo y no Apollo.

Ahora bien, según la documentación de Apollo, la forma correcta de añadir un encabezado es creando un enlace de middleware; sin embargo, esta no es una función que trabaje con procesos asíncronos y, por lo tanto, tuve que cambiar al uso del enlace setContext. https://www.apollographql.com/docs/link/links/context/

El problema con esto es que, si estás pasando otros atributos en el encabezado, estos no pasarán, y la documentación de setContext de Apollo no menciona cómo obtener los encabezados en una llamada. Fue a través de https://github.com/apollographql/apollo-client/issues/4990 que alguien compartió la sintaxis correcta para acceder a los encabezados.

La implementación final de AuthorizedApolloProvider, que permitirá pasar encabezados adicionales desde cada consulta, también implementó otros enlaces útiles. Por ejemplo, una pequeña solución si estás usando LogRocket:

import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { ApolloProvider } from 'react-apollo';
import { BatchHttpLink } from 'apollo-link-batch-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { onError } from 'apollo-link-error';
import { RetryLink } from 'apollo-link-retry';
import { useAuth0 } from '@auth0/auth0-react';
import LogRocket from 'logrocket';
import React from 'react';
import { setContext } from 'apollo-link-context';

// Si quieres habilitar/deshabilitar herramientas de desarrollo en diferentes entornos
const devTools = localStorage.getItem('apolloDevTools') || false;

const AuthorizedApolloProvider = ({ children }) => {
	const { getAccessTokenSilently } = useAuth0();
	const authMiddleware = setContext(async (_, { headers, ...context }) => {
		const token = await getAccessTokenSilently();
		
		if (typeof Storage !== 'undefined') {
			localStorage.setItem('token', token);
		}

		console.log('Network ID:', activeNetworkID);
		return {
			headers: {
				...headers,
				...(token ? { Authorization: `Bearer ${token}` } : {}),
			},
			...context,
		};
	});

	/**
	 * Añadiendo solución para mejorar la grabación de LogRocket
	 * https://docs.logrocket.com/docs/troubleshooting-sessions#apollo-client
	 */

	const fetcher = (...args) => {
		return window.fetch(...args);
	};

	const client = new ApolloClient({
		link: ApolloLink.from([
			onError(({ graphQLErrors, networkError }) => {
				if (graphQLErrors) {
					LogRocket.captureException(graphQLErrors);
					graphQLErrors.forEach(({ message, locations, path }) =>
						console.error(
							`[Error de GraphQL]: Mensaje: ${message}, Ubicación: ${locations}, Ruta: ${path}`
						)
					);
				}
				if (networkError) {
					// localStorage.removeItem('token');
					LogRocket.captureException(networkError);
					console.error(`[Error de red]:`, networkError);
				}
			}),
			authMiddleware,
			new RetryLink(),
			new BatchHttpLink({
				uri: `${getConfig().apiUrl}`,
				fetch: fetcher,
			}),
		]),
		cache: new InMemoryCache(),
		connectToDevTools: devTools,
	});

	return <ApolloProvider client={client}>{children}</ApolloProvider>;
};

export default AuthorizedApolloProvider;