I need to insert the headers into the adapter asynchronously because the token function should check if the token is not expired and refresh it (per Ajax) if it is expired and then return the new token. But it seems the adapter can't handle the returned promises. Can anybody help me out with that issue?
import DS from 'ember-data';
import config from '../config/environment';
export default DS.JSONAPIAdapter.extend({
// Application specific overrides go here
host: config.APP.api_endpoint,
headers: Ember.computed(function() {
return this.auth.getToken().then(
(accessToken) => {
if (accessToken) {
const auth = `Bearer ${accessToken}`;
return {
Authorization: auth
};
} else {
return {
};
}
});
}).volatile()
});
You returning promise from computed property, this will never work. There is a stable solution for authorization/authentication, ember-simple-auth. It will handle tasks with saving and refreshing tokens.
Related
I am calling an API defined using RTK Query, within a React Native + Redux Toolkit + Expo app. This is secured with an authentication / authorization system in place i.e. access token (short expiration) and refresh token (longer expiration).
I would like to avoid checking any access token expiration claim (I've seen people suggesting to use a Redux middleware). Rather, if possible, I'd like to trigger the access token renewal when the API being requested returns a 403 response code, i.e. when the access token is expired.
This is the code calling the API:
const SearchResults = () => {
// get the SearchForm fields and pass them as the request body
const { fields, updateField } = useUpdateFields();
// query the RTKQ service
const { data, isLoading, isSuccess, isError, error } =
useGetDataQuery(fields);
return ( ... )
the RTK Query API is defined as follows:
import { createApi, fetchBaseQuery } from "#reduxjs/toolkit/query/react";
import * as SecureStore from "expo-secure-store";
import { baseUrl } from "~/env";
export const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({
baseUrl: baseUrl,
prepareHeaders: async (headers, { getState }) => {
// retrieve the access_token from the Expo SecureStore
const access_token = await SecureStore.getItemAsync("access_token");
if (access_token) {
headers.set("Authorization", `Bearer ${access_token}`);
headers.set("Content-Type", "application/json");
}
return headers;
},
}),
endpoints: (builder) => ({
getData: builder.query({
// body holds the fields passed during the call
query: (body) => {
return {
url: "/data",
method: "POST",
body: body,
};
},
}),
}),
});
export const { useGetDataQuery } = api;
I understand that when the API returns isError = true and error = something 403 I need to renew the access token within the Expo SecureStore (and there's a function already in place for that). However I have no idea about how can I query the RTKQ API again, on the fly, when it returns a 403 response code, and virtually going unnoticed by the user.
Can someone please point me in the right direction?
I got the hang of it, massive thanks to #phry! I don't know how I could have missed this example from RTKQ docs but I'm a n00b for a reason after all.
This being said, here's how to refactor the RTKQ api to renew the access token on the fly, in case some other react native beginner ever has this problem. Hopefully this is a reasonable way of doing this
import { createApi, fetchBaseQuery } from "#reduxjs/toolkit/query/react";
import * as SecureStore from "expo-secure-store";
import { baseUrl } from "~/env";
import { renewAccessToken } from "~/utils/auth";
// fetchBaseQuery logic is unchanged, moved out of createApi for readability
const baseQuery = fetchBaseQuery({
baseUrl: baseUrl,
prepareHeaders: async (headers, { getState }) => {
// retrieve the access_token from the Expo SecureStore
const access_token = await SecureStore.getItemAsync("access_token");
if (access_token) {
headers.set("Authorization", `Bearer ${access_token}`);
headers.set("Content-Type", "application/json");
}
return headers;
},
});
const baseQueryWithReauth = async (args, api) => {
let result = await baseQuery(args, api);
if (result.error) {
/* try to get a new token if the main query fails: renewAccessToken replaces
the access token in the SecureStore and returns a response code */
const refreshResult = await renewAccessToken();
if (refreshResult === 200) {
// then, retry the initial query on the fly
result = await baseQuery(args, api);
}
}
return result;
};
export const apiToQuery = createApi({
reducerPath: "apiToQuery",
baseQuery: baseQueryWithReauth,
endpoints: (builder) => ({
getData: builder.query({
// body holds the fields passed during the call
query: (body) => {
return {
url: "/data",
method: "POST",
body: body,
};
},
}),
}),
});
export const { useGetDataQuery } = apiToQuery;
I spent an hour looking in the Chrome console and I cannot see where this bug comes from.
I am finishing an update of OAuth implementation in my Vue app.
The story begins when socialLink.js finds out that a new user must be created. Vue component Vue-authentication depends on the presence of access_token in a response so I return some dummy text:
return api.sendResponse(res, { email, name, socialId, access_token: 'abcd' });
The library stores this value in localStorage:
After a redirect, the SignUp.vue is rendered and I complete the form. The first communication with the server is a Vuex call to create a new user:
response = await this.$store.dispatch('CREATE_USER_PROFILE', payload);
Which returns a real short lived JWT token:
const token = auth.createToken(userId, nickname, new Date(), null, false, '1m');
return api.sendCreated(res, api.createResponse(token));
Which I store in the Vue page afterwards:
const { data } = response;
const token = data.data;
if (token === undefined) {
this.error = this.$t('sign-up.something-went-wrong');
return false;
}
I checked that the token contains what the server returned:
Request URL: https://beta.mezinamiridici.cz/api/v1/users
Request Method: POST
Status Code: 201 Created
{"success":true,"data":"eyJhbGciOiJIUzI1NiIs...Tl8JFw2HZ3VMXJk"}
Then I call another Vuex method and pass the current JWT token:
await this.$store.dispatch('UPDATE_USER_PROFILE', {
I checked in the Vuex devtools that there really is the correct JWT token. I then pass it further to api.js.
Here I create an Axios configuration holding an Authorization header:
function getAuthHeader(context, jwt = undefined, upload) {
const config = { headers: { } };
if (jwt || (context && context.rootState.users.userToken)) {
config.headers.Authorization = `bearer ${jwt || context.rootState.users.userToken}`;
}
Again, I checked that the correct JWT token is used there.
Finally, I pass all data to Axios:
function patch(endpoint, url, body, context, jwt) {
const headers = getAuthHeader(context, jwt);
console.log(headers);
if (endpoint === 'BFF') {
return axios.patch(`${VUE_APP_BFF_ENDPOINT}${url}`, body, headers);
} else {
return axios.patch(`${VUE_APP_API_ENDPOINT}${url}`, body, headers);
}
}
Which I log and can confirm the correct JWT is still there:
bearer eyJhbGciOiJIUzI1N....8JFw2HZ3VMXJk
There is nothing that could change the header now to abcd, but, the 'Network' tab shows it:
And the server fails with a parse error.
Has anybody got an idea why Axios uses the Authorization header with a different value than I pass it?
Ok, mystery solved. vue-authenticate is the reason, because, it creates Axios interceptors and handles the Authorization header itself.
vue-authenticate.common.js:
var defaultOptions = {
bindRequestInterceptor: function ($auth) {
var tokenHeader = $auth.options.tokenHeader;
$auth.$http.interceptors.request.use(function (config) {
if ($auth.isAuthenticated()) {
config.headers[tokenHeader] = [
$auth.options.tokenType, $auth.getToken()
].join(' ');
} else {
delete config.headers[tokenHeader];
}
return config
});
},
My code is more complex and it supports internal accounts with email/password so this code is breaking mine. The interceptor must be present and be a function, so the solution was:
Vue.use(VueAuthenticate, {
tokenName: 'jwt',
baseUrl: process.env.VUE_APP_API_ENDPOINT,
storageType: 'localStorage',
bindRequestInterceptor() {},
bindResponseInterceptor() {},
providers: {
facebook: {
clientId: process.env.VUE_APP_FACEBOOK_CLIENT_ID,
redirectUri: process.env.VUE_APP_FACEBOOK_REDIRECT_URI,
},
I have a problem on using the axios interceptor in my react app. I want to achieve putting the header token just once in my react app. So thats why Im putting it in the interceptor. At the same time, i also want to have just one declaration to get the error. So i dont need to show the error in every page. I’m wondering if i’m using it correctly in my code below? Is there a way that i can shorten it cause i’m declaring it twice for response and request?
export function getAxiosInstance() {
if (axiosInstance === null) {
axiosInstance = axios.create({
baseURL: API_URL,
});
}
axiosInstance.interceptors.request.use(
(config) => {
if (config.baseURL === API_URL && !config.headers.Authorization) {
const token = store.getState().auth.access_token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
console.log(config);
}
}
return config;
},
(error) => {
console.log(error);
store.dispatch(setAPIErrorMessage(error.message));
return Promise.reject(error);
}
);
axiosInstance.interceptors.response.use(
(config) => {
if (config.baseURL === API_URL && !config.headers.Authorization) {
const token = store.getState().auth.access_token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
console.log(config);
}
}
return config;
},
(error) => {
console.log(error);
store.dispatch(setAPIErrorMessage(error.message));
return Promise.reject(error);
}
);
return axiosInstance;
}
You don't need to set authorization header in interceptors.response, you only need this in request interceptor.
You could declare your error handling in a closure function (with the action dispatch) to avoid repeating yourself.
I would also suggest to avoid handling errors directly in axios instance. You could define async redux actions using https://github.com/reduxjs/redux-thunk, and handle network errors at redux level (using fetchBegin, fetchSuccess, fetchFailure actions pattern). Then axios setup and redux setup would not be coupled anymore, which will allow you to change these tools in the future.
I have components that are making get requests in their created methods. I am using oidc client for authorization. I would like to set the each request header with the token that I get from oidc. I have made a http.js file in the root of the project, that looks like this:
import axios from 'axios';
import AuthService from "./AuthService";
const authService = new AuthService();
let token;
axios.interceptors.request.use(async function (config) {
await authService.getUser().then(res => {
if (res) {
token = res.id_token;
config.headers['Authorization'] = `Bearer ${token}`;
}
});
// eslint-disable-next-line no-console
console.log('interceptor', config);
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
I am not sure if this is the way to set the interceptors and how to actually use them, because on each request I see that they are not being set and nothing is being logged in the console. How is this suppose to be set up?
How to create middleware which will catch all errors, for example I have request which required token, token can expired or damaged, so I need catch this errors on every request and be able to call queries and mutations.
For example:
On expired token, I must refetch token and repeat request.
On token damaged, I must logout user and refetch all queries.
And type of error witch I need to handle can be many.
In(react-apollo docs)
networkInterface.useAfter([{
applyAfterware({ response }, next) {
if (response.status === 401) {
logout();
}
next();
}
}]);
I can't access to graphql error, and call queries or mutations.
You can check to see if you have a token before every request is sent. If you do not have a token, you should handle that somewhere else in your application or potentially fetch another straight from the middleware function. You could make higher order component that wraps all of your components that must have a token. If for some reason there is no token, you can fetch another one and store it to localStorage if you are using the browser or asyncstorage if you are using React Native. Once you've assigned it to localStorage or asyncStorage, this middleware code snippet below will check for the token before every request you send, this includes all queries and mutations. If you find that your user doesn't have a token, you could also redirect them in your component them to a page where they must login again and then from there set the token to localstorage or asynstorage. Once again the apollo client's middleware will have access to it that way.
import ApolloClient, { createNetworkInterface } from 'apollo-client';
import { checkForSessionToken } from '../../utils/authentication';
const networkInterface = createNetworkInterface({
uri: 'https://localhost:4000'
});
networkInterface.use([{
applyMiddleware(req, next) {
// Create the header object if needed.
if (!req.options.headers) {
req.options.headers = {};
}
// get the authentication token from Async storage
// and assign it to the request object
checkForSessionToken()
.then(SESSION_TOKEN => {
if (SESSION_TOKEN === null || SESSION_TOKEN === undefined {
fetchNewToken()
.then(SESSION_TOKEN => {
localStorage.setItem('token', SESSION_TOKEN);
req.options.headers.Authorization = `Bearer
${SESSION_TOKEN}`;
}
} else {
req.options.headers.Authorization = `Bearer ${SESSION_TOKEN}`;
}
next();
})
.catch(error => {
fetchNewToken()
.then(SESSION_TOKEN => {
localStorage.setItem('token', token);
req.options.headers.Authorization = `Bearer
${SESSION_TOKEN}`;
}
next();
})
}
}]);
const client = new ApolloClient({
networkInterface,
dataIdFromObject: o => o.id
});
export default client;