chore(deps): Remove browserid-verifier packages and references

This commit is contained in:
Vijay Budhram
2025-04-29 16:09:44 -04:00
parent 540d66df9a
commit c8140fd85e
45 changed files with 0 additions and 2704 deletions

4
.gitignore vendored
View File

@@ -88,10 +88,6 @@ Thumbs.db
# circleci
.circleci/local.yml
# browserid-verifier
packages/browserid-verifier/loadtest/venv
packages/browserid-verifier/loadtest/*.pyc
# fxa-admin-server
packages/fxa-admin-server/src/config/local.json

View File

@@ -10,16 +10,6 @@ services:
# init: true
# ports:
# - "8080:8080"
browserid-verifier:
image: browserid-verifier:build
command: node server.js
environment:
- PORT=5050
- IP_ADDRESS=0.0.0.0
- FORCE_INSECURE_LOOKUP_OVER_HTTP=true
init: true
ports:
- "5050:5050"
auth:
image: fxa-auth-server:build
entrypoint: /bin/bash -c

View File

@@ -11,7 +11,6 @@
9292 # Fortress
8080 # 123done
10139 # 321done
5050 # browserid-verifier
3031 # payments server
7100 # support admin panel
8002 # pushbox

View File

@@ -11,7 +11,6 @@ spec:
- ./backstage/mysql.resources.yaml
- ./backstage/redis.resources.yaml
- ./packages/123done/backstage.yaml
- ./packages/browserid-verifier/backstage.yaml
- ./packages/fxa-admin-panel/backstage.yaml
- ./packages/fxa-admin-server/backstage.yaml
- ./packages/fxa-auth-server/backstage.yaml

View File

@@ -1,5 +0,0 @@
{
"extends": ["plugin:fxa/recommended"],
"plugins": ["fxa"],
"root": true
}

View File

@@ -1,14 +0,0 @@
{
"comment": "532, 534, 545 are various ReDoS that don't affect us.",
"comment_566": "Hoek merge vuln, which we don't use.",
"comment_1179": "1179 is prototype pollution in minimist, used by eslint, optimist, and mocha. Doesn't affect us, as we don't pass untrusted external inputs to any of these.",
"comment_1488": "Acorn DoS vuln (dep of browserify), only applies if passed untrusted user input.",
"exceptions": [
"https://nodesecurity.io/advisories/532",
"https://nodesecurity.io/advisories/534",
"https://nodesecurity.io/advisories/535",
"https://nodesecurity.io/advisories/566",
"https://npmjs.com/advisories/1179",
"https://npmjs.com/advisories/1488"
]
}

View File

@@ -1,4 +0,0 @@
LICENSE
.*
Dockerfile
loadtest/*

View File

@@ -1,141 +0,0 @@
## A BrowserID verification server
This repository contains a flexible BrowserID verification server authored in
Node.JS which uses the [local verification library](https://github.com/mozilla/browserid-local-verify).
## Getting Started
To run the verification server locally:
$ git clone https://github.com/mozilla/browserid-verifier
$ cd browserid-verifier
$ yarn install
$ yarn start
At this point, your verifier will be running and available to use locally over
HTTP.
## Configuration
There are several configuration variables which can change the behavior of the
server. You can inspect available configuration variables in `lib/config.js`.
You can specify a set of `json` configuration files using the `CONFIG_FILES`
environment variable (separate each path with a comma (`,`)). Finally, you can
inspect the current server configuration with:
$ node ./lib/server.js -c
## Health Checks
The server exports an endpoint at `/status` that can be polled for server health.
When the server is healthy, a `200` HTTP response is returned with a body of `OK`.
## Testing
This package uses [Mocha](https://mochajs.org/) to test its code. By default `npm test` will test all JS files under `tests/`.
Test specific tests with the following commands:
```bash
# Test only tests/health-check.js
npx mocha tests/health-check.js
# Grep for "test servers should start"
npx mocha -g "test servers should start"
```
Refer to Mocha's [CLI documentation](https://mochajs.org/#command-line-usage) for more advanced test configuration.
## API
The server exports an HTTP endpoint at `/v2` that can be POSTed to verify BrowserID
assertions. Arguments may be provided inside a JSON object. The following are
required:
1. Requests must be an HTTP POST.
2. Content-Type must equal `application/json`
3. POST body must be valid JSON.
The following arguments are supported:
### **required** (string) `assertion`
A BrowserID assertion
### **required** (string) `audience`
The origin of the site to which the assertion is expected to be bound.
### **optional** (array of strings) `trustedIssuers`
An array of domain names that are _trusted_ issuers. Assertions
signed by any of the domains in this set will be honored regardless of
the presence of a subject or principal in a BrowserID assertion.
### Error Response
Example:
$ curl -H 'Content-Type: application/json' \
-d '{ "audience": "http://example.com", "assertion": "bogus" }' \
https://verifier.mozcloud.org/v2
{
"status": "failure",
"reason": "no certificates provided"
}
Upon failure, the verifier returns a non-200 HTTP status code. Additionally, the
response body contains a JSON formated response containing a `status` key with the
value of `failure`. Additionally, more verbose developer readable information will
be available in a string value on the `reason` key.
### Success Response
Example:
$ curl -H 'Content-Type: application/json' \
-d '{ "audience": "http://123done.org" , "assertion": "eyJhbG...ZEe7A" }'
https://verifier.mozcloud.org/v2
{
"audience": "http://123done.org",
"expires": 1389791993675,
"issuer": "mockmyid.com",
"email": "lloyd@mockmyid.com",
"status": "okay"
}
Upon successful assertion verification, a 200 response will be sent with a JSON formatted body.
The body will always include `audience`, `issuer`, `status` (of "okay"), and `expires`.
### Extra IdP claims
The verifier will extract any number of additional claims from the
Identity Certificate generated by the Identity Provider. These claims
will be returned under a `idpClaims` top level key in the success response. Hence, an identity
certificate which looks like this:
{
"pubkey": { "...": "..." },
"sub": "60ae5097-8118-4c58-bb80-7db2742d137e",
"iat": 1389964111,
"exp": 1421500111,
"iss": "example.com",
"fxa-version": 1,
"fxa-generation": 504,
"email": "user@example.com"
}
Upon successful verification (which could only occur via `.trustedIssuers` because authority lookup will fail), will
result in a verifier response like this:
{
"audience": "...",
"expires": 1421500111,
"issuer": "example.com",
"idpClaims": {
"fxa-version": 1,
"fxa-generation": 504,
"email": "user@example.com"
},
"status": "okay"
}

View File

@@ -1,19 +0,0 @@
---
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: fxa-browserid-verifier
description: Verifies BrowserID assertions.
tags:
- typescript
- javascript
- node
- hapi
annotations:
sentry.io/project-slug: mozilla/fxa-browserid-verify
circleci.com/project-slug: github/mozilla/fxa
spec:
type: service
lifecycle: production
owner: fxa-devs
system: mozilla-accounts

View File

@@ -1,15 +0,0 @@
{
"logging": {
"handlers": {
"console": {
"class": "intel/handlers/console",
"formatter": "json"
}
},
"loggers": {
"bid.summary": {
"propagate": true
}
}
}
}

View File

@@ -1,77 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* This is browserid-local-verify wrapped in node-compute-cluster.
*
* It provides a "Verifier" class with a "verify" method just like the one
* in browserid-local-verify, except that it farms out work to subprocesses
* via node-compute-cluster rather than doing it inline.
*
* It's not a drop-in replacement for browserid-local-verify:
*
* * There is only verify(), not lookup() or other methods.
*
* * It doesn't emit async events like "debug" or "metrics" because
* there"s no support for that in node-compute-cluster. Yet...
*
*/
const util = require('util'),
events = require('events'),
path = require('path'),
log = require('../log')('ccverifier'),
config = require('../config'),
cc = require('compute-cluster'),
_ = require('underscore');
function Verifier(args) {
events.EventEmitter.call(this);
this.args = args;
this.cc = new cc({
module: path.join(__dirname, 'worker.js'),
max_processes: config.get('computecluster.maxProcesses'),
max_backlog: config.get('computecluster.maxBacklog'),
})
.on('error', function (err) {
log.error('computeCluster.error', { err });
})
.on('info', function (msg) {
log.info('computeCluster.info', { message: msg });
})
.on('debug', function (msg) {
log.debug('computeCluster.debug', { message: msg });
});
}
util.inherits(Verifier, events.EventEmitter);
const testServiceFailure = config.get('testServiceFailure');
Verifier.prototype.verify = function (args, cb) {
if (!cb) {
cb = args;
args = {};
}
args = _.extend({}, this.args, args);
this.cc.enqueue({ args: args }, function (err, res) {
if (err || testServiceFailure) {
// An error from the cluster itself.
return cb('compute cluster error: ' + err);
}
if (res.err) {
// An error from inside the verifier.
return cb(res.err);
} else {
// A valid result from the verifier.
return cb(null, res.res);
}
});
};
Verifier.prototype.shutdown = function () {
this.cc.exit();
};
module.exports = Verifier;

