How to handle API calls when session expires? - javascript

I'm accessing an API where you authorize first, and you get an access token back, which you use in successive calls. At some point (hours later), the access token expires. So I'm caching the token on the server so any users using the web app will initiate API calls using that token.
But when it expires, I do a recursive call after updating the access token. So, for example (in pseudo-JS):
function getDetails (id) {
data = HTTP.get(url, {params: ...});
if (!data.success) {
updateToken(function () {
return getDetails(id);
});
} else { /*.. we're good*/ }
}
There would also be a recursion depth check in there too. If there's a better way to do this, I'd love to hear it. Basically:
Call API
(failure)
Update token
Call API again

That is exactly how I set it up. However, if you have access to promises (e.g., bluebird or babel.js), it becomes even nicer syntactically. Here's your code rewritten with promises and es6:
function getDetails (id) {
// the return is optional; return the promise if you want to chain to `getDetails`
return HTTP.get(URL, id, params)
.catch((err) => {
if (err.VALIDATION_ERROR) {
return updateToken().then(() => HTTP.get(URL, id, params))
} else {
throw err
}
}).then((yourRequest) => {
// data here
}).catch((err) => {
// handle fatal error
})
}

The best way would be to keep a timer, since you know how long the session lasts for. That was you can preemptively ask for a new token before your current token/session expires. Using this method also ensures that you never have an interruption of service (unless your request for a new token fails of course). Other than that, you seem to have the rest.
Of course, you will still need to implement the code you have shown us to deal with unexpected session expiration (maybe the API owners decide to invalidate all sessions because of a data breach, etc).

Related

adminuserglobalsignout not returning any data or not awaiting promise

When awaiting on a promise as a result of adminuserglobalsignout the promise seems to return but the data contains nothing.
The next call after signout is to authenticate the user. correct accessToken is returned but it's already revoked which makes me think the promise is not awaiting correctly and the new credentials are getting signed out by the previous call which is still running.
We are using globalsignout to prevent users from having multiple sessions so the workflow is along lines of
authenticate -> success -> signout (to kill any other sessions) -> authenticate -> success -> return token
I have updated my lambda package to include the latest SDK version 2.469.0 and no improvement.
Sometimes the timing must be OK as the returned credentials are still valid and the token can be used.
In BOTH cases there appears to be zero data returned from the AWS call
section of lambda code that calls the signout method in the User library
try {
signOutResult = await User.globalSignOut(userId, process.env.COGNITO_POOL);
} catch (err) {
log.error("AWS Global Signout Error: " + JSON.stringify(err));
responseBody = Helper.buildCORSResponse(502, JSON.stringify({ message: err }));
return callback(null, responseBody);
}
globalsignout code in User library:
return new Promise((resolve, reject) => {
log.info(`globalSignOut: Signing ${Username} out from all devices in pool ${UserPoolId}`);
const signOutRequest = new AWS.CognitoIdentityServiceProvider({ apiVersion: "2016-04-18" }).adminUserGlobalSignOut({ Username, UserPoolId });
const signOutPromise = signOutRequest.promise();
signOutPromise.
then((data) => {
log.debug("globalSignOut: Cognito SignOut Success: " + JSON.stringify(data));
resolve(data);
}).catch((err) => {
log.error("globalSignOut: Cognito SignOut Error: " + err);
reject(err);
});
});
}
In every call, we reach the resolve with no issue and then we carry on to authenticate the user again.
log.debug("globalSignOut: Cognito SignOut Success: " + JSON.stringify(data));
resolve(data);
Does anyone see any issues that could be causing this? I've tried a few ways to specify the promise and using the same format that works fine for other services and waits for the promise of the result before code execution continues.
All advice greatly appreciated
Update from AWS Support on this behavior in case anyone else finds this issue. I can confirm that adding a small delay before re-authenticating the user after global signout works fine.
Thank you for getting back to us.
In order to troubleshoot this issue, I tried to replicate it on my end by testing the below mentioned flow (as provided by you in the ) :
Authenticate user —> Global Sign Out —> Authenticate again —-> Check the validity of the new token
I wrote a python code to implement the above flow. In the flow, after calling the globalSignOut method, I authenticated the user again and checked the validity of the token by making getUser API call. But, the getUser API call returned the following response : “An error occurred (NotAuthorizedException) when calling the GetUser operation: Access Token has been revoked”
Now, I added sleep function after the GlobalSignOut for 1 second and the flow worked correctly. I did a few tests with the sleep time and noticed that if we add a sleep period of 0.6 seconds or greater, the API works correctly. So, it seems that the GlobalSignOut API call returns the response immediately but, the global logging out process (revoking of tokens) still runs in the backend for approximately 0.6 seconds.
For this, I reached out to the Cognito development team to confirm this behavior of GlobalSignOut API call. The team has confirmed that this is an expected behavior of GlobalSignOut API call. When GlobalSignOut is called all the tokens that were issued before that time is considered invalid. If the gap between signout and authentication is very small ( from my tests, this is approximately 0.6 seconds ), the token issue after authentication can be treated to be issued before signout call and, for better security, is considered invalid.
I hope that the above information helps. If there is anything else I can do to help, please let me know. I will be more than happy to assist you.
Have a great day ahead.
Best regards,
Amazon Web Services

