Communication between React Context and Apollo Client Link - reactjs

I am working on a single page webapp using React and Apollo Client, and I am wondering about how to correctly communicate authentication state between Apollo Client links and React context.
Within the client, I have written an AuthProvider context that supplies the current user information, so that anywhere in the component tree I can do
const authState = useAuthState()
const dispatch = useAuthDispatch()
and thus query and update the authentication information as I need. I have used this, for example, to write a PrivateRoute component that redirects the viewer if she is not authenticated:
const PrivateRoute: FunctionComponent<RouteProps> = ({ children, ...rest }) => {
const authState = useAuthState()
return (
<Route
{...rest}
render={({ location }) =>
authState.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
)
}
This is all fine. My issue arises when combining this with my chosen form of authentication, which is JWT. I am storing the access token in the authState and the refresh token is set as an httpOnly cookie by the back-end on login.
But I have to send the access token as an Authorization: Bearer header on each request, which I want to do using an Apollo Link, as follows:
const authLink = setContext(async (_, { headers }) => {
const token = getTokenFromAuthStateSomehow()
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ""
}
}
})
But this is within an Apollo Link, where I of course don't have direct access to the React Context. getTokenFromAuthStateSomehow() is a function I do not know how to write.
Then the next issue is what happens when this request fails because the access token has expired. I want to use Apollo's onError to catch a 401 error from the API and retry the request by getting a refreshed token:
const retryLink = onError(({ networkError }) => {
if (networkError) {
const newToken = getRefreshedToken()
if (newToken) {
retryRequest()
setTokenInAuthStateSomehow(newToken)
}
}
})
But then we have the same problem - now I need to send the new token back to authState, i.e. to React Context: setTokenInAuthStateSomehow() is a function I do not know how to write either.
So, the overarching question is: how do I communicate between an Apollo Link and React Context? Do I have to setup some listeners or events somehow? I would love any information or push in the right direction.

Related

Protected Route by checking JWT saved in user's cookie

I just finished implementing Google social authentication in my NextJS + DjangoRest project following this blog post. I am trying to figure out how to make protected routes that will redirect users if they’re not logged in.
This is how I did it so far:
when user logs in, it saves the jwt_token in the cookie as httponly
uses axios with “withCredentials: true” to access the API endpoint which returns current user data(i.e. email)
saves the user data as a useContext(). When protected page loads, check if UserContext is empty or not and redirects to login page if it is empty.
The obvious problem is the UserContext is reset whenever user refreshes the page, even when the JWT token is still present in the cookies. And I have a feeling this isn’t the right way to implement this.
So how would I implement a similar feature in a non-hacky way? I cannot read jwt-token from cookies in the frontend as it is httponly. Is there a safe way to read user’s JWT token from cookies to test for authentication?
So if I am reading your question right then you can use getServerSide props on your page to detect if the user is authenticated with your api.
function Page({ isAuth }) {
return (
<>
<div>My secure page</div>
//if you return data from your token check api then you could do something like this
<div>Welcome back {isAuth.name}</div>
</>
)
}
export default Page
export async function getServerSideProps(context) {
const isAuth = await tokenChecker(context.cookies.jwt) // In your token checker function you can just return data or false.
if (!isAuth) { //if tokenChecker returns false then redirect the user to where you want them to go
return {
redirect: {
destination: `/login`,
}
};
}
//else return the page
return {
props: {
isAuth,
},
}
}
If this is not what you mean let me know and i can edit my answer.
I modified #Matt's answer slightly and typescript-friendly to solve my problem. It simply checks the user's cookies if they have a jwt_token value inside.
import cookies from 'cookies'
export const getServerSideProps = async ({
req,
}: {
req: { headers: { cookie: any } };
}) => {
function parseCookies(req: { headers: { cookie: any } }) {
var parsedCookie = cookie.parse(
req ? req.headers.cookie || '' : document.cookie
);
return parsedCookie.jwt_token;
}
const isAuth = parseCookies(req);
if (typeof isAuth === undefined) {
return {
redirect: {
destination: `/auth/sign_in`,
},
};
}
return {
props: {
isAuth,
},
};
};

Safe way to initialize Apollo Client upon asynchronous token