View File

@@ -1,27 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const LocalVerifier = require('browserid-local-verify');
var verifier = new LocalVerifier();
process.on('message', function (message) {
if (!message.args) {
message.args = {};
}
try {
verifier.verify(message.args, function (err, res) {
if (err) {
return process.send({ err: err });
}
return process.send({ res: res });
});
} catch (err) {
return process.send({ err: err });
}
});
process.on('uncaughtException', function () {
process.exit(8);
});

View File

@@ -1,167 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var convict = require('convict');
convict.addFormats(require('convict-format-with-moment'));
convict.addFormats(require('convict-format-with-validator'));
function loadConf() {
var conf = convict({
ip: {
doc: 'The IP address to bind.',
format: String,
default: 'localhost',
env: 'IP_ADDRESS',
},
port: {
doc: 'The port to bind.',
format: 'port',
default: 0,
env: 'PORT',
},
fallback: {
doc: 'The domain of the fallback server, authoritative when lookup fails.',
format: String,
default: '',
env: 'FALLBACK_DOMAIN',
},
httpTimeout: {
doc: '(s) how long to spend attempting to fetch support documents',
format: Number,
default: 8.0,
env: 'HTTP_TIMEOUT',
},
insecureSSL: {
doc: '(testing only) Ignore invalid SSL certificates',
format: Boolean,
default: false,
env: 'INSECURE_SSL',
},
forceInsecureLookupOverHTTP: {
doc: '(testing only) Lookup /.well-known/browserid documents over HTTP',
format: Boolean,
default: false,
env: 'FORCE_INSECURE_LOOKUP_OVER_HTTP',
},
toobusy: {
maxLag: {
doc: 'Max event-loop lag before toobusy reports failure',
format: Number,
default: 70,
env: 'TOOBUSY_MAX_LAG',
},
},
computecluster: {
maxProcesses: {
doc: 'Max worker processes to spawn for the compute cluster',
format: Number,
default: undefined,
env: 'COMPUTECLUSTER_MAX_PROCESSES',
},
maxBacklog: {
doc: 'Max length of work queue for the compute cluster',
format: Number,
default: undefined,
env: 'COMPUTECLUSTER_MAX_BACKLOG',
},
},
logging: {
app: {
default: 'browserid-verifier',
},
fmt: {
format: ['heka', 'pretty'],
default: 'heka',
},
level: {
env: 'LOG_LEVEL',
default: 'debug',
},
debug: {
env: 'LOG_DEBUG',
default: false,
},
},
sentry: {
dsn: {
doc: 'Sentry DSN for error and log reporting',
default: '',
format: 'String',
env: 'SENTRY_DSN',
},
env: {
doc: 'Environment name to report to sentry',
default: 'local',
format: ['local', 'ci', 'dev', 'stage', 'prod'],
env: 'SENTRY_ENV',
},
sampleRate: {
doc: 'Rate at which sentry errors are captured.',
default: 1.0,
format: 'Number',
env: 'SENTRY_SAMPLE_RATE',
},
serverName: {
doc: 'Name used by sentry to identify the server.',
default: 'browserid-verifier',
format: 'String',
env: 'SENTRY_SERVER_NAME',
},
tracesSampleRate: {
doc: 'Rate at which sentry traces are captured',
default: 0,
format: 'Number',
env: 'SENTRY_TRACES_SAMPLE_RATE',
},
},
testServiceFailure: {
doc: '(testing only) trigger a service failure in the verifier',
format: Boolean,
default: false,
env: 'TEST_SERVICE_FAILURE',
},
});
// load environment dependent configuration
if (process.env.CONFIG_FILES) {
var files = process.env.CONFIG_FILES.split(',');
files.forEach(function (file) {
conf.loadFile(file);
});
}
// validation configuration
conf.validate();
module.exports = conf;
process.nextTick(function () {
require('./log')('config').debug(
'current configuration:',
JSON.stringify(conf.get(), null, 2)
);
});
}
loadConf();
// command line options
var args = require('optimist')
.alias('h', 'help')
.describe('h', 'display this usage message')
.alias('c', 'config')
.describe('c', 'Display current configuration.');
var argv = args.argv;
if (argv.h) {
args.showHelp();
process.exit(1);
}
if (argv.c) {
console.log(module.exports.get());
process.exit(0);
}

View File

@@ -1,7 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
module.exports = require('mozlog');
module.exports.config(require('../config').get('logging'));

View File

@@ -1,186 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const Sentry = require('@sentry/node');
const express = require('express'),
bodyParser = require('body-parser'),
morgan = require('morgan'),
http = require('http'),
toobusy = require('toobusy-js'),
log = require('./log')('server'),
summary = require('./summary'),
config = require('./config'),
CCVerifier = require('./ccverifier'),
version = require('./version'),
v1api = require('./v1'),
v2api = require('./v2');
const {
tagCriticalEvent,
buildSentryConfig,
tagFxaName,
} = require('fxa-shared/sentry');
log.debug('starting');
var app = express();
var server = http.createServer(app);
// Initialize Sentry
const sentryConfig = config.get('sentry');
if (sentryConfig.dsn) {
const release = require('../package.json').version;
const opts = buildSentryConfig(
{
sentry: sentryConfig,
release,
},
log
);
Sentry.init({
...opts,
beforeSend(event, _hint) {
event = tagCriticalEvent(event);
event = tagFxaName(event, opts.serverName);
return event;
},
});
Sentry.setupExpressErrorHandler(app);
}
var verifier = new CCVerifier({
httpTimeout: config.get('httpTimeout'),
insecureSSL: config.get('insecureSSL'),
forceInsecureLookupOverHTTP: config.get('forceInsecureLookupOverHTTP'),
testServiceFailure: config.get('testServiceFailure'),
});
// handle shutdown
function shutdown(signal) {
return function () {
log.info('shutdown', { signal });
toobusy.shutdown();
verifier.shutdown();
server.close();
};
}
['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach(function (signal) {
process.on(signal, shutdown(signal.substr(3)));
});
// header manipulation
app.use(function (req, res, next) {
// no caching allowed, this is an API server.
res.setHeader(
'Cache-Control',
'private, no-cache, no-store, must-revalidate, max-age=0'
);
// security headers
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Strict-Transport-Security', 'max-age=31536000');
res.setHeader(
'Content-Security-Policy',
"default-src 'none'; frame-ancestors 'none'; report-uri /__cspreport__"
);
// shave some needless bytes
res.removeHeader('X-Powered-By');
res.setHeader('Connection', 'close');
next();
});
// health checks - registered before all other middleware.
app.use(function (req, res, next) {
switch (req.url) {
case '/status':
res.setHeader('Content-Type', 'text/plain');
res.send('OK');
break;
case '/__heartbeat__':
case '/__lbheartbeat__':
res.send({});
break;
case '/__version__':
version.getVersionInfo(function (info) {
res.send(info);
});
break;
default:
next();
}
});
// return 503 when the server is too busy
toobusy.maxLag(config.get('toobusy.maxLag'));
app.use(function (req, res, next) {
if (toobusy()) {
log.warn('tooBusy');
res.json(503, { status: 'failure', reason: 'too busy' });
} else {
next();
}
});
// log HTTP requests
app.use(
morgan('common', {
stream: {
write: function (message) {
// trim newlines as our logger inserts them for us.
if (typeof message === 'string') {
message = message.trim();
}
log.info('message', { message });
},
},
})
);
// log summary - GH24
app.use(summary());
app.use(bodyParser.json({ limit: '10kb' }));
app.use(bodyParser.urlencoded({ limit: '10kb' }));
app.post('/verify', v1api.bind(v1api, verifier));
app.post('/', v1api.bind(v1api, verifier));
app.post('/v2', v2api.bind(v2api, verifier));
function wrongMethod(req, res) {
return res.sendStatus(405);
}
['/verify', '/', '/v2'].forEach(function (route) {
app.get(route, wrongMethod);
});
if (sentryConfig.dsn) {
// Send errors to sentry.
app.use(Sentry.Handlers.errorHandler());
}
// error handler goes last, to receive any errors from previous middleware
app.use(function (err, req, res, next) {
if (err) {
if (err.status) {
res.statusCode = err.status;
} else {
res.statusCode = 500;
log.error(err);
}
res.end();
}
next();
});
server.listen(config.get('port'), config.get('ip'), function () {
log.info('running', {
url: 'http://' + server.address().address + ':' + server.address().port,
});
});

