This commit is contained in:
Danny Coates 2015-07-29 15:05:29 -07:00
Родитель 4a60edd6b0
Коммит ff5dd20a6c
9 изменённых файлов: 396 добавлений и 46 удалений

Просмотреть файл

@ -110,6 +110,7 @@ function main() {
customs.close()
mailer.stop()
database.close()
process.exit() //XXX: because of openid dep ಠ_ಠ
}
)
}

Просмотреть файл

@ -291,6 +291,12 @@ var conf = convict({
format: Boolean,
env: 'LOCKOUT_ENABLED',
default: false
},
openIdProviders: {
doc: 'root urls of allowed OpenID providers',
format: Array,
default: [],
env: 'OPENID_PROVIDERS'
}
})
@ -316,4 +322,9 @@ var options = {
conf.validate(options)
conf.set('isProduction', conf.get('env') === 'prod')
conf.set('openIdVerifyUrl', conf.get('publicUrl') + '/v1/account/openid/login')
module.exports = conf

Просмотреть файл

@ -368,6 +368,24 @@ module.exports = function (
)
}
DB.prototype.openIdRecord = function (id) {
log.trace({ op: 'DB.openIdRecord', id: id })
return this.pool.get('/openIdRecord/' + Buffer(id, 'utf8').toString('hex'))
.then(
function (body) {
var data = bufferize(body)
data.emailVerified = !!data.emailVerified
return data
},
function (err) {
if (err.statusCode === 404) {
err = error.unknownAccount()
}
throw err
}
)
}
DB.prototype.account = function (uid) {
log.trace({ op: 'DB.account', uid: uid })
return this.pool.get('/account/' + uid.toString('hex'))

Просмотреть файл

@ -7,6 +7,9 @@ var HEX_STRING = validators.HEX_STRING
var BASE64_JWT = validators.BASE64_JWT
var butil = require('../crypto/butil')
var openid = require('openid')
var url = require('url')
var qs = require('querystring')
module.exports = function (
log,
@ -18,16 +21,28 @@ module.exports = function (
db,
mailer,
Password,
redirectDomain,
verifierVersion,
isProduction,
domain,
resendBlackoutPeriod,
config,
customs,
isPreVerified,
checkPassword
) {
var openIdExtensions = [
new openid.AttributeExchange(
{
'http://axschema.org/contact/email': 'optional'
}
)
]
function isOpenIdProviderAllowed(id) {
return config.openIdProviders.some(
function (allowed) {
return id.indexOf(allowed) === 0
}
)
}
var routes = [
{
method: 'POST',
@ -39,7 +54,7 @@ module.exports = function (
authPW: isA.string().min(64).max(64).regex(HEX_STRING).required(),
preVerified: isA.boolean(),
service: isA.string().max(16).alphanum().optional(),
redirectTo: validators.redirectTo(redirectDomain).optional(),
redirectTo: validators.redirectTo(config.smtp.redirectDomain).optional(),
resume: isA.string().max(2048).optional(),
preVerifyToken: isA.string().max(2048).regex(BASE64_JWT).optional()
}
@ -74,7 +89,7 @@ module.exports = function (
.then(isPreVerified.bind(null, form.email, form.preVerifyToken))
.then(
function (preverified) {
var password = new Password(authPW, authSalt, verifierVersion)
var password = new Password(authPW, authSalt, config.verifierVersion)
return password.verifyHash()
.then(
function (verifyHash) {
@ -331,6 +346,196 @@ module.exports = function (
)
}
},
{
method: 'GET',
path: '/account/openid/authenticate',
handler: function (request, reply) {
var id = request.query.identifier
if (!isOpenIdProviderAllowed(id)) {
log.info({ op: 'Account.openid.authenticate', id: id })
return reply.redirect(
config.contentServer.url + '/openid?' + qs.stringify(
{
err: 'This OpenID Provider is not allowed'
}
)
)
}
openid.authenticate(
id,
config.openIdVerifyUrl,
null, // realm
false, // immediate
false, // stateless
function (err, authUrl) {
if (err) {
log.error({ op: 'Account.openid.authenticate', err: err })
return reply.redirect(
config.contentServer.url + '/openid?' + qs.stringify(
{
err: err.message
}
)
)
}
reply.redirect(authUrl)
},
openIdExtensions,
false // strict
)
}
},
{
method: 'GET',
path: '/account/openid/login',
handler: function (request, reply) {
if (!request.url.search) {
return reply(
'<?xml version="1.0" encoding="UTF-8"?>\n'
+ '<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)"><XRD>'
+ '<Service xmlns="xri://$xrd*($v*2.0)">'
+ '<Type>http://specs.openid.net/auth/2.0/return_to</Type>'
+ '<URI>' + config.openIdVerifyUrl + '</URI>'
+ '</Service></XRD></xrds:XRDS>'
).type('application/xrds+xml')
}
log.info({ op: 'Account.openid', url: request.url })
openid.verifyAssertion(
url.format(request.url),
function (err, assertion) {
if (err || !assertion || !assertion.authenticated) {
log.warn({ op: 'Account.openid', err: err, assertion: assertion })
return reply.redirect(
config.contentServer.url + '/openid?err=Unknown%20Account'
)
}
var id = assertion.claimedIdentifier
var locale = request.app.acceptLanguage
if (!isOpenIdProviderAllowed(id)) {
log.warn({op: 'Account.openid', id: id })
return reply.redirect(
config.contentServer.url + '/openid?' + qs.stringify(
{
err: 'This OpenID Provider is not allowed'
}
)
)
}
db.openIdRecord(id)
.then(
function (record) {
return record
},
function (err) {
if (err.errno !== 102) {
throw err
}
var uid = uuid.v4('binary')
var email = assertion.email || uid.toString('hex') + '@firefox.com'
var authSalt = crypto.randomBytes(32)
var kA = crypto.randomBytes(32)
return db.createAccount(
{
uid: uid,
createdAt: Date.now(),
email: email,
emailCode: crypto.randomBytes(16),
emailVerified: true,
kA: kA,
wrapWrapKb: crypto.randomBytes(32),
accountResetToken: null,
passwordForgotToken: null,
authSalt: authSalt,
verifierVersion: 0,
verifyHash: crypto.randomBytes(32),
openId: id,
verifierSetAt: Date.now(),
locale: locale
}
)
}
)
.then(
function (account) {
return db.createSessionToken(
{
uid: account.uid,
email: account.email,
emailCode: account.emailCode,
emailVerified: true,
verifierSetAt: account.verifierSetAt
}
)
.then(
function (sessionToken) {
return db.createKeyFetchToken(
{
uid: account.uid,
kA: account.kA,
// wrapKb is undefined without a password
// wrapWrapKb has the properties we need for this
// value; Its stable, random, and will change on
// account reset.
wrapKb: account.wrapWrapKb,
emailVerified: true
}
)
.then(
function (keyFetchToken) {
return {
sessionToken: sessionToken,
keyFetchToken: keyFetchToken,
unwrapBKey: butil.xorBuffers(
account.kA,
account.wrapWrapKb
)
}
}
)
}
)
.then(
function (tokens) {
reply.redirect(
config.contentServer.url + '/openid?' +
qs.stringify(
{
uid: tokens.sessionToken.uid.toString('hex'),
email: account.email,
session: tokens.sessionToken.data.toString('hex'),
key: tokens.keyFetchToken ?
tokens.keyFetchToken.data.toString('hex')
: undefined,
unwrap: tokens.unwrapBKey.toString('hex'),
service: 'sync',
context: 'fx_desktop_v2'
}
)
)
}
)
}
)
.catch(
function (err) {
log.error({ op: 'Account.openid', err: err })
reply.redirect(
config.contentServer.url + '/openid?' + qs.stringify(
{
err: err.toString()
}
)
)
}
)
},
false, // stateless
openIdExtensions,
false // strict
)
}
},
{
method: 'GET',
path: '/account/status',
@ -438,7 +643,7 @@ module.exports = function (
validate: {
payload: {
service: isA.string().max(16).alphanum().optional(),
redirectTo: validators.redirectTo(redirectDomain).optional(),
redirectTo: validators.redirectTo(config.smtp.redirectDomain).optional(),
resume: isA.string().max(2048).optional()
}
}
@ -449,7 +654,7 @@ module.exports = function (
db.updateSessionTokenInBackground(sessionToken, request.headers['user-agent'])
var service = request.payload.service || request.query.service
if (sessionToken.emailVerified ||
Date.now() - sessionToken.verifierSetAt < resendBlackoutPeriod) {
Date.now() - sessionToken.verifierSetAt < config.smtp.resendBlackoutPeriod) {
return reply({})
}
customs.check(
@ -523,7 +728,7 @@ module.exports = function (
payload: {
email: validators.email().required(),
service: isA.string().max(16).alphanum().optional(),
redirectTo: validators.redirectTo(redirectDomain).optional(),
redirectTo: validators.redirectTo(config.smtp.redirectDomain).optional(),
resume: isA.string().max(2048).optional()
}
}
@ -639,7 +844,7 @@ module.exports = function (
var accountResetToken = request.auth.credentials
var authPW = Buffer(request.payload.authPW, 'hex')
var authSalt = crypto.randomBytes(32)
var password = new Password(authPW, authSalt, verifierVersion)
var password = new Password(authPW, authSalt, config.verifierVersion)
return password.verifyHash()
.then(
function (verifyHash) {
@ -705,7 +910,7 @@ module.exports = function (
)
.then(
function () {
log.event('delete', { uid: emailRecord.uid.toString('hex') + '@' + domain })
log.event('delete', { uid: emailRecord.uid.toString('hex') + '@' + config.domain })
return {}
}
)
@ -716,7 +921,7 @@ module.exports = function (
}
]
if (isProduction) {
if (config.isProduction) {
delete routes[0].config.validate.payload.preVerified
} else {
// programmatic account lockout is only available in non-production mode.

Просмотреть файл

@ -20,7 +20,6 @@ module.exports = function (
config,
customs
) {
var isProduction = config.env === 'prod'
var isPreVerified = require('../preverifier')(error, config)
var defaults = require('./defaults')(log, P, db, error)
var idp = require('./idp')(log, serverPublicKey)
@ -35,11 +34,7 @@ module.exports = function (
db,
mailer,
Password,
config.smtp.redirectDomain,
config.verifierVersion,
isProduction,
config.domain,
config.smtp.resendBlackoutPeriod,
config,
customs,
isPreVerified,
checkPassword

54
npm-shrinkwrap.json сгенерированный
Просмотреть файл

@ -317,7 +317,7 @@
"dependencies": {
"esprima": {
"version": "1.0.4",
"from": "esprima@>=1.0.4 <1.1.0",
"from": "esprima@>=1.0.2 <1.1.0",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz"
}
}
@ -352,8 +352,8 @@
},
"fxa-auth-db-mysql": {
"version": "0.42.0",
"from": "git+https://github.com/mozilla/fxa-auth-db-mysql.git#a09790c20fa8c9f4a2c6f108b86aabf07038220f",
"resolved": "git+https://github.com/mozilla/fxa-auth-db-mysql.git#a09790c20fa8c9f4a2c6f108b86aabf07038220f",
"from": "git+https://github.com/mozilla/fxa-auth-db-mysql.git#master",
"resolved": "git+https://github.com/mozilla/fxa-auth-db-mysql.git#354fc512d181b002b7aa62740889eaf60511a2d7",
"dependencies": {
"bluebird": {
"version": "2.1.3",
@ -885,7 +885,7 @@
},
"isarray": {
"version": "0.0.1",
"from": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"from": "isarray@0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
},
"string_decoder": {
@ -1308,7 +1308,7 @@
},
"fxa-auth-mailer": {
"version": "1.0.7",
"from": "git+https://github.com/mozilla/fxa-auth-mailer.git#6aac195ff6d1692b239c7906fd1becd95ee85ebf",
"from": "git+https://github.com/mozilla/fxa-auth-mailer.git#master",
"resolved": "git+https://github.com/mozilla/fxa-auth-mailer.git#6aac195ff6d1692b239c7906fd1becd95ee85ebf",
"dependencies": {
"bluebird": {
@ -1421,8 +1421,8 @@
},
"fxa-content-server-l10n": {
"version": "0.0.0",
"from": "git://github.com/mozilla/fxa-content-server-l10n.git#4bbc8a6a71e45cf0abcf6d4de2219c6b0488c0e6",
"resolved": "git://github.com/mozilla/fxa-content-server-l10n.git#4bbc8a6a71e45cf0abcf6d4de2219c6b0488c0e6"
"from": "git://github.com/mozilla/fxa-content-server-l10n.git",
"resolved": "git://github.com/mozilla/fxa-content-server-l10n.git#e89599581ac4b362b8bde684565712e22195647c"
},
"handlebars": {
"version": "1.3.0",
@ -2153,11 +2153,6 @@
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz"
}
}
},
"dtrace-provider": {
"version": "0.2.8",
"from": "dtrace-provider@>=0.2.8 <0.3.0",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.2.8.tgz"
}
}
}
@ -2180,7 +2175,7 @@
"dependencies": {
"encoding": {
"version": "0.1.11",
"from": "encoding@>=0.1.11 <0.2.0",
"from": "encoding@*",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.11.tgz",
"dependencies": {
"iconv-lite": {
@ -2647,7 +2642,7 @@
},
"es5-ext": {
"version": "0.10.7",
"from": "es5-ext@>=0.10.6 <0.11.0",
"from": "es5-ext@>=0.10.4 <0.11.0",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.7.tgz",
"dependencies": {
"es6-symbol": {
@ -2659,7 +2654,7 @@
},
"es6-iterator": {
"version": "0.1.3",
"from": "es6-iterator@>=0.1.1 <0.2.0",
"from": "es6-iterator@>=0.1.3 <0.2.0",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-0.1.3.tgz",
"dependencies": {
"es6-symbol": {
@ -2698,12 +2693,12 @@
},
"es5-ext": {
"version": "0.10.7",
"from": "es5-ext@>=0.10.6 <0.11.0",
"from": "es5-ext@>=0.10.4 <0.11.0",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.7.tgz"
},
"es6-iterator": {
"version": "0.1.3",
"from": "es6-iterator@>=0.1.1 <0.2.0",
"from": "es6-iterator@>=0.1.3 <0.2.0",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-0.1.3.tgz"
},
"es6-symbol": {
@ -2726,9 +2721,9 @@
}
},
"espree": {
"version": "2.2.3",
"version": "2.2.4",
"from": "espree@>=2.0.1 <3.0.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-2.2.3.tgz"
"resolved": "https://registry.npmjs.org/espree/-/espree-2.2.4.tgz"
},
"estraverse": {
"version": "2.0.0",
@ -2741,9 +2736,9 @@
"resolved": "https://registry.npmjs.org/estraverse-fb/-/estraverse-fb-1.3.1.tgz"
},
"globals": {
"version": "8.3.0",
"version": "8.4.0",
"from": "globals@>=8.0.0 <9.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-8.3.0.tgz"
"resolved": "https://registry.npmjs.org/globals/-/globals-8.4.0.tgz"
},
"inquirer": {
"version": "0.8.5",
@ -2929,9 +2924,9 @@
"resolved": "https://registry.npmjs.org/levn/-/levn-0.2.5.tgz"
},
"fast-levenshtein": {
"version": "1.0.6",
"version": "1.0.7",
"from": "fast-levenshtein@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.0.6.tgz"
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.0.7.tgz"
}
}
},
@ -3414,7 +3409,7 @@
"dependencies": {
"inherits": {
"version": "2.0.1",
"from": "inherits@>=2.0.1 <2.1.0",
"from": "inherits@>=2.0.1 <3.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"typedarray": {
@ -3722,7 +3717,7 @@
"dependencies": {
"ansi-regex": {
"version": "0.2.1",
"from": "ansi-regex@>=0.2.1 <0.3.0",
"from": "ansi-regex@>=0.2.0 <0.3.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz"
}
}
@ -3734,7 +3729,7 @@
"dependencies": {
"ansi-regex": {
"version": "0.2.1",
"from": "ansi-regex@>=0.2.1 <0.3.0",
"from": "ansi-regex@>=0.2.0 <0.3.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz"
}
}
@ -3845,6 +3840,11 @@
}
}
},
"openid": {
"version": "0.5.13",
"from": "openid@0.5.13",
"resolved": "https://registry.npmjs.org/openid/-/openid-0.5.13.tgz"
},
"pem-jwk": {
"version": "1.5.1",
"from": "pem-jwk@1.5.1",
@ -3857,7 +3857,7 @@
"dependencies": {
"inherits": {
"version": "2.0.1",
"from": "inherits@>=2.0.0 <3.0.0",
"from": "inherits@>=2.0.1 <3.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"minimalistic-assert": {

Просмотреть файл

@ -37,6 +37,7 @@
"hkdf": "0.0.2",
"joi": "6.4.1",
"mozlog": "2.0.1",
"openid": "0.5.13",
"poolee": "1.0.0",
"request": "2.55.0",
"scrypt-hash": "1.1.12",

116
test/local/openid_tests.js Normal file
Просмотреть файл

@ -0,0 +1,116 @@
/* 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 test = require('../ptaptest')
var TestServer = require('../test_server')
var qs = require('querystring')
var url = require('url')
var Pool = require('poolee')
process.env.OPENID_PROVIDERS = 'https://me.yahoo.com,https://openid.example.com'
var config = require('../../config').getProperties()
TestServer.start(config)
.then(function main(server) {
test(
'openid authenticate redirects to provider',
function (t) {
Pool.request(
config.publicUrl + '/v1/account/openid/authenticate?' + qs.stringify(
{
identifier: 'https://me.yahoo.com'
}
),
function (err, res) {
t.equal(res.statusCode, 302)
var location = url.parse(res.headers.location)
t.equal(location.hostname, 'open.login.yahooapis.com')
t.end()
}
)
}
)
test(
'openid authenticate rejects providers not on the allow list',
function (t) {
Pool.request(
config.publicUrl + '/v1/account/openid/authenticate?' + qs.stringify(
{
identifier: 'https://example.com'
}
),
function (err, res) {
t.equal(res.statusCode, 302)
var location = url.parse(res.headers.location, true)
t.equal(location.query.err, 'This OpenID Provider is not allowed')
t.end()
}
)
}
)
test(
'openid authenticate rejects allowed but invalid providers',
function (t) {
// this should never happen in real life
Pool.request(
config.publicUrl + '/v1/account/openid/authenticate?' + qs.stringify(
{
identifier: 'https://openid.example.com'
}
),
function (err, res) {
t.equal(res.statusCode, 302)
var location = url.parse(res.headers.location, true)
t.equal(location.query.err, 'No providers found for the given identifier')
t.end()
}
)
}
)
test(
'bare request to openid login returns the XRDS doc',
function (t) {
Pool.request(
config.publicUrl + '/v1/account/openid/login',
function (err, res, body) {
t.equal(res.statusCode, 200)
t.equal(res.headers['content-type'], 'application/xrds+xml')
t.end()
}
)
}
)
test(
'openid login rejects invalid assertions',
function (t) {
Pool.request(
config.publicUrl + '/v1/account/openid/login?foo=bar',
function (err, res) {
t.equal(res.statusCode, 302)
var location = url.parse(res.headers.location, true)
t.equal(location.query.err, 'Unknown Account')
t.end()
}
)
}
)
// test(
// 'openid login rejects valid assertions for non-allowed providers'
// )
test(
'teardown',
function (t) {
server.stop()
t.end()
}
)
})

Просмотреть файл

@ -34,6 +34,9 @@ require('ass')
var tap = require('tap')
module.exports = function(name, testfunc) {
if (!testfunc) {
return tap.test(name)
}
var wrappedtestfunc = function(t) {
var res = testfunc(t)
if (typeof res !== 'undefined') {