Invoking Rest API from Lambda (JS; Web Console)

_
MY CHALLENGE:
I would like to access a third party Rest API from within my Lambda function. (e.g."http://www.mocky.io/v2/5c62a4523000004a00019907").
This will provide back a JSON file which I will then use for data extraction
_
MY CURRENT CODE:
var http = require('http');
exports.handler = function(event, context, callback) {
console.log('start request to Mocky');
http.get('http://www.mocky.io/v2/5c62a4523000004a00019907', function(res) {
console.log(res);
})
.on('error', function(e) {
console.log("Got error: " + e.message);
});
};
This does not throw an error but also does not seem to provide back the JSON
_
MY OPEN QUESTIONS:
1) How can I extract the JSON so that I can work on it
2) I will probably need to also send through an Authentification in the request header (Bearer) in the future. Will this also be possible with this method?
The problem is likely that your lambda function is exiting before logging the response.
We use Authorization headers all the time to call our lambdas. The issue of if you can use one to call the third party API is up to them, not you, so check the documentation.
Since your HTTP call is executed asynchronously, the execution of the lambda continues while that call is being resolved. Since there are no more commands in the lambda, it exits before your response returns and can be logged.
EDIT: the http.get module is difficult to use cleanly with async/await. I usually use superagent, axios, or request for that reason, or even node-fetch. I'll use request in my answer. If you must use the native module, then see EG this answer. Otherwise, npm install request request-promise and use my answer below.
The scheme that many people use these days for this kind of call uses async/await, for example (Requires Node 8+):
var request = require('request-promise')
exports.handler = async function(event, context, callback) {
console.log('start request to Mocky');
try {
const res = await request.get('http://www.mocky.io/v2/5c62a4523000004a00019907')
console.log(res)
callback(null, { statusCode: 200, body: JSON.stringify(res) })
}
catch(err) {
console.error(err.message)
callback('Got error ' + err.message)
}
};
The async/await version is much easier to follow IMO.
Everything inside an async function that is marked with await with be resolved before the execution continues. There are lots of articles about this around, try this one.
There are a lot of guys having an equal problem already solved... Look at that
or that

Cannot construct a Request with a Request whose mode is 'navigate' and a non-empty RequestInit

