Async Calls Catch 401 Errors Javascript - javascript

I have written a small application using React Native and TS. In my application I use authorization through access_tokens with lifetime for one hour. Every time the token expires, I launch refreshToken function where I send access_token and refresh_token that were saved previously in local storage and send it through POST method.
basically something like this:
if (token_from_storage !== is_expired) {
//assuming token is indeed in storage
return access_token;
} else {
//this Function calls an api with refresh token
const { tokens }: { tokens: ITokens } = await refreshToken(refresh_token);
saveTokens(tokens); //here we save new access_token and refresh_token
}
after refreshToken has finished the new tokens are saved locally (and were updated on DB level) and must be used if you want refresh tokens again.
For example, we launch an application with an expired token. The methods on the start will be getUserInfo() and getApplicationInfo() that get some important data. They are launched through redux-saga or something similar (async).
//first app screen
useEffect(() => {
dispatch(ACTION_GET_USER_INFO);
dispatch(ACTION_GET_APPLICATION_INFO);
},[])
first method would launch refresh_token about the same time as the second one. The tokens will be rebuilt for the first method and update on the database level. By the time second method calls refreshToken (he was launched at the same time; he also thinks that the token is dead), the refresh_token will be changed on DB level and would not answer 200 on the refreshToken() call.
What should be done to achieve proper refreshing?

If I am understanding your issue correctly it sounds like you are calling two async function that both refresh tokens if the current token has expired.
This indicates you are experiencing a race condition in your useEffect statement, and because the requests handle refreshing their token independently at least one of the request will have an invalid token.
Personally, I would not being handling tokens in the client at all especially with local storage.
If you must however, you can fix the race condition by adding a service layer that handles requests, and tokens singularly. Your dispatch functions will pass through the service layer which can determine the need to refresh a token and if a token is being refreshed already in a synchronous manner before attempting to send the requests.

Related

How to get the refreshed token in a Linnworks embedded app?

I thought I'd try posting my problem here given the non-existent support that Linnworks provide.
I've created a private embedded app within Linnworks that displays orders in a spreadsheet format. The app is built with Vue.js and uses axios to pull the data from Linnworks APIs. Everything is working as it should be here, except that I'm only returning 100 orders at a time to keep things quick.
I've added a "load more orders" button which appends an additional 100 orders to the end of the sheet, but after a period of inactivity, this causes a "401 unauthorised error" because the token has expired.
Because it's an embedded app, Linnworks store the token within the src of the iframe when the app is initialised, so when it has expired, it doesn't get physically refreshed by the system.
<iframe src="https://example.com/sheet.html?token=9b11e8ff-4791-aca5-b58d-f6da84e996a6"></iframe>
Is there a way of getting the refreshed token without reloading the entire app?
Tokens have a default TTL of 30 minutes, just poll the API with a simple method like /api/Main/Ping to keep your token/session active
I got the following response from Linnworks, which fixed the problem:
After further investigation, this appears to be due to the the pinging of the AuthorizeByApplication call. To help reduce the risk of being returned a 401 Unauthorised "Token has expired. please re-verify the user", it is recommended that when the application is opened, call AuthorizeByApplication and save the response.
Once the session has been created, AuthorizeByApplication should not have to be called again. The token returned in this session has a TTL of 30 minutes. If this token is used in a further call, the TTL of the token is reset back to 30 minutes. Therefore, as suggested in the response of your Stackoverflow question, briefly calling "api/Main/Ping" will reset the 30 minute TTL with little impact on your applications performance.
To Prevent Applications From Using Expired Tokens:
Upon launching application, call AuthorizeByApplication and save session response.
To keep the session from ending, call "api/Main/Ping" using the saved session token to reset the TTL of the saved session.
For any calls made by the application, use the original saved session token.

How do identify a new user using auth0-js WebAuth?

