Skip to content
This repository has been archived by the owner. It is now read-only.

feat(server): add basket proxy server #2460

Merged
merged 8 commits into from May 28, 2015
Merged

Conversation

@zaach
Copy link
Contributor

@zaach zaach commented May 24, 2015

@zaach
Copy link
Contributor Author

@zaach zaach commented May 24, 2015

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.

@coveralls
Copy link

@coveralls coveralls commented May 24, 2015

Coverage Status

Coverage decreased (-0.11%) to 98.25% when pulling 4cbfed3 on issue-2443-basket-proxy into ebc3bee on master.

@shane-tomlinson

This comment has been minimized.

Copy link
Member

@shane-tomlinson shane-tomlinson commented on server/bin/basket-proxy-server.js in 4cbfed3 May 25, 2015

@zaach - this app will need to be able to respond to CORS requests.

@shane-tomlinson

This comment has been minimized.

Copy link
Member

@shane-tomlinson shane-tomlinson commented on server/bin/basket-proxy-server.js in 4cbfed3 May 25, 2015

When token arrives here, it still has Bearer prepended, which causes the OAuth server to reject it.

A simple fix is:

token = token.replace(/^Bearer /, '');
verifyOAuthToken(req.headers.authorization)
.then(function (creds) {
basketRequest('/subscribe', 'post', {
email: creds.email,

This comment has been minimized.

@rfk

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.

.then(function (creds) {
basketRequest('/unsubscribe', 'post', {
email: creds.email,
newsletter_id: req.params.newsletter_id

This comment has been minimized.

@rfk

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.

if (body.code >= 400) {
logger.debug('unauthorized', body);
return defer.reject(new Error(body.message));
}

This comment has been minimized.

@rfk

rfk May 25, 2015
Member

Need to check for body.scope.indexOf("basket:write") >= 0 in the response body here :-)

});
});

app.post('/unsubscribe', function (req, res) {

This comment has been minimized.

@rfk

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.

This comment has been minimized.

@rfk

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.


function initApp() {
var app = express();
app.use(bodyParser.json());

This comment has been minimized.

@rfk

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.

This comment has been minimized.

@shane-tomlinson

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.

This comment has been minimized.

@rfk

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

@shane-tomlinson
Copy link
Member

@shane-tomlinson shane-tomlinson commented May 25, 2015

@zaach - this is great stuff. I had to make some updates to perform end to end testing, so I opened a PR (#2467) against this PR.

@shane-tomlinson shane-tomlinson added this to the train 38 milestone May 26, 2015
Shane Tomlinson and others added 2 commits May 25, 2015
* 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.
@jrgm
Copy link
Contributor

@jrgm jrgm commented May 27, 2015

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 require('http-proxy'). Might be due to the stale version of http-proxy@1.2.

@pdehaan
Copy link
Contributor

@pdehaan pdehaan commented May 27, 2015

Yeah, what @jrgm said. But it worked for me after I deleted the npm-shrinkwrap.json and did npm-next i http-proxy@latest -S and then npm start (using node 0.10.38 and npm 2.11.0).

¯_(ツ)_/¯

}

function listen(app) {
var serverUrl = url.parse(config.get('marketing_email.api_url'));

This comment has been minimized.

@rfk

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?

This comment has been minimized.

@zaach

zaach May 28, 2015
Author Contributor

👍 SGTM

return;
}

var token = authHeader.replace(/^Bearer /, '');

This comment has been minimized.

@rfk

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
}

logger.debug('auth.valid', body);

res.locals.creds = body;

This comment has been minimized.

@rfk

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

This comment has been minimized.

@rfk

rfk May 28, 2015
Member

We need to check body.scopes for the presence of basket:write before allowing this through.

@rfk
Copy link
Member

@rfk rfk commented May 28, 2015

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);

This comment has been minimized.

@jrgm

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.

This comment has been minimized.

@zaach

zaach May 28, 2015
Author Contributor

Aye. We should be able to reuse the route logger we have for the main server.

@zaach zaach force-pushed the issue-2443-basket-proxy branch from 8c9b2f9 to dcb48de May 28, 2015
return;
}

if (! body.scopes.match(/basket:write/)) {

This comment has been minimized.

@rfk

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

This comment has been minimized.

@zaach

zaach May 28, 2015
Author Contributor

Ah, you're right. Fixing.

@zaach zaach force-pushed the issue-2443-basket-proxy branch from a3da6da to fa5aa01 May 28, 2015
}

if (! authHeader.match(/^Bearer /)) {
res.status(400).json(errorResponse('invalid authorization header', BASKET_ERRORS.USAGE_ERROR));

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 28, 2015
Member

Add error logging.

return;
}

if (body.scopes.indexOf('basket:write') === -1) {

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 28, 2015
Member

Do we need to ensure scopes exists before calling indexOf?

This comment has been minimized.

@shane-tomlinson

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)

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 28, 2015
Member

ahha, the oauth server returns scope, not scopes.

This comment has been minimized.

@rfk

rfk May 28, 2015
Member

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: {

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 28, 2015
Member

We might as well take the marketing_email section out of this PR and add it to #2452.

shane-tomlinson pushed a commit that referenced this pull request May 28, 2015
Contains @zaach's proxy from #2460

issue #2443
shane-tomlinson pushed a commit that referenced this pull request May 28, 2015
Contains @zaach's proxy from #2460

issue #2443
shane-tomlinson pushed a commit that referenced this pull request May 28, 2015
feat(server): add basket proxy server

Thank you @zaach for this. Awesome.

r=@shane-tomlinson
@shane-tomlinson shane-tomlinson merged commit 8040fcf into master May 28, 2015
3 checks passed
3 checks passed
continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details
coverage/coveralls Coverage remained the same at 98.39%
Details
shane-tomlinson pushed a commit that referenced this pull request May 28, 2015
Contains @zaach's proxy from #2460

issue #2443
shane-tomlinson pushed a commit that referenced this pull request May 28, 2015
Contains @zaach's proxy from #2460

issue #2443
shane-tomlinson pushed a commit that referenced this pull request Jun 5, 2015
Contains @zaach's proxy from #2460

issue #2443

(cherry picked from commit 8c4246e)
shane-tomlinson pushed a commit that referenced this pull request Jun 5, 2015
Contains @zaach's proxy from #2460

issue #2443

(cherry picked from commit 8c4246e)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

6 participants