I have some middleware for middleware that processes GET requests from my client application for subsequent requests to a separate API server using OAuth2 tokens. I also use express-session to store these tokens.
In my middleware that makes an outgoing request, I added processing to deal with cases when an access token expires (the API server sends 403 back) and makes a request to update tokens, after which it will issue the same original outgoing request to the API server therefore the client does not know about it. Newly received tokens are then saved back to the session store via express-session for use in subsequent requests. Tokens are also used to set the header of the bearer channel marker, as you will see below.
Here is the part of my express code that involved:
routes.controller.js
//Currently handling GET API requests from client module.exports.fetch = function(req, res) { var options = helpers.buildAPIRequestOptions(req); helpers.performOutgoingRequest(req, res, options); };
helpers.js
module.exports.buildAPIRequestOptions = function(req, url) { var options = {}; options.method = req.method; options.uri = 'http://someurl.com' + req.path; options.qs = req.query; options.headers = { 'Authorization': 'Bearer ' + req.session.accessToken }; return options; }; module.exports.performOutgoingRequest = function(req, res, options) { request(options, function(err, response, body){ if(response.statusCode === 401){ console.log(chalk.red('\n--- 401 RESPONSE RECEIVED TRY REFRESHING TOKENS ---'));
auth.controller.js
module.exports.refreshToken = function(req, res, next) { var formData = { grant_type: 'refresh_token', refresh_token: req.session.refreshToken }, headers = { 'Authorization' : 'Basic ' + consts.CLIENT_KEY_SECRET_BASE64 }; request.post({url:consts.ACCESS_TOKEN_REQUEST_URL, form:formData, headers: headers, rejectUnauthorized: false}, function(err, response, body){ var responseBody = JSON.parse(body); if (response.statusCode === 200) { req.session.accessToken = responseBody.access_token; req.session.refreshToken = responseBody.refresh_token; next(); } else { console.log(chalk.yellow('A problem occurred refreshing tokens, sending 401 HTTP response back to client...')); res.status(401).send(); } }); };
In most cases, the above works just fine
When a user first logs in, some additional user profile data is retrieved from the API server before being sent to the applicationโs main page.
Some of the pages in the application also retrieve page load data and therefore undergo access token checks.
During normal use, therefore, when a user logs in and starts clicking on pages, I see that tokens are uploaded and stored in the session store via express-session as they expire. The new access token is correctly used for subsequent requests in accordance with the middleware that I wrote.
Now I have a scenario where my middleware is not working.
So say that I'm on a page loading data to load a page, say, my order page. If I wait until the set token time has expired on the API server and then updates the browser, the client application will first query the user information and, if successful, request the order data needed for the page (using AngularJS promises)
In my express application, the user information request is received by 403 from the API server, so the tokens are updated after my middleware above, and req.session.accessToken receives the update that I see through console logging in my server application. But the next selection of data for orders ends with the use of a previously installed access token, and this causes an additional unauthorized error from the API server, since the request is executed with an invalid token.
If I update the browser again, both user information and orders are retrieved using the correct updated token from the previous middleware stream.
So, I'm not sure what is going on here, I wonder if there was a problem with the req.session object not being saved to the session store on time for the next request?
Anyone have any idea what could be here?
thanks
Update 1
As indicated in the comments, here are the request and response headers for the two completed requests.
First request (which uses the updated side of the token server)
Request Headers
GET /api/userinfo HTTP/1.1 Host: localhost:5000 Connection: keep-alive Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36 Referer: https://localhost:5000/ Accept-Encoding: gzip, deflate, sdch Accept-Language: en-GB,en-US;q=0.8,en;q=0.6 Cookie: interact.sid=s%3A0NDG_bn67NeGQAYl1wP1-TmM19ExavFm.Zjv65e9BtSyNBuo%2FDxZEk2Np0963frVur4zHyYw3y5I
Answer Headers
HTTP/1.1 200 OK X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN Strict-Transport-Security: max-age=86400 X-Download-Options: noopen X-XSS-Protection: 1; mode=block Content-Type: text/html; charset=utf-8 Content-Length: 364 ETag: W/"16c-4AIbpZmTm3I+Yl+SbZdirw" set-cookie: interact.sid=s%3A0NDG_bn67NeGQAYl1wP1-TmM19ExavFm.Zjv65e9BtSyNBuo%2FDxZEk2Np0963frVur4zHyYw3y5I; Path=/; Expires=Fri, 13 May 2016 11:54:56 GMT; HttpOnly; Secure Date: Fri, 13 May 2016 11:24:56 GMT Connection: keep-alive
The second request (which uses the server of the old server)
Request Headers
GET /api/customers HTTP/1.1 Host: localhost:5000 Connection: keep-alive Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36 Referer: https://localhost:5000/ Accept-Encoding: gzip, deflate, sdch Accept-Language: en-GB,en-US;q=0.8,en;q=0.6 Cookie: interact.sid=s%3A0NDG_bn67NeGQAYl1wP1-TmM19ExavFm.Zjv65e9BtSyNBuo%2FDxZEk2Np0963frVur4zHyYw3y5I
Answer Headers
HTTP/1.1 401 Unauthorized X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN Strict-Transport-Security: max-age=86400 X-Download-Options: noopen X-XSS-Protection: 1; mode=block set-cookie: interact.sid=s%3A0NDG_bn67NeGQAYl1wP1-TmM19ExavFm.Zjv65e9BtSyNBuo%2FDxZEk2Np0963frVur4zHyYw3y5I; Path=/; Expires=Fri, 13 May 2016 11:54:56 GMT; HttpOnly; Secure Date: Fri, 13 May 2016 11:24:56 GMT Connection: keep-alive Content-Length: 0
Update 2
I should also mention that I use connect-mongo for my session store, I tried to use the default memory store, but the same behavior exists.