I'm writing an app that uses Apollo Client to make graphql requests against a MongoDB Realm database.
This is the abstract structure of it:
<MongoContext>
<ApolloContext>
<App/>
</ApolloContext>
</MongoContext>
The top level component handles user authentication and provides a context. The next component down initiates Apollo Client and the caching logic and sets the context to the whole app.
The expected data flow is shown in the diagram on this page. The default behavior for a useQuery is that Apollo:
Tries to fetch from cache;
Tries to fetch from server; If successful, saves to cache.
My goal is to achieve offline capability. So, referring to the diagram again, the very first query should be resolved by the cache when it holds the data. Apollo's default cache mechanism is in memory, so I'm using apollo-cache-persist to cache it to localStorage.
More specifically, these are the conditions required:
The app should be responsive upon start.
At first render, the app is either offline or hasn't authenticated yet
Therefore it must read from cache, if available
If there's no cache, don't make any requests to the server (they'd all fail)
If the user is online, the app should get the authentication token for the requests
The token is requested asynchronously
While the token is unknown, read from the cache only (As 1.2 above)
When the token is known, use the data flow described above
My main problems are specifically with 1.2 and 2.2 above. I.e. preventing Apollo from making requests to the server when we already know it will fail.
I was also looking for a global solution, so modifying individual queries with skip or useLazyQuery aren't options. (And I'm not even sure that would work - I still needed the queries to be executed against the cache.)
Code:
ApolloContext component:
import * as React from 'react';
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
createHttpLink,
NormalizedCacheObject,
} from '#apollo/client';
import { setContext } from '#apollo/client/link/context';
import { persistCache } from 'apollo-cache-persist';
import { PersistentStorage } from 'apollo-cache-persist/types';
const ApolloContext: React.FC = ({ children }) => {
// this hook gets me the token asynchronously
// token is '' initially but eventually resolves... or not
const { token } = useToken();
const cache = new InMemoryCache();
const [client, setClient] = React.useState(createApolloClient(token, cache))
// first initialize the client without the token, then again upon receiving it
React.useEffect(() => {
const initPersistCache = async () => {
await persistCache({
cache,
storage: capacitorStorageMethods,
debug: true,
});
};
const initApollo = async () => {
await initPersistCache();
setClient(createApolloClient(token, cache));
};
if (token) {
initApollo();
} else {
initPersistCache();
}
}, [token]);
console.log('id user', id, user);
return <ApolloProvider client={client}>{children}</ApolloProvider>;
};
function createApolloClient(
token: string,
cache: InMemoryCache
) {
const graphql_url = `https://realm.mongodb.com/api/client/v2.0/app/${realmAppId}/graphql`;
const httpLink = createHttpLink({
uri: graphql_url,
});
const authorizationHeaderLink = setContext(async (_, { headers }) => {
return {
headers: {
...headers,
Authorization: `Bearer ${token}`,
},
};
});
return new ApolloClient({
link: authorizationHeaderLink.concat(httpLink),
cache,
});
}
What I've tried:
After attempting many different things. I found something that works, but it looks terrible. The trick is to give Apollo a custom fetch that rejects all requests when the user is not logged in:
const customFetch = (input: RequestInfo, init?: RequestInit | undefined) => {
return user.isLoggedIn
? fetch(input, init)
: Promise.reject(new Response());
};
const httpLink = createHttpLink({
uri: graphql_url,
fetch: customFetch,
});
Another way to prevent outbound requests is to just omit the link property:
return new ApolloClient({
link: user.isLoggedIn
? authorizationHeaderLink.concat(httpLink)
: undefined,
cache,
});
}
That looks way cleaner but now the problem is that make queries that can't be fulfilled by the cache to hang on loading forever.(related issue)
I'm looking for a cleaner and safer way to do this.

ReactJS render component before async conditional [duplicate]

This question already has answers here:
How can I block a React component to be rendered until I fetched all informations?
(6 answers)
PrivateRoute - how to wait a response from async?
(4 answers)
Closed 2 years ago.
Hello im trying to create a private Route
PrivateRoute.js
const PrivateRoute = ({ children, ...rest }) => {
return (
<Route
{...rest}
render={({ location }) =>
Authenticate() ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location },
}}
/>
)
}
/>
);
};
export default PrivateRoute;
When I request a private route, the child component renders and after a while the Autheticate function is completed. In the Authenticate function I make a request that returns the token data and saves it in the sessionStorage
Authenticate.js
const Authenticate = async () => {
let token = localStorage.getItem("token");
if (token) {
token = "Bearer " + token;
let response = await tokenRequest(token);
if (response) {
if (response.status === 200) {
await sessionStorage.setItem(
"userData",
JSON.stringify(response.data.token)
);
return true;
} else {
localStorage.clear();
sessionStorage.clear();
return false;
}
} else {
return false;
}
} else {
localStorage.clear();
return false;
}
};
export default Authenticate;
How could I finish the function's task and then redirect to the child component?
Within your login route, you can use the history api's state that you are setting in your redirect to navigate back once the user logs in. The state gets stored in the location variable in react router dom. https://reacttraining.com/react-router/web/api/location
Other than that, your Authenticate function currently returns a Promise of a boolean. Promises are always truthy, so your Redirect is likely never being run.
For the logic of how to get the PrivateRoute to work with asynchronous checks, I'll refer you to this which is from a similar issue: authorized route vs unauthorized routes reactjs not properly loading