View File

@@ -1,34 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const logger = require('./log')('summary');
module.exports = function middlewareFactory() {
return function summary(req, res, next) {
function log() {
res.removeListener('finish', log);
res.removeListener('close', log);
var summary = res._summary;
summary.code = res.statusCode;
logger.info('info', summary);
}
res._summary = {};
// Add useful request-level info to the summary automatically.
res._summary.agent = req.headers['user-agent'] || '';
var xff = (req.headers['x-forwarded-for'] || '').split(/\s*,\s*/);
xff.push(req.connection.remoteAddress);
res._summary.remoteAddressChain = xff.filter(function (x) {
return x;
});
res.on('finish', log);
res.on('close', log);
next();
};
};

View File

@@ -1,129 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const log = require('./log')('v1'),
config = require('./config'),
_ = require('underscore'),
util = require('util');
function verify(verifier, req, res) {
req.query = req.query || {};
req.body = req.body || {};
res._summary.api = 1;
var assertion = req.query.assertion
? req.query.assertion
: req.body.assertion;
var audience = req.query.audience ? req.query.audience : req.body.audience;
var forceIssuer = req.query.experimental_forceIssuer
? req.query.experimental_forceIssuer
: req.body.experimental_forceIssuer;
var allowUnverified = req.query.experimental_allowUnverified
? req.query.experimental_allowUnverified
: req.body.experimental_allowUnverified;
res._summary.rp = audience;
if (!(assertion && audience)) {
// why couldn't we extract these guys? Is it because the request parameters weren't encoded as we expect? GH-643
const want_ct = ['application/x-www-form-urlencoded', 'application/json'];
var reason;
var ct = 'none';
try {
ct = req.headers['content-type'] || ct;
if (ct.indexOf(';') !== -1) {
ct = ct.substr(0, ct.indexOf(';'));
}
if (want_ct.indexOf(ct) === -1) {
throw new Error('wrong content type');
}
} catch (e) {
reason = util.format(
'Unsupported Content-Type: %s (expected ' + want_ct.join(' or ') + ')',
ct
);
log.info('verify', {
result: 'failure',
reason: reason,
rp: audience,
});
res._summary.err = e;
return res.json(415, { status: 'failure', reason: reason });
}
reason = util.format(
'missing %s parameter',
assertion ? 'audience' : 'assertion'
);
log.info('verify', {
result: 'failure',
reason: reason,
rp: audience,
});
res._summary.err = reason;
return res.json(400, { status: 'failure', reason: reason });
}
var trustedIssuers = [];
if (forceIssuer) {
trustedIssuers.push(forceIssuer);
}
var startTime = new Date();
verifier.verify(
{
assertion: assertion,
audience: audience,
trustedIssuers: trustedIssuers,
fallback: config.get('fallback'),
},
function (err, r) {
var reqTime = new Date() - startTime;
log.info('assertion_verification_time', { reqTime });
res._summary.assertion_verification_time = reqTime;
if (err) {
if (typeof err !== 'string') {
err = 'unexpected error';
}
if (err.indexOf('compute cluster') === 0) {
log.info('service_failure');
res.json(503, { status: 'failure', reason: 'service unavailable' });
} else {
log.info('assertion_failure');
res.json(200, { status: 'failure', reason: err }); //Could be 500 or 200 OK if invalid cert
}
res._summary.err = err;
log.info('verify', {
result: 'failure',
reason: err,
assertion: assertion,
trustedIssuers: trustedIssuers,
rp: audience,
});
} else {
if (allowUnverified) {
if (r.idpClaims && r.idpClaims['unverified-email']) {
r['unverified-email'] = r.idpClaims['unverified-email'];
}
}
res.json(
_.extend(r, {
status: 'okay',
audience: audience, // NOTE: we return the audience formatted as the RP provided it, not normalized in any way.
expires: new Date(r.expires).valueOf(),
})
);
log.info('verify', {
result: 'success',
rp: r.audience,
});
}
}
);
}
module.exports = verify;

View File

@@ -1,129 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const log = require('./log')('v2'),
config = require('./config'),
_ = require('underscore'),
util = require('util');
function validateTrustedIssuers(obj) {
var ti = obj.trustedIssuers;
if (!ti) {
return [];
}
if (!_.isArray(ti)) {
throw {
reason: 'trusted issuers must be an array',
code: 400,
};
}
ti.forEach(function (hostname) {
if (typeof hostname !== 'string') {
throw {
reason: 'trusted issuers must be an array of strings',
code: 400,
};
}
});
return ti;
}
function verify(verifier, req, res) {
res._summary.api = 2;
try {
// content-type must be application/json
var ct = req.headers['content-type'] || 'none';
if (ct.indexOf('application/json') !== 0) {
throw {
reason: util.format(
'Unsupported Content-Type: %s (expected application/json)',
ct
),
code: 415,
};
}
req.body = req.body || {};
res._summary.rp = req.body.audience;
// assertion and audience are required
['assertion', 'audience'].forEach(function (field) {
if (!req.body[field]) {
throw {
reason: util.format('missing %s parameter', field),
code: 400,
};
}
});
// validate and extract trusted issuers
var trustedIssuers = validateTrustedIssuers(req.body);
var startTime = new Date();
verifier.verify(
{
assertion: req.body.assertion,
audience: req.body.audience,
trustedIssuers: trustedIssuers,
fallback: config.get('fallback'),
},
function (err, r) {
var reqTime = new Date() - startTime;
log.info('assertion_verification_time', { reqTime });
res._summary.assertion_verification_time = reqTime;
if (err) {
if (typeof err !== 'string') {
err = 'unexpected error';
}
if (err.indexOf('compute cluster') === 0) {
log.info('service_failure');
res.json(503, { status: 'failure', reason: 'service unavailable' });
} else {
log.info('assertion_failure');
res.json(200, { status: 'failure', reason: err }); //Could be 500 or 200 OK if invalid cert
}
res._summary.err = err;
log.info('verify', {
result: 'failure',
reason: err,
assertion: req.body.assertion,
trustedIssuers: trustedIssuers,
rp: req.body.audience,
});
} else {
res.json(
_.extend(r, {
status: 'okay',
audience: req.body.audience, // NOTE: we return the audience formatted as the RP provided it, not normalized in any way.
expires: new Date(r.expires).valueOf(),
})
);
log.info('verify', {
result: 'success',
rp: r.audience,
});
}
}
);
} catch (err) {
var reason = err.reason ? err.reason : err.toString();
res._summary.err = reason;
log.info('verify', {
result: 'failure',
reason: reason,
rp: req.body.audience,
});
res.json(err.code ? err.code : 500, {
status: 'failure',
reason: reason,
});
}
}
module.exports = verify;

View File

@@ -1,71 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const path = require('path'),
cp = require('child_process');
const UNKNOWN = 'unknown';
// For production builds, we write version into to a json
// file for easy reporting in /__version__ endpoint.
var commitHash;
var sourceRepo;
const version = require('../package.json').version;
try {
var versionJson = path.join(__dirname, '..', 'version.json');
var info = require(versionJson);
commitHash = info.version.hash;
sourceRepo = info.version.source;
} catch (e) {
/* ignore */
}
module.exports = {
getVersionInfo: function getVersionInfo(cb) {
if (commitHash) {
return cb({
version: version,
commit: commitHash,
source: sourceRepo,
});
}
// ignore errors and default to 'unknown' if not found
var gitDir = path.resolve(__dirname, '..', '..', '..', '.git');
cp.exec('git rev-parse HEAD', { cwd: gitDir }, function (err, stdout1) {
if (err != null) {
console.error('Error getting git commit hash: ' + err.message);
return cb({
version: version,
commit: UNKNOWN,
source: UNKNOWN,
});
}
var configPath = path.join(gitDir, 'config');
var cmd = 'git config --get remote.origin.url';
cp.exec(
cmd,
{ env: { GIT_CONFIG: configPath } },
function (err, stdout2) {
if (err != null) {
console.error('Error getting git config: ' + err.message);
return cb({
version: version,
commit: UNKNOWN,
source: UNKNOWN,
});
}
commitHash = (stdout1 && stdout1.trim()) || UNKNOWN;
sourceRepo = (stdout2 && stdout2.trim()) || UNKNOWN;
return cb({
version: version,
commit: commitHash,
source: sourceRepo,
});
}
);
});
},
};