I'm trying to keep things simple and using auth0-js WebAuth to authenticate users. However, as there is a redirect involved, I'm not in control of the sign-up functionality at that point.
My specific use-case is to call a createUser graphql mutation using Graphcool to create a user in my database, but I only want to do this if the user is a new user, obviously.
MY QUESTION: Using auth0-js, is it possible to identify if a user is a new or existing user in my client application after the redirect from Auth0 back to my client application (assuming authentication is successful)?
There are two general approaches here, and both require you to persist the Auth0 token in local storage after receiving it. You can use a middleware for your GraphQL client that checks local storage for a token for every request and includes it as the Authorization: Bearer <token> header if present.
Let's now look at the two approaches.
Always try to create the user
Trying to create the user using the createUser mutation as soon as receiving the token is a fairly simple approach. This is how the mutation looks like:
mutation signUp($token: String!) {
createUser(authProvider: {
auth0: {
idToken: $token
}
}) {
id
}
}
Now, if the token is valid and matches the configuration of the Auth0 integration in Graphcool, there are two possible scenarios. Note, a token corresponds to a user if the auth0UserId it embeds matches.
there is already a registered user corresponding to the token. In this case, a GraphQL error Code 3023: CannotSignUpUserWithCredentialsExist will be returned (compare with the error reference documentation). In your application you can catch this error to proceed normally.
there is no registered user yet corresponding to the token. The createUser mutation will return id and all is good!
Check if the user is already signed in
If you have a more elaborate sign up flow, you might want to redirect your users to a sign up form, which is not really possible with the first approach. Instead, we can check if the currently used token corresponds to a registered user before proceeding. You can use the user query to do that:
query {
user {
id
}
}
Again, there are the same two scenarios as above:
there is already a registered user corresponding to the token. In this case, a the query returns a user object with the corresponding user id. So we can proceed the flow in the app normally.
there is no registered user yet corresponding to the token. The date returned from the user query will be null, so we need to call the createUser mutation, or switch to a sign up form or similar.
Compare this to this FAQ article in the Graphcool documentation.
In that case, the simplest solution will be to use auth0 rule and use context.stats.loginsCount field to detect the user is new or not.
https://auth0.com/docs/rules/references/context-object
You can add context.stats.loginsCount field value as a custom claim in the token using rule. Therefore, in your application, you can make a HTTP request to /userinfo endpoint to get the token data.
function (user, context, callback) {
const count=context.stats.loginsCount;
context.idToken["http://mynamespace/logincounts"] = count;
callback(null, user, context);
}
https://auth0.com/docs/api-auth/tutorials/adoption/scope-custom-claims
If the counts are equal to 1, create the user in your DB.

Retrieve Cookie, store, and use within Node

I'm using npm package 'request' to make API calls. Upon initial login, I should receive a cookie back, I need to store that cookie indefinitely to make subsequent calls.
I'm doing this in Python with requests like so:
#set up the session
s = requests.session()
#logs in and stores the cookie in session to be used in future calls
request = s.post(url, data)
How do I accomplish this in node? I'm not tied to anything right now, the request package seems easy to work with, except I'm having issues getting known username and passwords to work, that said, I'm sure that's mostly my inexperience with JS/node.js.
This is all backend code, no browsers involved.
I need to essentially run a logon function, store the returned encrypted cookie and use for all subsequent calls against that API. These calls can have any number of parameters so I'm not sure a callback in the logon function would be a good answer, but am toying with that, although that would defeat the purpose of 'logon once, get encrypted cookie, make calls'.
Any advice, direction appreciated on this, but really in need of a way to get the cookie data retrieved/stored for future use.
The request package can retain cookies by setting jar: true -
let request = request.defaults({jar: true})
request('http://www.google.com', function () {
request('http://images.google.com')
})
The above is copied near-verbatim from the request documentation: https://github.com/request/request/blob/master/README.md#requestoptions-callback

Saving OAuth2 access tokens for sessionless authentication

I set up a sessionless app that uses OAuth2 password grant authentication. When a user logs into my app with a username and password I save the access token in sessionStorage which is valid for 30 minutes. I also save a refresh token in sessionStorage in case I need to extend the session longer than 30 minutes. The refresh token is valid for 30 days.
If the 'remember me' checkbox is selected on login I save the access and refresh tokens in localStorage so they will persist as long as the refresh token is valid.
Both of these seem to work fine except for a couple of issues:
If the browser is left open and the user doesn't log out the session could potentially last for 30 days.
sessionsStorage doesn't persist between windows/tabs so if the user opens a new window they need to log in again. This is not an issue when the 'remember me' checkbox is selected since localStorage does persist between windows.
I think using refresh tokens is not safe for JavaScript applications - you need to access the /token endpoint and authenticate using the application's secret. But the secret gets public in such applications.
I would prefer the OAuth2 implicit flow and getting new token from the /auth endpoint with prompt=none parameter (from OpenID Connect). But with the implicit flow, you would either need to get a longer living ID token (and ask for an access token with the ID token later) or to implement the "remember me" at the OAuth2 (better option - can be used by any application). That would also solve the problem #2 with passing tokens between tabs.
By the "session" you mean using the refresh token to generate access tokens for 30 days? If that's a problem, you can implement some activity detector which would log the user out if there is no activity for e.g. 30 minutes.
It's possible to use the localStorage as a kind of message passing service, so you can keep the tokens in the sessionStorage, but a new tab can use the localStorage to request the token from existing tabs. For more info see http://www.codediesel.com/javascript/sharing-messages-and-data-across-windows-using-localstorage/
Code example from the linked article:
function eventListener(e) {
if (e.key == 'storage-event') {
output.innerHTML = e.newValue;
}
}
function triggerEvent() {
localStorage.setItem('storage-event', this.value);
}
window.addEventListener("storage", eventListener, true);
data.addEventListener("keyup", triggerEvent, true);
The workflow would be like this:
New tab is opened and writes an arbitrary value to the localStorage with a key indicating that it needs a token. The key can be "newTabOpened". The new tab starts listening to changes of another key "oauth2token".
The existing tab listens to the changes of the "newTabOpened" key and as a reaction, it writes its token value under the "oauth2token" key.
The new tab reads the token and removes it from the localStorage.

