diff --git a/packages/auth/src/AuthProvider.tsx b/packages/auth/src/AuthProvider.tsx index 2b6447a5af09..2cffda95b2c6 100644 --- a/packages/auth/src/AuthProvider.tsx +++ b/packages/auth/src/AuthProvider.tsx @@ -16,7 +16,10 @@ export interface AuthContextInterface { /* Determining your current authentication state */ loading: boolean isAuthenticated: boolean - authToken: string | null + /** + * @deprecated auth tokens are now refreshed when they expire, use getToken() instead. authToken will be removed from this context in future releases + */ + authToken: string | null // @WARN! deprecated, will always be null /* The current user's data from the `getCurrentUser` function on the api side */ currentUser: null | CurrentUser /* The user's metadata from the auth provider */ @@ -55,7 +58,7 @@ export interface AuthContextInterface { export const AuthContext = React.createContext({ loading: true, isAuthenticated: false, - authToken: null, + authToken: null, // @WARN! deprecated, will always be null userMetadata: null, currentUser: null, }) @@ -69,7 +72,7 @@ type AuthProviderProps = { type AuthProviderState = { loading: boolean isAuthenticated: boolean - authToken: string | null + authToken: string | null // @WARN! deprecated, will always be null userMetadata: null | Record currentUser: null | CurrentUser hasError: boolean @@ -96,7 +99,7 @@ export class AuthProvider extends React.Component< state: AuthProviderState = { loading: true, isAuthenticated: false, - authToken: null, + authToken: null, // @WARN! deprecated, will always be null userMetadata: null, currentUser: null, hasError: false, @@ -115,6 +118,8 @@ export class AuthProvider extends React.Component< } getCurrentUser = async (): Promise> => { + // Always get a fresh token, rather than use the one in state + const token = await this.getToken() const response = await window.fetch( `${window.__REDWOOD__API_PROXY_PATH}/graphql`, { @@ -122,7 +127,7 @@ export class AuthProvider extends React.Component< headers: { 'content-type': 'application/json', 'auth-provider': this.rwClient.type, - authorization: `Bearer ${this.state.authToken}`, + authorization: `Bearer ${token}`, }, body: JSON.stringify({ query: @@ -173,15 +178,13 @@ export class AuthProvider extends React.Component< } getToken = async () => { - const authToken = await this.rwClient.getToken() - this.setState({ ...this.state, authToken }) - return authToken + return this.rwClient.getToken() } reauthenticate = async () => { const notAuthenticatedState: AuthProviderState = { isAuthenticated: false, - authToken: null, + authToken: null, // @WARN! deprecated, will always be null currentUser: null, userMetadata: null, loading: false, diff --git a/packages/auth/src/__tests__/AuthProvider.test.tsx b/packages/auth/src/__tests__/AuthProvider.test.tsx index 8ea14eb37eaf..6c1d9241d405 100644 --- a/packages/auth/src/__tests__/AuthProvider.test.tsx +++ b/packages/auth/src/__tests__/AuthProvider.test.tsx @@ -1,5 +1,7 @@ require('whatwg-fetch') +import { useEffect, useState } from 'react' + import { render, screen, fireEvent, waitFor } from '@testing-library/react' import '@testing-library/jest-dom/extend-expect' import { graphql } from 'msw' @@ -41,9 +43,9 @@ const AuthConsumer = () => { const { loading, isAuthenticated, - authToken, logOut, logIn, + getToken, userMetadata, currentUser, reauthenticate, @@ -52,6 +54,17 @@ const AuthConsumer = () => { error, } = useAuth() + const [authToken, setAuthToken] = useState(null) + + const retrieveToken = async () => { + const token = await getToken() + setAuthToken(token) + } + + useEffect(() => { + retrieveToken() + }, []) + if (loading) { return <>Loading... } diff --git a/packages/web/src/components/RedwoodApolloProvider.tsx b/packages/web/src/components/RedwoodApolloProvider.tsx index 83f678c2b984..d1e1cca1676d 100644 --- a/packages/web/src/components/RedwoodApolloProvider.tsx +++ b/packages/web/src/components/RedwoodApolloProvider.tsx @@ -2,12 +2,15 @@ import { ApolloProvider, ApolloClientOptions, ApolloClient, + ApolloLink, InMemoryCache, useQuery, useMutation, + createHttpLink, } from '@apollo/client' +import { setContext } from '@apollo/client/link/context' -import type { AuthContextInterface } from '@redwoodjs/auth' +import { AuthContextInterface, useAuth } from '@redwoodjs/auth' import { FetchConfigProvider, @@ -20,12 +23,46 @@ const ApolloProviderWithFetchConfig: React.FunctionComponent<{ config?: Omit, 'cache'> }> = ({ config = {}, children }) => { const { uri, headers } = useFetchConfig() + const { getToken, type: authProviderType, isAuthenticated } = useAuth() + + const withToken = setContext(async () => { + if (isAuthenticated && getToken) { + const token = await getToken() + + return { token } + } + + return { token: null } + }) + + const authMiddleware = new ApolloLink((operation, forward) => { + const { token } = operation.getContext() + + // Only add auth headers when token is present + // Token is null, when !isAuthenticated + const authHeaders = token + ? { + 'auth-provider': authProviderType, + authorization: `Bearer ${token}` + } + : {} + + operation.setContext(() => ({ + headers: { + ...headers, + // Duped auth headers, because we may remove FetchContext at a later date + ...authHeaders, + }, + })) + return forward(operation) + }) + + const httpLink = createHttpLink({ uri }) const client = new ApolloClient({ cache: new InMemoryCache(), - uri, - headers, ...config, + link: ApolloLink.from([withToken, authMiddleware.concat(httpLink)]), }) return {children}