View File

@@ -1,36 +0,0 @@
SERVER_URL = https://verifier.stage.mozaws.net
# Hackety-hack around OSX system python bustage.
# The need for this should go away with a future osx/xcode update.
ARCHFLAGS = -Wno-error=unused-command-line-argument-hard-error-in-future
INSTALL = ARCHFLAGS=$(ARCHFLAGS) ./venv/bin/pip install
.PHONY: build clean test bench megabench
# Build virtualenv, to ensure we have all the dependencies.
build:
virtualenv --no-site-packages ./venv
$(INSTALL) pexpect
$(INSTALL) gevent
$(INSTALL) https://github.com/mozilla-services/loads/archive/master.zip
$(INSTALL) PyBrowserID
# Clean all the things installed by `make build`.
clean:
rm -rf ./venv *.pyc
# Run a single test from the venv machine, for sanity-checking.
test:
./venv/bin/loads-runner --config=./config/test.ini --server-url=$(SERVER_URL) loadtest.VerifierLoadTest.test_verifier
# Run a bench of 20 concurrent users.
bench:
./venv/bin/loads-runner --config=./config/bench.ini --server-url=$(SERVER_URL) loadtest.VerifierLoadTest.test_verifier
# Run a much bigger bench, by submitting to broker in AWS.
megabench:
./venv/bin/loads-runner --config=./config/megabench.ini --user-id=$(USER) --server-url=$(SERVER_URL) loadtest.VerifierLoadTest.test_verifier
# Purge any currently-running loadtest runs.
purge:
./venv/bin/loads-runner --config=./config/megabench.ini --purge-broker

View File

@@ -1,26 +0,0 @@
This directory contains some very simple loadtests, written using
the "loads" framework:
https://github.com/mozilla/loads
To run them, you will need the following dependencies:
* Python development files (e.g. python-dev or python-devel package)
* Virtualenv (e.g. python-virtualenv package)
* ZeroMQ development files (e.g. libzmq-dev package)
* (for megabench) ssh access to the mozilla loads cluster
Then do the following:
$> make build # installs local environment with all dependencies
$> make test # runs a single test, to check that everything's working
$> make bench # runs a longer, higher-concurrency test.
$> make megabench # runs a really-long, really-high-concurrency test
# using https://loads.services.mozilla.com
To hit a specific server you can specify the SERVER_URL make variable, like
this:
$> make test SERVER_URL=https://verifier.stage.mozaws.net

View File

@@ -1,3 +0,0 @@
[loads]
users = 20
duration = 300

View File

@@ -1,9 +0,0 @@
[loads]
users = 20
duration = 1800
include_file = ./loadtest.py
python_dep = PyBrowserID
agents = 5
detach = true
observer = irc
ssh = ubuntu@loads.services.mozilla.com

View File

@@ -1,3 +0,0 @@
[loads]
hits = 1
users = 1

View File

@@ -1,129 +0,0 @@
import json
import time
import random
import browserid
import browserid.jwt
from browserid.tests.support import make_assertion
from loads import TestCase
PERCENT_INVALID_REQUESTS = 5
ONE_YEAR = 60 * 60 * 24 * 365
MOCKMYID_DOMAIN = "mockmyid.s3-us-west-2.amazonaws.com"
MOCKMYID_PRIVATE_KEY = browserid.jwt.DS128Key({
"algorithm": "DS",
"x": "385cb3509f086e110c5e24bdd395a84b335a09ae",
"y": "738ec929b559b604a232a9b55a5295afc368063bb9c20fac4e53a74970a4db795"
"6d48e4c7ed523405f629b4cc83062f13029c4d615bbacb8b97f5e56f0c7ac9bc1"
"d4e23809889fa061425c984061fca1826040c399715ce7ed385c4dd0d40225691"
"2451e03452d3c961614eb458f188e3e8d2782916c43dbe2e571251ce38262",
"p": "ff600483db6abfc5b45eab78594b3533d550d9f1bf2a992a7a8daa6dc34f8045a"
"d4e6e0c429d334eeeaaefd7e23d4810be00e4cc1492cba325ba81ff2d5a5b305a"
"8d17eb3bf4a06a349d392e00d329744a5179380344e82a18c47933438f891e22a"
"eef812d69c8f75e326cb70ea000c3f776dfdbd604638c2ef717fc26d02e17",
"q": "e21e04f911d1ed7991008ecaab3bf775984309c3",
"g": "c52a4a0ff3b7e61fdf1867ce84138369a6154f4afa92966e3c827e25cfa6cf508b"
"90e5de419e1337e07a2e9e2a3cd5dea704d175f8ebf6af397d69e110b96afb17c7"
"a03259329e4829b0d03bbc7896b15b4ade53e130858cc34d96269aa89041f40913"
"6c7242a38895c9d5bccad4f389af1d7a4bd1398bd072dffa896233397a",
})
INVALID_PRIVATE_KEY = browserid.jwt.DS128Key({
"algorithm": "DS",
"x": "abcdef0123456789abcdef0123456789abcdef01",
"y": "738ec929b559b604a232a9b55a5295afc368063bb9c20fac4e53a74970a4db795"
"6d48e4c7ed523405f629b4cc83062f13029c4d615bbacb8b97f5e56f0c7ac9bc1"
"d4e23809889fa061425c984061fca1826040c399715ce7ed385c4dd0d40225691"
"2451e03452d3c961614eb458f188e3e8d2782916c43dbe2e571251ce38262",
"p": "ff600483db6abfc5b45eab78594b3533d550d9f1bf2a992a7a8daa6dc34f8045a"
"d4e6e0c429d334eeeaaefd7e23d4810be00e4cc1492cba325ba81ff2d5a5b305a"
"8d17eb3bf4a06a349d392e00d329744a5179380344e82a18c47933438f891e22a"
"eef812d69c8f75e326cb70ea000c3f776dfdbd604638c2ef717fc26d02e17",
"q": "e21e04f911d1ed7991008ecaab3bf775984309c3",
"g": "c52a4a0ff3b7e61fdf1867ce84138369a6154f4afa92966e3c827e25cfa6cf508b"
"90e5de419e1337e07a2e9e2a3cd5dea704d175f8ebf6af397d69e110b96afb17c7"
"a03259329e4829b0d03bbc7896b15b4ade53e130858cc34d96269aa89041f40913"
"6c7242a38895c9d5bccad4f389af1d7a4bd1398bd072dffa896233397a",
})
class VerifierLoadTest(TestCase):
server_url = "https://verifier.stage.mozaws.net"
def _make_assertion(self, email=None, audience=None, **kwds):
if email is None:
email = "user%d@%s" % (random.randint(0, 1000000), MOCKMYID_DOMAIN)
if audience is None:
audience = "https://secret.mozilla.com"
if "exp" not in kwds:
kwds["exp"] = int((time.time() + ONE_YEAR) * 1000)
if "issuer" not in kwds:
kwds["issuer"] = MOCKMYID_DOMAIN
if "issuer_keypair" not in kwds:
kwds["issuer_keypair"] = (None, MOCKMYID_PRIVATE_KEY)
return make_assertion(email, audience, **kwds)
def _verify_assertion(self, assertion, audience=None, **kwds):
if assertion is None:
assertion = self._make_assertion()
if audience is None:
audience = "https://secret.mozilla.com"
body = {
"assertion": assertion,
"audience": audience,
}
body.update(kwds)
body = json.dumps(body)
r = self.session.post(self.server_url + "/v2", body, headers={
"Content-Type": "application/json"
})
self.assertEquals(r.status_code, 200)
return json.loads(r.content)
def test_verifier(self):
if random.randint(0, 99) < PERCENT_INVALID_REQUESTS:
self.test_invalid_assertion()
else:
self.test_valid_assertion()
def test_valid_assertion(self):
assertion = self._make_assertion()
data = self._verify_assertion(assertion)
self.assertEquals(data["status"], "okay")
def test_invalid_assertion(self):
# Randomly pick and run a _test_invalid_assertion_<foo>() method.
tests = []
for nm in dir(self):
if nm.startswith("_test_invalid_assertion"):
tests.append(getattr(self, nm))
random.choice(tests)()
def _test_invalid_assertion_nonprimary(self):
assertion = self._make_assertion("test@mozilla.com")
data = self._verify_assertion(assertion)
self.assertEquals(data["status"], "failure")
def _test_invalid_assertion_expired(self):
exp = int(time.time() - ONE_YEAR) * 1000,
assertion = self._make_assertion(exp=exp)
data = self._verify_assertion(assertion)
self.assertEquals(data["status"], "failure")
def _test_invalid_assertion_wrongissuer(self):
assertion = self._make_assertion(issuer="login.mozilla.org")
data = self._verify_assertion(assertion)
self.assertEquals(data["status"], "failure")
def _test_invalid_assertion_wrongkey(self):
issuer_keypair = (None, INVALID_PRIVATE_KEY)
assertion = self._make_assertion(issuer_keypair=issuer_keypair)
data = self._verify_assertion(assertion)
self.assertEquals(data["status"], "failure")