Making server to server request to Google Calendar API

I am building a SPA using vue.js which has a PHP backend server (slim framework 3). These are two separate projects, leave on two different servers and the backend has no front end at all.
SPA (vue.js) makes requests to backend via ajax.
Now I want to implement Google Calendar API to create a calendar and events every time user creates a todo item. To do that I need server to server access to Google Calendar API (I might need to make changes to the event on GCAL even if user is not logged in).
What I am trying to understand, how can I get the access token (and refresh token) using Google JS library using vue.js and save this in the db so that my backend can use it to make offline requests to GCAL Api.
When I use the Oauth v.2 using the JS library, all I get is the access_token which cannot be using for server to server communications.
[UPDATE]
Ok, a little bit more information. I am following the guides from Google and my front end looks like this at the moment
jsbin
So I can successfully authorise user and access their calendar using the javascript sdk. But the token Javascript SDK returns is something like this
{
_aa: "1"
access_token: "xxxxxxx"
client_id: "yyyyyyyyyy"
cookie_policy: undefined
expires_at: "1456400189"
expires_in: "3600"
g_user_cookie_policy: undefined
issued_at: "1456396589"
response_type: "token"
scope: "https://www.googleapis.com/auth/calendar"
state: ""
status: Object
google_logged_in: false
method: "AUTO"
signed_in: true
token_type: "Bearer"
}
I send this token to my backend server and try to make a request to GCAL api as follows
$token = $request->getParam('token');
$client = new Google_Client();
$client->setApplicationName('Web');
$client->setScopes([Google_Service_Calendar::CALENDAR]);
$client->setAuthConfigFile(ROOT_DIR . '/client_secret.json');
$client->setAccessType('offline');
$client->setAccessToken(json_encode($token));
$service = new Google_Service_Calendar($client);
$calendarId = 'primary';
$optParams = array(
'maxResults' => 10,
'orderBy' => 'startTime',
'singleEvents' => TRUE,
'timeMin' => date('c'),
);
$results = $service->events->listEvents($calendarId, $optParams);
And it returns error saying the token is expired. I checked the Google Code and found out the reason it returns this error is because of these lines
public function isAccessTokenExpired()
{
if (!$this->token || !isset($this->token['created'])) {
return true;
}
// If the token is set to expire in the next 30 seconds.
$expired = ($this->token['created']
+ ($this->token['expires_in'] - 30)) < time();
return $expired;
}
As you can see the token that comes from the front end doesn't have created field as well as no refresh_token field.
Thanks for updating the question! I am thinking the issue is that using the client-side flow does not allow you to get a refresh token. From the docs:
OAuth 2.0 client-side flow (AKA Implicit flow) is used to obtain
access tokens (it does not support the issuance of refresh tokens) and
is optimized for public clients known to operate a particular
redirection URI. These clients are typically implemented in a browser
using a scripting language such as JavaScript.
The authorization server MUST NOT issue a refresh token.
see for more: How to get refresh token while using Google API JS Client
You'd need to use the server-auth flow to get a token you can refresh and use long-term. Here's a quickstart guide for PHP.
One other thing to consider is that you will only receive a refresh_token the first time someone authorizes your app. After that, auth attempts will only return an access token. So if you lose the refresh token, you will need to either disable the authorization from your google account, or use the "force re-auth" option in the API.

Categories