I am trying to restrict my API to only logged users. To do so I am sending firebase token and trying to verify it on server-side exactly how Google says to do.
However, I get an exception like this:
FirebaseAuthError: Decoding Firebase ID token failed. Make sure you passed the entire string JWT which represents an ID token. See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.
Printing out those tokens on client-side as well as on server-side proves that on both sides they are identical.
Do I miss something?
My code:
const checkAuth = (token) => {
if (token === undefined) return false;
console.log(token);
admin
.auth()
.verifyIdToken(token)
.then((decodedToken) => {
const uid = decodedToken.uid;
admin
.auth()
.getUser(uid)
.then(() => {
return true;
})
.catch(() => {
return false;
});
})
.catch((error) => console.log(error));
};
// example usage
app.get("/fileslist/:id", async function (req, res) {
const authorized = checkAuth(req.headers.token);
if (authorized) {
const directoryPath = path.join(__dirname, "uploads/" + req.params.id);
fs.readdir(directoryPath, function (err, files) {
var array = [];
if (err) {
return console.log("Unable to scan directory: " + err);
}
files.forEach(function (file) {
array.push(file);
});
res.status(200).send(array);
});
} else {
res.status(403).send();
}
});
And client-side:
//getting token
const getToken = () => {
currentUser
.getIdToken(true)
.then(function (idToken) {
setToken(idToken);
})
.catch((error) => {
console.log(error);
});
};
//calling an api
const getFilesById = async (id, token) => {
console.log(token);
return await client.get(
"/fileslist/" + id,
{},
{ headers: { token: token } }
);
};
//api definition
import { create } from "apisauce";
const api = create({
baseURL: "http://localhost:3005",
});
export default api;
You need to return the entire Promises chain in the checkAuth() function, as follows:
const checkAuth = (token) => {
if (token === undefined) return false;
console.log(token);
return admin // <== see return
.auth()
.verifyIdToken(token)
.then((decodedToken) => {
const uid = decodedToken.uid;
return admin // <== see return
.auth()
.getUser(uid)
.then(() => {
return true;
})
.catch(() => {
return false;
});
})
.catch((error) => console.log(error));
};
You could actually have only one catch block, as follows:
const checkAuth = (token) => {
if (token === undefined) return false;
console.log(token);
return admin // <== see return
.auth()
.verifyIdToken(token)
.then((decodedToken) => {
const uid = decodedToken.uid;
return admin // <== see return
.auth()
.getUser(uid);
})
.then(() => {
return true;
})
.catch((error) => {
console.log(error);
return false;
});
};
Related
I have an react-admin with an auth provider like the below.
I want to refresh my token, but I don't know how to do it.
I tried to follow this blog post, but my auth is a little different and I can't make it work (the error "httpClient(...).then" is not a function and others make me leave it).
I can make it with a more simple solution, does not need to be in memory. I tried to call my refresh endpoint to get my refresh token, but my call go without the current token.
My endpoint to refresh the token is:
/auth/jwt/refresh
I need to call it like this:
curl -X 'GET' \
'http://localhost:8000/auth/jwt/refresh' \
-H 'accept: application/json' \
-H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
And My response body would be: (and I need to save it to my localstorage or the in memory way)
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZTUwZDdhZDctOWE5Ni00NzQyLTgxNWEtZTNmZmJmNGRiMTVjIiwiYXVkIjpbImZhc3RhcGktdXNlcnM6YXV0aCJdLCJleHAiOjE2Mzk4NDE1MDF9.-o2yk56sCj_MZx_VA6PxH7gZ-KKSMmopbDNDiapHmn0",
"token_type": "bearer"
}
My inMemoryJWTManager file:
const inMemoryJWTManager = () => {
let inMemoryJWT = null;
let isRefreshing = null;
let logoutEventName = 'ra-logout';
let refreshEndpoint = '/auth/jwt/refresh';
let refreshTimeOutId;
const setLogoutEventName = name => logoutEventName = name;
const setRefreshTokenEndpoint = endpoint => refreshEndpoint = endpoint;
// This countdown feature is used to renew the JWT before it's no longer valid
// in a way that is transparent to the user.
const refreshToken = (delay) => {
refreshTimeOutId = window.setTimeout(
getRefreshedToken,
delay * 1000 - 5000
); // Validity period of the token in seconds, minus 5 seconds
};
const abordRefreshToken = () => {
if (refreshTimeOutId) {
window.clearTimeout(refreshTimeOutId);
}
};
const waitForTokenRefresh = () => {
if (!isRefreshing) {
return Promise.resolve();
}
return isRefreshing.then(() => {
isRefreshing = null;
return true;
});
}
// The method make a call to the refresh-token endpoint
// If there is a valid cookie, the endpoint will set a fresh jwt in memory.
const getRefreshedToken = () => {
const request = new Request(refreshEndpoint, {
method: 'GET',
headers: new Headers({ 'Content-Type': 'application/json' }),
credentials: 'include',
});
isRefreshing = fetch(request)
.then((response) => {
if (response.status !== 200) {
ereaseToken();
global.console.log(
'Token renewal failure'
);
return { token: null };
}
return response.json();
})
.then(({ token, tokenExpiry }) => {
if (token) {
setToken(token, tokenExpiry);
return true;
}
ereaseToken();
return false;
});
return isRefreshing;
};
const getToken = () => inMemoryJWT;
const setToken = (token, delay) => {
inMemoryJWT = token;
refreshToken(delay);
return true;
};
const ereaseToken = () => {
inMemoryJWT = null;
abordRefreshToken();
window.localStorage.setItem(logoutEventName, Date.now());
return true;
}
// This listener will allow to disconnect a session of ra started in another tab
window.addEventListener('storage', (event) => {
if (event.key === logoutEventName) {
inMemoryJWT = null;
}
});
return {
ereaseToken,
getRefreshedToken,
getToken,
setLogoutEventName,
setRefreshTokenEndpoint,
setToken,
waitForTokenRefresh,
}
};
export default inMemoryJWTManager();
This is my auth provider: (updated, using inMemoryJWTManager)
import inMemoryJWTManager from './inMemoryJWT'
const apiUrl = 'http://localhost:8000'
const authProvider = {
login: ({username, password}) => {
const oAuthParams = {
username,
password
}
const body = Object.keys(oAuthParams).map((key) => {
return encodeURIComponent(key) + '=' + encodeURIComponent(oAuthParams[key]);
}).join('&');
const request = new Request(`${apiUrl}/auth/jwt/login`, {
method: 'POST',
body: body,
headers: new Headers({'Content-Type': 'application/x-www-form-urlencoded'}),
});
return fetch(request)
.then(response => {
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
}
return response.json();
})
.then(( {access_token} ) => {
inMemoryJWTManager.setToken(access_token);
});
},
checkError: (error) => {
const status = error.status;
if (status === 401 || status === 403) {
inMemoryJWTManager.ereaseToken();
return Promise.reject({redirectTo: '/login'});
}
// other error code (404, 500, etc): no need to log out
return Promise.resolve();
},
checkAuth: () => inMemoryJWTManager.getToken()
? Promise.resolve()
: Promise.reject({ message: 'Login necessário', redirectTo: 'login' }),
logout: () => {
inMemoryJWTManager.ereaseToken();
return Promise.resolve();
},
getPermissions: () => {
return inMemoryJWTManager.getToken() ? Promise.resolve() : Promise.reject();
},
};
export default authProvider;
My updated httpClient code using inMemoryJWTManager: (and I'm using: const dataProvider = jsonServerProvider(apiUrl, httpClient); with modifications to it, but I think it is irrelevant)
const httpClient = (url) => {
const options = {
headers: new Headers({ Accept: 'application/json' }),
};
const token = inMemoryJWTManager.getToken();
console.log(token)
if (token) {
options.headers.set('Authorization', `Bearer ${token}`);
return fetchUtils.fetchJson(url, options);
} else {
inMemoryJWTManager.setRefreshTokenEndpoint(`${apiUrl}/auth/jwt/refresh`);
return inMemoryJWTManager.getRefreshedToken().then((gotFreshToken) => {
if (gotFreshToken) {
options.headers.set('Authorization', `Bearer ${inMemoryJWTManager.getToken()}`);
};
return fetchUtils.fetchJson(url, options);
});
}
};
My problem is that, when I call my refresh token endpoint, my request go without the {'Authorization': Bearer... and it is not renewed and I got logged out. The other endpoints are fine, they go with the token.
You must check token expire before each requests, if token expired you must get new from /auth/jwt/refresh, then you can send current request. All this information is in the article post. Example:
const httpClient = (url) => {
const options = {
headers: new Headers({ Accept: 'application/json' }),
};
const token = inMemoryJWT.getToken();
if (token) {
options.headers.set('Authorization', `Bearer ${token}`);
return fetchUtils.fetchJson(url, options);
} else {
inMemoryJWT.setRefreshTokenEndpoint('http://localhost:8001/refresh-token');
return inMemoryJWT.getRefreshedToken().then((gotFreshToken) => {
if (gotFreshToken) {
options.headers.set('Authorization', `Bearer ${inMemoryJWT.getToken()}`);
};
return fetchUtils.fetchJson(url, options);
});
}
};
network.services.js
axiosCall = (axiosURL) => {
// const axiosURL = "https://api.github.com/user"
axios.get(axiosURL, {
headers: {
'Authorization': `qwdvryjutmnevw`,
}
}).then((res) => {
console.log(res.data);
return res.data;
}).catch((error) => {
throw error.message;
// console.error(error);
// toast.error(error.message);
})
}
component.js
const getData = async () => {
const asyncExample = async () => {
const result = await networkServices.axiosCall("/api/v1/calendars");
const responseData = await result;
console.log(responseData);
return responseData;
}
const data = asyncExample()
data.then(function(result) {
console.log(result); // "Some User token"
})
}
Trying to get data from service to my component in const result, console form service is consoling data but component is always returning undefined instead of data from the service file. SetTimeout function is also not working in component.
You have many mistakes. I advise you to take a look at documentation about Promises
First one:
You don't return data in axiosCall
A way to return data:
axiosCall = (axiosURL) => new Promise((resolve, reject) => {
axios.get(axiosURL, {
headers: {
'Authorization': `yourTokenHere`,
}
}).then((res) => {
// return a response data
resolve(res.data);
}).catch((error) => {
// return only error message
reject(error.message);
})
})
to use axiosCall:
try {
// don't forgot to configure axios with base url
const data = await axiosCall('/api/v1/calendars');
// do something with your data
} catch (e) {
// do something with error message
console.log(e);
}
Second:
Your make mistakes when call async function
Look at this example:
const getData = () => {
networkServices
.axiosCall("/api/v1/calendars")
.then(function(result) {
// when promise resolve
console.log(result);
})
.catch(error => {
// when promise reject
console.log(error)
})
}
I have a small booking program to practice ReactJS + Redux + ReduxSaga + Axios + Axios Interceptor + JWT authentication
Here is the code in the component BookingDialog after the submit button click
bookingDialog.js
const handleSubmit = (event) => {
event.preventDefault();
let payload = {
selectedDate: selectedDate,
carId : carDetail.id,
userId : user.login.id, //or pass by jwt accesstoken
remarks: remarks
}
console.log(payload);
dispatch(createBooking(payload));
}
And there is saga watcher which take latest of action createBooking to function handleCreateBooking
bookingSaga.js
export function* handleCreateBooking(action) {
try {
const response = yield call(createBooking, action.payload);
const { data } = response;
console.log("handleCreateBooking");
console.log(response);
if (data && data.result && data.result > 0){
console.log("booked successfully");
yield put(setMessageBarOpen({type: "success", content: "booked successfully"}));
yield put(setCreateBookingOpen(false));
}
else{
console.log("booked failed");
//yield put(setMessageBarOpen({type: "error", content: "booked failed"}));
//yield put(setCreateBookingOpen(false));
}
} catch (error) {
console.log(error);
}
}
bookingRequest.js
const createBooking = (payload) => {
return postUrl(apiURL.createBooking.url, payload).then((res) => {
return res
});
}
The program works as expected. Success message shown and booking dialog closed after submission.
If the jwt is expired, the program will retrieve the access token by refresh token and resubmit the original request with the new access token.
The problem is that, after the original request is sent and booking is created successfully, the follow up actions (setMessageBarOpen & setCreateBookingOpen) are not performed as the posting of original request is not under the function handleCreateBooking in bookingSaga.js
axiosInstance.js
import axios from 'axios';
import apiURL from "requests/apiURL";
const ax = axios.create();
ax.interceptors.request.use(
request => {
const accessToken = JSON.parse(localStorage.getItem('token')) && JSON.parse(localStorage.getItem('token')).accessToken;
if (accessToken) {
let auth = false;
for (const [key, value] of Object.entries(apiURL)) {
if (request.url.includes(value.url)) {
auth = value.auth;
break;
}
}
if (auth) {
request.headers.authorization = `Bearer ${accessToken}`;
}
}
return request;
},
error => {
return Promise.reject(error);
}
);
const sendRefreshToken = (refreshToken) => {
return new Promise((resolve, reject) => {
console.log("refreshToken");
postUrl(apiURL.token.url, { token: refreshToken })
.then((res) => {
console.log(res);
if (res.data) {
console.log(res.data);
localStorage.setItem('token', JSON.stringify({accessToken: res.data.accessToken, refreshToken: refreshToken}));
resolve(res);
}
})
.catch(error => {
reject(error);
});
})
}
ax.interceptors.response.use(
(response) => {
return response;
},
error => {
console.log("axios.interceptors.response");
console.log(error);
const status = error.response ? error.response.status : null;
const originalRequest = error.config;
let isRefreshing = false;
if (status === 403) {
if (!isRefreshing) {
const refreshToken = JSON.parse(localStorage.getItem('token')) && JSON.parse(localStorage.getItem('token')).refreshToken;
console.log("403, refreshToken:");
console.log(refreshToken);
isRefreshing = true;
sendRefreshToken(refreshToken)
.then(({ status }) => {
console.log(status);
if (status === 200 || status === 204) {
isRefreshing = false;
console.log("start resendRequest");
console.log(originalRequest);
return ax(originalRequest);
}
})
.catch(error => {
console.error(error);
});
}
}
return error;
}
);
export const getUrl = async (url, opt) => {
const response = await ax.get(url, opt);
return response;
}
export const postUrl = async (url, data, opt) => {
const axios_res = await ax.post(url, data, opt);
return axios_res;
}
How should I handle the response from the resubmitted original request?
Thanks.
My backend API route is /api/updateUser/:id
How am I supposed to POST data into this API? I'm familiar with POST request for non params APIs but this one has an /:id in the route.
Can someone show me an example with this demo code
state = {
username: "random123",
password: "random123",
userid: "qwertyuiop",
};
saveDetails = async () => {
const { username, password, userid } = this.state;
let data = new FormData();
data.append('username',username);
data.append('password',password);
axios
.put(apiEndPoint+'?id='+this.state.userid, data) //this is where I need help
.then(async (response) => {
if (response.data) {
console.log("success");
} else {
console.log("issue");
}
})
.catch((err) => {
console.log("error",err);
});
};
This is the working example for Path Parameter Axios PUT request -
saveDetails = async () => {
const { username, password, userid } = this.state;
axios
.put(apiEndPoint+"updateUser/"+userid, {
username:username,
password:password,
})
.then(async (response) => {
if (response.data) {
console.log("done");
} else {
console.log("error");
}
})
.catch((err) => {
console.log("error",err);
});
};
The following code I did allows to refresh the access token, when on the first request the response return an Unauthorized, run the service to refresh the access token.
The code works fine, the refresh token service runs on the backend made in Java.
The code is incomplete, there is only the call to the refresh token method.
On the following shown the code.
const apiMiddleware = ({ dispatch }) => (next) => (action) => {
const result = next(action);
if (action.type !== API) {
return result;
}
const {
url,
method,
data,
onSuccess,
types,
} = action.payload;
const axiosInstance = createAxios();
const refreshToken = async(originalRequest) => {
const axiosInstance = createAxios();
const data = authUtil.getUserLoggedIn();
const response = await axiosInstance({
url: `${AUTH_ENDPOINT}/refresh-token`,
method: 'PUT',
data: { token: data.refreshToken },
});
if (response.status === 200) {
authUtil.setUserLoggedIn(response.data);
originalRequest.headers['Authorization'] = response.data.accessToken;
axiosInstance(originalRequest)
.then(({ data }) => {
handlerSuccess({ data });
return result;
});
} else {
dispatch(apiError(types[1], response.error));
}
};
const handlerSuccess = ({ data }) => {
dispatch(onSuccess(data));
};
axiosInstance({
method,
url,
data,
})
.then(({ data }) => {
handlerSuccess({ data });
return result;
})
.catch((error) => {
if (error.response && error.response.status === 403) {
dispatch(accessDenied(types[2], window.location.pathname));
} else if (error.response && error.response.status === 401) {
refreshToken(error.config);
}
return result;
})
};
export default apiMiddleware;
Somebody can help me to improve this code