I would like an elegant way to dynamically set a Authentication header on all requests. My current solution is using a superagent-defaults package but it dooesn't handle dynamic headers
Take the sample code below
superagentWrapper.js
import defaults from 'superagent-defaults';
import localStorage from 'localStorage';
const superagent = defaults();
superagent
.set('Authorization', `Bearer ${localStorage.getItem('access_token')}`);
export default superagent;
api.js
import request from '../../../utils/superagentWrapper';
ExampleAuthenticatedCall: (x) => {
return new Promise((resolve, reject) => {
request
.post(sources.exampleCall())
.accept('json')
.set('Content-Type', 'application/json')
.timeout(40000)
.send({ exampleCall: "xxx"})
.end((error, res) => {
res.body.error ? reject(res.body.error) : resolve(res.body);
});
});
},
So the issue is that my superAgentWrapper is required by all my api files at the time of page load. This means that the example works fine as long as the access_token never changes ( Which it does do potentially multiple times before a page refresh ).
I've found a solution to this issue here https://www.crowdsync.io/blog/2017/10/16/setting-defaults-for-all-your-superagent-requests/.
Whilst this could potentially work an ideal solution would be to dynamically mirror all the methods the request library has rather than manually having to define them ( new methods may be added / removed in the future so this approach is a little brittle ).
Perhaps anyone could point me in the right direction of how this might be possible perhaps by using proxies / reflection?
Use the standard superagent, and instead of returning the same request, return a function that generates a new request, and gets the current token from the localStorage:
import request from 'superagent';
import localStorage from 'localStorage';
const superagent = () => request
.set('Authorization', `Bearer ${localStorage.getItem('access_token')}`);
export default superagent;
Usage (see request and promises):
import request from '../../../utils/superagentWrapper';
AuthenticatedCall: (x) => request() // generate a new request
.get(sources.AuthenticatedCall(x)) // a request returns a promise by default
If you don't won't to convert the wrapper to a function, you can use a proxy with a get handler. The proxy will produce a new request, with the current token, whenever it's called:
import request from 'superagent';
import localStorage from 'localStorage';
const handler = {
get: function(target, prop, receiver) {
return request
.set('Authorization', `Bearer ${localStorage.getItem('access_token')}`)[prop];
}
};
const requestProxy = new Proxy(request, handler);
export default requestProxy;
Usage (see request and promises):
import request from '../../../utils/superagentWrapper';
AuthenticatedCall: (x) => request // generate a new request
.get(sources.AuthenticatedCall(x)) // a request returns a promise by default
Related
In the Apollographql documentation it states:
The onError link can retry a failed operation based on the type of GraphQL error that's returned. For example, when using token-based authentication, you might want to automatically handle re-authentication when the token expires.
This is followed up by their sample code:
onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let err of graphQLErrors) {
switch (err.extensions.code) {
// Apollo Server sets code to UNAUTHENTICATED
// when an AuthenticationError is thrown in a resolver
case "UNAUTHENTICATED":
// Modify the operation context with a new token
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: getNewToken(),
},
});
// Retry the request, returning the new observable
return forward(operation);
}
}
}
// To retry on network errors, we recommend the RetryLink
// instead of the onError link. This just logs the error.
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
});
My question is in regards to the getNewToken(), as no code was provided for this function, I want to know (assuming this is another request to the backend and I am not sure how it could not be), if you are able to and or supposed to use query/mutation in graphql or make the request through axios for example.
One problem, if it can/should be a graphql query or mutation, is to get the new token, the onError code is defined in the same file as the ApolloClient as ApolloClient needs access to onError, thus when trying to implement this as retrieving a new token through a graphql mutation I got the following error:
React Hook "useApolloClient" is called in function "refresh" that is
neither a React function component nor a custom React Hook function.
After trying to useQuery/useMutation hook and realizing I cannot outside of a react component and at the top level I found this post whose answers suggested you can use useApolloClient.mutate instead but I still ran into issues. My code was (and tried multiple iterations of this same code like useApolloClient() outside of the function and inside etc.):
const refresh = () => {
const client = useApolloClient();
const refreshFunc = () => {
client
.mutate({ mutation: GET_NEW_TOKEN })
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err);
});
};
refreshFunc();
};
I could capitalize Refresh but this still would not work and would break the rules of hooks.
And to clarify all the above would do is I would replace the console.logs with setting session storage to the retrieved new token and then re trying the original request with onError.
Now in another post I found when looking into this, the users getNewToken request was a rest request using axios:
const getNewToken = async () => {
try {
const { data } = await axios.post(
"https://xxx/api/v2/refresh",
{ token: localStorage.getItem("refreshToken") }
);
localStorage.setItem("refreshToken", data.refresh_token);
return data.access_token;
} catch (error) {
console.log(error);
}
};
Now from my understanding, if I wanted to implement it this way I would have to change my backend to include express as I am only using apolloserver. Now I could definitely be wrong about that as my backend knowledge is quite limited and would love to be corrected their.
So my question is, what is the best way to do this, whether natively using graphql queries/mutations (if possible), doing it with axios, or maybe their is another best practice for this seemingly common task I am unaware of.
Our react app is using window.fetch method for the http calls in most or all of its areas. Before, it had only one token for accessing the content, and what we did is that, we added a header with every request individually, to send the token etc. Below is the sample code of such request:
useEffect(() => {
// GET S3 CREDANTIONS
fetch("/dashboard/de/question/s3credentials", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${loginReducer}`,
},
})
But now we have configured our api to access two tokens, one for accessing content, the other for refreshing the access token. And for that, after searching from internet, we decided to add an axios interceptor, to check if the access_token is valid, before each http call.
So, I made the interceptor and put it inside the index.js of the react app. to check globally for every http request before sending it, if the access_token is valid; and refresh if it is not. I got this idea from this link:
https://blog.bitsrc.io/setting-up-axios-interceptors-for-all-http-calls-in-an-application-71bc2c636e4e
Other than this link, I studied it from many other websites including stackoverflow, and I understood this more so applied it.
Below is the axios interceptor's code in the index.js file (as a whole):
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import axios from "axios";
import AdminIndex from "./AdminPanel/Pages/AdminIndex";
import Cookies from "js-cookie";
// React Redux
import allReducers from "./reducers/index";
import { createStore } from "redux";
import { Provider } from "react-redux";
import StateLoader from "./reducers/PresistedState";
const stateLoader = new StateLoader();
let store = createStore(
allReducers,
stateLoader.loadState(),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
axios.interceptors.request.use(
async (config) => {
let serverCallUrl = new URL(config.url);
if (serverCallUrl.pathname.includes("/superuser/login")) return config;
let access_token = Cookies.get("access");
let refresh_token = localStorage.getItem("refresh_token");
await AdminIndex.hasAccess(access_token, refresh_token);
return config;
},
(error) => {
return Promise.reject(error);
}
);
store.subscribe(() => {
stateLoader.saveState(store.getState());
});
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
there is some problem with the axios interceptor I think. Because when I start the app and enter the login credentials, the app says wrong username and password etc.. I wonder if the problem is because our app is using the fetch method instead of axios? Because we are trying to implement axios interceptor for the fetch http calls. And just for the login code, we have applied axios to authenticate the user. Below is the handleSubmit function of login:
const handleSubmit = async (e) => {
e.preventDefault();
axios({
method: "post",
url: "/superuser/login",
headers: {
"content-type": "application/x-www-form-urlencoded;charset=utf-8",
},
data: `username=${values.username}&useremail=${values.useremail}`,
})
.then((res) => {
const { access_token, refresh_token } = res.data;
Cookies.set("access", access_token);
localStorage.setItem("refresh_token", refresh_token);
props.set_login(res.data.access_token);
history.push("/admin/panel/papers");
})
.catch((err) => setDialogStatus(true));
};
Please have a look what is causing the error to not being able to log in the app. If I remove the interceptor, the app logs in successfully, but then I won't be able to refresh the access_token when it expires. In the interceptor, I have put the function to refresh the access_token and store it again in the props of app. Hopefully I have clearly stated my issue. But still if something is missing, please don't block the question just add a comment and I will immediately respond to it.
Thank you in advance!
No! axios interceptors is the internal mechanism of axios; which means, it only intercepts the requests that are sent by the axios library itself. If we want to intercept the window.fetch requests, we'll need to setup custom interceptors for that, for example filtering in the catch block if the response from api is a 403 error, then perform specific action.
But even still, the most preferred approach is to just use the axios library totally and be replaced all over the project.
I have a file called api.js in my root directory which takes care of calls to the external API (request and response interceptors). I am using this to inject the access_token, so my request interceptor looks like so;
import axios from 'axios';
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL
});
// Add request interceptor
api.interceptors.request.use(
async config => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers['Authorization'] = 'Bearer ' + token;
}
config.headers['Content-Type'] = 'application/json';
config.headers['Accept'] = 'application/json';
return config;
},
error => {
Promise.reject(error);
}
);
In my pages directory, i have a file called users.js ... All i want to do is return the list of users from my external API and display it in a grid when the /users page is loaded. My users.js file currently looks like so;
import api from "../services/api"
export default function Users() {
return (
<div>
</div>
)
}
export async function getStaticProps() {
const res = await api.get('/accounts');
}
But when I run this, i am getting the following error;
ReferenceError: localStorage is not defined
The error references the api.js file at the following line;
const token = localStorage.getItem('access_token');
I cant seem to figure out whats going on. Any help would be greatly appreciated
You are in server-side while executing fetch in getStaticProps() and localStorage doesn't exist in server side.So if you fetch data in client side the localStorage will not be undefined.
getStaticProps — fetches data at build time.
getStaticPaths — pre-render dynamic routes at build time.
getServerSideProps — fetches data on each request.
swr — fetches data from the Client at run time.
If you need to fetch data on the client, you must use swr.
import api from "../services/api"
const fetcher = url => api.get('/accounts'); // .then(res => res.data)
function Users() {
const { data, error } = useSWR('/api/data', fetcher)
// ...
}
Check this for swr.
Check this link for getStaticProps.
Also check this for more information about client side/server side fetch.
I have an accessToken in my redux store. I am creating a axios client like this:
import axios from 'axios';
import store from '../redux/store';
const apiClient = axios.create({
baseURL: 'http://localhost:5002/api/',
headers: {
Authorization:`Bearer ${store.getState().auth.accessToken}` // ???
}
});
apiClient.defaults.headers.common['Authorization'] = `Bearer ${store.getState().auth.accessToken}`; // ???
export default apiClient;
I want every request that is sent by the apiClient to contain the current value of the accessToken from the redux store (at the time the request is sent). And with the lines that are marked with ???, I'm not sure whether the Authorization header is only set once when the apiClient is created, or whether the value of the header also changes when the accessToken in the store has been updated.
Later I want to make a request like this:
apiClient.get<User[]>('Users/Friends');
And I want the request to include the accessToken that is in the store at the time the request is sent not at the time the client is created. Is there a way to achieve this while the creation of the client or do I always have to put the Authorization header config with the current accessToken in every request?
Now I added this code:
const apiClient = axios.create({
baseURL: 'http://192.168.178.188:5002/api/'
});
apiClient.interceptors.request.use(config => {
config.headers['Authorization'] = `Bearer ${store.getState().auth.accessToken}`
return config;
});
Is this the most elegant solution?
I have a function that returns a new Request object;
import * as _url from 'url';
// pathname starts with '/content/'
const isContentUrl = (path) => /^\/content\//.test(path);
export default function(url) {
let urlObj = _url.parse(url);
if (isContentUrl(urlObj.pathname)) {
urlObj.pathname = `/offline${urlObj.pathname}`;
}
return new Request(_url.format(urlObj), {
credentials: 'same-origin',
headers: {
'x-requested-with': 'sw'
}
});
}
Now I'm writing unit tests for this function and although I know there isn't actually much that changes here but say for example the headers could change for whatever reason, how can I assert parts of the request object like the headers, credentials or the URL?
Is there a nice way to be able to parse it for testing?
Ideally I'd like to do something like
it('should return a Request object with the correct headers', () => {
const url = '/content/2c509c50-e4ba-11e6-9645-c9357a75844a';
const request = offlineContent(url);
const result = request.headers;
const expected = {'x-requested-with': 'sw'};
expect(result).to.eql(expected);
});
in my test
Assuming that request here the request HTTP client, the returned object is an instance of the Request constructor defined here.
If you follow through the code, you will see that headers provided are available simply through an object member and thus headers, and other members being attached to self can be easily introspected through javascript.
In addition, the http module through which requests are dispatched is available as self.httpModule and can be mocked by an implementation that is compliant with node http module and requests dispatched through the library can be intercepted through spies.
MDN details the methods on the Request and Headers object. Unfortunately I haven't found a way to convert this to an object that I could assert with a deep equal. But I can use these to assert against.
request.url;
request.credentials;
request.headers.get('x-requested-with');
Unfortunately the Headers.getAll() method has been deprecated so in order to fail tests if someone added a new header I have asserted against;
Array.from(request.headers.keys()).length