View File

@@ -1,57 +0,0 @@
{
"author": "Mozilla (https://mozilla.org/)",
"license": "MPL-2.0",
"name": "browserid-verifier",
"description": "A node.js verification server for BrowserID assertions.",
"version": "0.0.0",
"repository": {
"type": "git",
"url": "https://github.com/mozilla/fxa.git"
},
"homepage": "https://github.com/mozilla/fxa/tree/main/packages/browserid-verifier/",
"bugs": "https://github.com/mozilla/fxa/issues/",
"main": "lib/server.js",
"dependencies": {
"async": "3.2.4",
"body-parser": "^1.20.3",
"browserid-local-verify": "0.5.2",
"compute-cluster": "0.0.9",
"convict": "^6.2.4",
"convict-format-with-moment": "^6.2.0",
"convict-format-with-validator": "^6.2.0",
"express": "^4.21.2",
"intel": "1.2.0",
"morgan": "^1.10.0",
"mozlog": "^3.0.2",
"optimist": "0.6.1",
"toobusy-js": "0.5.1",
"underscore": "^1.13.1"
},
"devDependencies": {
"audit-filter": "0.5.0",
"eslint": "^7.32.0",
"fxa-shared": "workspace:*",
"mocha": "^10.4.0",
"pm2": "^5.4.2",
"prettier": "^3.5.3",
"request": "^2.88.2",
"should": "13.2.3",
"temp": "0.9.4",
"walk": "^2.3.15"
},
"scripts": {
"audit": "npm audit --json | audit-filter --nsp-config=.nsprc --audit=-",
"lint": "eslint .",
"test": "mocha --exit -t 5000 -R spec tests/*.js",
"format": "prettier --write --config ../../_dev/.prettierrc '**'",
"start": "pm2 start pm2.config.js",
"stop": "pm2 stop pm2.config.js",
"restart": "pm2 restart pm2.config.js",
"delete": "pm2 delete pm2.config.js"
},
"nx": {
"tags": [
"scope:server"
]
}
}

View File

@@ -1,29 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const PATH = process.env.PATH.split(':')
.filter((p) => !p.includes(process.env.TMPDIR))
.join(':');
module.exports = {
apps: [
{
name: 'browserid',
script: 'node server.js',
cwd: __dirname,
env: {
PATH,
PORT: '5050',
IP_ADDRESS: '0.0.0.0',
FORCE_INSECURE_LOOKUP_OVER_HTTP: 'true',
SENTRY_ENV: 'local',
SENTRY_DSN: process.env.SENTRY_DSN_BROWSERID,
},
filter_env: ['npm_'],
max_restarts: '1',
min_uptime: '2m',
time: true,
},
],
};

View File

@@ -1,7 +0,0 @@
#!/usr/bin/env node
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
require('./lib/server.js');

View File

@@ -1,102 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global describe,it,before,after */
var IdP = require('browserid-local-verify/testing').IdP,
Client = require('browserid-local-verify/testing').Client,
Verifier = require('./lib/verifier.js'),
should = require('should'),
shouldReturnSecurityHeaders = require('./lib/should-return-security-headers.js'),
request = require('request');
describe('audience tests', function () {
var verifier = new Verifier();
var idp = new IdP();
var client;
before(async () => {
await new Promise((resolve) => idp.start(resolve));
await new Promise((resolve) => verifier.start(resolve));
client = new Client({ idp: idp });
});
after(async () => {
await new Promise((resolve) => verifier.stop(resolve));
await new Promise((resolve) => idp.stop(resolve));
});
var assertion;
it('test assertion should be created', function (done) {
client.assertion({ audience: 'http://example.com' }, function (err, ass) {
assertion = ass;
done(err);
});
});
function submitWithAudience(audience, cb) {
request(
{
method: 'post',
url: verifier.url(),
json: true,
body: {
assertion: assertion,
audience: audience,
},
},
cb
);
}
it('should verify with complete audience', function (done) {
submitWithAudience('http://example.com', function (err, r) {
should.not.exist(err);
'okay'.should.equal(r.body.status);
shouldReturnSecurityHeaders(r);
done();
});
});
it('should fail to verify with different domain as audience', function (done) {
submitWithAudience('incorrect.com', function (err, r) {
should.not.exist(err);
r.body.status.should.equal('failure');
r.body.reason.should.equal('audience mismatch: domain mismatch');
shouldReturnSecurityHeaders(r);
done(err);
});
});
it('should fail to verify with different port', function (done) {
submitWithAudience('http://example.com:8080', function (err, r) {
should.not.exist(err);
r.body.status.should.equal('failure');
r.body.reason.should.equal('audience mismatch: port mismatch');
shouldReturnSecurityHeaders(r);
done(err);
});
});
it('should fail to verify with incorrect scheme', function (done) {
submitWithAudience('https://example.com', function (err, r) {
should.not.exist(err);
r.body.status.should.equal('failure');
r.body.reason.should.equal('audience mismatch: scheme mismatch');
shouldReturnSecurityHeaders(r);
done(err);
});
});
it('should fail to verify if audience is missing', function (done) {
submitWithAudience(undefined, function (err, r) {
should.not.exist(err);
r.body.status.should.equal('failure');
r.body.reason.should.equal('missing audience parameter');
shouldReturnSecurityHeaders(r);
done(err);
});
});
});

View File

@@ -1,91 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global describe,it,before,after */
var IdP = require('browserid-local-verify/testing').IdP,
Client = require('browserid-local-verify/testing').Client,
Verifier = require('./lib/verifier.js'),
should = require('should'),
shouldReturnSecurityHeaders = require('./lib/should-return-security-headers.js'),
request = require('request');
describe('basic verifier test', function () {
var idp = new IdP();
var verifier = new Verifier();
before(async () => {
await new Promise((resolve) => idp.start(resolve));
await new Promise((resolve) => verifier.start(resolve));
});
after(async () => {
await new Promise((resolve) => verifier.stop(resolve));
await new Promise((resolve) => idp.stop(resolve));
});
it('should verify an assertion', function (done) {
var client = new Client({ idp: idp });
client.assertion(
{ audience: 'http://example.com' },
function (err, assertion) {
should.not.exist(err);
request(
{
method: 'post',
url: verifier.url(),
json: true,
body: {
assertion: assertion,
audience: 'http://example.com',
},
},
function (err, r) {
should.not.exist(err);
r.body.email.should.equal(client.email());
r.body.issuer.should.equal(idp.domain());
r.body.status.should.equal('okay');
r.body.audience.should.equal('http://example.com');
r.statusCode.should.equal(200);
shouldReturnSecurityHeaders(r);
done();
}
);
}
);
});
it('should return 405 for GET requests', function (done) {
request(
{
method: 'get',
url: verifier.url(),
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(405);
shouldReturnSecurityHeaders(r);
done();
}
);
});
it('should handle any errors', function (done) {
request(
{
method: 'post',
url: verifier.url(),
body: "{ 'a' }",
headers: { 'content-type': 'application/json' },
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(400);
shouldReturnSecurityHeaders(r);
done();
}
);
});
});

View File