Consider this sample index.html file.
<!DOCTYPE html>
<html><head><title>test page</title>
<script>navigator.serviceWorker.register('sw.js');</script>
</head>
<body>
<p>test page</p>
</body>
</html>
Using this Service Worker, designed to load from the cache, then fallback to the network if necessary.
cacheFirst = (request) => {
var mycache;
return caches.open('mycache')
.then(cache => {
mycache = cache;
cache.match(request);
})
.then(match => match || fetch(request, {credentials: 'include'}))
.then(response => {
mycache.put(request, response.clone());
return response;
})
}
addEventListener('fetch', event => event.respondWith(cacheFirst(event.request)));
This fails badly on Chrome 62. Refreshing the HTML fails to load in the browser at all, with a "This site can't be reached" error; I have to shift refresh to get out of this broken state. In the console, it says:
Uncaught (in promise) TypeError: Failed to execute 'fetch' on 'ServiceWorkerGlobalScope': Cannot construct a Request with a Request whose mode is 'navigate' and a non-empty RequestInit.
"construct a Request"?! I'm not constructing a request. I'm using the event's request, unmodified. What am I doing wrong here?
Based on further research, it turns out that I am constructing a Request when I fetch(request, {credentials: 'include'})!
Whenever you pass an options object to fetch, that object is the RequestInit, and it creates a new Request object when you do that. And, uh, apparently you can't ask fetch() to create a new Request in navigate mode and a non-empty RequestInit for some reason.
In my case, the event's navigation Request already allowed credentials, so the fix is to convert fetch(request, {credentials: 'include'}) into fetch(request).
I was fooled into thinking I needed {credentials: 'include'} due to this Google documentation article.
When you use fetch, by default, requests won't contain credentials such as cookies. If you want credentials, instead call:
fetch(url, {
credentials: 'include'
})
That's only true if you pass fetch a URL, as they do in the code sample. If you have a Request object on hand, as we normally do in a Service Worker, the Request knows whether it wants to use credentials or not, so fetch(request) will use credentials normally.
https://developers.google.com/web/ilt/pwa/caching-files-with-service-worker
var networkDataReceived = false;
// fetch fresh data
var networkUpdate = fetch('/data.json').then(function(response) {
return response.json();
}).then(function(data) {
networkDataReceived = true;
updatePage(data);
});
// fetch cached data
caches.match('mycache').then(function(response) {
if (!response) throw Error("No data");
return response.json();
}).then(function(data) {
// don't overwrite newer network data
if (!networkDataReceived) {
updatePage(data);
}
}).catch(function() {
// we didn't get cached data, the network is our last hope:
return networkUpdate;
}).catch(showErrorMessage).then(console.log('error');
Best example of what you are trying to do, though you have to update your code accordingly. The web example is taken from under Cache then network.
for the service worker:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open('mycache').then(function(cache) {
return fetch(event.request).then(function(response) {
cache.put(event.request, response.clone());
return response;
});
})
);
});
Problem
I came across this problem when trying to override fetch for all kinds of different assets. navigate mode was set for the initial Request that gets the index.html (or other html) file; and I wanted the same caching rules applied to it as I wanted to several other static assets.
Here are the two things I wanted to be able to accomplish:
When fetching static assets, I want to sometimes be able to override the url, meaning I want something like: fetch(new Request(newUrl))
At the same time, I want them to be fetched just as the sender intended; meaning I want to set second argument of fetch (i.e. the RequestInit object mentioned in the error message) to the originalRequest itself, like so: fetch(new Request(newUrl), originalRequest)
However the second part is not possible for requests in navigate mode (i.e. the initial html file); at the same time it is not needed, as explained by others, since it will already keep it's cookies, credentials etc.
Solution
Here is my work-around: a versatile fetch that...
can override the URL
can override RequestInit config object
works with both, navigate as well as any other requests
function fetchOverride(originalRequest, newUrl) {
const fetchArgs = [new Request(newUrl)];
if (request.mode !== 'navigate') {
// customize the request only if NOT in navigate mode
// (since in "navigate" that is not allowed)
fetchArgs.push(request);
}
return fetch(...fetchArgs);
}
In my case I was contructing a request from a serialized form in a service worker (to handle failed POSTs). In the original request it had the mode attribute set, which is readonly, so before one reconstructs the request, delete the mode attribute:
delete serializedRequest["mode"];
request = new Request(serializedRequest.url, serializedRequest);

How should AngularJS handle 403 error in $http.post due to outdated XSRF token?

