fix(server): hide session token lastAccessTime updates behind a flag

This commit is contained in:
Phil Booth 2016-09-22 17:44:34 +01:00
Родитель 1c9c864033
Коммит 51d7cdd081
45 изменённых файлов: 1441 добавлений и 1087 удалений

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

@ -5,12 +5,12 @@
var config = require('../config').getProperties()
var log = require('../lib/log')(config.log.level, 'fxa-email-bouncer')
var error = require('../lib/error')
var Token = require('../lib/tokens')(log, config.tokenLifetimes)
var Token = require('../lib/tokens')(log, config)
var SQSReceiver = require('../lib/sqs')(log)
var bounces = require('../lib/bounces')(log, error)
var DB = require('../lib/db')(
config.db.backend,
config,
log,
error,
Token.SessionToken,

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

@ -34,7 +34,7 @@ function main() {
}
var error = require('../lib/error')
var Token = require('../lib/tokens')(log, config.tokenLifetimes)
var Token = require('../lib/tokens')(log, config)
var Password = require('../lib/crypto/password')(log, config)
var signer = require('../lib/signer')(config.secretKeyFile, config.domain)
@ -79,7 +79,7 @@ function main() {
mailer = m
var DB = require('../lib/db')(
config.db.backend,
config,
log,
error,
Token.SessionToken,

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

@ -433,11 +433,13 @@ var conf = convict({
signinConfirmation: {
enabled: {
doc: 'enable signin confirmation',
format: Boolean,
default: false,
env: 'SIGNIN_CONFIRMATION_ENABLED'
},
sample_rate: {
doc: 'signin confirmation sample rate, between 0.0 and 1.0',
format: Number,
default: 1.0,
env: 'SIGNIN_CONFIRMATION_RATE'
},
@ -461,7 +463,7 @@ var conf = convict({
doc: 'If feature enabled, force sign-in confirmation for email addresses matching this regex.',
format: Array,
default: [
'.+@mozilla\.com$'
'.+@mozilla\\.com$'
],
env: 'SIGNIN_CONFIRMATION_FORCE_EMAIL_REGEX'
}
@ -472,6 +474,25 @@ var conf = convict({
default: true,
env: 'SECURITY_HISTORY_ENABLED'
}
},
lastAccessTimeUpdates: {
enabled: {
doc: 'enable updates to the lastAccessTime session token property',
format: Boolean,
default: false,
env: 'LASTACCESSTIME_UPDATES_ENABLED'
},
sampleRate: {
doc: 'sample rate for updates to the lastAccessTime session token property, in the range 0..1',
format: Number,
default: 1,
env: 'LASTACCESSTIME_UPDATES_SAMPLE_RATE'
},
enabledEmailAddresses: {
doc: 'regex matching enabled email addresses for updates to the lastAccessTime session token property',
default: '.+@mozilla\\.com$',
env: 'LASTACCESSTIME_UPDATES_EMAIL_ADDRESSES'
}
}
})

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

@ -12,7 +12,7 @@ var unbuffer = butil.unbuffer
var bufferize = butil.bufferize
module.exports = function (
backend,
config,
log,
error,
SessionToken,
@ -21,6 +21,8 @@ module.exports = function (
PasswordForgotToken,
PasswordChangeToken) {
const features = require('./features')(config)
function DB(options) {
this.pool = new Pool(options.url)
}
@ -435,7 +437,7 @@ module.exports = function (
return bufferize({
id: item.id,
sessionToken: item.sessionTokenId,
lastAccessTime: item.lastAccessTime,
lastAccessTime: marshallLastAccessTime(item.lastAccessTime, uid, item.email),
name: item.name,
type: item.type,
pushCallback: item.callbackURL,
@ -463,6 +465,17 @@ module.exports = function (
)
}
function marshallLastAccessTime (lastAccessTime, uid, email) {
// Updating lastAccessTime on session tokens may not be enabled.
// If it isn't, return null instead of the timestamp so that the
// content server knows not to display it to users.
if (features.isLastAccessTimeEnabledForUser(uid, email)) {
return lastAccessTime
}
return null
}
DB.prototype.sessionWithDevice = function (id) {
log.trace({ op: 'DB.sessionWithDevice', id: id })
return this.pool.get('/sessionToken/' + id.toString('hex') + '/device')

114
lib/features.js Normal file
Просмотреть файл

@ -0,0 +1,114 @@
/* 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/. */
'use strict'
const crypto = require('crypto')
module.exports = config => {
const lastAccessTimeUpdates = config.lastAccessTimeUpdates
const signinConfirmation = config.signinConfirmation
return {
/**
* Predicate that indicates whether updates to sessionToken.lastAccessTime
* are enabled for a given user, based on their uid and email address.
*
* @param uid Buffer or String
* @param email String
*/
isLastAccessTimeEnabledForUser (uid, email) {
return lastAccessTimeUpdates.enabled && (
isSampledUser(lastAccessTimeUpdates.sampleRate, uid, 'lastAccessTimeUpdates') ||
new RegExp(lastAccessTimeUpdates.enabledEmailAddresses).test(email)
)
},
/**
* Predicate that indicates whether sign-in confirmation is enabled
* for a given user, based on their uid and email address.
*
* @param uid Buffer or String
* @param email String
*/
isSigninConfirmationEnabledForUser (uid, email, request) {
if (! signinConfirmation.enabled) {
return false
}
// Always create unverified tokens if customs-server
// has said the request is suspicious.
if (request.app.isSuspiciousRequest) {
return true
}
// Or if the email address matches one of these regexes.
if (signinConfirmation.forceEmailRegex.some(regex => new RegExp(regex).test(email))) {
return true
}
// While we're testing this feature, there may be some funky
// edge-cases in device login flows that haven't been fully tested.
// Temporarily avoid them for regular users by checking the `context` flag,
// and create pre-verified sessions for unsupported clients.
// This check will go away in the final version of this feature.
const context = request.payload &&
request.payload.metricsContext &&
request.payload.metricsContext.context
if (signinConfirmation.supportedClients.indexOf(context) === -1) {
return false
}
// Check to see if user in roll-out cohort.
return isSampledUser(signinConfirmation.sample_rate, uid, 'signinConfirmation')
},
/**
* Predicate that indicates whether a user belongs to the sampled cohort,
* based on a sample rate, their uid and a key that identifies the feature.
*
* @param sampleRate Number in the range 0..1
* @param uid Buffer or String
* @param key String
*/
isSampledUser: isSampledUser
}
}
const HASH_LENGTH = hash('', '').length
const MAX_SAFE_HEX = Number.MAX_SAFE_INTEGER.toString(16)
const MAX_SAFE_HEX_LENGTH = MAX_SAFE_HEX.length - MAX_SAFE_HEX.indexOf('f')
const COHORT_DIVISOR = parseInt(Array(MAX_SAFE_HEX_LENGTH).fill('f').join(''), 16)
function isSampledUser (sampleRate, uid, key) {
if (sampleRate === 1) {
return true
}
if (sampleRate === 0) {
return false
}
if (Buffer.isBuffer(uid)) {
uid = uid.toString('hex')
}
// Extract the maximum entropy we can safely handle as a number then reduce
// it to a value between 0 and 1 for comparison against the sample rate.
const cohort = parseInt(
hash(uid, key).substr(HASH_LENGTH - MAX_SAFE_HEX_LENGTH),
16
) / COHORT_DIVISOR
return cohort < sampleRate
}
function hash (uid, key) {
// Note that we don't need anything cryptographically secure here,
// speed and a good distribution are the requirements.
const h = crypto.createHash('sha1')
h.update(uid)
h.update(key)
return h.digest('hex')
}

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

@ -54,6 +54,7 @@ module.exports = function (
supportedLanguages: config.i18n.supportedLanguages,
defaultLanguage: config.i18n.defaultLanguage
})
const features = require('../features')(config)
var securityHistoryEnabled = config.securityHistory && config.securityHistory.enabled
@ -211,7 +212,8 @@ module.exports = function (
}
function createSessionToken () {
var enableTokenVerification = requestHelper.shouldEnableTokenVerification(account, config, request)
const enableTokenVerification =
features.isSigninConfirmationEnabledForUser(account.uid, account.email, request)
// Verified sessions should only be created for preverified tokens
// and when sign-in confirmation is disabled or not needed.
@ -419,7 +421,7 @@ module.exports = function (
// * the request wants keys, since unverified sessions are fine to use for e.g. oauth login.
// * the email is verified, since content-server triggers a resend of the verification
// email on unverified accounts, which doubles as sign-in confirmation.
if (! requestHelper.shouldEnableTokenVerification(emailRecord, config, request)) {
if (! features.isSigninConfirmationEnabledForUser(emailRecord.uid, emailRecord.email, request)) {
tokenVerificationId = undefined
mustVerifySession = false
doSigninConfirmation = false

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

@ -12,56 +12,6 @@ function wantsKeys (request) {
return request.query && request.query.keys === 'true'
}
/**
* Returns whether or not to use token-verification feature on a request.
*
* @param account
* @param config
* @param request
* @returns {boolean}
*/
function shouldEnableTokenVerification(account, config, request) {
var confirmLogin = config.signinConfirmation && config.signinConfirmation.enabled
if (!confirmLogin) {
return false
}
// Always create unverified tokens if customs-server
// has said the request is suspicious.
if (request.app.isSuspiciousRequest) {
return true
}
// Or if the email address matching one of these regexes.
var email = account.email
var isValidEmail = config.signinConfirmation.forceEmailRegex.some(function (reg) {
var emailReg = new RegExp(reg)
return emailReg.test(email)
})
if (isValidEmail) {
return true
}
// While we're testing this feature, there may be some funky
// edge-cases in device login flows that haven't been fully tested.
// Temporarily avoid them for regular users by checking the `context` flag,
// and create pre-verified sessions for unsupported clients.
// This check will go away in the final version of this feature.
var context = request.payload && request.payload.metricsContext && request.payload.metricsContext.context
var isValidContext = context && (config.signinConfirmation.supportedClients.indexOf(context) > -1)
if (!isValidContext) {
return false
}
// Check to see if user in roll-out cohort.
// Cohort is determined by user's uid.
var uid = account.uid.toString('hex')
var uidNum = parseInt(uid.substr(0, 4), 16) % 100
return uidNum < (config.signinConfirmation.sample_rate * 100)
}
/**
* Returns whether or not to send the verify account email on a login
* attempt. This never sends a verification email to an already verified email.
@ -82,6 +32,5 @@ function shouldSendVerifyAccountEmail(account, request) {
module.exports = {
wantsKeys: wantsKeys,
shouldEnableTokenVerification: shouldEnableTokenVerification,
shouldSendVerifyAccountEmail: shouldSendVerifyAccountEmail
}

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

@ -2,34 +2,35 @@
* 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 crypto = require('crypto')
var inherits = require('util').inherits
const crypto = require('crypto')
const inherits = require('util').inherits
var P = require('../promise')
var hkdf = require('../crypto/hkdf')
var butil = require('../crypto/butil')
const P = require('../promise')
const hkdf = require('../crypto/hkdf')
const butil = require('../crypto/butil')
var error = require('../error')
const error = require('../error')
module.exports = function (log, lifetimes) {
lifetimes = lifetimes || {
module.exports = (log, config) => {
config = config || {}
const lifetimes = config.tokenLifetimes || {
accountResetToken: 1000 * 60 * 15,
passwordChangeToken: 1000 * 60 * 15,
passwordForgotToken: 1000 * 60 * 15
}
var Bundle = require('./bundle')(crypto, P, hkdf, butil, error)
var Token = require('./token')(log, crypto, P, hkdf, Bundle, error)
const Bundle = require('./bundle')(crypto, P, hkdf, butil, error)
const Token = require('./token')(log, crypto, P, hkdf, Bundle, error)
var KeyFetchToken = require('./key_fetch_token')(log, inherits, Token, P, error)
var AccountResetToken = require('./account_reset_token')(
const KeyFetchToken = require('./key_fetch_token')(log, inherits, Token, P, error)
const AccountResetToken = require('./account_reset_token')(
log,
inherits,
Token,
crypto,
lifetimes.accountResetToken
)
var SessionToken = require('./session_token')(log, inherits, Token)
var PasswordForgotToken = require('./password_forgot_token')(
const SessionToken = require('./session_token')(log, inherits, Token, config)
const PasswordForgotToken = require('./password_forgot_token')(
log,
inherits,
Token,
@ -37,7 +38,7 @@ module.exports = function (log, lifetimes) {
lifetimes.passwordForgotToken
)
var PasswordChangeToken = require('./password_change_token')(
const PasswordChangeToken = require('./password_change_token')(
log,
inherits,
Token,

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

@ -10,7 +10,8 @@ var ONE_HOUR = 60 * 60 * 1000
// See https://github.com/mozilla/fxa-auth-server/pull/1169
var TOKEN_FRESHNESS_THRESHOLD = 50 * 365 * 24 * ONE_HOUR // 50 years or post Y2038 ;-)
module.exports = function (log, inherits, Token) {
module.exports = (log, inherits, Token, config) => {
const features = require('../features')(config)
function SessionToken(keys, details) {
Token.call(this, keys, details)
@ -82,7 +83,10 @@ module.exports = function (log, inherits, Token) {
this.uaOS === freshData.uaOS &&
this.uaOSVersion === freshData.uaOSVersion &&
this.uaDeviceType === freshData.uaDeviceType &&
this.lastAccessTime + TOKEN_FRESHNESS_THRESHOLD > freshData.lastAccessTime
(
! features.isLastAccessTimeEnabledForUser(this.uid, this.email) ||
this.lastAccessTime + TOKEN_FRESHNESS_THRESHOLD > freshData.lastAccessTime
)
log.info({
op: 'SessionToken.isFresh',

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

@ -7,7 +7,7 @@ const crypto = require('crypto')
const commander = require('commander')
const P = require('../../lib/promise')
const Client = require('../../test/client')
const Client = require('../../test/client')()
const mailbox = require('../../test/mailbox')
const validateEmail = require('./validate-email')

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

@ -23,7 +23,7 @@ var error = require('../lib/error')
var log = require('../lib/log')(config.log.level)
var P = require('../lib/promise')
var path = require('path')
var Token = require('../lib/tokens')(log, config.tokenLifetimes)
var Token = require('../lib/tokens')(log, config)
commandLineOptions
.option('-i, --input <filename>', 'JSON input file')
@ -37,7 +37,7 @@ requiredOptions.forEach(checkRequiredOption)
var DB = require('../lib/db')(
config.db.backend,
config,
log,
error,
Token.SessionToken,

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

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-disable no-console */
var Client = require('../client')
var Client = require('../client')()
var config = {
origin: 'http://127.0.0.1:9000',

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -2,286 +2,287 @@
* 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 P = require('../../lib/promise')
var ClientApi = require('./api')
var butil = require('../../lib/crypto/butil')
var pbkdf2 = require('../../lib/crypto/pbkdf2')
var hkdf = require('../../lib/crypto/hkdf')
var tokens = require('../../lib/tokens')({ trace: function () {}})
module.exports = config => {
var P = require('../../lib/promise')
const ClientApi = require('./api')(config)
var butil = require('../../lib/crypto/butil')
var pbkdf2 = require('../../lib/crypto/pbkdf2')
var hkdf = require('../../lib/crypto/hkdf')
const tokens = require('../../lib/tokens')({ trace: function () {}}, config)
function Client(origin) {
this.uid = null
this.authAt = 0
this.api = new ClientApi(origin)
this.email = null
this.emailVerified = false
this.authToken = null
this.sessionToken = null
this.accountResetToken = null
this.keyFetchToken = null
this.passwordForgotToken = null
this.kA = null
this.wrapKb = null
this.options = {}
}
Client.Api = ClientApi
Client.prototype.setupCredentials = function (email, password) {
this.email = email
return pbkdf2.derive(Buffer(password), hkdf.KWE('quickStretch', email), 1000, 32)
.then(
function (stretch) {
return hkdf(stretch, 'authPW', null, 32)
.then(
function (authPW) {
this.authPW = authPW
return hkdf(stretch, 'unwrapBKey', null, 32)
}.bind(this)
)
}.bind(this)
)
.then(
function (unwrapBKey) {
this.unwrapBKey = unwrapBKey
return this
}.bind(this)
)
}
Client.create = function (origin, email, password, options) {
var c = new Client(origin)
c.options = options || {}
return c.setupCredentials(email, password)
.then(
function() {
return c.create()
}
)
}
Client.login = function (origin, email, password, opts) {
var c = new Client(origin)
return c.setupCredentials(email, password)
.then(
function (c) {
return c.auth(opts)
}
)
}
Client.changePassword = function (origin, email, oldPassword, newPassword, headers) {
var c = new Client(origin)
return c.setupCredentials(email, oldPassword)
.then(
function () {
return c.changePassword(newPassword, headers)
.then(
function () {
return c
}
)
}
)
}
Client.createAndVerify = function (origin, email, password, mailbox, options) {
return Client.create(origin, email, password, options)
.then(
function (client) {
return mailbox.waitForCode(email)
.then(
function (code) {
return client.verifyEmail(code, options)
}
)
.then(
function () {
// clear the post verified email, if one was sent
if (options && options.service === 'sync') {
return mailbox.waitForEmail(email)
}
}
)
.then(
function () {
return client
}
)
}
)
}
Client.loginAndVerify = function (origin, email, password, mailbox, options) {
if (! options ) {
options = {}
function Client(origin) {
this.uid = null
this.authAt = 0
this.api = new ClientApi(origin)
this.email = null
this.emailVerified = false
this.authToken = null
this.sessionToken = null
this.accountResetToken = null
this.keyFetchToken = null
this.passwordForgotToken = null
this.kA = null
this.wrapKb = null
this.options = {}
}
options.keys = options.keys || true
Client.Api = ClientApi
return Client.login(origin, email, password, options)
.then(
function (client) {
return mailbox.waitForCode(email)
.then(
function (code) {
return client.verifyEmail(code, options)
}
)
.then(
function () {
return client
}
)
}
)
}
Client.prototype.create = function () {
return this.api.accountCreate(
this.email,
this.authPW,
this.options
)
.then(
function (a) {
this.uid = a.uid
this.authAt = a.authAt
this.sessionToken = a.sessionToken
this.keyFetchToken = a.keyFetchToken
this.device = a.device
return this
}.bind(this)
)
}
Client.prototype._clear = function () {
this.authToken = null
this.sessionToken = null
this.srpSession = null
this.accountResetToken = null
this.keyFetchToken = null
this.passwordForgotToken = null
this.kA = null
this.wrapKb = null
}
Client.prototype.stringify = function () {
return JSON.stringify(this)
}
Client.prototype.auth = function (opts) {
return this.api.accountLogin(this.email, this.authPW, opts)
.then(
function (data) {
this.uid = data.uid
this.sessionToken = data.sessionToken
this.keyFetchToken = data.keyFetchToken || null
this.emailVerified = data.verified
this.authAt = data.authAt
this.device = data.device
this.verificationReason = data.verificationReason
this.verificationMethod = data.verificationMethod
this.verified = data.verified
return this
}.bind(this)
)
}
Client.prototype.login = function (opts) {
return this.auth(opts)
}
Client.prototype.destroySession = function () {
var p = P.resolve(null)
if (this.sessionToken) {
p = this.api.sessionDestroy(this.sessionToken)
Client.prototype.setupCredentials = function (email, password) {
this.email = email
return pbkdf2.derive(Buffer(password), hkdf.KWE('quickStretch', email), 1000, 32)
.then(
function () {
this.sessionToken = null
return {}
function (stretch) {
return hkdf(stretch, 'authPW', null, 32)
.then(
function (authPW) {
this.authPW = authPW
return hkdf(stretch, 'unwrapBKey', null, 32)
}.bind(this)
)
}.bind(this)
)
.then(
function (unwrapBKey) {
this.unwrapBKey = unwrapBKey
return this
}.bind(this)
)
}
return p
}
Client.prototype.verifyEmail = function (code, options) {
return this.api.recoveryEmailVerifyCode(this.uid, code, options)
}
Client.create = function (origin, email, password, options) {
var c = new Client(origin)
c.options = options || {}
Client.prototype.emailStatus = function () {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
return c.setupCredentials(email, password)
.then(
function() {
return c.create()
}
)
}
Client.login = function (origin, email, password, opts) {
var c = new Client(origin)
return c.setupCredentials(email, password)
.then(
function (c) {
return c.auth(opts)
}
)
}
Client.changePassword = function (origin, email, oldPassword, newPassword, headers) {
var c = new Client(origin)
return c.setupCredentials(email, oldPassword)
.then(
function () {
return c.changePassword(newPassword, headers)
.then(
function () {
return c
}
)
}
)
}
Client.createAndVerify = function (origin, email, password, mailbox, options) {
return Client.create(origin, email, password, options)
.then(
function (client) {
return mailbox.waitForCode(email)
.then(
function (code) {
return client.verifyEmail(code, options)
}
)
.then(
function () {
// clear the post verified email, if one was sent
if (options && options.service === 'sync') {
return mailbox.waitForEmail(email)
}
}
)
.then(
function () {
return client
}
)
}
)
}
Client.loginAndVerify = function (origin, email, password, mailbox, options) {
if (! options ) {
options = {}
}
options.keys = options.keys || true
return Client.login(origin, email, password, options)
.then(
function (client) {
return mailbox.waitForCode(email)
.then(
function (code) {
return client.verifyEmail(code, options)
}
)
.then(
function () {
return client
}
)
}
)
}
Client.prototype.create = function () {
return this.api.accountCreate(
this.email,
this.authPW,
this.options
)
.then(
function (a) {
this.uid = a.uid
this.authAt = a.authAt
this.sessionToken = a.sessionToken
this.keyFetchToken = a.keyFetchToken
this.device = a.device
return this
}.bind(this)
)
}
Client.prototype._clear = function () {
this.authToken = null
this.sessionToken = null
this.srpSession = null
this.accountResetToken = null
this.keyFetchToken = null
this.passwordForgotToken = null
this.kA = null
this.wrapKb = null
}
Client.prototype.stringify = function () {
return JSON.stringify(this)
}
Client.prototype.auth = function (opts) {
return this.api.accountLogin(this.email, this.authPW, opts)
.then(
function (data) {
this.uid = data.uid
this.sessionToken = data.sessionToken
this.keyFetchToken = data.keyFetchToken || null
this.emailVerified = data.verified
this.authAt = data.authAt
this.device = data.device
this.verificationReason = data.verificationReason
this.verificationMethod = data.verificationMethod
this.verified = data.verified
return this
}.bind(this)
)
}
Client.prototype.login = function (opts) {
return this.auth(opts)
}
Client.prototype.destroySession = function () {
var p = P.resolve(null)
if (this.sessionToken) {
p = this.api.sessionDestroy(this.sessionToken)
.then(
function () {
this.sessionToken = null
return {}
}.bind(this)
)
}
return p
}
Client.prototype.verifyEmail = function (code, options) {
return this.api.recoveryEmailVerifyCode(this.uid, code, options)
}
Client.prototype.emailStatus = function () {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
function () {
return this.api.recoveryEmailStatus(this.sessionToken)
}.bind(this)
)
}
}
Client.prototype.requestVerifyEmail = function () {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
function () {
return this.api.recoveryEmailResendCode(this.sessionToken, this.options)
}.bind(this)
)
}
Client.prototype.requestVerifyEmail = function () {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
function () {
return this.api.recoveryEmailResendCode(this.sessionToken, this.options)
}.bind(this)
)
}
Client.prototype.sign = function (publicKey, duration, locale, options) {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
Client.prototype.sign = function (publicKey, duration, locale, options) {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
function () {
return this.api.certificateSign(this.sessionToken, publicKey, duration, locale, options)
}.bind(this)
)
.then(
function (x) {
return x.cert
}
)
}
Client.prototype.changePassword = function (newPassword, headers, sessionToken) {
return this.api.passwordChangeStart(this.email, this.authPW, headers)
.then(
function (json) {
this.keyFetchToken = json.keyFetchToken
this.passwordChangeToken = json.passwordChangeToken
return this.keys()
}.bind(this)
function (x) {
return x.cert
}
)
.then(
function (/* keys */) {
return this.setupCredentials(this.email, newPassword)
}.bind(this)
)
.then(
function () {
this.wrapKb = butil.xorBuffers(this.kB, this.unwrapBKey)
return this.api.passwordChangeFinish(this.passwordChangeToken, this.authPW, this.wrapKb, headers, sessionToken)
}.bind(this)
)
.then(
function (res) {
this._clear()
}
// Update to new tokens if needed
this.sessionToken = res.sessionToken ? res.sessionToken : this.sessionToken
this.authAt = res.authAt ? res.authAt : this.authAt
this.keyFetchToken = res.keyFetchToken ? res.keyFetchToken : this.keyFetchToken
Client.prototype.changePassword = function (newPassword, headers, sessionToken) {
return this.api.passwordChangeStart(this.email, this.authPW, headers)
.then(
function (json) {
this.keyFetchToken = json.keyFetchToken
this.passwordChangeToken = json.passwordChangeToken
return this.keys()
}.bind(this)
)
.then(
function (/* keys */) {
return this.setupCredentials(this.email, newPassword)
}.bind(this)
)
.then(
function () {
this.wrapKb = butil.xorBuffers(this.kB, this.unwrapBKey)
return this.api.passwordChangeFinish(this.passwordChangeToken, this.authPW, this.wrapKb, headers, sessionToken)
}.bind(this)
)
.then(
function (res) {
this._clear()
return res
}.bind(this)
)
}
// Update to new tokens if needed
this.sessionToken = res.sessionToken ? res.sessionToken : this.sessionToken
this.authAt = res.authAt ? res.authAt : this.authAt
this.keyFetchToken = res.keyFetchToken ? res.keyFetchToken : this.keyFetchToken
Client.prototype.keys = function () {
var o = this.keyFetchToken ? P.resolve(null) : this.login()
return o.then(
return res
}.bind(this)
)
}
Client.prototype.keys = function () {
var o = this.keyFetchToken ? P.resolve(null) : this.login()
return o.then(
function () {
return this.api.accountKeys(this.keyFetchToken)
}.bind(this)
@ -309,136 +310,135 @@ Client.prototype.keys = function () {
throw err
}.bind(this)
)
}
}
Client.prototype.devices = function () {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
function () {
return this.api.accountDevices(this.sessionToken)
}.bind(this)
)
}
Client.prototype.devices = function () {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
function () {
return this.api.accountDevices(this.sessionToken)
}.bind(this)
)
}
Client.prototype.updateDevice = function (info) {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
function () {
return this.api.accountDevice(this.sessionToken, info)
}.bind(this)
)
.then(
function (device) {
if (! this.device || this.device.id === device.id) {
this.device = device
}
return device
}.bind(this)
)
}
Client.prototype.updateDevice = function (info) {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
function () {
return this.api.accountDevice(this.sessionToken, info)
}.bind(this)
)
.then(
function (device) {
if (! this.device || this.device.id === device.id) {
this.device = device
}
return device
}.bind(this)
)
}
Client.prototype.destroyDevice = function (id) {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
function () {
return this.api.deviceDestroy(this.sessionToken, id)
}.bind(this)
)
.then(
function () {
delete this.sessionToken
}.bind(this)
)
}
Client.prototype.destroyDevice = function (id) {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
function () {
return this.api.deviceDestroy(this.sessionToken, id)
}.bind(this)
)
.then(
function () {
delete this.sessionToken
}.bind(this)
)
}
Client.prototype.sessionStatus = function () {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
Client.prototype.sessionStatus = function () {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
function () {
return this.api.sessionStatus(this.sessionToken)
}.bind(this)
)
}
Client.prototype.accountProfile = function (oauthToken) {
if (oauthToken) {
return this.api.accountProfile(null, { Authorization: 'Bearer ' + oauthToken })
} else {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
function () {
return this.api.accountProfile(this.sessionToken)
}.bind(this)
)
}
}
Client.prototype.destroyAccount = function () {
return this.api.accountDestroy(this.email, this.authPW)
.then(this._clear.bind(this))
}
Client.prototype.forgotPassword = function (lang) {
return this.api.passwordForgotSendCode(this.email, this.options, lang)
.then(
function (x) {
this.passwordForgotToken = x.passwordForgotToken
}.bind(this)
)
}
Client.prototype.reforgotPassword = function () {
return this.api.passwordForgotResendCode(this.passwordForgotToken, this.email)
}
Client.prototype.verifyPasswordResetCode = function (code, headers) {
return this.api.passwordForgotVerifyCode(this.passwordForgotToken, code, headers)
.then(
function (result) {
this.accountResetToken = result.accountResetToken
}.bind(this)
)
}
Client.prototype.lockAccount = function () {
return this.api.accountLock(this.email, this.authPW)
}
Client.prototype.resendAccountUnlockCode = function (lang) {
return this.api.accountUnlockResendCode(this.email, this.options, lang)
}
Client.prototype.verifyAccountUnlockCode = function (uid, code) {
return this.api.accountUnlockVerifyCode(uid, code)
}
Client.prototype.resetPassword = function (newPassword, headers, options) {
if (!this.accountResetToken) {
throw new Error('call verifyPasswordResetCode before calling resetPassword')
Client.prototype.accountProfile = function (oauthToken) {
if (oauthToken) {
return this.api.accountProfile(null, { Authorization: 'Bearer ' + oauthToken })
} else {
var o = this.sessionToken ? P.resolve(null) : this.login()
return o.then(
function () {
return this.api.accountProfile(this.sessionToken)
}.bind(this)
)
}
}
// this will generate a new wrapKb on the server
return this.setupCredentials(this.email, newPassword)
.then(
function (/* bundle */) {
return this.api.accountReset(
this.accountResetToken,
this.authPW,
headers,
options
)
.then(function (response) {
// Update to the new verified tokens
this.sessionToken = response.sessionToken
this.keyFetchToken = response.keyFetchToken
return response
})
Client.prototype.destroyAccount = function () {
return this.api.accountDestroy(this.email, this.authPW)
.then(this._clear.bind(this))
}
}.bind(this)
)
Client.prototype.forgotPassword = function (lang) {
return this.api.passwordForgotSendCode(this.email, this.options, lang)
.then(
function (x) {
this.passwordForgotToken = x.passwordForgotToken
}.bind(this)
)
}
Client.prototype.reforgotPassword = function () {
return this.api.passwordForgotResendCode(this.passwordForgotToken, this.email)
}
Client.prototype.verifyPasswordResetCode = function (code, headers) {
return this.api.passwordForgotVerifyCode(this.passwordForgotToken, code, headers)
.then(
function (result) {
this.accountResetToken = result.accountResetToken
}.bind(this)
)
}
Client.prototype.lockAccount = function () {
return this.api.accountLock(this.email, this.authPW)
}
Client.prototype.resendAccountUnlockCode = function (lang) {
return this.api.accountUnlockResendCode(this.email, this.options, lang)
}
Client.prototype.verifyAccountUnlockCode = function (uid, code) {
return this.api.accountUnlockVerifyCode(uid, code)
}
Client.prototype.resetPassword = function (newPassword, headers, options) {
if (!this.accountResetToken) {
throw new Error('call verifyPasswordResetCode before calling resetPassword')
}
// this will generate a new wrapKb on the server
return this.setupCredentials(this.email, newPassword)
.then(
function (/* bundle */) {
return this.api.accountReset(
this.accountResetToken,
this.authPW,
headers,
options
)
.then(function (response) {
// Update to the new verified tokens
this.sessionToken = response.sessionToken
this.keyFetchToken = response.keyFetchToken
return response
})
}.bind(this)
)
}
return Client
}
//TODO recovery methods, session status/destroy
module.exports = Client

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

@ -34,6 +34,8 @@ var makeRoutes = function (options, requireMocks) {
supportedLanguages: ['en'],
defaultLanguage: 'en'
}
config.lastAccessTimeUpdates = {}
config.signinConfirmation = config.signinConfirmation || {}
var log = options.log || mocks.mockLog()
var Password = options.Password || require('../../lib/crypto/password')(log, config)
@ -709,7 +711,8 @@ test('/account/login', function (t) {
newLoginNotificationEnabled: true,
securityHistory: {
enabled: true
}
},
signinConfirmation: {}
}
var mockRequest = mocks.mockRequest({
query: {
@ -914,12 +917,10 @@ test('/account/login', function (t) {
})
t.test('sign-in confirmation enabled', function (t) {
t.plan(13)
config.signinConfirmation = {
enabled: true,
supportedClients: [ 'fx_desktop_v3' ],
forceEmailRegex: [ '.+@mozilla\.com$', 'fennec@fire.fox' ]
}
t.plan(10)
config.signinConfirmation.enabled = true
config.signinConfirmation.supportedClients = [ 'fx_desktop_v3' ]
config.signinConfirmation.forceEmailRegex = [ '.+@mozilla\\.com$', 'fennec@fire.fox' ]
mockDB.emailRecord = function () {
return P.resolve({
authSalt: crypto.randomBytes(32),
@ -945,7 +946,6 @@ test('/account/login', function (t) {
t.ok(tokenData.tokenVerificationId, 'sessionToken was created unverified')
t.equal(mockMailer.sendVerifyCode.callCount, 0, 'mailer.sendVerifyCode was not called')
t.equal(mockMailer.sendNewDeviceLoginNotification.callCount, 0, 'mailer.sendNewDeviceLoginNotification was not called')
t.equal(mockMailer.sendVerifyLoginEmail.callCount, 1, 'mailer.sendVerifyLoginEmail was called')
t.notOk(response.verified, 'response indicates account is not verified')
t.equal(response.verificationMethod, 'email', 'verificationMethod is email')
t.equal(response.verificationReason, 'login', 'verificationReason is login')
@ -959,15 +959,7 @@ test('/account/login', function (t) {
t.deepEqual(args[1], 'account.confirmed', 'second argument was event name')
t.equal(args[2], mockRequest.payload.metricsContext, 'third argument was metrics context')
t.deepEqual(mockMetricsContext.stash.args[2][1], 'account.keyfetch', 'third call was for account.keyfetch')
}).then(function () {
mockMailer.sendVerifyLoginEmail.reset()
mockDB.createSessionToken.reset()
mockMetricsContext.stash.reset()
})
})
t.test('location data is present in sign-in confirmation email', function (t) {
return runTest(route, mockRequest, function (response) {
t.equal(mockMailer.sendVerifyLoginEmail.callCount, 1, 'mailer.sendVerifyLoginEmail was called')
t.equal(mockMailer.sendVerifyLoginEmail.getCall(0).args[2].location.city, 'Mountain View')
t.equal(mockMailer.sendVerifyLoginEmail.getCall(0).args[2].location.country, 'United States')
@ -975,48 +967,7 @@ test('/account/login', function (t) {
}).then(function () {
mockMailer.sendVerifyLoginEmail.reset()
mockDB.createSessionToken.reset()
})
})
t.test('on for sample', function (t) {
// Force uid to '01...'
uid.fill(0, 0, 1)
uid.fill(1, 1, 2)
config.signinConfirmation.sample_rate = 0.02
return runTest(route, mockRequest, function (response) {
t.equal(mockDB.createSessionToken.callCount, 1, 'db.createSessionToken was called')
var tokenData = mockDB.createSessionToken.getCall(0).args[0]
t.ok(tokenData.mustVerify, 'sessionToken must be verified before use')
t.ok(tokenData.tokenVerificationId, 'sessionToken was created unverified')
t.equal(mockMailer.sendNewDeviceLoginNotification.callCount, 0, 'mailer.sendNewDeviceLoginNotification was not called')
t.equal(mockMailer.sendVerifyLoginEmail.callCount, 1, 'mailer.sendVerifyLoginEmail was called')
t.notOk(response.verified, 'response indicates account is not verified')
t.equal(response.verificationMethod, 'email', 'verificationMethod is email')
t.equal(response.verificationReason, 'login', 'verificationReason is login')
}).then(function () {
mockMailer.sendVerifyLoginEmail.reset()
mockDB.createSessionToken.reset()
})
})
t.test('off for sample', function (t) {
config.signinConfirmation.sample_rate = 0.01
return runTest(route, mockRequest, function (response) {
t.equal(mockDB.createSessionToken.callCount, 1, 'db.createSessionToken was called')
var tokenData = mockDB.createSessionToken.getCall(0).args[0]
t.notOk(tokenData.mustVerify, 'mustVerify was not set')
t.notOk(tokenData.tokenVerificationId, 'sessionToken was created verified')
t.equal(mockMailer.sendVerifyCode.callCount, 0, 'mailer.sendVerifyCode was not called')
t.equal(mockMailer.sendNewDeviceLoginNotification.callCount, 1, 'mailer.sendNewDeviceLoginNotification was called')
t.equal(mockMailer.sendVerifyLoginEmail.callCount, 0, 'mailer.sendVerifyLoginEmail was not called')
t.ok(response.verified, 'response indicates account is verified')
t.notOk(response.verificationMethod, 'verificationMethod doesn\'t exist')
t.notOk(response.verificationReason, 'verificationReason doesn\'t exist')
}).then(function () {
mockMailer.sendNewDeviceLoginNotification.reset()
mockDB.createSessionToken.reset()
mockMetricsContext.stash.reset()
})
})
@ -1120,6 +1071,7 @@ test('/account/login', function (t) {
})
t.test('off for email regex mismatch', function (t) {
config.signinConfirmation.sample_rate = 0
mockRequest.payload.email = 'moz@fire.fox'
mockDB.emailRecord = function () {
return P.resolve({

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

@ -0,0 +1,232 @@
/* 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/. */
'use strict'
const test = require('../ptaptest')
const sinon = require('sinon')
const proxyquire = require('proxyquire')
let hashResult = Array(40).fill('0')
const hash = {
update: sinon.spy(),
digest: sinon.spy(() => hashResult)
}
const crypto = {
createHash: sinon.spy(() => hash)
}
const config = {
lastAccessTimeUpdates: {},
signinConfirmation: {}
}
const features = proxyquire('../../lib/features', {
crypto: crypto
})(config)
test(
'interface is correct',
t => {
t.equal(typeof features, 'object', 'object type should be exported')
t.equal(Object.keys(features).length, 3, 'object should have two properties')
t.equal(typeof features.isSampledUser, 'function', 'isSampledUser should be function')
t.equal(typeof features.isLastAccessTimeEnabledForUser, 'function', 'isLastAccessTimeEnabledForUser should be function')
t.equal(typeof features.isSigninConfirmationEnabledForUser, 'function', 'isSigninConfirmationEnabledForUser should be function')
t.equal(crypto.createHash.callCount, 1, 'crypto.createHash should have been called once on require')
let args = crypto.createHash.args[0]
t.equal(args.length, 1, 'crypto.createHash should have been passed one argument')
t.equal(args[0], 'sha1', 'crypto.createHash algorithm should have been sha1')
t.equal(hash.update.callCount, 2, 'hash.update should have been called twice on require')
args = hash.update.args[0]
t.equal(args.length, 1, 'hash.update should have been passed one argument first time')
t.equal(typeof args[0], 'string', 'hash.update data should have been a string first time')
args = hash.update.args[1]
t.equal(args.length, 1, 'hash.update should have been passed one argument second time')
t.equal(typeof args[0], 'string', 'hash.update data should have been a string second time')
t.equal(hash.digest.callCount, 1, 'hash.digest should have been called once on require')
args = hash.digest.args[0]
t.equal(args.length, 1, 'hash.digest should have been passed one argument')
t.equal(args[0], 'hex', 'hash.digest ecnoding should have been hex')
crypto.createHash.reset()
hash.update.reset()
hash.digest.reset()
t.end()
}
)
test(
'isSampledUser',
t => {
let uid = Buffer.alloc(32, 0xff)
let sampleRate = 1
hashResult = Array(40).fill('f').join('')
t.equal(features.isSampledUser(sampleRate, uid, 'foo'), true, 'should always return true if sample rate is 1')
t.equal(crypto.createHash.callCount, 0, 'crypto.createHash should not have been called')
t.equal(hash.update.callCount, 0, 'hash.update should not have been called')
t.equal(hash.digest.callCount, 0, 'hash.digest should not have been called')
sampleRate = 0
hashResult = Array(40).fill('0').join('')
t.equal(features.isSampledUser(sampleRate, uid, 'foo'), false, 'should always return false if sample rate is 0')
t.equal(crypto.createHash.callCount, 0, 'crypto.createHash should not have been called')
t.equal(hash.update.callCount, 0, 'hash.update should not have been called')
t.equal(hash.digest.callCount, 0, 'hash.digest should not have been called')
sampleRate = 0.05
// First 27 characters are ignored, last 13 are 0.04 * 0xfffffffffffff
hashResult = '0000000000000000000000000000a3d70a3d70a6'
t.equal(features.isSampledUser(sampleRate, uid, 'foo'), true, 'should return true if sample rate is greater than the extracted cohort value')
t.equal(crypto.createHash.callCount, 1, 'crypto.createHash should have been called once')
let args = crypto.createHash.args[0]
t.equal(args.length, 1, 'crypto.createHash should have been passed one argument')
t.equal(args[0], 'sha1', 'crypto.createHash algorithm should have been sha1')
t.equal(hash.update.callCount, 2, 'hash.update should have been called twice')
args = hash.update.args[0]
t.equal(args.length, 1, 'hash.update should have been passed one argument first time')
t.equal(args[0], uid.toString('hex'), 'hash.update data should have been stringified uid first time')
args = hash.update.args[1]
t.equal(args.length, 1, 'hash.update should have been passed one argument second time')
t.equal(args[0], 'foo', 'hash.update data should have been key second time')
t.equal(hash.digest.callCount, 1, 'hash.digest should have been called once')
args = hash.digest.args[0]
t.equal(args.length, 1, 'hash.digest should have been passed one argument')
t.equal(args[0], 'hex', 'hash.digest ecnoding should have been hex')
crypto.createHash.reset()
hash.update.reset()
hash.digest.reset()
sampleRate = 0.04
t.equal(features.isSampledUser(sampleRate, uid, 'bar'), false, 'should return false if sample rate is equal to the extracted cohort value')
t.equal(crypto.createHash.callCount, 1, 'crypto.createHash should have been called once')
t.equal(hash.update.callCount, 2, 'hash.update should have been called twice')
t.equal(hash.update.args[0][0], uid.toString('hex'), 'hash.update data should have been stringified uid first time')
t.equal(hash.update.args[1][0], 'bar', 'hash.update data should have been key second time')
t.equal(hash.digest.callCount, 1, 'hash.digest should have been called once')
crypto.createHash.reset()
hash.update.reset()
hash.digest.reset()
sampleRate = 0.03
t.equal(features.isSampledUser(sampleRate, uid, 'foo'), false, 'should return false if sample rate is less than the extracted cohort value')
crypto.createHash.reset()
hash.update.reset()
hash.digest.reset()
uid = Array(64).fill('7').join('')
sampleRate = 0.03
// First 27 characters are ignored, last 13 are 0.02 * 0xfffffffffffff
hashResult = '000000000000000000000000000051eb851eb852'
t.equal(features.isSampledUser(sampleRate, uid, 'wibble'), true, 'should return true if sample rate is greater than the extracted cohort value')
t.equal(hash.update.callCount, 2, 'hash.update should have been called twice')
t.equal(hash.update.args[0][0], uid, 'hash.update data should have been stringified uid first time')
t.equal(hash.update.args[1][0], 'wibble', 'hash.update data should have been key second time')
crypto.createHash.reset()
hash.update.reset()
hash.digest.reset()
t.end()
}
)
test(
'isLastAccessTimeEnabledForUser',
t => {
const uid = 'foo'
const email = 'bar@mozilla.com'
// First 27 characters are ignored, last 13 are 0.02 * 0xfffffffffffff
hashResult = '000000000000000000000000000051eb851eb852'
config.lastAccessTimeUpdates.enabled = true
config.lastAccessTimeUpdates.sampleRate = 0
config.lastAccessTimeUpdates.enabledEmailAddresses = '.+@mozilla\\.com$'
t.equal(features.isLastAccessTimeEnabledForUser(uid, email), true, 'should return true when email address matches')
config.lastAccessTimeUpdates.enabledEmailAddresses = '.+@mozilla\\.org$'
t.equal(features.isLastAccessTimeEnabledForUser(uid, email), false, 'should return false when email address does not match')
config.lastAccessTimeUpdates.sampleRate = 0.03
t.equal(features.isLastAccessTimeEnabledForUser(uid, email), true, 'should return true when sample rate matches')
config.lastAccessTimeUpdates.sampleRate = 0.02
t.equal(features.isLastAccessTimeEnabledForUser(uid, email), false, 'should return false when sample rate does not match')
config.lastAccessTimeUpdates.enabled = false
config.lastAccessTimeUpdates.sampleRate = 0.03
config.lastAccessTimeUpdates.enabledEmailAddresses = '.+@mozilla\\.com$'
t.equal(features.isLastAccessTimeEnabledForUser(uid, email), false, 'should return false when feature is disabled')
t.end()
}
)
test(
'isSigninConfirmationEnabledForUser',
t => {
const uid = 'wibble'
const email = 'blee@mozilla.com'
const request = {
app: {
isSuspiciousRequest: true
},
payload: {
metricsContext: {
context: 'iframe'
}
}
}
// First 27 characters are ignored, last 13 are 0.02 * 0xfffffffffffff
hashResult = '000000000000000000000000000051eb851eb852'
config.signinConfirmation.enabled = true
config.signinConfirmation.sample_rate = 0.03
config.signinConfirmation.forceEmailRegex = [ 'wibble', '.+@mozilla\\.com$' ]
config.signinConfirmation.supportedClients = [ 'wibble', 'iframe' ]
t.equal(features.isSigninConfirmationEnabledForUser(uid, email, request), true, 'should return true when request is suspicious')
config.signinConfirmation.sample_rate = 0.02
request.app.isSuspiciousRequest = false
t.equal(features.isSigninConfirmationEnabledForUser(uid, email, request), true, 'should return true when email address matches')
config.signinConfirmation.forceEmailRegex[1] = '.+@mozilla\\.org$'
request.payload.metricsContext.context = 'iframe'
t.equal(features.isSigninConfirmationEnabledForUser(uid, email, request), false, 'should return false when email address and sample rate do not match')
config.signinConfirmation.sample_rate = 0.03
t.equal(features.isSigninConfirmationEnabledForUser(uid, email, request), true, 'should return true when sample rate and context match')
request.payload.metricsContext.context = ''
t.equal(features.isSigninConfirmationEnabledForUser(uid, email, request), false, 'should return false when context does not match')
config.signinConfirmation.enabled = false
config.signinConfirmation.forceEmailRegex[1] = '.+@mozilla\\.com$'
request.payload.metricsContext.context = 'iframe'
t.equal(features.isSigninConfirmationEnabledForUser(uid, email, request), false, 'should return false when feature is disabled')
t.end()
}
)

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

@ -7,7 +7,10 @@ var test = require('../ptaptest')
var log = { trace: function() {}, info: function () {} }
var crypto = require('crypto')
var tokens = require('../../lib/tokens')(log)
const config = {
lastAccessTimeUpdates: {}
}
const tokens = require('../../lib/tokens')(log, config)
var SessionToken = tokens.SessionToken
var TOKEN_FRESHNESS_THRESHOLD = require('../../lib/tokens/session_token').TOKEN_FRESHNESS_THREADHOLD
@ -153,8 +156,11 @@ test(
)
test(
'SessionToken.isFresh',
function (t) {
'SessionToken.isFresh with lastAccessTime updates enabled',
t => {
config.lastAccessTimeUpdates.enabled = true
config.lastAccessTimeUpdates.sampleRate = 1
config.lastAccessTimeUpdates.enabledEmailAddresses = '.+'
return SessionToken.create({
uaBrowser: 'foo',
uaBrowserVersion: 'bar',
@ -162,7 +168,7 @@ test(
uaOSVersion: 'qux',
uaDeviceType: 'wibble',
lastAccessTime: 0
}).then(function (token) {
}).then(token => {
t.equal(token.isFresh({
uaBrowser: 'foo',
uaBrowserVersion: 'bar',
@ -231,6 +237,30 @@ test(
}
)
test(
'SessionToken.isFresh with lastAccessTime updates disabled',
t => {
config.lastAccessTimeUpdates.enabled = false
return SessionToken.create({
uaBrowser: 'foo',
uaBrowserVersion: 'bar',
uaOS: 'baz',
uaOSVersion: 'qux',
uaDeviceType: 'wibble',
lastAccessTime: 0
}).then(token => {
t.equal(token.isFresh({
uaBrowser: 'foo',
uaBrowserVersion: 'bar',
uaOS: 'baz',
uaOSVersion: 'qux',
uaDeviceType: 'wibble',
lastAccessTime: TOKEN_FRESHNESS_THRESHOLD
}), true, 'returns true when lastAccessTime is TOKEN_FRESHNESS_THRESHOLD milliseconds newer')
})
}
)
test(
'SessionToken.update on fresh token',
function (t) {

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

@ -5,7 +5,7 @@
var test = require('tap').test
var TestServer = require('../test_server')
var crypto = require('crypto')
var Client = require('../client')
const Client = require('../client')()
var config = require('../../config').getProperties()
// XXX: update this later to avoid issues.
process.env.NODE_ENV = 'dev'

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

@ -4,7 +4,7 @@
var test = require('../ptaptest')
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
var config = require('../../config').getProperties()

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

@ -4,7 +4,7 @@
var test = require('../ptaptest')
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
var config = require('../../config').getProperties()
var key = {

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

@ -2,7 +2,7 @@
* 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 Client = require('../client')
const Client = require('../client')()
var crypto = require('crypto')
var test = require('tap').test
var TestServer = require('../test_server')

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

@ -4,7 +4,7 @@
var test = require('tap').test
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
var JWTool = require('fxa-jwtool')
var config = require('../../config').getProperties()

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

@ -5,7 +5,7 @@
var path = require('path')
var test = require('../ptaptest')
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
process.env.CONFIG_FILES = path.join(__dirname, '../config/mock_oauth.json')
var config = require('../../config').getProperties()

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

@ -4,7 +4,7 @@
var test = require('tap').test
var url = require('url')
var Client = require('../client')
const Client = require('../client')()
var TestServer = require('../test_server')
var config = require('../../config').getProperties()

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

@ -4,7 +4,7 @@
var test = require('../ptaptest')
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
test(
'signin confirmation can be disabled',

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

@ -4,7 +4,7 @@
var test = require('../ptaptest')
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
var config = require('../../config').getProperties()
var url = require('url')
var jwtool = require('fxa-jwtool')

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

@ -4,7 +4,7 @@
var test = require('../ptaptest')
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
var config = require('../../config').getProperties()

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

@ -4,7 +4,7 @@
var test = require('../ptaptest')
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
var config = require('../../config').getProperties()

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

@ -6,7 +6,7 @@ process.env.PUBLIC_URL = 'http://127.0.0.1:9000/auth'
var test = require('../ptaptest')
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
var P = require('../../lib/promise')
var request = require('request')

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

@ -4,7 +4,7 @@
var test = require('../ptaptest')
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
var jwtool = require('fxa-jwtool')
var config = require('../../config').getProperties()

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

@ -4,7 +4,7 @@
var test = require('tap').test
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
var P = require('../../lib/promise')
var config = require('../../config').getProperties()

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

@ -11,9 +11,16 @@ var log = { trace: console.log, info: console.log } // eslint-disable-line no-co
var config = require('../../config').getProperties()
var P = require('../../lib/promise')
var TestServer = require('../test_server')
var Token = require('../../lib/tokens')(log)
var DB = require('../../lib/db')(
config.db.backend,
const lastAccessTimeUpdates = {
enabled: true,
enabledEmailAddresses: '.*',
sampleRate: 1
}
const Token = require('../../lib/tokens')(log, {
lastAccessTimeUpdates: lastAccessTimeUpdates
})
const DB = require('../../lib/db')(
{ lastAccessTimeUpdates: lastAccessTimeUpdates },
log,
Token.error,
Token.SessionToken,
@ -227,10 +234,12 @@ test(
return db.emailRecord(ACCOUNT.email)
.then(function (emailRecord) {
emailRecord.tokenVerificationId = ACCOUNT.tokenVerificationId
// Create a session token
return db.createSessionToken(emailRecord, 'Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0')
})
.then(function (result) {
sessionToken = result
// Attempt to update a non-existent device
return db.updateDevice(ACCOUNT.uid, sessionToken.tokenId, deviceInfo)
.then(function () {
t.fail('updating a non-existent device should have failed')
@ -240,6 +249,7 @@ test(
})
})
.then(function () {
// Attempt to delete a non-existent device
return db.deleteDevice(ACCOUNT.uid, deviceInfo.id)
.then(function () {
t.fail('deleting a non-existent device should have failed')
@ -249,6 +259,7 @@ test(
})
})
.then(function () {
// Fetch all of the devices for the account
return db.devices(ACCOUNT.uid)
.catch(function () {
t.fail('getting devices should not have failed')
@ -257,6 +268,7 @@ test(
.then(function (devices) {
t.ok(Array.isArray(devices), 'devices is array')
t.equal(devices.length, 0, 'devices array is empty')
// Create a device
return db.createDevice(ACCOUNT.uid, sessionToken.tokenId, deviceInfo)
.catch(function (err) {
t.fail('adding a new device should not have failed')
@ -270,6 +282,7 @@ test(
t.equal(device.pushCallback, deviceInfo.pushCallback, 'device.pushCallback is correct')
t.equal(device.pushPublicKey, deviceInfo.pushPublicKey, 'device.pushPublicKey is correct')
t.equal(device.pushAuthKey, deviceInfo.pushAuthKey, 'device.pushAuthKey is correct')
// Attempt to create a device with a duplicate session token
return db.createDevice(ACCOUNT.uid, sessionToken.tokenId, deviceInfo)
.then(function () {
t.fail('adding a device with a duplicate session token should have failed')
@ -279,6 +292,7 @@ test(
})
})
.then(function () {
// Fetch all of the devices for the account
return db.devices(ACCOUNT.uid)
})
.then(function (devices) {
@ -304,6 +318,7 @@ test(
deviceInfo.pushCallback = ''
deviceInfo.pushPublicKey = ''
deviceInfo.pushAuthKey = ''
// Update the device and the session token
return P.all([
db.updateDevice(ACCOUNT.uid, sessionToken.tokenId, deviceInfo),
db.updateSessionToken(sessionToken, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:44.0) Gecko/20100101 Firefox/44.0')
@ -313,6 +328,7 @@ test(
})
})
.then(function (device) {
// Fetch all of the devices for the account
return db.devices(ACCOUNT.uid)
})
.then(function (devices) {
@ -320,6 +336,7 @@ test(
return devices[0]
})
.then(function (device) {
t.ok(device.lastAccessTime > 0, 'device.lastAccessTime is set')
t.equal(device.name, deviceInfo.name, 'device.name is correct')
t.equal(device.type, deviceInfo.type, 'device.type is correct')
t.equal(device.pushCallback, deviceInfo.pushCallback, 'device.pushCallback is correct')
@ -330,12 +347,24 @@ test(
t.equal(device.uaOS, 'Mac OS X', 'device.uaOS is correct')
t.equal(device.uaOSVersion, '10.10', 'device.uaOSVersion is correct')
t.equal(device.uaDeviceType, null, 'device.uaDeviceType is correct')
// Disable the lastAccessTime property
lastAccessTimeUpdates.enabled = false
// Fetch all of the devices for the account
return db.devices(ACCOUNT.uid)
})
.then(function(devices) {
t.equal(devices.length, 1, 'devices array still contains one item')
t.equal(devices[0].lastAccessTime, null, 'device.lastAccessTime should be null')
// Reinstate the lastAccessTime property
lastAccessTimeUpdates.enabled = true
// Delete the device
return db.deleteDevice(ACCOUNT.uid, deviceInfo.id)
.catch(function () {
t.fail('deleting a device should not have failed')
})
})
.then(function () {
// Fetch all of the devices for the account
return db.devices(ACCOUNT.uid)
})
.then(function (devices) {

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

@ -4,12 +4,17 @@
var test = require('../ptaptest')
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
var config = require('../../config').getProperties()
var crypto = require('crypto')
var base64url = require('base64url')
var P = require('../../lib/promise')
// HACK: Force-enable devices.lastAccessTime in the spawned server process
process.env.LASTACCESSTIME_UPDATES_ENABLED = 'true'
process.env.LASTACCESSTIME_UPDATES_EMAIL_ADDRESSES = '.*'
process.env.LASTACCESSTIME_UPDATES_SAMPLE_RATE = '1'
TestServer.start(config)
.then(function main(server) {
test(

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

@ -4,7 +4,7 @@
var test = require('../ptaptest')
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
var P = require('../../lib/promise')
var config = require('../../config').getProperties()

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

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var test = require('tap').test
var Client = require('../client')
const Client = require('../client')()
var TestServer = require('../test_server')
var jwtool = require('fxa-jwtool')

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

@ -4,7 +4,7 @@
var test = require('../ptaptest')
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
var P = require('../../lib/promise')
var hawk = require('hawk')
var request = require('request')

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

@ -2,7 +2,7 @@
* 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 Client = require('../client')
const Client = require('../client')()
var config = require('../../config').getProperties()
var test = require('../ptaptest')
var TestServer = require('../test_server')

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

@ -4,7 +4,7 @@
var test = require('tap').test
var url = require('url')
var Client = require('../client')
const Client = require('../client')()
var TestServer = require('../test_server')
var crypto = require('crypto')
var base64url = require('base64url')

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

@ -14,8 +14,8 @@ var log = { trace: console.log, info: console.log } // eslint-disable-line no-co
var config = require('../../config').getProperties()
var TestServer = require('../test_server')
var Token = require('../../lib/tokens')(log)
var DB = require('../../lib/db')(
config.db.backend,
const DB = require('../../lib/db')(
config,
log,
Token.error,
Token.SessionToken,

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

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var test = require('../ptaptest')
var Client = require('../client')
const Client = require('../client')()
var TestServer = require('../test_server')
var config = require('../../config').getProperties()

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

@ -4,7 +4,7 @@
var test = require('../ptaptest')
var url = require('url')
var Client = require('../client')
const Client = require('../client')()
var TestServer = require('../test_server')
var config = require('../../config').getProperties()

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

@ -4,7 +4,7 @@
var test = require('tap').test
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
var config = require('../../config').getProperties()

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

@ -4,7 +4,7 @@
var test = require('../ptaptest')
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
process.env.PASSWORD_CHANGE_TOKEN_TTL = '1'
var config = require('../../config').getProperties()

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

@ -11,7 +11,7 @@ var config = require('../../config').getProperties()
var TestServer = require('../test_server')
var Token = require('../../lib/tokens')(log)
var DB = require('../../lib/db')(
config.db.backend,
config,
log,
Token.error,
Token.SessionToken,

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

@ -4,7 +4,7 @@
var test = require('tap').test
var TestServer = require('../test_server')
var Client = require('../client')
const Client = require('../client')()
var createDBServer = require('fxa-auth-db-mysql')
var log = { trace: console.log } // eslint-disable-line no-console
@ -15,7 +15,7 @@ process.env.SIGNIN_CONFIRMATION_ENABLED = false
var Token = require('../../lib/tokens')(log)
var DB = require('../../lib/db')(
config.db.backend,
config,
log,
Token.error,
Token.SessionToken,