Using a client-side only, readable store `set`, to refresh API access token - sveltekit

I am experimenting with using a readable store as the auth token for a web app.
The set function would fetch an authentication token from an API, and then set a timeout for X seconds to refresh the token before it expires. This would then continuously run every X seconds to set the token.
// auth.ts
export const ssr = false;
type LoginResponse = {
access_token: string;
expires: string;
}
export const authToken: Readable<string> = readable("", (set) => {
const setAuth = async () => {
const auth: LoginResponse = await authenticate();
set(auth.access_token);
authTimer(auth);
};
const authTimer = async (auth: LoginResponse) => {
const timeRemaining = Math.round(((new Date(auth.expires).getTime() - Date.now())) * 0.9);
setTimeout(async () => {
if (auth.access_token === authToken) {
console.log("refreshing auth token");
setAuth();
}
}, timeRemaining);
}
setTimeout(setAuth, 0)
});
The main issue I'm having is that this set function is called from the backend, causing an exception to raise when the authenticate() call fails. It should only be called once the front-end is loaded, as there needs to be client side headers set for the request to succeed.
Is this a generally good approach or is this not what set is intended for?
Also I seem to be unable to make value comparisons with the value of the readable (e.g. auth.access_token === authToken will always return false).
Finally, how can I queue API calls to only execute when the authToken is not an empty string?

Related

Is there a way to update headers of Chime Messaging API of aws-sdk/clients/chimesdkmessaging in ReactJs?

I am using aws-sdk/clients/chimesdkmessaging to make requests to get/delete/send channel messages.
But the issue is that the temporary credentials are expired after 1 hour and I cannot use the services without renewing the credentials. I am able to renew them, but couldn't set them in the API Request header.
I tried to set the request header as :
request.httpRequest.headers["x-amz-security-token"] = NEW_TOKEN
But it didn't work. Also, I couldn't find anything on adding a middleware to these API's.
Here's my code
const ChimeMessaging = require("aws-sdk/clients/chimesdkmessaging");
const chimeMessaging = new ChimeMessaging();
async function sendChannelMessage(
channelArn,
messageContent,
persistence,
type,
member,
options = null
) {
const chimeBearerArn = createMemberArn(member.userId);
const params = {
ChimeBearer: chimeBearerArn,
ChannelArn: channelArn,
Content: messageContent,
Persistence: persistence, // Allowed types are PERSISTENT and NON_PERSISTENT
Type: type,
};
const request = (await chimeMessagingClient()).sendChannelMessage(params)
request.on("complete", function (err, data) {
request.httpRequest.headers["x-amz-security-token"] = NEW_TOKEN
});
const response = await request.promise();
const sentMessage = {
response: response,
CreatedTimestamp: new Date(),
Sender: { Arn: createMemberArn(member.userId), Name: member.username },
};
return sentMessage;
}

Is there a way to update headers of Chime Messaging API of aws-sdk/clients/chimesdkmessaging in ReactJs?

I am using aws-sdk/clients/chimesdkmessaging to make requests to get/delete/send channel messages.
But the issue is that the temporary credentials are expired after 1 hour and I cannot use the services without renewing the credentials. I am able to renew them, but couldn't set them in the API Request header.
I tried to set the request header as :
request.httpRequest.headers["x-amz-security-token"] = NEW_TOKEN
But it didn't work. Also, I couldn't find anything on adding a middleware to these API's.
Here's my code
const ChimeMessaging = require("aws-sdk/clients/chimesdkmessaging");
const chimeMessaging = new ChimeMessaging();
async function sendChannelMessage(
channelArn,
messageContent,
persistence,
type,
member,
options = null
) {
const chimeBearerArn = createMemberArn(member.userId);
const params = {
ChimeBearer: chimeBearerArn,
ChannelArn: channelArn,
Content: messageContent,
Persistence: persistence, // Allowed types are PERSISTENT and NON_PERSISTENT
Type: type,
};
const request = (await chimeMessagingClient()).sendChannelMessage(params)
request.on("complete", function (err, data) {
request.httpRequest.headers["x-amz-security-token"] = NEW_TOKEN
});
const response = await request.promise();
const sentMessage = {
response: response,
CreatedTimestamp: new Date(),
Sender: { Arn: createMemberArn(member.userId), Name: member.username },
};
return sentMessage;
}

How to pass MSAL access token to react elastic search App connector