An AngularJS version 1.4.8 app is getting an unhandled 403 error when its login form sends data to a backend REST authentication service after the user's browser has been left open for many (16 in this case) hours. Upon deeper analysis, the root cause is that the client AngularJS app has outdated cookies for XSRF-TOKEN and JSESSIONID, which causes the backend Spring Security to reject the request to the public /login1 service because Spring thinks the request is cross site request forgery.
The problem can be resolved manually if the user closes all browser windows and then re-opens a new browser window before making the request again. But this is not an acceptable user experience. I have read the AngularJS documentation at this link, and I see that I can add an errorCallback function, but how specifically should i re-write the function to handle the 403 error?
Here is the original this.logForm() method in the authorization service, which you can see does not handle 403 errors:
this.logForm = function(isValid) {
if (isValid) {
var usercredentials = {type:"resultmessage", name: this.credentials.username, encpwd: this.credentials.password };
$http.post('/login1', usercredentials)
.then(
function(response, $cookies) {
if(response.data.content=='login1success'){// do some stuff
} else {// do other stuff
}
}
);
}
};
Here is my very rough attempt at a revised version of the this.logForm() method attempting to handle a 403 error following the example in the AngularJS documentation:
this.logForm = function(isValid) {
if (isValid) {
var usercredentials = {type:"resultmessage", name: this.credentials.username, encpwd: this.credentials.password };
$http({ method: 'POST', url: '/login1', usercredentials })
.then(
function successCallback(response, $cookies) {
// this callback will be called asynchronously when the response is available
if(response.data.content=='login1success'){// do some stuff
} else {// do other stuff
}
},
function errorCallback(response, status) {// is status a valid parameter to place here to get the error code?
// called asynchronously if an error occurs or server returns response with an error status.
if(status == 403){
this.clearCookies();
// try to call this POST method again, but how? And how avoid infinite loop?
}
}
);
}
};
What specific changes need to be made to the code above to handle the 403 error due to server-perceived XSRF-TOKEN and JSESSIONID issues? And how can the post be called a second time after deleting the cookies without leading to an infinite loop in the case where deleting the cookies does not resolve the 403 error?
I am also looking into global approaches to error handling, but there is a combination of public and secure backend REST services, which would need to be handled separately, leading to complexity. This login form is the first point of user entry, and I want to handle it separately before looking at global approaches which would retain a separate handling of the login form using methods developed in reply to this OP.
You could restructure your http calls to auto retry, and use promises in your controllers (or whatever)
var httpPostRetry = function(url, usercredentials) {
var promise = new Promise(function(resolve, reject) {
var retries = 0;
var postRetry = function(url, usercredentials) {
if (retries < 3) {
$http({ method: 'POST', url: '/login1', usercredentials })
.then(function(result) {
resolve(result);
}).catch(function(result) {
retries ++;
postRetry(url, usercredentials);
});
} else {
reject(result);
}
};
}.bind(this));
return promise;
}
and then you would call
httpPostRetry(bla, bla).then(function(result) {
// one of the 3 tries must of succeeded
}).catch(function(result) {
// tried 3 times and failed each time
});
To handle specific http errors you can broadcast that specific error and handle that case in a specific controller. Or use a service to encapsulate the status and have some other part of your code handle the UI flow for that error.
$rootScope.$broadcast('unauthorized http error', { somedata: {} });
Does this help?
Have a look at the angular-http-auth module and how things are done there. I think one key element you would want to use is a http interceptor.
For purposes of global error handling, authentication, or any kind of
synchronous or asynchronous pre-processing of request or
postprocessing of responses, it is desirable to be able to intercept
requests before they are handed to the server and responses before
they are handed over to the application code that initiated these
requests. The interceptors leverage the promise APIs to fulfill this
need for both synchronous and asynchronous pre-processing.
After playing around with interceptors you can look at the angular-http-auth http buffer and the way they handle rejected requests there. If their interceptor receives a responseError, they add the config object - which basically stores all information about your request - to a buffer, and then any time they want they can manipulate elements in that buffer. You could easily adept their code to manipulate the config's xsrfHeaderName, xsrfCookieName, or parameters on your behalf when you receive a 403.
I hope that helps a little.

Is it insecure to send a request to reset token to previous state?

I am using Meteor and its account system. My enrollment process involves a few steps on resetting the password.
Template['enrollment'].events({
'submit #reset-password-form': function (e, template) {
e.preventDefault();
let token = Session.get('_resetPasswordToken');
let user = Meteor.users.findOne({ "services.password.reset.token": token });
let password = $(e.target).find('input#password').val();
if (AutoForm.validateForm('reset-password-form')) {
resetPasswordAsync(token, password)
.then(() => {
return Meteor.promise('Orders.initialize', template.data);
})
// a few more `.then()s`
.catch((error) => {
Notify.warn(error.message);
Meteor.call('User._resetToken', user, token);
})
}
}
});
The reason for this is because if anything fails in the promise chain, then they will remain on the same page but have an "uninitialized" state.
I use a meteor method, because a user should not be able to change his/her services to change their token back.
Meteor.methods({
'User._resetToken': function (user, token) {
check(user, Meteor.users.simpleSchema());
check(token, String);
Meteor.users.update(user._id, {
"services.password.reset.token": token
});
}
});
I vaguely feel like this is insecure, but can't quite tell why. Is there any exploits where resetting the user token on a callback can be exploited?
Firstly don't provide the user object to the method, a user might be able to reset a different user's reset token.
Secondly, just because you've put it in a server method doesn't make it secure. A user from the console can type Meteor.call(user,token) and reset their token or any other user's token (assuming they know the other user's _id.
If the user has already reset their password the token is no longer necessary at all. It only needs to lead a short life.

Categories