In my app, I have an access token (Spotify's) which must be valid at all times. When this access token expires, the app must hit a refresh token endpoint, and fetch another access token, every 60 minutes.
Authorize functions
For security reasons, these 2 calls, to /get_token and /refresh_token are dealt with python, server-side, and states are currently being handled at my Parent App.jsx, like so:
class App extends Component {
constructor() {
super();
this.state = {
users: [],
isAuthenticated: false,
isAuthorizedWithSpotify: false,
spotifyToken: '',
isTokenExpired:false,
isTokenRefreshed:false,
renewing: false,
id: '',
};
componentDidMount() {
this.userId(); //<--- this.getSpotifyToken() is called here, inside then(), after async call;
};
getSpotifyToken(event) {
const options = {
url: `${process.env.REACT_APP_WEB_SERVICE_URL}/get_token/${this.state.id}`,
method: 'get',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${window.localStorage.authToken}`,
}
};
// needed for sending cookies
axios.defaults.withCredentials = true
return axios(options)
.then((res) => {
console.log(res.data)
this.setState({
spotifyToken: res.data.access_token,
isTokenExpired: res.data.token_expired // <--- jwt returns expiration from server
})
// if token has expired, refresh it
if (this.state.isTokenExpired === true){
console.log('Access token was refreshed')
this.refreshSpotifyToken();
}
})
.catch((error) => { console.log(error); });
};
refreshSpotifyToken(event) {
const options = {
url: `${process.env.REACT_APP_WEB_SERVICE_URL}/refresh_token/${this.state.id}`,
method: 'get',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${window.localStorage.authToken}`,
}
};
axios.defaults.withCredentials = true
return axios(options)
.then((res) => {
console.log(res.data)
this.setState({
spotifyToken: res.data.access_token,
isTokenRefreshed: res.data.token_refreshed,
isTokenExpired: false,
isAuthorizedWithSpotify: true
})
})
.catch((error) => { console.log(error); });
};
Then, I pass this.props.spotifyToken to all my Child components, where requests are made with the access token, and it all works fine.
Watcher Function
The problem is that, when the app stays idle on a given page for more than 60 minutes and the user makes a request, this will find the access token expired, and its state will not be updated, so the request will be denied.
In order to solve this, I thought about having, at App.jsx, a watcher function tracking token expiration time on the background, like so:
willTokenExpire = () => {
const accessToken = this.state.spotifyToken;
console.log('access_token in willTokenExpire', accessToken)
const expirationTime = 3600
const token = { accessToken, expirationTime } // { accessToken, expirationTime }
const threshold = 300 // 300s = 5 minute threshold for token expiration
const hasToken = token && token.spotifyToken
const now = (Date.now() / 1000) + threshold
console.log('NOW', now)
if(now > token.expirationTime){this.getSpotifyToken();}
return !hasToken || (now > token.expirationTime)
}
handleCheckToken = () => {
if (this.willTokenExpire()) {
this.setState({ renewing: true })
}
}
and:
shouldComponentUpdate(nextProps, nextState) {
return this.state.renewing !== nextState.renewing
}
componentDidMount() {
this.userId();
this.timeInterval = setInterval(this.handleCheckToken, 20000)
};
Child component
Then, from render() in Parent App.jsx, I would pass handleCheckToken() as a callback function, as well as this.props.spotifyToken, to the child component which might be idle, like so:
<Route exact path='/tracks' render={() => (
<Track
isAuthenticated={this.state.isAuthenticated}
isAuthorizedWithSpotify={this.state.isAuthorizedWithSpotify}
spotifyToken={this.state.spotifyToken}
handleCheckToken={this.handleCheckToken}
userId={this.state.id}
/>
)} />
and in the Child component, I would have:
class Tracks extends Component{
constructor (props) {
super(props);
this.state = {
playlist:[],
youtube_urls:[],
artists:[],
titles:[],
spotifyToken: this.props.spotifyToken
};
};
componentDidMount() {
if (this.props.isAuthenticated) {
this.props.handleCheckToken();
};
};
and a call where the valid, updated spotifyToken is needed, like so:
getTrack(event) {
const {userId} = this.props
const options = {
url: `${process.env.REACT_APP_WEB_SERVICE_URL}/get-tracks/${userId}/${this.props.spotifyToken}`,
method: 'get',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${window.localStorage.authToken}`
}
};
return axios(options)
.then((res) => {
console.log(res.data.message)
})
.catch((error) => { console.log(error); });
};
But this is not working.
At regular intervals, new tokens are being fetched with handleCheckToken, even if I'm idle at Child. But if I make the request after 60 minutes, in Child, this.props.spotifyToken being passed is expired, so props is not being passed down to Child.jsx correctly.
What am I missing?
You are talking about exchanging refreshToken to accessToken mechanism and I think that you over complicated it.
A background, I've a similar setup, login generates an accessToken (valid for 10 mins) and a refreshToken as a cookie on the refreshToken end point (not necessary).
Then all my components are using a simple api service (which is a wrapper around Axios) in order to make ajax requests to the server.
All of my end points are expecting to get a valid accessToken, if it expired, they returns 401 with an expiration message.
My Axios has a response interceptor which check if the response is with status 401 & the special message, if so, it makes a request to the refreshToken endpoint, if the refreshToken is valid (expires after 12 hours) it returns an accessToken, otherwise returns 403.
The interceptor gets the new accessToken and retries (max 3 times) the previous failed request.
The cool think is that in this way, accessToken can be saved in memory (not localStorage, since it is exposed to XSS attack). I save it on my api service, so, no Component handles anything related to tokens at all.
The other cool think is that it is valid for a full page reload as well, because if the user has a valid cookie with a refreshToken, the first api will fail with 401, and the entire mechanism will work, otherwise, it will fail.
// ApiService.js
import Axios from 'axios';
class ApiService {
constructor() {
this.axios = Axios.create();
this.axios.interceptors.response.use(null, this.authInterceptor);
this.get = this.axios.get.bind(this.axios);
this.post = this.axios.post.bind(this.axios);
}
async login(username, password) {
const { accessToken } = await this.axios.post('/api/login', {
username,
password,
});
this.setAccessToken(accessToken);
return accessToken; // return it to the component that invoked it to store in some state
}
async getTrack(userId, spotifyToken) {
return this.axios.get(
`${process.env.REACT_APP_WEB_SERVICE_URL}/get-tracks/${userId}/${spotifyToken}`
);
}
async updateAccessToken() {
const { accessToken } = await this.axios.post(`/api/auth/refresh-token`, {});
this.setAccessToken(accessToken);
}
async authInterceptor(error) {
error.config.retries = error.config.retries || {
count: 0,
};
if (this.isUnAuthorizedError(error) && this.shouldRetry(error.config)) {
await this.updateAccessToken(); // refresh the access token
error.config.retries.count += 1;
return this.axios.rawRequest(error.config); // if succeed re-fetch the original request with the updated accessToken
}
return Promise.reject(error);
}
isUnAuthorizedError(error) {
return error.config && error.response && error.response.status === 401;
}
shouldRetry(config) {
return config.retries.count < 3;
}
setAccessToken(accessToken) {
this.axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`; // assign all requests to use new accessToken
}
}
export const apiService = new ApiService(); // this is a single instance of the service, each import of this file will get it
This mechanism is based on this article
Now with this ApiService you can create a single instance and import it in each Component that whats to make an api request.
import {apiService} from '../ApiService';
class Tracks extends React.Component {
constructor(props) {
super(props);
this.state = {
playlist: [],
youtube_urls: [],
artists: [],
titles: [],
spotifyToken: this.props.spotifyToken,
};
}
async componentDidMount() {
if (this.props.isAuthenticated) {
const {userId, spotifyToken} = this.props;
const tracks = await apiService.getTracks(userId, spotifyToken);
this.setState({tracks});
} else {
this.setState({tracks: []});
}
}
render() {
return null;
}
}
Edit (answers to comments)
Handling of login flow can be done using this service as well, you can extract the accessToken from the login api, set it as a default header and return it to the caller (which may save it in a state for other component logic such as conditional rendering)(updated my snippet).
it is just an example of component which needs to use api.
there is only one instance of the ApiService it is created in the "module" of the file (at the end you can see the new ApiService), after that you just importing this exported instance to all the places that need to make an api call.
If you want to force a rerender of your Child component when the state of the parent component changes, you can use the key prop. Use the spotify token as the key. When the spotify token is re-fetched and updated, it will remount your child component with the new token as well:
<Route exact path='/child' render={() => (
<Child
isAuthenticated={this.state.isAuthenticated}
isAuthorizedWithSpotify={this.state.isAuthorizedWithSpotify}
spotifyToken={this.state.spotifyToken}
key={this.state.spotifyToken}
handleCheckToken={this.handleCheckToken}
userId={this.state.id}
/>
)} />
Though this may reset any internal state that you had in your child component, as it is essentially unmounting and remounting the Child.
Edit: More details
The key prop is a special prop used in React components. React uses keys to determine whether or not a component is unique, from one component to another, or from one render to another. They are typically used when mapping an array to a set of components, but can be used in this context as well. The react docs have an excellent explanation. Basically when the key prop of a component changes, it tells React that this is now a new component, and therefore must be completely rerendered. So when you fetch a new spotifyToken, and assign that new token as the key, React will completely remount the Child with the new props. Hopefully that makes things more clear.
The key prop will not be available from within your Child - this.props.key inside of your child will not be useful to try to access. But in your case, you are passing the same value to the spotifyToken inside the Child, so you'll have the value available there. Its common to use another prop with the same value as key when that value is needed in the child component.
props will not updates on the child, because for a child component they are like immutable options: https://github.com/uberVU/react-guide/blob/master/props-vs-state.md
So you will need some ways to re-render the Child component.
The Child component has already been constructed so will not update and re-render.
So you will need to use "getDerivedStateFromProps()" as a replacement from the deprecated "componentWillReceiveProps" function inside the Child component, so that when receiving updated props from the parent the child will re-render, like this:
class Child extends Component {
state = {
spotifyToken: null,
};
static getDerivedStateFromProps(props, state) {
console.log("updated props", props.spotifyToken);
if (props.spotifyToken !== state.spotifyToken) {
return {
spotifyToken: props.spotifyToken,
};
}
// Return null if the state hasn't changed
return null;
}
getTrack(event) {
const {userId} = this.props
const options = {
url: `${process.env.REACT_APP_WEB_SERVICE_URL}/get-tracks/${userId}/${this.state.spotifyToken}`,
method: 'get',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${window.localStorage.authToken}`
}
};
return axios(options)
.then((res) => {
console.log(res.data.message)
console.log(options.url)
})
.catch((error) => { console.log(error); });
}
};
Note that in the getTrack function you will use the Child state value and not the props.
Related
I am trying to implement an React solution with Strapi as backend where authorization is done using JWT-keys. My login form is implemented using the function below:
const handleLogin = async (e) => {
let responsekey = null
e.preventDefault();
const data = {
identifier: LoginState.username,
password: LoginState.password
}
await http.post(`auth/local`, data).then((response) => {
setAuth({
userid: response.data.user.id,
loggedin: true
})
responsekey = response.data.jwt
setLoginState({...LoginState, success: true});
sessionStorage.setItem('product-authkey', responsekey);
navigate('/profile');
}).catch(function(error) {
let result = ErrorHandlerAPI(error);
setLoginState({...LoginState, errormessage: result, erroroccurred: true});
});
}
The API-handler should return an Axios item which can be used to query the API. That function is also shown below. If no API-key is present it should return an Axios object without one as for some functionality in the site no JWT-key is necessary.
const GetAPI = () => {
let result = null
console.log(sessionStorage.getItem("product-authkey"))
if (sessionStorage.getItem("product-authkey") === null) {
result = axios.create(
{
baseURL: localurl,
headers: {
'Content-type': 'application/json'
}
}
)
} else {
result = axios.create({
baseURL: localurl,
headers: {
'Content-type': 'application/json',
'Authorization': `Bearer ${sessionStorage.getItem("product-authkey")}`
}
})
}
return result
}
export default GetAPI()
However, once the user is redirected to the profile page (on which an API-call is made which needs an JWT-key), the request fails as there is no key present in the sessionStorage. The console.log also shows 'null'. If I look at the DevTools I do see that the key is there... And if I refresh the profile page the request goes through with the key, so the key and backend are working as they should.
I tried making the GetAPI function to be synchronous and to move the navigate command out of the await part in the handleLogin function, but that didn't help.
Does someone have an idea?
Thanks!
Sincerely,
Jelle
UPDATE:
Seems to work now, but I need to introduce the getAPI in the useEffect hook, I am not sure if that is a good pattern. This is the code of the profile page:
useEffect(() => {
let testapi = GetAPI()
const getMatches = async () => {
const response = await testapi.get(`/profile/${auth.userid}`)
const rawdata = response.data.data
... etc
}, [setMatchState]
export default GetAPI() this is the problematic line. You are running the GetApi function when the module loads. Basically you only get the token when you visit the site and the js files are loaded. Then you keep working with null. When you reload the page it can load the token from the session storage.
The solution is to export the function and call it when you need to make an api call.
I am trying to store jwt token in local storage on app startup but the render method gets called even before the token is stored in local storage. Could you please help to componentWillMount to wait until token is stored in local storage?
Index.js
componentWillMount() {
AuthRepository.getToekn();
}
AuthRepository.js
class AuthRepository {
constructor(callback) {
this.callback = callback;
}
async getToekn() {
const reponse = await axios
.post(`${baseUrl}/auth/local`, {
identifier:(process.env.IDENTIFIER).trim(),
password:(process.env.IDENTIFIER_PWD).trim(),
})
.then((response) => {
setTrapiToken(response.data.jwt)
return response.data.jwt;
})
.catch((error) => ({ error: JSON.stringify(error) }));
return reponse;
}
}
export default new AuthRepository();
Function to set token
export const setTrapiToken = (token) => {
console.log('here')
console.log(token)
try{
localStorage.setItem(process.env.JWT_KEY,token);
return true;
}catch(e){
console.log('false')
return false;
}
};
React's rendering never waits - and so mounting also never waits.
If you like it or not, that component is mounted. So all you can do is check if the data is already in place and display some kind of loading indicator if it isn't yet.
I have a component GigRegister - one of it's functions is to get all the documents from a collection, and return only the documents created by the currently logged in user:
authListener() {
auth().onAuthStateChanged(user => {
if(user){
this.setState({
userDetails:user
},
() =>
firebase.firestore().collection('gig-listing').onSnapshot(querySnapshot => {
let filteredGigs = querySnapshot.docs.filter(snapshot => {
return snapshot.data().user === this.state.userDetails.uid
})
this.setState({
filterGigs: filteredGigs
})
})
) //end of set state
} else {
this.setState({
userDetails:null
})
console.log('no user signed in')
}
})
}
componentDidMount() {
this.authListener();
}
Another function of this component is to capture data from the user and then post it to firebase, after which it redirects to another component.
handleSubmit(e) {
let user = auth().currentUser.uid;
const gigData = {
name: this.state.name,
venue: this.state.venue,
time: this.state.time,
date: this.state.date,
genre: this.state.genre,
tickets: this.state.tickets,
price: this.state.price,
venueWebsite: this.state.venueWebsite,
bandWebsite: this.state.bandWebsite,
user: user
};
auth()
.currentUser.getIdToken()
.then(function (token) {
axios(
"https://us-central1-gig-fort.cloudfunctions.net/api/createGigListing",
{
method: "POST",
headers: {
"content-type": "application/json",
Authorization: "Bearer " + token,
},
data: gigData,
}
);
})
.then((res) => {
this.props.history.push("/Homepage");
})
.catch((err) => {
console.error(err);
});
}
So here's the issue. Sometimes this component works as it should, and the data submit and redirect work as intended. Occasionally though, I'll hit submit but trigger the message TypeError: Cannot read property 'uid' of null . Interestingly, the post request is still made.
I've been logged in both when it succeeds and fails, and I can only assume that this.state.userDetails.uid evaluating to null means that auth state has expired, or that the component is rendering before userDetails can be assigned a value?
The issue I have is that I can't tell if this is an async problem, or an auth state persistence problem, and I also can't figure why it's a sporadic failure.
This line of code might be causing you trouble:
let user = auth().currentUser.uid;
currentUser will be null if there is no user signed in at the time it was accessed (or it's not known for sure if that is the case). This is covered in the API documentation.
Ideally, you should never use currentUser, and instead rely on the state provided by onAuthStateChanged. I talk about this in detail in this blog post. If you do need to use currentUser, you should check it for null before referencing properties on it.
You should also know that getting an ID token is best done by using a listener as well. The call is onIdTokenChanged, and it works like the auth state listener.
Keep in mind also that setState is asynchronous and doesn't set the state immediately. It's possible that your Firestore query isn't getting the state it needs immediately.
I can't figure this out, if you can point me to right direction. On my NavMenu.js I have LogIn (two inputs and a submit button, on which I call handleSubmit()
In handleSubmit() I am checking for user login credentials which works great, but after I confirm login, i procede with doing next fetch for checking user roles (and returning promise) and visibility in application
Helper.js
function getAllRoles(formName, sessionVarName) {
var roleData = [];
var roleId = sessionStorage.getItem('UserLoggedRoleId');
fetch('api/User/GetUserRole/', {
'method': 'POST',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
'body':
JSON.stringify({
'roleId': roleId,
'formName': formName
})
}).then(roleResponse => roleResponse.json())
.then(data => {
sessionStorage.setItem(sessionVarName, JSON.stringify(data));
roleData = data;
});
var result = Promise.resolve(roleData)
return result;
}
function checkRoleVisibility(userRoles, role) {} <-- return true or false
export {
getAllRoles ,
checkRoleVisibility
};
In NavMenu.js
import { getAllRoles, checkRoleVisibility } from './common/Helper';
handleSubmit() {
fetch('api/User/UserAuthentification/',
....then(response => {
if (response.ok) {
this.setState(prevState => ({ userLogged: !prevState.userLogged }))
response.json().then(value => {
sessionStorage.setItem('UserLogged', true);
sessionStorage.setItem('UserLabelSession', this.state.userLabel);
sessionStorage.setItem('UserLoggedRoleId', value.userRole.roleID);
getAllRoles('NavMenu', 'UserLoggedRoles')
.then(roleData => {
this.setState({
loggedUserRoles: roleData,
loading: false
});
});
this.props.alert.success("Dobro dosli");
})
}
}
But here comes the problem, is that in render() itself, I have a control navBar which calls
checkRoleVisibility(this.state.loggedUserRoles, 'SeeTabRadniNalozi')
from helper, which send this.state.loggedUserRoles as parameter, which suppose to be filled in fetch from handleSubmit() but it's undefined, fetch finish, set loading on true, rendering completes and it doesn't show anything
I have this.state.loading and based on it I show control ...
let navMenu = this.state.loading ? <div className="loaderPosition">
<Loader type="Watch" color="#1082F3" height="200" width="200" />
</div> : !this.state.userLogged ? logInControl : navBar;
If I put a breakpoint on controler I can see my method is being called by getAllRoles function, but also I can see that application keep rendering on going and doesn't wait for promise to return to fill state of loggedUserRoles.
So question is, why rendering doesn't wait for my role fetch nor doesn't wait for promise to resolve before setting loading on true?
I checked this answer Wait for react-promise to resolve before render and I put this.setState({ loading: false }); on componentDidMount() but then loading div is shown full time
From what I see and understand is, that you're calling
this.setState(prevState => ({ userLogged: !prevState.userLogged }))
if the response is okay. This will lead in an asynchronous call from React to set the new State whenever it has time for it.
But you need the resource from
this.setState({
loggedUserRoles: roleData,
loading: false
});
whenever you want to set userLogged to true, right?
So it might be that React calls render before you set the loggedUserRoles but after userLogged is set to true. That would result in an error if I understand you correctly.
You could simply set the userLogged state with the others together like this:
this.setState({
userLogged: true,
loggedUserRoles: roleData,
loading: false
});
EDIT:
Moreover you want to change your Promise creation inside of getAllRoles. You're returning
var result = Promise.resolve(roleData)
return result;
This will directly lead to the empty array since this is the initial value of roleData. Therefore you're always returning an empty array from that method and don't bother about the fetched result.
You've multiple ways to solve this. I would prefer the clean and readable way of using async/await:
async getAllRoles(formName, sessionVarName){
var roleId = sessionStorage.getItem('UserLoggedRoleId');
const response = await fetch(...);
const data = response.json();
sessionStorage.setItem(sessionVarName, JSON.stringify(data));
return data;
Or if you want to stay with Promise:
function getAllRoles(formName, sessionVarName){
return new Promise((resolve, reject) => {
var roleId = sessionStorage.getItem('UserLoggedRoleId');
fetch(...)
.then(roleResponse => roleResponse.json())
.then(data => {
sessionStorage.setItem(sessionVarName, JSON.stringify(data));
resolve(data);
});
I'm using apollo-client and connecting to an express server using that's using express-jwt for authentication via the header. My current client side components look like this: App -> Login.
Here's what my client side initialization file looks like (index.js). I've omitted unnecessary code for brevity:
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token');
return {
headers: {
...headers,
Authorization: token ? `Bearer ${token}` : null
}
};
});
const client = new ApolloClient({
link: authLink.concat(new HttpLink({ uri: '/graphql' })),
cache: new InMemoryCache()
});
That's basically setting up Apollo middleware that checks for a token in localStorage and adds it to the header on each subsequent request so that my logged in user can be authenticated. Here's my top-level App component (which is using react-router v4 - irrelevant code ommited):
class App extends Component {
render() {
const { currentUser } = this.props.data;
console.log(currentUser);
return (
<Switch>
<Route
path="/login"
render={routeProps => <Login {...routeProps} {...this.props} />}
/>
</Switch>
);
}
}
export default graphql(currentUserQuery, {
options: { fetchPolicy: 'network-only' }
})(withRouter(App));
As you might guess, when my App component is created it calls the currentUserQuery to retrieve the current user from my graphql endpoint. Here's the relevant code for my Login component:
class Login extends Component {
handleSubmit = async e => {
e.preventDefault();
this.props
.mutate({
variables: { email: this.state.email, password: this.state.password }
})
.then(result => {
const { jwt } = result.data.login;
if (jwt) {
localStorage.setItem('token', jwt);
this.props.history.push('/');
return;
}
})
.catch(error => {
console.log(error.message);
});
};
}
export default graphql(userLoginMutation)(Login);
My problem is that once my userLoginMutation succeeds, I'd like to route back to my App component and expect to see my currentUser object there. Unfortunately the App component does not trigger a new graphql query to attempt to get the currentUser object when I route back to it. I also tried to use Apollo's refetchQueries method on the login mutation to try and refetch the current user, but that runs the query before the login promise is finished and the token is not yet in the middleware, so the request does nothing. It's worth noting that if I simply do a hard refresh on the page, I get access to my currentUser object because it is pulling the token from localStorage and placing it in the middleware properly.
Any help or advice would be appreciated.
We need a way to trigger the only refetch after localStorage is updated, but before you navigate away. Here's one way to do that: Wrap your Login component with another HOC for the currentUser query:
export default compose(
graphql(userLoginMutation),
graphql(currentUserQuery),
)(MyComponent)
Now your component's props will include both mutate and data, so we can do something like:
this.props.mutate({
variables: { email: this.state.email, password:this.state.password}
})
.then(result => {
const { jwt } = result.data.login;
if (jwt) {
localStorage.setItem('token', jwt);
return this.props.data.refetch()
}
// if there's no jwt, we still want to return a Promise although
// you could do Promise.reject() instead and trigger the catch
return Promise.resolve();
})
.then(() => this.props.history.push('/'))
.catch(error => {
console.log(error.message);
});