How do I intercept a request, ping a different route for an ID, store the ID in a session, then continue the original request (especially PUT/POST with payloads) using the ID I just got?
I am using HapiJS (8) to proxy requests from the client to an existing API (where I have no control over processes/logic). The API requires each request to contain a 'session ID' in either the query string or the payload (depending on the http method). In order to get a session ID, all I have to do is ask for one... there is no username/pwd required (it uses Basic auth in the headers). The session ID expires every 24 hours if it isn't renewed. Each client has their own session ID.
I am currently using hapi-auth-cookie to store the value of the session ID which gets queried when the ID is needed. If the ID is expired or null, I need to request a new one before the client's request can be successfully proxy'd to the API.
When the client's request method is 'GET', I am handling this challenge quite gracefully utilizing the appendNext
configuration described in the hapi-auth-cookie docs. The request is intercepted by hapi-auth-cookie, if a new session ID is needed, a request is sent to that particular route to get it, the API returns the ID which is then assigned to the Hapi session, and then (using Wreck) a reply.redirect
returns to the original GET request where it completes. Seamless and graceful.
However, I cannot figure out how to accomplish this same flow with different http methods that contain payloads of data.
Is there something besides reply.redirect
that will accomplish the same goal while maintaining original payloads and methods? Or is there a much better way to do this in general?
main app file (hapi-auth-cookie configs)
# register plugins
server.register require('./plugins')
, (err) ->
throw err if err
# set authentication (NLS) configs
server.auth.strategy 'session', 'cookie',
password: 'session_pwd'
cookie: 'ghsid'
redirectTo: '/api/SessionID' #get new session ID
isSecure: config.get 'ssl'
appendNext: true
ttl: config.get 'session_length'
Controller that leverages session authentication and invokes hapi-auth-cookie plugin:
simpleRequest:
auth: 'session'
handler: (request, reply) ->
qs = Qs.stringify request.query
request.papi_url = "/api/route/sample?#{qs}"
reply.proxy
mapUri: (request, reply) ->
auth = config.get 'basic_auth'
api_host = config.get 'api_host'
papi_url = request.papi_url
path = api_host + papi_url
next null, path, {authorization: auth}
Route for getting a new session ID
module.exports = [
{
path: '/api/SessionID'
method: 'GET'
config: SessionController.session
}
]
Session Controller
Wreck = require 'wreck'
config = require 'config'
module.exports =
session:
description: 'Get new session ID'
auth:
mode: 'try'
strategy: 'session'
plugins:
'hapi-auth-cookie':
redirectTo: false
handler: (request, reply) ->
# request configs
papi_url = "/Session"
api_host = config.get 'api_host'
url = api_host + papi_url
opts =
headers:
'Authorization': config.get 'basic_auth'
'content-type': 'application/json;charset=UTF-8'
# make request to PAPI
Wreck.post url, opts, (err, res, body) ->
throw new Error err if err
try
bdy = JSON.parse body
sess =
nls: bdy.SessionId
if bdy.SessionId
# authenticate user with NLS
request.auth.session.set sess
# redirect to initial route
reply.redirect request.url.query.next
else
return throw new Error
catch err
throw new Error err
Based on Matt Harrison's answer, I created a custom plugin that gets registered as an authentication scheme so I can control this per route.
Here's the plugin code:
Wreck = require 'wreck'
config = require 'config'
exports.register = (server, options, next) ->
server.auth.scheme 'cookie', internals.implementation
next()
exports.register.attributes =
name: 'Hapi Session Interceptor'
version: '1.0.0'
internals = {}
internals.implementation = (server, options, next) ->
scheme = authenticate: (request, reply) ->
validate = ->
session = request.state.sessionID
unless session
return unauthenticated()
reply.continue(credentials: {'session': session})
unauthenticated = ->
api_url = "/SessionID"
api_host = config.get 'api_host'
url = api_host + api_url
opts =
headers:
'Authorization': config.get 'basic_auth'
'content-type': 'application/json;charset=UTF-8'
# make request to API
Wreck.post url, opts, (err, res, body) ->
throw new Error err if err
bdy = JSON.parse body
sess =
session: bdy.SessionId
if bdy.SessionId
reply.state 'sessionID', bdy.SessionId
reply.continue(credentials: sess)
else
return throw new Error
validate()
return scheme
Although not entirely faithful to your code, I've put together an example that has all the pieces I think you're working with.
I've made a service
plugin to represent your API. The upstream
plugin represents the actual upstream API you're proxying to.
All requests passthrough the service
and are proxied to the upstream
, which just prints out all of the headers and payload it received.
If the original request doesn't contain a cookie with a sessionId, a route is hit on the upstream
to get one. A cookie is then set with this value when the response comes back down stream.
The code is here: https://github.com/mtharrison/hapijs-proxy-trouble
Try it out with curl and your browser.
GET: curl http://localhost:4000
POST W/PAYLOAD: curl -X POST -H "content-type: application/json" -d '{"example":"payload"}' http://localhost:4000
index.js
var Hapi = require('hapi');
var server = new Hapi.Server();
server.connection({ port: 4000, labels: ['service'] }); // Your service
server.connection({ port: 5000, labels: ['upstream']}); // Pretend upstream API
server.state('session', {
ttl: 24 * 60 * 60 * 1000,
isSecure: false,
path: '/',
encoding: 'base64json'
});
server.register([{
register: require('./service')
}, {
register: require('./upstream')
}],
function (err) {
if (err) {
throw err;
}
server.start(function () {
console.log('Started!');
});
});
service.js
var Wreck = require('wreck');
exports.register = function (server, options, next) {
// This is where the magic happens!
server.select('service').ext('onPreHandler', function (request, reply) {
var sessionId = request.state.session;
var _done = function () {
// Set the cookie and proceed to the route
request.headers['X-Session-Id'] = sessionId;
reply.state('session', sessionId);
reply.continue();
}
if (typeof sessionId !== 'undefined')
return _done();
// We don't have a sessionId, let's get one
Wreck.get('http://localhost:5000/sessionId', {json: true}, function (err, res, payload) {
if(err) {
throw err;
}
sessionId = payload.id;
_done();
});
});
server.select('service').route({
method: '*',
path: '/{p*}', // Proxies all routes and methods
handler: {
proxy: {
host: 'localhost',
port: 5000,
protocol: 'http',
passThrough: true
}
}
});
next();
};
exports.register.attributes = {
name: 'your-service'
};
upstream.js
exports.register = function (server, options, next) {
server.select('upstream').route([{
method: '*',
path: '/{p*}',
handler: function (request, reply) {
// Just prints out what it received for headers and payload
// To prove we got send the original payload and the sessionID header
reply({
originalHeaders: request.headers,
originalPayload: request.payload,
})
}
}, {
method: 'GET',
path: '/sessionId',
handler: function (request, reply) {
// Returns a random session id
reply({ id: (Math.floor(Math.random() * 1000)) });
}
}]);
next();
};
exports.register.attributes = {
name: 'upstream'
};