@@ -1,60 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global describe,it */
const should = require('should');
var Verifier = require('./lib/verifier.js'),
async = require('async'),
temp = require('temp'),
fs = require('fs');
describe('cascading configuration files', function () {
var verifier;
it('create a couple configuration files', function (done) {
var aPath = temp.path({ suffix: '.json' }),
bPath = temp.path({ suffix: '.json' });
// because "b" is specified later, it should over-ride "a"
verifier = new Verifier({
files: aPath + ',' + bPath,
});
async.parallel(
[
function (cb) {
fs.writeFile(
aPath,
JSON.stringify({ fallback: 'a.example.com' }),
cb
);
},
function (cb) {
fs.writeFile(
bPath,
JSON.stringify({ fallback: 'b.example.com' }),
cb
);
},
],
done
);
});
it('test servers should start and finish cleanly', async () => {
verifier.buffer(true);
should(verifier.process).equal(undefined);
await new Promise((resolve, error) => verifier.start(resolve));
should(verifier.process).not.equal(null);
await new Promise((resolve, error) => verifier.stop(resolve));
should(verifier.process).equal(null);
});
it('verifier should have determined proper configuration', () => {
verifier.buffer().indexOf('a.example.com').should.equal(-1);
verifier.buffer().indexOf('b.example.com').should.not.equal(-1);
});
});

View File

