feat(server): add basket proxy server #2460
Conversation
|
I wasn't quite sure how to test this... Perhaps a mock basket server that we configure the proxy to talk to in dev/test mode. |
This comment has been minimized.
This comment has been minimized.
|
@zaach - this app will need to be able to respond to CORS requests. |
This comment has been minimized.
This comment has been minimized.
|
When A simple fix is: token = token.replace(/^Bearer /, ''); |
| verifyOAuthToken(req.headers.authorization) | ||
| .then(function (creds) { | ||
| basketRequest('/subscribe', 'post', { | ||
| email: creds.email, |
rfk
May 25, 2015
Member
This creds.email property will only exist if the token had profile:email scope. @shane-tomlinson the simplest thing might be to request scope="basket:write profile:email" for the basket token, although when this functionality merges into basket proper that will no longer be necessary.
This creds.email property will only exist if the token had profile:email scope. @shane-tomlinson the simplest thing might be to request scope="basket:write profile:email" for the basket token, although when this functionality merges into basket proper that will no longer be necessary.
| .then(function (creds) { | ||
| basketRequest('/unsubscribe', 'post', { | ||
| email: creds.email, | ||
| newsletter_id: req.params.newsletter_id |
rfk
May 25, 2015
Member
For basket API compatibility, this needs to be { "newsletters": "comma,separated,list,of,ids" } here and in the client code.
For basket API compatibility, this needs to be { "newsletters": "comma,separated,list,of,ids" } here and in the client code.
| if (body.code >= 400) { | ||
| logger.debug('unauthorized', body); | ||
| return defer.reject(new Error(body.message)); | ||
| } |
rfk
May 25, 2015
Member
Need to check for body.scope.indexOf("basket:write") >= 0 in the response body here :-)
Need to check for body.scope.indexOf("basket:write") >= 0 in the response body here :-)
| }); | ||
| }); | ||
|
|
||
| app.post('/unsubscribe', function (req, res) { |
rfk
May 25, 2015
Member
I had to add a trailing slash to /subscribe/ and /unsubscribe/ when talking to the dev basket server by hand, otherwise it would give me a 405.
I had to add a trailing slash to /subscribe/ and /unsubscribe/ when talking to the dev basket server by hand, otherwise it would give me a 405.
rfk
May 25, 2015
Member
Ah, this is probably basket trying to redirect me to the trailing-slash variant, and my HTTP client following it via GET rather than retrying the POST there.
Ah, this is probably basket trying to redirect me to the trailing-slash variant, and my HTTP client following it via GET rather than retrying the POST there.
|
|
||
| function initApp() { | ||
| var app = express(); | ||
| app.use(bodyParser.json()); |
rfk
May 25, 2015
Member
AFAICT basket only accepts form-encded input, not JSON. We could use JSON for our own needs but should support form-encoded here as well.
AFAICT basket only accepts form-encded input, not JSON. We could use JSON for our own needs but should support form-encoded here as well.
shane-tomlinson
May 25, 2015
Member
I'm a bit "meh" on supporting form-encoded input if our other services do not. Basket is a bit of an implementation detail, and AFAICT, all our other services only accept JSON input.
I'm a bit "meh" on supporting form-encoded input if our other services do not. Basket is a bit of an implementation detail, and AFAICT, all our other services only accept JSON input.
rfk
May 25, 2015
Member
Well, hopefully long-term this oauth functionality will live directly in basket rather than in a proxy. In which case we'll be talking directly to basket and limited by whatever input formats it supports.
But yeah, JSON FTW, so let's upstream this: https://bugzilla.mozilla.org/show_bug.cgi?id=1168218
Well, hopefully long-term this oauth functionality will live directly in basket rather than in a proxy. In which case we'll be talking directly to basket and limited by whatever input formats it supports.
But yeah, JSON FTW, so let's upstream this: https://bugzilla.mozilla.org/show_bug.cgi?id=1168218
* Strip the "Bearer " portion of a token before verification. * Enable CORS from the content server. * Handle errors from both the OAuth and Basket servers.
Update the basket proxy server to support the full flow.
|
FYI, neither @pdehaan or myself can do a 'git checkout clean && npm install && npm start' with this branch on osx. It dies on a hard to explain error about loading eventemitter3 when calling |
|
Yeah, what @jrgm said. But it worked for me after I deleted the npm-shrinkwrap.json and did ¯_(ツ)_/¯ |
| } | ||
|
|
||
| function listen(app) { | ||
| var serverUrl = url.parse(config.get('marketing_email.api_url')); |
rfk
May 28, 2015
Member
Per discussion with @jrgm in IRC; the marketing_email.api_url is the publicly-visible URL of this API. For hosting purposes we will want to listen on a localhost port. I suggest making a new basket.proxy_url config option to set the local listen address for this proxy to bind to, and keeping marketing_email.api_url as the public-facing one.
In practice, we'll host with some sort of nginx proxy so the settings might be something like:
marketing_email.api_url: "https://accounts.firefox.com/basket/news"
basket.proxy_url: "http://localhost:3034"
@zaach thoughts?
Per discussion with @jrgm in IRC; the marketing_email.api_url is the publicly-visible URL of this API. For hosting purposes we will want to listen on a localhost port. I suggest making a new basket.proxy_url config option to set the local listen address for this proxy to bind to, and keeping marketing_email.api_url as the public-facing one.
In practice, we'll host with some sort of nginx proxy so the settings might be something like:
marketing_email.api_url: "https://accounts.firefox.com/basket/news"basket.proxy_url: "http://localhost:3034"
@zaach thoughts?
zaach
May 28, 2015
Author
Contributor
👍 SGTM
| return; | ||
| } | ||
|
|
||
| var token = authHeader.replace(/^Bearer /, ''); |
rfk
May 28, 2015
Member
IMHO it would be better to explicitly check for "Bearer" and error out if there's some other weirdy auth type in the header, e.g.:
var authz = authHeader..split(/\s+/);
if (authz.length !== 2 || authz[0].toLowerCase() !== "bearer") {
// error out
}
IMHO it would be better to explicitly check for "Bearer" and error out if there's some other weirdy auth type in the header, e.g.:
var authz = authHeader..split(/\s+/);
if (authz.length !== 2 || authz[0].toLowerCase() !== "bearer") {
// error out
}
|
|
||
| logger.debug('auth.valid', body); | ||
|
|
||
| res.locals.creds = body; |
rfk
May 28, 2015
Member
We should check that we actually got the email back as part of the response; the oauth server will only send it if the token had correct scopes
We should check that we actually got the email back as part of the response; the oauth server will only send it if the token had correct scopes
rfk
May 28, 2015
Member
We need to check body.scopes for the presence of basket:write before allowing this through.
We need to check body.scopes for the presence of basket:write before allowing this through.
|
Those notes aside, this is looking really solid! |
| var params = req.body; | ||
| var creds = res.locals.creds; | ||
|
|
||
| basketRequest('/lookup-user/?email=' + creds.email, 'get', params).pipe(res); |
jrgm
May 28, 2015
Contributor
Yeah, so this needs to change here and other places where we just pipe through the response. It needs to log errors at minimum, but really should be standard HTTP response code logging for success and failure for every request.
Yeah, so this needs to change here and other places where we just pipe through the response. It needs to log errors at minimum, but really should be standard HTTP response code logging for success and failure for every request.
zaach
May 28, 2015
Author
Contributor
Aye. We should be able to reuse the route logger we have for the main server.
Aye. We should be able to reuse the route logger we have for the main server.
| return; | ||
| } | ||
|
|
||
| if (! body.scopes.match(/basket:write/)) { |
rfk
May 28, 2015
Member
IIUC body.scopes is supposed to come back as an array; if it's a string here then we need to cross-check behaviour with oauth server
IIUC body.scopes is supposed to come back as an array; if it's a string here then we need to cross-check behaviour with oauth server
zaach
May 28, 2015
Author
Contributor
Ah, you're right. Fixing.
Ah, you're right. Fixing.
| } | ||
|
|
||
| if (! authHeader.match(/^Bearer /)) { | ||
| res.status(400).json(errorResponse('invalid authorization header', BASKET_ERRORS.USAGE_ERROR)); |
shane-tomlinson
May 28, 2015
Member
Add error logging.
Add error logging.
| return; | ||
| } | ||
|
|
||
| if (body.scopes.indexOf('basket:write') === -1) { |
shane-tomlinson
May 28, 2015
Member
Do we need to ensure scopes exists before calling indexOf?
Do we need to ensure scopes exists before calling indexOf?
shane-tomlinson
May 28, 2015
Member
We do, while testing with the null proxy server:
fxa-content-server.CRITICAL: TypeError: Cannot call method 'match' of undefined
at Request._callback (/Users/stomlinson/development/fxa-content-server/server/bin/basket-proxy-server.js:103:25)
at Request.self.callback (/Users/stomlinson/development/fxa-content-server/node_modules/request/request.js:123:22)
at Request.emit (events.js:98:17)
at Request.<anonymous> (/Users/stomlinson/development/fxa-content-server/node_modules/request/request.js:1047:14)
at Request.emit (events.js:117:20)
at IncomingMessage.<anonymous> (/Users/stomlinson/development/fxa-content-server/node_modules/request/request.js:998:12)
at IncomingMessage.emit (events.js:117:20)
at _stream_readable.js:943:16
at process._tickCallback (node.js:419:13)
We do, while testing with the null proxy server:
fxa-content-server.CRITICAL: TypeError: Cannot call method 'match' of undefined
at Request._callback (/Users/stomlinson/development/fxa-content-server/server/bin/basket-proxy-server.js:103:25)
at Request.self.callback (/Users/stomlinson/development/fxa-content-server/node_modules/request/request.js:123:22)
at Request.emit (events.js:98:17)
at Request.<anonymous> (/Users/stomlinson/development/fxa-content-server/node_modules/request/request.js:1047:14)
at Request.emit (events.js:117:20)
at IncomingMessage.<anonymous> (/Users/stomlinson/development/fxa-content-server/node_modules/request/request.js:998:12)
at IncomingMessage.emit (events.js:117:20)
at _stream_readable.js:943:16
at process._tickCallback (node.js:419:13)
shane-tomlinson
May 28, 2015
Member
ahha, the oauth server returns scope, not scopes.
ahha, the oauth server returns scope, not scopes.
rfk
May 28, 2015
Member
ugh, that's unfortunate; its API docs say it should return scopes but the code indeed returns scope.
ugh, that's unfortunate; its API docs say it should return scopes but the code indeed returns scope.
| @@ -283,6 +283,31 @@ var conf = module.exports = convict({ | |||
| doc: 'Location of "report-uri"', | |||
| default: '/_/csp-violation', | |||
| } | |||
| }, | |||
| marketing_email: { | |||
feat(server): add basket proxy server Thank you @zaach for this. Awesome. r=@shane-tomlinson
Hi @shane-tomlinson !