First-time Stack Overflow poster here!
I’m following an Angular 4 tutorial, and completing its authentication section with Firebase (link: ). I was able to successfully signup and login, but receive errors when passing the user’ token, via ‘getIdToken’, to my GET request to limit certain actions to authenticated users.
When passing my token to my GET request, I get the following error:
* Response {_body: "{↵ "error" : "Could not parse auth token."↵}↵", status: 401, ok: false, statusText: "Unauthorized", headers: Headers…}
I also experience this issue when copying and pasting tokens from the console into code
I’ve posted the (potentially) most relevant code below for debugging:
header.component.ts
onFetchData() {
this.dataStorageService.getRecipes();
}
data-storage.service.ts
getRecipes() {
const token = this.auth.getTokenGrabber()
this.http.get('https://recipes-fe1ba.firebaseio.com/.json?auth=' + token)
.map(
(response: Response) => {
console.log(response.json())
const recipes: Recipe[] = response.json();
for (let recipe of recipes) {
if (!recipe['ingredients']) {
recipe['ingredients'] = [];
}
}
return recipes;
}
)
.subscribe(
(recipes: Recipe[]) => {
this.recipeService.setRecipes(recipes);
}
);
}
authentication.service.ts
signinUser(email, password){
firebase.auth().signInWithEmailAndPassword(email, password)
.then(
(response) => {
firebase.auth().currentUser.getIdToken(
).then(
(token:string) => {
// console.log(token)
this.token = token
console.log('user was found')
// console.log(this.token)
}
)
}
).catch((erorr) => {
console.log('user was not found')
}
)
}
getTokenGrabber(){
firebase.auth().currentUser.getIdToken()
.then(
(token:string) =>
this.token = token
)
return this.token
}
}
The REST API documentation indicates the query parameter is access_token, not auth. Give this a try:
this.http.get('https://recipes-fe1ba.firebaseio.com/.json?access_token=' + token)
Related
I've encountered a very strange problem, implementing axios interceptors for handling the expired token and refreshing it.
Setting
I'm implementing the JWT authentication with access and refresh tokens.
When the request is being sent to the API route that requires JWT authentication, request interceptor is here to make sure the headers contain an Authorization with Bearer token. The response interceptor checks if the new access token is needed, sends a request to refresh it, and finally updates the axios instance with the new config.
I wrote the code following the Dave Gray's video, but with TypeScript.
Problem
When testing this code, I set the refresh token lifetime to be very long, while setting the access token lifetime to be 5 seconds. After it expires, when the request to the protected route is happening, everything goes according to the plan—the logs from the backend contain two successfully completed requests: (1) to the protected route with 401 response and then (2) the refresh request.
At this point, I see the DOMException in the browser console (Chrome and Safari), which states that setRequestHeader fails to execute because a source code function is not a valid header value. Which, of course, it is not! The piece of code is this.
Code
const axiosPrivate = axios.create({
baseURL: BASE_URL,
headers: { "Content-Type": "application/json" },
withCredentials: true,
});
interface IRequestConfig extends AxiosRequestConfig {
sent?: boolean;
}
const useAxiosPrivate = () => {
const { auth } = useAuth()!;
const refresh = useRefreshToken();
React.useEffect(() => {
const requestInterceptor = axiosPrivate.interceptors.request.use(
(config: AxiosRequestConfig) => {
config.headers = config.headers ?? {};
if (!config.headers["Authorization"]) {
config.headers["Authorization"] = `Bearer ${auth?.token}`;
}
return config;
},
async (error: AxiosError): Promise<AxiosError> => {
return Promise.reject(error);
}
);
const responseInterceptor = axiosPrivate.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: AxiosError): Promise<AxiosError> => {
const prevRequestConfig = error.config as IRequestConfig;
if (error?.response?.status === 401 && !prevRequestConfig?.sent) {
const newAccessToken = await refresh();
prevRequestConfig.sent = true;
prevRequestConfig.headers = prevRequestConfig.headers!;
prevRequestConfig.headers[
"Authorization"
] = `Bearer ${newAccessToken}`;
return axiosPrivate(prevRequestConfig);
}
return Promise.reject(error);
}
);
return () => {
axiosPrivate.interceptors.request.eject(requestInterceptor);
axiosPrivate.interceptors.response.eject(responseInterceptor);
};
}, [auth, refresh]);
return axiosPrivate;
};
Error
DOMException: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': 'function (header, parser) {
header = normalizeHeader(header);
if (!header) return undefined;
const key = findKey(this, header);
if (key) {
const value = this[key];
if (!parser) {
return value;
}
if (parser === true) {
return parseTokens(value);
}
if (_utils_js__WEBPACK_IMPORTED_MODULE_0__["default"].isFunction(parser)) {
return parser.call(this, value, key);
}
if (_utils_js__WEBPACK_IMPORTED_MODULE_0__["default"].isRegExp(parser)) {
return parser.exec(value);
}
throw new TypeError('parser must be boolean|regexp|function');
}
}' is not a valid HTTP header field value.
Research
So far, I've only found one similar issue in the internet, which has links to some others. One of them gives me a hint, that it may be the problem with how axios reads the configuration given to an axios instance.
I'm not sure if the problem is indeed somewhere in axios. I'll be extremely grateful for any useful thoughts on this problem!
I had the same problem, I solved it by manually giving value to axiosPrivate instead of axiosPrivate(prevRequestConfig).
const responseIntercept = axiosPrivate.interceptors.response.use(
response => response,
async (error)=>{
const prevRequest = error?.config;
if (error?.response?.status === 403 && !prevRequest?.sent){
const newAccessToken = await refresh();
// console.log(prevRequest);
return axiosPrivate({
...prevRequest,
headers: {...prevRequest.headers, Authorization: `Bearer ${newAccessToken}`},
sent: true
});
}
return Promise.reject(error);
}
);
Thanks to Daniel Dan's solution I could modify Dave's tutorial code:
const responseInterceptor = axiosPrivate.interceptors.response.use(
(response: AxiosResponse) => {
return response;
},
async (error: AxiosError): Promise<AxiosError> => {
const prevRequestConfig = error.config as AxiosRequestConfig;
if (error?.response?.status === 401 && !prevRequestConfig.sent) {
prevRequestConfig.sent = true;
const newAccessToken = await refresh();
/* --- The modified line --- */
prevRequestConfig.headers = { ...prevRequestConfig.headers };
/* ------------------------- */
prevRequestConfig.headers[
"Authorization"
] = `Bearer ${newAccessToken}`;
return axiosPrivate(prevRequestConfig);
}
return Promise.reject(error);
}
);
Just Do This in your response interceptor
const responseInterceptor = axiosPrivate.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: AxiosError): Promise<AxiosError> => {
const prevRequestConfig = error.config as IRequestConfig;
if (error?.response?.status === 401 && !prevRequestConfig?.sent) {
const newAccessToken = await refresh();
prevRequestConfig.sent = true;
prevRequestConfig.headers["Authorization"] = `Bearer ${newAccessToken}`;
return axiosPrivate({
...prevRequestConfig,
...{
headers: prevRequestConfig.headers.toJSON(),
},
});
}
return Promise.reject(error);
}
);
When re-sending the request with updated creds, i.e axiosPrivate(config), the headers property needs to be a plain javascript Object but instead it is converted internally to be an AxiosInstance object.
To fix it, just pass a plain Javascript object to the headers property of your prevRequestConfig object.
I am developing react project using JWT authentication and write this code.
const service = axios.create({
baseURL: API_BASE_URL,
timeout: 60000
});
service.interceptors.response.use( (response) => {
return response.data
}, async (error) => {
const originalRequest = error.config;
// Remove token and redirect
if (error.response.status === 400 || error.response.status === 403) {
localStorage.removeItem(AUTH_TOKEN)
history.push(ENTRY_ROUTE)
window.location.reload();
}
// Unauthorized error token should refresh with access token
if (error.response.status === 401) {
const jwtToken = localStorage.getItem(AUTH_TOKEN)
const token = JSON.parse(jwtToken)
const refresh_token = token['refresh'];
const tokenParts = JSON.parse(atob(refresh_token.split('.')[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
if (tokenParts.exp > now) {
try {
const res = await service.post('/authentication/token/refresh/', {refresh: refresh_token});
localStorage.setItem(AUTH_TOKEN, JSON.stringify(res));
service.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res['access'];
originalRequest.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res['access'];
return service(originalRequest);
}
catch (error) {
}
}
else {
localStorage.removeItem(AUTH_TOKEN);
history.push(ENTRY_ROUTE);
window.location.reload();
}
}
return Promise.reject(error);
});
This is working well and have no errors, but I have something to ask. When the token is expired and some API is called, I get 401 status code to get access token with refresh token. And it returns the access token correctly.
But there is no way to re-call the failed API ( just called with expired access token ). That's why there are cases I could have no response from the backend ( Imagine, user clicks button, but no response, so he should click the button again to see the response. This is because of calling with new access token again )
This code is used in all parts of the project and I have many API callings, so it seems impossible to re-call failed API in each react components.
How can I fix this problem?
Best bet is to invoke axios again with the original request (with new token). I haven't tried setting up JWT server & executing this scenario, but something like below should work:
const service = axios.create({
baseURL: API_BASE_URL,
timeout: 60000,
});
service.interceptors.response.use(
(response) => {
return response.data;
},
async (error) => {
const originalRequest = error.config;
// Remove token and redirect
if (error.response.status === 400 || error.response.status === 403) {
localStorage.removeItem(AUTH_TOKEN);
history.push(ENTRY_ROUTE);
window.location.reload();
}
if (error.response.status != 401) return Promise.reject(error);
// Unauthorized error token should refresh with access token
const jwtToken = localStorage.getItem(AUTH_TOKEN);
const token = JSON.parse(jwtToken);
const refresh_token = token["refresh"];
const tokenParts = JSON.parse(atob(refresh_token.split(".")[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
if (tokenParts.exp > now) {
try {
const res = await service.post("/authentication/token/refresh/", {
refresh: refresh_token,
});
localStorage.setItem(AUTH_TOKEN, JSON.stringify(res));
service.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res["access"];
originalRequest.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res["access"];
axios
.request(originalRequest)
.then((response) => {
Promise.resolve(response);
})
.catch((err) => {
Promise.reject(err);
});
//return service(originalRequest);
} catch (error) {
Promise.reject(error);
}
} else {
localStorage.removeItem(AUTH_TOKEN);
history.push(ENTRY_ROUTE);
window.location.reload();
Promise.reject(error);
}
}
);
Hope this helps 👍
After some days of investigating my code, I finally found the answer.
Let me write my code and hope it will be helpful.
let refreshToken;
...
if (tokenParts.exp > now) {
try {
if (!refreshToken) {
refreshToken = service.post('/authentication/token/refresh/', {refresh: refresh_token}).then(token => {
refreshToken = null;
return token;
});
}
return refreshToken.then(res => {
localStorage.setItem(AUTH_TOKEN, JSON.stringify(res));
service.defaults.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res['access'];
originalRequest.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res['access'];
return service(originalRequest);
})
}
catch (error) {
}
}
else {
localStorage.removeItem(AUTH_TOKEN);
history.push(ENTRY_ROUTE);
window.location.reload();
}
This prevents multiple token refresh API request and surely made the failed API call again with new access token.
I have been trying to figure this out for weeks and either can't seem to understand the documentation, or something. I appreciate any help you can give.
I am using the Firebase SDK
I have my server-side route, in which I can access the token and could send it to the front:
const admin = require("firebase-admin")
admin.initializeApp()
exports.loginRoute = (req, res) => {
const user = {
email: req.body.email,
password: req.body.password
}
const { valid, errors } = validateLoginData(user)
if (!valid) {
return res.status(400).json(errors)
}
admin
.auth()
.signInWithEmailAndPassword(user.email, user.password)
.then((data) => {
console.log(data.user.refreshToken, "refresh token")
return data.user.getIdToken(true)
})
.then((token) => {
return res.json({ token })
})
.catch((err) => {
console.error(err)
if (err.code === "auth/user-not-found") {
return res.status(400).json({ general: "User not found" })
} else if (err.code === "auth/wrong-password") {
return res
.status(400)
.json({ password: "User credentials don't match" })
} else {
res.status(500).json({
error: "Something went wrong, please try again."
})
}
})
}
Here is where I could use the refresh token (on the front end) to fetch a new authentication token, but I can't figure out how to create a route to do this:
if (token) {
const decodedToken = jwtDecode(token)
if (decodedToken.exp * 1000 < Date.now()) {
localStorage.setItem("Authentication", false)
//axios request to persist authentication would go here
}
}
Does anyone have a route that would work, or advice on what to do?
EDIT
const login = async (credentials) => {
let token
await axios
.post("/api/login", credentials)
.then((res) => {
token = res.data.token
const FBIdToken = `Bearer ${token}`
localStorage.setItem("token", token)
localStorage.setItem("FBIdToken", FBIdToken)
localStorage.setItem("Authentication", true)
context.setAuthenticated((prev) => true)
})
.then(() => {
context.getUserData()
})
.then(() => {
context.setUserState((prevUserState) => ({
...prevUserState,
token
}))
})
.catch((err) => {
context.setUserErrors((prev) => ({
...prev,
errors: err.response.data
}))
})
history.push("/")
}
Observer (client-side):
firebase.auth().onAuthStateChanged((user) => {
if (user) {
firebase
.auth()
.currentUser.getIdToken(/* forceRefresh */ true)
.then((idToken) => {
const FBIdToken = `Bearer ${idToken}`
localStorage.setItem("FBIdToken", FBIdToken)
})
.catch((err) => {
console.log(err)
})
} else {
localStorage.removeItem("FBIdToken")
}
})
If you sign in with the Firebase Authentication JavaScript SDK in the client-side code, it already persists the user's sign-in state, and tries to restore it when you reload the page. You shouldn't have to do anything for that yourself.
It seems like you were using the same SDK in a server-side environment though, which is quite unusual. If you want to mint tokens yourself in a server-side environment, you should use the Firebase Admin SDK to do so. You can then send that token back to the client, and use it to sign in to Firebase Authentication there.
But for the vast majority of use-cases, I recommend using the Firebase Authentication SDK in your client-side code, so that the SDK managed refreshing of the token for you. If you then want to pass the token to the server, you can use getIdToken() as you do now. You can also monitor ID token generation, or more commonly monitor if a user's sign-in session is restored as shown in the first example of the documentation on detecting the current user.
Im facing an issue now with my authentication system that is based on django-simplejwt and redux-saga.
Whenever i click on the log out button , the following saga is run:
export function* workerLogout(action) {
const r_token = localStorage.getItem('refresh_token');
yield call(() => axios.post('http://127.0.0.1:8000/api/blacklist/', {
"refresh_token": r_token
})) //<--- API end-point to blacklist the tokens
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token'); // <--- removing the tokens
yield call(() => axiosInstance.defaults.headers['Authorization'] = null ); //<--- setting the headers to null
yield put({ type: "LOGOUT_SUCCESS" }) //<---- This will go to the reducer to set the states to null
}
Which will then acccess the reducer here:
const authLogout = (state, action) => {
return updateObject(state, {
access_token: null,
refresh_token: null,
isAuthenticated: false,
group: {},
username: null,
});
}
This works and my redux tool bar shows that the state of the tab is indeed cleared:
isAuthenticated(pin):false
access_token(pin):null
refresh_token(pin):null
group(pin):
username(pin):null
My interceptor:
axiosInstance.interceptors.response.use(
response => {
return response
},async error => {
const originalRequest = error.config;
// Prevent infinite loops
if (error.response.status === 401 && originalRequest.url === '/token/refresh/') {
store.dispatch({type: "LOGOUT"});
return Promise.reject(error);
}
if (error.response.data.code === "token_not_valid" &&
error.response.status === 401 &&
error.response.statusText === "Unauthorized")
{
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken){
const tokenParts = JSON.parse(atob(refreshToken.split('.')[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
if (tokenParts.exp > now) {
console.log('access token is expired , attempting refresh')
try {
const response = await axiosInstance
.post('/token/refresh/', { refresh: refreshToken });
localStorage.setItem('access_token', response.data.access);
localStorage.setItem('refresh_token', response.data.refresh);
axiosInstance.defaults.headers['Authorization'] = "JWT " + response.data.access;
originalRequest.headers['Authorization'] = "JWT " + response.data.access;
axiosInstance.get('group/get/').then(groups => {
store.dispatch({type: "LOGIN_SUCCESS" , payload:[response.data.access, refreshToken, groups.data, jwt_decode(response.data.access).username]});
})
return axiosInstance(originalRequest);
}
catch (err) {
store.dispatch({type: "GET_ERRORS" , error : err.response.data})
}
}else{
store.dispatch({type: "LOGOUT"});
}
}
}
store.dispatch({type: "GET_ERRORS" , error : error.response.data})
return Promise.reject(error);
}
);
However , if let's say a second tab was open at the point of the logout , i can still access my server's resources from the second tab. Upon closer inspection of the redux tool bar , it seems the state of the second tab is not affected by the first tab.
Is this an expected behavior? If so how do i make it in synced across all tabs as this seems like a bad way to secure the application.
I would suggest to store your Accesstoken and refreshtoken in Browser's local storage. Clearing accesstoken from redux store will not impact the other tabs.
On your Login success you can store accesstoken as
static saveToken(token, refreshToken){
window.localStorage.setItem('token', token);
window.localStorage.setItem('refresh_token', refreshToken);
}
On your logout success you can just remove the token by
static clearAccessToken(){
window.localStorage.removeItem('token');
window.localStorage.removeItem('refresh_token');
}
In your request Interceptor you can access the accesstoken/refreshtoken by
static isAuthenticated(){
return window.localStorage.token != null;
}
static getAccessToken(){
return window.localStorage.token;
}
static getRefreshToken(){
return window.localStorage.refresh_token;
}
By doing this way when you logout from one tab it will logged out from your browser.
You can contorl the logout error in authInterceptor
axios.interceptors.response.use((response) => {
return response
}, (error) => {
const { config, response: { status } } = error;
const originalRequest = config;
if (status === 401) {
if(config.url.endsWith('oauth/token') || config.url.endsWith('users/logout')){
return Promise.reject(error);
}
if (!this.isAlreadyFetchingAccessToken) {
this.isAlreadyFetchingAccessToken = true;
var refreshTokenValue = AuthService.getRefreshToken();
refreshToken(refreshTokenValue).then((token) => {
this.isAlreadyFetchingAccessToken = false
if(token && token.data && token.data.access_token){
AuthService.saveToken(token.data.access_token, token.data.refresh_token);
this.onAccessTokenFetched(token.data.access_token);
}
}).catch(err => {
this.isAlreadyFetchingAccessToken = false;
this.subscribers = [];
this.history.push('/login');
return Promise.reject("Auth error");
})
}
const retryOriginalRequest = new Promise((resolve) => {
this.addSubscriber(access_token => {
originalRequest.headers.Authorization = 'Bearer ' + access_token
resolve(axios(originalRequest))
})
})
return retryOriginalRequest
}
console.log("Unknown error :" + error);
let errorMessage = '';
if(error.response && error.response.data) {
return Promise.reject(error.response.data);
} else {
return Promise.reject({
code: UNKNOWN_ERROR,
message: "Oops, Unable to complete the request"
});
}
})
This is my request interceptor
axios.interceptors.request.use(function (config) {
if(AuthService.isAuthenticated()){
if(config.url.startsWith(apiUrl) && !config.url.startsWith(OAUTH_URL)){
config.headers.Authorization = 'Bearer ' + window.localStorage.token;
}
}
return config;
});
In the above code I am reading my access token directly from localstorage. Upon logout the Authorization header by default becomes invalid and throws 401 now In your reponse interceptor you can redirect to logout page when refresh token fails. Hope this will help.
We have a react-redux app that fetches data using multiple API calls with every page load. The app follows the OAuth2 protocol. It has an access token that expires frequently and a refresh token to be used to get a new access token.
If an API call is made with an expired access token, a 401 error is received with error message "API token is expired." Then we need to get a new token from the auth server.
My problem is this:
When a page loads, say 8 API calls were dispatched. We receive 3 successful 200s but from the 4th response onwards, we receive 401 "API token is expired." At that point, I want to put all API calls that I have already made but didn't receive a response or received 401 error in a queue until I refresh the access token. After the access token is successfully refreshed, I want to re-do the API calls saved in the queue. How can I achieve this?
Looking for this online, I found that redux-saga might work, but didn't see any indication that it can be used for this use case.
I also used to handle this case. This is my solution:
/**
* Connect to API
*/
export const makeRequest = (args) => {
const request = fetch(args)//example
return _retryRequestIfExpired(request, args)
}
/**
* Fake get access token.
*/
const _getNewAccessToken = () => {
return new Promise((resolve, reject) => {
resolve('xyz')
})
}
const _retryRequestIfExpired = (request, args) => {
return request.catch(error => {
if (error === 'abc') {//Any reason you want
return _refreshAccessToken()
.then(newAccessToken => {
const updateArgs = {
...args,
headers: {
'Authorization': newAccessToken
}
}
//Retry with new access token
return makeRequest(updateArgs)
})
}
throw error
})
}
/**
* Important
*/
let _isRefreshingToken = false
let _subscribers = []
const _subscribe = subscriber => {
if (typeof subscriber !== 'function' || _subscribers.indexOf(subscriber) !== -1) {
return false
}
_subscribers = [].concat(_subscribers, [subscriber])
}
const _broadcast = (error = null, data) => {
_isRefreshingToken = false
_subscribers.forEach(subscriber => {
subscriber(error, data)
})
_subscribers = []
}
const _refreshAccessToken = () => {
if (_isRefreshingToken) {//If a request is creating new access token
return new Promise((resolve, reject) => {
const subscriber = (error, accessToken) => {
if (error) {
return reject(error)
}
return resolve(accessToken)
}
_subscribe(subscriber)
})
}
_isRefreshingToken = true
return _getNewAccessToken()
.then(accessToken => {
_broadcast(null, accessToken)
return accessToken
})
.catch(error => {
_broadcast(error)
throw error
})
}
/**
* End Important
*/
In this way, only the first request will actually create a new access token and remaining requests will temporarily be stopped until a new access token is created.