AM trying to get a access token for MSAL to pass to our backend API(that eventually calls APP Search). As the access token is from msal promise i cannot initialize the token somewhere globally and access with in other function. is there a way to wait until the promise completed ?
Here is some code:
export default function App() {
const isAuthenticated = useIsAuthenticated();
const {instance, accounts, inProgress} = useMsal();
const { hostIdentifier, searchKey, endpointBase, engineName } = getConfig();
if (!isAuthenticated && inProgress === InteractionStatus.None) {
instance.loginRedirect(loginRequest).catch(e => {
console.error(e);
});
}
const connector= null;
if( accounts[0]){
const request = {
...{scopes:['api://.....']},
account: accounts[0]
};
console.log('Before access token');
getAccessToken(request);
//console.log(`After access token ${accessToken}`);
}
let config;
async function getAccessToken(request){
const accessTokenPromise = await instance.acquireTokenSilent(request);
const searchKey = accessTokenPromise.accessToken;
const connector = new AppSearchAPIConnector({
searchKey,
engineName,
hostIdentifier,
endpointBase,
});
console.log('After COnnector'+connector);
config = {
searchQuery: {
facets: buildFacetConfigFromConfig(),
...buildSearchOptionsFromConfig(),
},
autocompleteQuery: buildAutocompleteQueryConfig(),
apiConnector: connector,
alwaysSearchOnInitialLoad: false,
};
}
return (
<PageLayout>
<AuthenticatedTemplate>
<SearchProvider config={config}>
<WithSearch
mapContextToProps={({ wasSearched }) => ({ wasSearched })}
........
with this code , am getting below error:
Events.js:16 Uncaught No onSearch handler provided and no Connector provided. You must configure one or the other.
any idea , on how to pass access token to App connector ?
Yes, there is a way to wait until the promise is complete. You can use await keyword before the promise and when you use the await keyword you must add the async keyword in function declaration because adding async keyword is required when you use await keyword before the promise code.
When you complete the promise and use the await keyword then you can store the access token in some variable and lastly send it to the app connector.

Reactjs Cannot Update During an Existing State Transition

I'm creating a global function that checks whether the jwt token is expired or not.
I call this function if I'm fetching data from the api to confirm the user but I'm getting the error that I cannot update during an existing state transition and I don't have a clue what it means.
I also notice the the if(Date.now() >= expiredTime) was the one whose causing the problem
const AuthConfig = () => {
const history = useHistory();
let token = JSON.parse(localStorage.getItem("user"))["token"];
if (token) {
let { exp } = jwt_decode(token);
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime) {
localStorage.removeItem("user");
history.push("/login");
} else {
return {
headers: {
Authorization: `Bearer ${token}`,
},
};
}
}
};
I'm not sure if its correct but I call the function like this, since if jwt token is expired it redirect to the login page.
const config = AuthConfig()
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
I updated this peace of code and I could login to the application but when the jwt expires and it redirect to login using history.push I till get the same error. I tried using Redirect but its a little slow and I could still navigate in privateroutes before redirecting me to login
// old
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime)
// change
if (exp < Date.now() / 1000)
i would start from the beginning telling you that if this is a project that is going to production you always must put the auth token check in the backend especially if we talk about jwt authentication.
Otherwise if you have the strict necessity to put it in the React component i would suggest you to handle this with Promises doing something like this:
const config = Promise.all(AuthConfig()).then(()=> productData());
I would even consider to change the productData function to check if the data variable is not null before saving the state that is the reason why the compiler is giving you that error.
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
data && setProduct(data);
};
Finally consider putting this in the backend. Open another question if you need help on the backend too, i'll be glad to help you.
Have a nice day!
I'm still not sure how your code is used within a component context.
Currently your API and setProduct are called regardless whether AuthConfig() returns any value. During this time, you are also calling history.push(), which may be the reason why you encountered the error.
I can recommend you to check config for value before you try to call the API.
const config = AuthConfig()
if (config) {
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
}
I'm assuming that AuthConfig is a hook, since it contains a hook. And that it's consumed in a React component.
Raise the responsibility of redirecting to the consumer and try to express your logic as effects of their dependencies.
const useAuthConfig = ({ onExpire }) => {
let token = JSON.parse(localStorage.getItem("user"))["token"];
const [isExpired, setIsExpired] = useState(!token);
// Only callback once, when the expired flag turns on
useEffect(() => {
if (isExpired) onExpire();
}, [isExpired]);
// Check the token every render
// This doesn't really make sense cause the expired state
// will only update when the parent happens to update (which
// is arbitrary) but w/e
if (token) {
let { exp } = jwt_decode(token);
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime) {
setIsExpired(true);
return null;
}
}
// Don't make a new reference to this object every time
const header = useMemo(() => !isExpired
? ({
headers: {
Authorization: `Bearer ${token}`,
},
})
: null, [isExpired, token]);
return header;
};
const Parent = () => {
const history = useHistory();
// Let the caller decide what to do on expiry,
// and let useAuthConfig just worry about the auth config
const config = useAuthConfig({
onExpire: () => {
localStorage.removeItem("user");
history.push("/login");
}
});
const productData = async (config) => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
useEffect(() => {
if (config) {
productData(config);
}
}, [config]);
};

How to pause and restart API calls in react-redux app while access token is being refreshed?

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.

Resources