I'm using Observable for updating token when its expiered.the process works properly and when token has been expiered it'll send a request and gets new token and then retries the last request .the request gets data correctly and I can see it in the network but when I'm trying to use the data in the component I get undefined with this error :
index.js:1 Missing field 'query name' while writing result {}
here is my config for apollo :
import {
ApolloClient,
createHttpLink,
InMemoryCache,
from,
gql,
Observable,
} from "#apollo/client";
import { setContext } from "#apollo/client/link/context";
import { onError } from "#apollo/client/link/error";
import store from "../store";
import { set_user_token } from "../store/actions/login_actions";
const httpLink = createHttpLink({
uri: "http://myserver.com/graphql/",
});
const authLink = setContext((_, { headers }) => {
const token = JSON.parse(localStorage.getItem("carfillo"))?.Login?.token;
return {
headers: {
...headers,
"authorization-bearer": token || null,
},
};
});
const errorHandler = onError(({ graphQLErrors, operation, forward }) => {
if (graphQLErrors?.length) {
if (
graphQLErrors[0].extensions.exception.code === "ExpiredSignatureError"
) {
const refreshToken = JSON.parse(localStorage.getItem("carfillo"))?.Login
?.user?.refreshToken;
const getToken = gql`
mutation tokenRefresh($refreshToken: String) {
tokenRefresh(refreshToken: $refreshToken) {
token
}
}
`;
return new Observable((observer) => {
client
.mutate({
mutation: getToken,
variables: { refreshToken: refreshToken },
})
.then((res) => {
const token = res.data.tokenRefresh.token;
store.dispatch(set_user_token(token));
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
"authorization-bearer": token || null,
},
}));
})
.then(() => {
const subscriber = {
next: observer.next(() => observer),
error: observer.error(observer),
complete: observer.complete(observer),
};
return forward(operation).subscribe(subscriber);
});
});
}
}
});
export const client = new ApolloClient({
link: from([errorHandler, authLink, httpLink]),
cache: new InMemoryCache(),
});
Related
here i have bit problem with my custom useFetch
i want to call the token first and the fetch code function later. I was tried in the useEffect this way
its creating the token firstly and queryCall(tokenKey) function still taking the null token
value if the valid token available
here is the my logic in useEffect
React.useEffect(() => {
if (!tokenKey) {
const getToken = () => {
tokenKey = ApplicationToken(true);
queryCall(tokenKey);
};
getToken();
} else {
queryCall(tokenKey);
}
}, [query]);
here is the ApplicationToken function is responsible for creating new token based on the useEffect condition (!tokenKey)
ApplicationToken.js
import qs from 'qs';
import axios from 'axios';
const ApplicationToken = IsError => {
// eslint-disable-next-line no-undef
const Grant = window?._env_?.REACT_APP_GRANT;
// eslint-disable-next-line no-undef
const Client = window?._env_?.REACT_APP_CLIENT;
// eslint-disable-next-line no-undef
const Key = window?._env_?.REACT_APP_KEY;
// eslint-disable-next-line no-undef
const Auth = window?._env_?.REACT_APP_AUTH;
// eslint-disable-next-line no-undef
if (IsError || window.localStorage.getItem('applicationToken') === null) {
let data = qs.stringify({
grant_type: `${Grant}`,
client_id: `${Client}`,
client_secret: `${Key}`,
});
let config = {
method: 'POST',
url: `${Auth}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: data,
};
axios(config)
.then(response => {
window.localStorage.setItem(
'applicationToken',
JSON.stringify(response)
);
})
.catch(function (error) {
console.log(error);
});
return JSON.parse(window.localStorage.getItem('applicationToken'));
} else {
return JSON.parse(window.localStorage.getItem('applicationToken'));
}
};
export default ApplicationToken;
Im storing the access token in local storage
here is the useFetch.js
import React from 'react';
import qs from 'qs';
import axios from 'axios';
import ApplicationToken from './ApplicationToken';
var tokenKey = ApplicationToken(false);
const useFetch = query => {
const [status, setStatus] = React.useState('idle');
const [result, setResult] = React.useState([]);
const newResult = query.substituteDataValue.toLowerCase();
var data = qs.stringify({
query: `${query.api}`,
});
// eslint-disable-next-line no-undef
const Query = window?._env_?.REACT_APP_QUERY;
const queryCall = React.useCallback(
async token => {
if (!token) {
return;
} else {
setStatus('Loading');
var config = {
method: 'POST',
url: `${Query}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization:
token?.data?.token_type + ' ' + token?.data?.access_token,
},
data: data,
};
setStatus('Loading');
axios(config)
.then(response => {
// console.log(response.data.results.bindings);
setStatus('success');
setResult(response.data.results.bindings);
})
.catch(function (error) {
setStatus('error');
if (error.response.status === 401) {
tokenKey = ApplicationToken(true);
queryCall(tokenKey);
}
});
}
},
[data]
);
React.useEffect(() => {
if (!tokenKey) {
tokenKey = ApplicationToken(true);
queryCall(tokenKey);
} else {
queryCall(tokenKey);
}
}, [query]);
return [status, result];
};
export default useFetch;
i want the output process like this
step-1.create newToken if existing token fail
step-2. call the queryCall(tokenKey) with updated token tokenKey
1 & 2 are in useEffect only in my code
when im doing it, the firstStep is working fine but second step is working with older token
so the effect of result is useFetch file if(!token) return;
how does queryCall() function wait until newToken created in useEffect ?
I can successfully view my GraphQL query via apollo-graphql-studio: The resolver is correctly configured, but I'm not sure how to render data.
I know that the next-js swr react-hook is performant, so I'd like to fetch data via the swr method:
import useSWR from "swr";
const Query = `
books {
title
}
`;
export default function Home() {
const fetcher = async () => {
const response = await fetch("/api/graphql", {
body: JSON.stringify({ query: Query }),
headers: { "Content-type": "application/json" },
method: "POST"
});
const { data } = await response.json();
return data;
};
const { data, error } = useSWR([Query], fetcher);
if (error) return <div>failed to load</div>;
if (!data) return <div>loading...</div>;
return (
<div>
<div>hello {data?.books?.title}</div>
</div>
);
}
This is just returning loading... so the data is clearly not correctly fetched. Although, as I said, I can retrieve it via the Apollo-graphql-studio IDE.
The console error is a 400 Bad Request on the API route: /api/graphql, so this is where the problem is.
How can I render the data?
Here's the GraphQL API:
import Cors from 'micro-cors'
import { gql, ApolloServer } from 'apollo-server-micro'
import { Client, Map, Paginate, Documents, Collection, Lambda, Get } from 'faunadb'
const client = new Client({
secret: process.env.FAUNA_SECRET,
domain: "db.fauna.com",
})
export const config = {
api: {
bodyParser: false
}
}
const typeDefs = gql`
type Book {
title: String
author: String
}
type Query {
books: [Book]
}
`
const resolvers = {
Query: {
books: async () => {
const response = await client.query(
Map(
Paginate(Documents(Collection('Book'))),
Lambda((x) => Get(x))
)
)
const books = response.data.map(item => item.data)
return [...books]
},
},
}
const cors = Cors()
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
},
introspection: true,
playground: true,
})
const serversStart = apolloServer.start()
export default cors(async (req, res) => {
if (req.method === "OPTIONS") {
res.end();
return false;
}
await serversStart;
await apolloServer.createHandler({ path: '/api/graphql' })(req, res)
})
I am using firebase for authentication in my Next.js app and also I have an express server that serves a REST API, which has a middleware that uses firebase-admin to verify idToken that is sent from my app, to pass the authenticated routes
Currently
The idToken generated by firebase lasts for one hour and if the client is still on my app and
hits any route that needs idToken and if the idToken is expired then the server just throws an error as unauthenticated, which is pretty good work, but this is not desired, I know my user is in there and just idToken is expired
Question
How do I refresh my idToken of a user if it has expired, without having to do a full refresh in the browser to get new idToken
Some Code
AuthContext.tsx
/* eslint-disable no-unused-vars */
import { useRouter } from 'next/router'
import nookies from 'nookies'
import { createContext, useContext, useEffect, useState } from 'react'
import { axios } from '../config/axios'
import firebase from '../config/firebase'
import { AuthUser } from '../types'
import { BaseUser } from '../types/user'
import { getProvider } from '../utils/oAuthProviders'
type AuthContextType = {
user: AuthUser | null
login: (email: string, password: string) => Promise<any>
signup: (email: string, password: string) => Promise<any>
logout: () => Promise<any>
oAuthLogin: (provider: string) => Promise<any>
}
const AuthContext = createContext<AuthContextType>({} as AuthContextType)
export const useAuth = () => useContext(AuthContext)
const fromPaths = ['/login', '/signup']
const formatUser = (user: BaseUser, idToken: string): AuthUser => {
return {
...user,
idToken,
}
}
export const AuthContextProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState<AuthUser | null>(null)
const [loading, setLoading] = useState(true)
const router = useRouter()
console.log(user)
useEffect(() => {
const unsub = firebase.auth().onIdTokenChanged((user) => {
if (user) {
user
.getIdToken()
.then(async (idToken) => {
try {
const userResp = await axios.get('/user/me', {
headers: {
Authorization: `Bearer ${idToken}`,
},
})
nookies.set(undefined, 'idk', idToken, { path: '/' })
const {
data: { userFullDetials },
} = userResp
setUser(formatUser(userFullDetials, idToken))
setLoading(false)
if (fromPaths.includes(router.pathname)) {
router.push('/home')
}
} catch (err) {
console.log(err)
setUser(null)
setLoading(false)
}
})
.catch((err) => {
console.log(err.message)
setUser(null)
setLoading(false)
})
} else {
setLoading(false)
setUser(null)
}
})
return () => unsub()
}, [router])
const login = (email: string, password: string) => {
return firebase.auth().signInWithEmailAndPassword(email, password)
}
const signup = (email: string, password: string) => {
return firebase.auth().createUserWithEmailAndPassword(email, password)
}
const oAuthLogin = (provider: string) => {
return firebase.auth().signInWithPopup(getProvider(provider))
}
const logout = async () => {
setUser(null)
await firebase.auth().signOut()
}
const returnObj = {
user,
login,
signup,
logout,
oAuthLogin,
}
return (
<AuthContext.Provider value={returnObj}>
{loading ? (
<div className="flex items-center justify-center w-full h-screen bg-gray-100">
<h1 className="text-indigo-600 text-8xl">S2Media</h1>
</div>
) : (
children
)}
</AuthContext.Provider>
)
}
// auth.ts
// Auth Middleware in express
import { NextFunction, Request, Response } from 'express'
import fbadmin from 'firebase-admin'
import { DecodedIdToken } from '../types/index'
export default async (req: Request, res: Response, next: NextFunction) => {
const authorization = req.header('Authorization')
if (!authorization || !authorization.startsWith('Bearer')) {
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
const idToken = authorization.split(' ')[1]
if (!idToken) {
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
try {
const decodedToken = await fbadmin.auth().verifyIdToken(idToken)
req.user = decodedToken as DecodedIdToken
return next()
} catch (err) {
console.log(err.message)
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
}
The Firebase SDK does that for you. Whenever you call user.getIdToken() it will return a valid token for sure. If the existing token has expired, it will refresh and return a new token.
You can use onIdTokenChanged()and which will trigger whenever a token is refreshed and store it in your state.
However, I don't see any cons in using getIdToken() method whenever you are making an API request to server. You won't have to deal with IdToken observer and get valid token always.
const makeAPIRequest = async () => {
// get token before making API request
const token = await user.getIdToken()
// pass the token in request headers
}
Right now your code makes a request to server to get user's information whenever a token refreshes and that may be redundant.
I am using apollo client and server and from the server the image uploads to Cloudinary no problem. but In the app whoever I tried to send the file to the server, it says createReadStream is not a function, code: INTERNAL_SERVER_ERROR. I am using image picker dependency of react-native which gives me a file path and other file specs and base64 whether I send the whole file or only the path or base64 it does not work.
the file is not going through correctly
my apolloConfig.js is:
import { ApolloClient, ApolloLink } from "#apollo/client";
import { Observable } from "apollo-link";
import { withClientState } from "apollo-link-state";
import { InMemoryCache } from "apollo-cache-inmemory";
import { onError } from "#apollo/client/link/error";
import { createUploadLink } from "apollo-upload-client";
import { buildAxiosFetch } from "#lifeomic/axios-fetch";
import { Config } from "App/Config";
import axios from "axios";
import AsyncStorage from "#react-native-community/async-storage";
const SERVER_URL = Config.API_URL;
const cache = new InMemoryCache({});
const httpLink = createUploadLink({
uri: SERVER_URL,
credentials: "same-origin",
fetch: buildAxiosFetch(axios, (config, input, init) => ({
...config,
onUploadProgress: init.onUploadProgress,
})),
});
const request = async (operation) => {
const token = await AsyncStorage.getItem("token");
console.log(token);
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : "",
},
});
};
export const requestLink = new ApolloLink(
(operation, forward) =>
new Observable((observer) => {
let handle;
Promise.resolve(operation)
.then((oper) => request(oper))
.then(() => {
handle = forward(operation).subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
});
})
.catch(observer.error.bind(observer));
return () => {
if (handle) handle.unsubscribe();
};
})
);
const link = ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
console.log("[graphQLErrors]", graphQLErrors);
graphQLErrors.map(({ message, extensions }) => {
console.log(
`[GraphQL error]: Message: ${message}, code: ${extensions.code}`
);
if (extensions.code === "UNAUTHENTICATED") {
AsyncStorage.clear();
}
});
}
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
}),
requestLink,
httpLink,
]);
const client = new ApolloClient({ link, cache });
export { client };
my uploadAvatar.js
try {
const { data } = await uploadImage({
variables: { file: image },
});
console.log(data);
} catch (error) {
console.log(error);
Toast.show({
type: "error",
position: "bottom",
text1: "Authentication Failed",
text2: "Something went wrong while uploading photo!",
});
}
});
};
You have to convert your file to a ReactNativeFile as the docs of apollo-upload-client mentioned
import { ReactNativeFile } from 'apollo-upload-client';
const file = new ReactNativeFile({
uri: uriFromCameraRoll,
name: 'a.jpg',
type: 'image/jpeg',
});
more info : https://github.com/jaydenseric/apollo-upload-client
Since I want to setup Axios interceptors with React Context, the only solution that seems viable is creating an Interceptor component in order to use the useContext hook to access Context state and dispatch.
The problem is, this creates a closure and returns old data to the interceptor when it's being called.
I am using JWT authentication using React/Node and I'm storing access tokens using Context API.
This is how my Interceptor component looks like right now:
import React, { useEffect, useContext } from 'react';
import { Context } from '../../components/Store/Store';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
const ax = axios.create();
const Interceptor = ({ children }) => {
const [store, dispatch] = useContext(Context);
const history = useHistory();
const getRefreshToken = async () => {
try {
if (!store.user.token) {
dispatch({
type: 'setMain',
loading: false,
error: false,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
});
const { data } = await axios.post('/api/auth/refresh_token', {
headers: {
credentials: 'include',
},
});
if (data.user) {
dispatch({
type: 'setStore',
loading: false,
error: false,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
authenticated: true,
token: data.accessToken,
id: data.user.id,
name: data.user.name,
email: data.user.email,
photo: data.user.photo,
stripeId: data.user.stripeId,
country: data.user.country,
messages: {
items: [],
count: data.user.messages,
},
notifications:
store.user.notifications.items.length !== data.user.notifications
? {
...store.user.notifications,
items: [],
count: data.user.notifications,
hasMore: true,
cursor: 0,
ceiling: 10,
}
: {
...store.user.notifications,
count: data.user.notifications,
},
saved: data.user.saved.reduce(function (object, item) {
object[item] = true;
return object;
}, {}),
cart: {
items: data.user.cart.reduce(function (object, item) {
object[item.artwork] = true;
return object;
}, {}),
count: Object.keys(data.user.cart).length,
},
});
} else {
dispatch({
type: 'setMain',
loading: false,
error: false,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
});
}
}
} catch (err) {
dispatch({
type: 'setMain',
loading: false,
error: true,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
});
}
};
const interceptTraffic = () => {
ax.interceptors.request.use(
(request) => {
request.headers.Authorization = store.user.token
? `Bearer ${store.user.token}`
: '';
return request;
},
(error) => {
return Promise.reject(error);
}
);
ax.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
console.log(error);
if (error.response.status !== 401) {
return new Promise((resolve, reject) => {
reject(error);
});
}
if (
error.config.url === '/api/auth/refresh_token' ||
error.response.message === 'Forbidden'
) {
const { data } = await ax.post('/api/auth/logout', {
headers: {
credentials: 'include',
},
});
dispatch({
type: 'resetUser',
});
history.push('/login');
return new Promise((resolve, reject) => {
reject(error);
});
}
const { data } = await axios.post(`/api/auth/refresh_token`, {
headers: {
credentials: 'include',
},
});
dispatch({
type: 'updateUser',
token: data.accessToken,
email: data.user.email,
photo: data.user.photo,
stripeId: data.user.stripeId,
country: data.user.country,
messages: { items: [], count: data.user.messages },
notifications:
store.user.notifications.items.length !== data.user.notifications
? {
...store.user.notifications,
items: [],
count: data.user.notifications,
hasMore: true,
cursor: 0,
ceiling: 10,
}
: {
...store.user.notifications,
count: data.user.notifications,
},
saved: data.user.saved,
cart: { items: {}, count: data.user.cart },
});
const config = error.config;
config.headers['Authorization'] = `Bearer ${data.accessToken}`;
return new Promise((resolve, reject) => {
axios
.request(config)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(error);
});
});
}
);
};
useEffect(() => {
getRefreshToken();
if (!store.main.loading) interceptTraffic();
}, []);
return store.main.loading ? 'Loading...' : children;
}
export { ax };
export default Interceptor;
The getRefreshToken function is called every time a user refreshes the website to retrieve an access token if there is a refresh token in the cookie.
The interceptTraffic function is where the issue persists.
It consists of a request interceptor which appends a header with the access token to every request and a response interceptor which is used to handle access token expiration in order to fetch a new one using a refresh token.
You will notice that I am exporting ax (an instance of Axios where I added interceptors) but when it's being called outside this component, it references old store data due to closure.
This is obviously not a good solution, but that's why I need help organizing interceptors while still being able to access Context data.
Note that I created this component as a wrapper since it renders children that are provided to it, which is the main App component.
Any help is appreciated, thanks.
Common Approach (localStorage)
It is a common practice to store the JWT in the localStorage with
localStorage.setItem('token', 'your_jwt_eykdfjkdf...');
on login or page refresh, and make a module that exports an Axios instance with the token attached. We will get the token from localStorage
custom-axios.js
import axios from 'axios';
// axios instance for making requests
const axiosInstance = axios.create();
// request interceptor for adding token
axiosInstance.interceptors.request.use((config) => {
// add token to request headers
config.headers['Authorization'] = localStorage.getItem('token');
return config;
});
export default axiosInstance;
And then, just import the Axios instance we just created and make requests.
import axios from './custom-axios';
axios.get('/url');
axios.post('/url', { message: 'hello' });
Another approach (when you've token stored in the state)
If you have your JWT stored in the state or you can grab a fresh token from the state, make a module that exports a function that takes the token as an argument and returns an axios instance with the token attached like this:
custom-axios.js
import axios from 'axios';
const customAxios = (token) => {
// axios instance for making requests
const axiosInstance = axios.create();
// request interceptor for adding token
axiosInstance.interceptors.request.use((config) => {
// add token to request headers
config.headers['Authorization'] = token;
return config;
});
return axiosInstance;
};
export default customAxios;
And then import the function we just created, grab the token from state, and make requests:
import axios from './custom-axios';
// logic to get token from state (it may vary from your approach but the idea is same)
const token = useSelector(token => token);
axios(token).get('/url');
axios(token).post('/url', { message: 'hello' });
I have a template that works in a system with millions of access every day.
This solved my problems with refresh token and reattemp the request without crashing
First I have a "api.js" with axios, configurations, addresses, headers.
In this file there are two methods, one with auth and another without.
In this same file I configured my interceptor:
import axios from "axios";
import { ResetTokenAndReattemptRequest } from "domain/auth/AuthService";
export const api = axios.create({
baseURL: process.env.REACT_APP_API_URL,
headers: {
"Content-Type": "application/json",
},
});
export const apiSecure = axios.create({
baseURL: process.env.REACT_APP_API_URL,
headers: {
Authorization: "Bearer " + localStorage.getItem("Token"),
"Content-Type": "application/json",
},
export default api;
apiSecure.interceptors.response.use(
function (response) {
return response;
},
function (error) {
const access_token = localStorage.getItem("Token");
if (error.response.status === 401 && access_token) {
return ResetTokenAndReattemptRequest(error);
} else {
console.error(error);
}
return Promise.reject(error);
}
);
Then the ResetTokenAndReattemptRequest method. I placed it in another file, but you can place it wherever you want:
import api from "../api";
import axios from "axios";
let isAlreadyFetchingAccessToken = false;
let subscribers = [];
export async function ResetTokenAndReattemptRequest(error) {
try {
const { response: errorResponse } = error;
const retryOriginalRequest = new Promise((resolve) => {
addSubscriber((access_token) => {
errorResponse.config.headers.Authorization = "Bearer " + access_token;
resolve(axios(errorResponse.config));
});
});
if (!isAlreadyFetchingAccessToken) {
isAlreadyFetchingAccessToken = true;
await api
.post("/Auth/refresh", {
Token: localStorage.getItem("RefreshToken"),
LoginProvider: "Web",
})
.then(function (response) {
localStorage.setItem("Token", response.data.accessToken);
localStorage.setItem("RefreshToken", response.data.refreshToken);
localStorage.setItem("ExpiresAt", response.data.expiresAt);
})
.catch(function (error) {
return Promise.reject(error);
});
isAlreadyFetchingAccessToken = false;
onAccessTokenFetched(localStorage.getItem("Token"));
}
return retryOriginalRequest;
} catch (err) {
return Promise.reject(err);
}
}
function onAccessTokenFetched(access_token) {
subscribers.forEach((callback) => callback(access_token));
subscribers = [];
}
function addSubscriber(callback) {
subscribers.push(callback);
}