@@ -1,228 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global describe,it,before,after */
var IdP = require('browserid-local-verify/testing').IdP,
Client = require('browserid-local-verify/testing').Client,
Verifier = require('./lib/verifier.js'),
should = require('should'),
shouldReturnSecurityHeaders = require('./lib/should-return-security-headers.js'),
request = require('request');
describe('content-type tests', function () {
var verifier = new Verifier();
var idp = new IdP();
var client;
before(async () => {
await new Promise((resolve) => idp.start(resolve));
await new Promise((resolve) => verifier.start(resolve));
client = new Client({ idp: idp });
});
after(async () => {
await new Promise((resolve) => verifier.stop(resolve));
await new Promise((resolve) => idp.stop(resolve));
});
var assertion;
it('test assertion should be created', function (done) {
client.assertion({ audience: 'http://example.com' }, function (err, ass) {
assertion = ass;
done(err);
});
});
it('(v2 api) should fail to verify when content-type is unsupported', function (done) {
request(
{
method: 'post',
url: verifier.url(),
headers: {
'Content-Type': 'text/plain',
},
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(415);
(function () {
r.body = JSON.parse(r.body);
}.should.not.throw());
r.body.status.should.equal('failure');
r.body.reason.should.startWith('Unsupported Content-Type: text/plain');
shouldReturnSecurityHeaders(r);
done();
}
);
});
it('(v2 api) should fail to verify when content-type is not specified', function (done) {
request(
{
method: 'post',
url: verifier.url(),
headers: {},
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(415);
(function () {
r.body = JSON.parse(r.body);
}.should.not.throw());
r.body.status.should.equal('failure');
r.body.reason.should.startWith('Unsupported Content-Type: none');
shouldReturnSecurityHeaders(r);
done();
}
);
});
it('(v1 api) should fail to verify when content-type is unsupported', function (done) {
request(
{
method: 'post',
url: verifier.v1url(),
headers: {
'Content-Type': 'text/plain',
},
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(415);
(function () {
r.body = JSON.parse(r.body);
}.should.not.throw());
r.body.status.should.equal('failure');
r.body.reason.should.startWith('Unsupported Content-Type: text/plain');
shouldReturnSecurityHeaders(r);
done();
}
);
});
it('(v1 api) should fail to verify when content-type is not specified', function (done) {
request(
{
method: 'post',
url: verifier.v1url(),
headers: {},
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(415);
(function () {
r.body = JSON.parse(r.body);
}.should.not.throw());
r.body.status.should.equal('failure');
r.body.reason.should.startWith('Unsupported Content-Type: none');
shouldReturnSecurityHeaders(r);
done();
}
);
});
it("(v2 api) shouldn't get confused when extended content-types are used", function (done) {
request(
{
method: 'post',
url: verifier.url(),
headers: {
'Content-Type': 'application/json; charset: utf-8',
},
body: JSON.stringify({
assertion: assertion,
}),
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(400);
(function () {
r.body = JSON.parse(r.body);
}.should.not.throw());
r.body.status.should.equal('failure');
shouldReturnSecurityHeaders(r);
done();
}
);
});
it("(v1 api) shouldn't get confused when extended content-types are used", function (done) {
request(
{
method: 'post',
url: verifier.v1url(),
headers: {
'Content-Type': 'application/json; charset: utf-8',
},
body: JSON.stringify({
assertion: assertion,
}),
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(400);
(function () {
r.body = JSON.parse(r.body);
}.should.not.throw());
r.body.status.should.equal('failure');
shouldReturnSecurityHeaders(r);
done();
}
);
});
it("(v2 api) shouldn't support x-www-form-urlencoded", function (done) {
request(
{
method: 'post',
url: verifier.url(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: require('querystring').stringify({
assertion: assertion,
}),
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(415);
(function () {
r.body = JSON.parse(r.body);
}.should.not.throw());
r.body.status.should.equal('failure');
r.body.reason.should.startWith('Unsupported Content-Type');
shouldReturnSecurityHeaders(r);
done();
}
);
});
it('(v1 api) should support x-www-form-urlencoded', function (done) {
request(
{
method: 'post',
url: verifier.v1url(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: require('querystring').stringify({
assertion: assertion,
}),
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(400);
(function () {
r.body = JSON.parse(r.body);
}.should.not.throw());
r.body.status.should.equal('failure');
r.body.reason.should.startWith('missing audience');
shouldReturnSecurityHeaders(r);
done();
}
);
});
});

View File

@@ -1,111 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global describe,it,before,after */
var IdP = require('browserid-local-verify/testing').IdP,
Client = require('browserid-local-verify/testing').Client,
Verifier = require('./lib/verifier.js'),
should = require('should'),
shouldReturnSecurityHeaders = require('./lib/should-return-security-headers.js'),
request = require('request');
describe('fallback configuration test', function () {
var idp = new IdP();
var verifier = new Verifier();
before(async function () {
this.timeout(10000);
await new Promise((resolve) => idp.start(resolve));
verifier.setFallback(idp);
await new Promise((resolve) => verifier.start(resolve));
});
after(async () => {
await new Promise((resolve) => verifier.stop(resolve));
await new Promise((resolve) => idp.stop(resolve));
});
it('should verify an assertion vouched by the configured fallback', function (done) {
var client = new Client({
idp: idp,
email: 'lloyd@nonidp.example.com',
});
client.assertion(
{
audience: 'http://rp.example.com',
},
function (err, assertion) {
should.not.exist(err);
request(
{
method: 'post',
url: verifier.url(),
json: true,
body: {
assertion: assertion,
audience: 'http://rp.example.com',
},
},
function (err, r) {
should.not.exist(err);
r.body.status.should.equal('okay');
r.body.email.should.equal(client.email());
r.body.issuer.should.equal(idp.domain());
r.body.audience.should.equal('http://rp.example.com');
r.statusCode.should.equal(200);
shouldReturnSecurityHeaders(r);
done();
}
);
}
);
});
it('verifier should restart with cleared fallback', function (done) {
verifier.setFallback(null);
verifier.stop(function (e) {
verifier.start(function (e1) {
done(e || e1);
});
});
});
it('should fail to verify an assertion when fallback is not configured', function (done) {
var client = new Client({
idp: idp,
email: 'lloyd@nonidp.example.com',
});
client.assertion(
{
audience: 'http://rp.example.com',
},
function (err, assertion) {
should.not.exist(err);
request(
{
method: 'post',
url: verifier.url(),
json: true,
body: {
assertion: assertion,
audience: 'http://rp.example.com',
},
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(200);
r.body.status.should.equal('failure');
// XXX: better error message
r.body.reason.should.startWith('untrusted issuer');
shouldReturnSecurityHeaders(r);
done();
}
);
}
);
});
});

View File

@@ -1,131 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global describe,it,before,after */
var IdP = require('browserid-local-verify/testing').IdP,
Client = require('browserid-local-verify/testing').Client,
Verifier = require('./lib/verifier.js'),
should = require('should'),
shouldReturnSecurityHeaders = require('./lib/should-return-security-headers.js'),
request = require('request');
describe('force issuer', function () {
var idp = new IdP();
var fallback = new IdP();
var client;
var verifier = new Verifier();
before(async () => {
await new Promise((resolve, error) => idp.start(resolve));
await new Promise((resolve, error) => fallback.start(resolve));
verifier.setFallback(idp);
await new Promise((resolve, error) => verifier.start(resolve));
});
after(async () => {
await new Promise((resolve, error) => verifier.stop(resolve));
await new Promise((resolve, error) => fallback.stop(resolve));
await new Promise((resolve, error) => idp.stop(resolve));
});
it('assertion by fallback when primary support is present should fail', function (done) {
// user has an email from idp, but fallback will be used for certificate
client = new Client({
idp: fallback,
email: 'user@' + idp.domain(),
});
client.assertion(
{ audience: 'http://example.com' },
function (err, assertion) {
request(
{
method: 'post',
url: verifier.url(),
json: true,
body: {
assertion: assertion,
audience: 'http://example.com',
},
},
function (_, r) {
should.not.exist(err);
r.statusCode.should.equal(200);
r.body.status.should.equal('failure');
r.body.reason.should.startWith('untrusted issuer');
shouldReturnSecurityHeaders(r);
done();
}
);
}
);
});
it('(v1) forceIssuer should over-ride authority discovery', function (done) {
// user has an email from idp, but fallback will be used for certificate
client = new Client({
idp: fallback,
email: 'user@' + idp.domain(),
});
client.assertion(
{ audience: 'http://example.com' },
function (_, assertion) {
request(
{
method: 'post',
url: verifier.v1url(),
json: true,
body: {
assertion: assertion,
audience: 'http://example.com',
experimental_forceIssuer: fallback.domain(),
},
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(200);
r.body.status.should.equal('okay');
shouldReturnSecurityHeaders(r);
done();
}
);
}
);
});
it('(v2) trustedIssuers should over-ride authority discovery', function (done) {
// user has an email from idp, but fallback will be used for certificate
client = new Client({
idp: fallback,
email: 'user@' + idp.domain(),
});
client.assertion(
{ audience: 'http://example.com' },
function (_, assertion) {
request(
{
method: 'post',
url: verifier.url(),
json: true,
body: {
assertion: assertion,
audience: 'http://example.com',
trustedIssuers: [fallback.domain()],
},
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(200);
r.body.status.should.equal('okay');
shouldReturnSecurityHeaders(r);
done();
}
);
}
);
});
});

View File

@@ -1,99 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global describe,it,before,after */
require('should');
var Verifier = require('./lib/verifier.js'),
shouldReturnSecurityHeaders = require('./lib/should-return-security-headers.js'),
request = require('request');
describe('health check', function () {
var verifier = new Verifier();
before(async () => {
await new Promise((resolve) => verifier.start(resolve));
});
after(async () => {
await new Promise((resolve) => verifier.stop(resolve));
});
it('health check should return OK', function (done) {
request(
{
url: verifier.baseurl() + '/status',
},
function (err, r) {
r.statusCode.should.equal(200);
r.body.should.equal('OK');
shouldReturnSecurityHeaders(r);
done(err);
}
);
});
it('__heartbeat__ should return success', function (done) {
request(
{
url: verifier.baseurl() + '/__heartbeat__',
},
function (err, r) {
r.statusCode.should.equal(200);
r.body.should.equal('{}');
shouldReturnSecurityHeaders(r);
done(err);
}
);
});
it('__lbheartbeat__ should return success', function (done) {
request(
{
url: verifier.baseurl() + '/__lbheartbeat__',
},
function (err, r) {
r.statusCode.should.equal(200);
r.body.should.equal('{}');
shouldReturnSecurityHeaders(r);
done(err);
}
);
});
it('__version__ should return version info', function (done) {
request(
{
url: verifier.baseurl() + '/__version__',
},
function (err, r) {
r.statusCode.should.equal(200);
var obj = JSON.parse(r.body);
obj.version.should.match(/^[0-9.]+$/);
obj.commit.should.match(/^[a-z0-9]{40}$/);
obj.source.should.be.a.String();
shouldReturnSecurityHeaders(r);
done(err);
}
);
});
it('__version__ should return version info (cached)', function (done) {
request(
{
url: verifier.baseurl() + '/__version__',
},
function (err, r) {
r.statusCode.should.equal(200);
var obj = JSON.parse(r.body);
obj.version.should.match(/^[0-9.]+$/);
obj.commit.should.match(/^[a-z0-9]{40}$/);
obj.source.should.be.a.String();
shouldReturnSecurityHeaders(r);
done(err);
}
);
});
});

View File

@@ -1,24 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var should = require('should');
function shouldReturnSecurityHeaders(res) {
var expect = {
'strict-transport-security': 'max-age=31536000',
'x-content-type-options': 'nosniff',
'x-xss-protection': '1; mode=block',
'x-frame-options': 'DENY',
'cache-control': 'private, no-cache, no-store, must-revalidate, max-age=0',
'content-security-policy':
"default-src 'none'; frame-ancestors 'none'; report-uri /__cspreport__",
};
Object.keys(expect).forEach(function (header) {
should.exist(res.headers[header]);
res.headers[header].should.equal(expect[header]);
});
}
module.exports = shouldReturnSecurityHeaders;

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env node
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
require('../../lib/server.js');

View File

@@ -1,148 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* jshint curly:false */
const cp = require('child_process'),
path = require('path');
function later(cb /* args */) {
var args = Array.prototype.slice.call(arguments, 1);
process.nextTick(function () {
cb.apply(null, args);
});
}
function Verifier(args) {
if (!args) {
args = {};
}
this.config = args;
}
Verifier.prototype.setFallback = function (idp) {
if (idp === null) delete this.config.fallback;
else this.config.fallback = idp.domain();
};
Verifier.prototype.setHTTPTimeout = function (timeo) {
this.config.httpTimeout = timeo;
};
Verifier.prototype.buffer = function (b) {
if (b !== undefined) {
if (!b) {
delete this._outBuf;
} else if (!this._outBuf) {
this._outBuf = '';
}
}
return this._outBuf;
};
Verifier.prototype.url = function () {
return this.baseurl() + '/v2';
};
Verifier.prototype.v1url = function () {
return this.baseurl();
};
Verifier.prototype.baseurl = function () {
if (!this._url) {
throw new Error('verifier not running');
}
return this._url;
};
Verifier.prototype.start = function (cb) {
var self = this;
var repoBaseDir = path.join(__dirname, '..', '..');
if (this.process) {
return later(cb, null);
}
var e = {
INSECURE_SSL: true,
HTTP_TIMEOUT: this.config.httpTimeout || 8.0,
};
if (this.config.fallback) {
e.FALLBACK_DOMAIN = this.config.fallback;
}
if (this.config.files) {
e.CONFIG_FILES = this.config.files;
}
if (this.config.testServiceFailure) {
e.TEST_SERVICE_FAILURE = this.config.testServiceFailure;
}
this.process = cp.spawn(
process.execPath,
[path.join(repoBaseDir, 'tests', 'lib', 'test-server.js')],
{
cwd: repoBaseDir,
stdio: 'pipe',
env: e,
timeout: 4 * 1000,
}
);
this.process.stdout.on('data', function (line) {
if (typeof self._outBuf === 'string') {
self._outBuf += line;
}
// figure out the bound port
try {
var m = JSON.parse(line);
if (m.Type === 'server.running') {
self._url = m.Fields.url;
if (cb) {
cb(null, m.Fields.url);
}
cb = null;
}
} catch (err) {}
if (process.env.VERBOSE) {
console.log(line.toString());
}
});
this.errBuf = '';
this.process.stderr.on('data', function (d) {
self.errBuf += d.toString();
});
this.process.on('exit', function (code) {
var msg = 'exited';
if (code !== 0) {
msg += ' with code ' + code + ' (' + self.errBuf + ')';
}
if (cb) cb(msg);
cb = null;
self._url = null;
self.process = null;
});
};
Verifier.prototype.stop = function (cb) {
if (!this.process || !this._url) {
throw new Error('verifier not running');
}
this.process.on('exit', function (code) {
cb(!code ? null : 'non-zero exit code: ' + code);
});
const pid = this.process.pid;
// Really really kill it. This shouldn't be necesary, but sometimes when
// running tests in a loop, the process doesn't die with just a SIGINT.
cp.spawn('kill', ['-9', pid]);
};
module.exports = Verifier;

View File

@@ -1,43 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global describe,it,before,after */
var Verifier = require('./lib/verifier.js'),
should = require('should'),
shouldReturnSecurityHeaders = require('./lib/should-return-security-headers.js'),
request = require('request');
describe('missing assertion test', function () {
var verifier = new Verifier();
before(async () => {
await new Promise((resolve) => verifier.start(resolve));
});
after(async () => {
await new Promise((resolve) => verifier.stop(resolve));
});
it('should fail to verify when assertion is missing', function (done) {
request(
{
method: 'post',
url: verifier.url(),
json: true,
body: {
audience: 'http://example.com',
},
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(400);
r.body.status.should.equal('failure');
r.body.reason.should.equal('missing assertion parameter');
shouldReturnSecurityHeaders(r);
done();
}
);
});
});

View File

@@ -1,80 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global describe,it,before,after */
var IdP = require('browserid-local-verify/testing').IdP,
Client = require('browserid-local-verify/testing').Client,
Verifier = require('./lib/verifier.js'),
should = require('should'),
shouldReturnSecurityHeaders = require('./lib/should-return-security-headers.js'),
request = require('request');
describe('audience tests', function () {
var verifier = new Verifier({ testServiceFailure: true });
var idp = new IdP();
var client;
before(async () => {
await new Promise((resolve) => idp.start(resolve));
await new Promise((resolve) => verifier.start(resolve));
client = new Client({ idp: idp });
});
after(async () => {
await new Promise((resolve) => verifier.stop(resolve));
await new Promise((resolve) => idp.stop(resolve));
});
var assertion;
it('test assertion should be created', function (done) {
client.assertion({ audience: 'http://example.com' }, function (err, ass) {
assertion = ass;
done(err);
});
});
it('(v1 api) should return a 503 on service failure', function (done) {
request(
{
method: 'post',
url: verifier.v1url(),
json: true,
body: {
assertion: assertion,
audience: 'http://example.com',
},
},
function (err, r) {
should.not.exist(err);
(503).should.equal(r.statusCode);
'failure'.should.equal(r.body.status);
shouldReturnSecurityHeaders(r);
done();
}
);
});
it('(v2 api) should return a 503 on service failure', function (done) {
request(
{
method: 'post',
url: verifier.url(),
json: true,
body: {
assertion: assertion,
audience: 'http://example.com',
},
},
function (err, r) {
should.not.exist(err);
(503).should.equal(r.statusCode);
'failure'.should.equal(r.body.status);
shouldReturnSecurityHeaders(r);
done();
}
);
});
});

View File

@@ -1,102 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global describe,it,before,after */
var IdP = require('browserid-local-verify/testing').IdP,
Client = require('browserid-local-verify/testing').Client,
Verifier = require('./lib/verifier.js'),
should = require('should'),
shouldReturnSecurityHeaders = require('./lib/should-return-security-headers.js'),
request = require('request');
describe('audience tests', function () {
var verifier = new Verifier();
var idp = new IdP();
var client;
before(async () => {
await new Promise((resolve) => idp.start(resolve));
await new Promise((resolve) => verifier.start(resolve));
client = new Client({
idp: idp,
// note, using an email not rooted at the idp. trustIssuer is the only
// way this can work
email: 'user@example.com',
});
});
after(async () => {
await new Promise((resolve) => verifier.stop(resolve));
await new Promise((resolve) => idp.stop(resolve));
});
var assertion;
it('test assertion should be created', function (done) {
client.assertion({ audience: 'http://example.com' }, function (err, ass) {
assertion = ass;
done(err);
});
});
function submitWithTrustedIssuers(ti, cb) {
request(
{
method: 'post',
url: verifier.url(),
json: true,
body: {
assertion: assertion,
audience: 'http://example.com',
trustedIssuers: ti,
},
},
cb
);
}
it('should verify when trusted issuers is specified', function (done) {
submitWithTrustedIssuers([idp.domain()], function (err, r) {
should.not.exist(err);
'okay'.should.equal(r.body.status);
shouldReturnSecurityHeaders(r);
done();
});
});
it('should fail when trusted issuers is not specified', function (done) {
submitWithTrustedIssuers(undefined, function (err, r) {
should.not.exist(err);
'failure'.should.equal(r.body.status);
shouldReturnSecurityHeaders(r);
done();
});
});
it('should fail when trusted issuers is not an array', function (done) {
submitWithTrustedIssuers(idp.domain(), function (err, r) {
should.not.exist(err);
'failure'.should.equal(r.body.status);
'trusted issuers must be an array'.should.equal(r.body.reason);
shouldReturnSecurityHeaders(r);
done();
});
});
it('should fail when trusted issuers contains non-strings', function (done) {
submitWithTrustedIssuers(
[idp.domain(), ['example.com']],
function (err, r) {
should.not.exist(err);
'failure'.should.equal(r.body.status);
'trusted issuers must be an array of strings'.should.equal(
r.body.reason
);
shouldReturnSecurityHeaders(r);
done();
}
);
});
});

View File

@@ -1,136 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global describe,it,before,after */
var IdP = require('browserid-local-verify/testing').IdP,
Client = require('browserid-local-verify/testing').Client,
Verifier = require('./lib/verifier.js'),
should = require('should'),
shouldReturnSecurityHeaders = require('./lib/should-return-security-headers.js'),
request = require('request');
describe('unverified email', function () {
var fallback = new IdP();
var verifier = new Verifier();
var client;
before(async () => {
await new Promise((resolve) => fallback.start(resolve));
verifier.setFallback(fallback);
await new Promise((resolve) => verifier.start(resolve));
});
after(async () => {
await new Promise((resolve) => verifier.stop(resolve));
await new Promise((resolve) => fallback.stop(resolve));
});
it('(v1) assertion with unverified email address should fail to verify', function (done) {
client = new Client({
idp: fallback,
principal: { 'unverified-email': 'bob@example.com' },
});
// clear email
client.email(null);
client.assertion(
{ audience: 'http://example.com' },
function (_, assertion) {
request(
{
method: 'post',
url: verifier.v1url(),
json: true,
body: {
assertion: assertion,
audience: 'http://example.com',
},
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(200);
r.body.status.should.equal('failure');
r.body.reason.should.startWith('untrusted assertion');
shouldReturnSecurityHeaders(r);
done();
}
);
}
);
});
it('(v1) assertion with unverified email address and forceIssuer should verify', function (done) {
client = new Client({
idp: fallback,
principal: { 'unverified-email': 'bob@example.com' },
});
client.assertion(
{ audience: 'http://example.com' },
function (_, assertion) {
request(
{
method: 'post',
url: verifier.url(),
json: true,
body: {
assertion: assertion,
audience: 'http://example.com',
experimental_forceIssuer: fallback.domain(),
},
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(200);
r.body.status.should.equal('okay');
r.body.idpClaims.should.be.type('object');
r.body.idpClaims['unverified-email'].should.equal(
'bob@example.com'
);
r.body.should.not.have.property('unverified-email');
shouldReturnSecurityHeaders(r);
done();
}
);
}
);
});
it('(v1) allowUnverified causes extraction of unverified email addresses', function (done) {
client = new Client({
idp: fallback,
principal: { 'unverified-email': 'bob@example.com' },
});
client.assertion(
{ audience: 'http://example.com' },
function (_, assertion) {
request(
{
method: 'post',
url: verifier.v1url(),
json: true,
body: {
assertion: assertion,
audience: 'http://example.com',
experimental_forceIssuer: fallback.domain(),
experimental_allowUnverified: true,
},
},
function (err, r) {
should.not.exist(err);
r.statusCode.should.equal(200);
r.body.status.should.equal('okay');
r.body.idpClaims.should.be.type('object');
r.body.idpClaims['unverified-email'].should.equal(
'bob@example.com'
);
r.body.should.have.property('unverified-email');
shouldReturnSecurityHeaders(r);
done();
}
);
}
);
});
});

View File

@@ -18,7 +18,6 @@ CI=false NODE_ENV=test npx nx run-many \
--verbose \
-p \
123done \
browserid-verifier \
fxa-auth-server \
fxa-content-server \
fxa-graphql-api \

View File

@@ -20,7 +20,6 @@ spec:
providesApis:
- api:fxa-auth
dependsOn:
- component:fxa-browserid-verifier
- component:fxa-customs-server
- resource:fxa-auth-database
- resource:fxa-oauth-database

View File

@@ -20,7 +20,6 @@ CI=false NODE_ENV=test npx nx run-many \
--parallel=3 \
-p \
123done \
browserid-verifier \
fxa-auth-server \
fxa-content-server \
fxa-graphql-api \