How to access react-router v3 prop in function?

I'm using refresh-fetch for authentication token refreshing. If the app receives not 200 http status code response I need to handle that by redirecting the user to logout page. How could I achieve this using react-router v3.
browserHistory.push('/logout');
I think this is not an option because I'm using basename.
const refreshToken = () => {
return fetchJSONWithToken(`${API_ROOT}user/login/refresh`, {
method: 'POST',
body: JSON.stringify({ refresh_token: retrieveToken() })
})
.then(({body}) => {
saveToken(body.access_token, body.refresh_token);
return body;
})
.catch(error => {
//TODO: redirect user to /logout
throw error;
});
};
Or maybe there is a better way of doing this?
You need to store your browserHistory instance and reuse it.
Example:
import { createHistory, useBasename } from 'history'
// Run our app under the /base URL.
const yourCustomHistoryWithBasename = useBasename(createHistory)({
basename: '/base'
})
// Re-use the same history, which includes the basename
yourCustomHistoryWithBasename.push('/logout') // push /base/logout
yourCustomHistoryWithBasename.replace('/logout') // replace current history entry with /base/logout
Source for this example

React Relay Modern redirecting to another page when receiving 401 error on network environment

I´m using JWT authentication inside my ReactJS RelayJS network environment. All the token retrieval and processing in server and client are fine. I´m using react router v4 for routing.
My problem is when I receive a Unauthorized message from server (status code 401). This happens if the user points to an application page after the token has expired, ie. What I need to do is to redirect to login page. This is the code I wish I could have:
import { Environment, Network, RecordSource, Store } from 'relay-runtime';
const SERVER = 'http://localhost:3000/graphql';
const source = new RecordSource();
const store = new Store(source);
function fetchQuery(operation, variables, cacheConfig, uploadables) {
const token = localStorage.getItem('jwtToken');
return fetch(SERVER, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
Accept: 'application/json',
'Content-type': 'application/json'
},
body: JSON.stringify({
query: operation.text, // GraphQL text from input
variables
})
})
.then(response => {
// If not authorized, then move to default route
if (response.status === 401)
this.props.history.push('/login') <<=== THIS IS NOT POSSIBLE AS THERE IS NO this.history.push CONTEXT AT THIS POINT
else return response.json();
})
.catch(error => {
throw new Error(
'(environment): Error while fetching server data. Error: ' + error
);
});
}
const network = Network.create(fetchQuery);
const handlerProvider = null;
const environment = new Environment({
handlerProvider, // Can omit.
network,
store
});
export default environment;
Naturally calling this.props.history.push is not possible as the network environment is not a ReactJS component and therefore has no properties associated.
I´ve tried to throw an error at this point, like:
if (response.status === 401)
throw new Error('Unauthorized');
but I saw the error on the browser console, and this cannot be treated properly in the code.
All I wanna do is to redirect to login page in case of 401 error received, but I can´t find a proper way of doing it.
I am not using relay but a render prop. I experienced kind of the same issue. I was able to solve it using the window object.
if (response.statusText === "Unauthorized") {
window.location = `${window.location.protocol}//${window.location.host}/login`;
} else {
return response.json();
}
You can go with useEnvironment custom hook.
export const useEnvironment = () => {
const history = useHistory(); // <--- Any hook using context works here
const fetchQuery = (operation, variables) => {
return fetch(".../graphql", {...})
.then(response => {
//...
// history.push('/login');
//...
})
.catch(...);
};
return new Environment({
network: Network.create(fetchQuery),
store: new Store(new RecordSource())
});
};
// ... later in the code
const environment = useEnvironment();
Or you can create HOC or render-prop component if you are using class-components.
btw: this way you can also avoid usage of the localStorage which is slowing down performance.

Resources