feat(client): joi like validation of query parameters
Use [VAT](https://github.com/shane_tomlinson/vat) to import, transform, and validate query parameters in the reliers and auth brokers.
This commit is contained in:
Родитель
aad7e50f80
Коммит
d9e18eaaaa
|
@ -642,14 +642,16 @@ define(function (require, exports, module) {
|
|||
},
|
||||
|
||||
_getErrorPage: function (err) {
|
||||
if (OAuthErrors.is(err, 'MISSING_PARAMETER') ||
|
||||
if (AuthErrors.is(err, 'INVALID_PARAMETER') ||
|
||||
AuthErrors.is(err, 'MISSING_PARAMETER') ||
|
||||
OAuthErrors.is(err, 'INVALID_PARAMETER') ||
|
||||
OAuthErrors.is(err, 'MISSING_PARAMETER') ||
|
||||
OAuthErrors.is(err, 'UNKNOWN_CLIENT')) {
|
||||
var queryString = Url.objToSearchString({
|
||||
client_id: err.client_id, //eslint-disable-line camelcase
|
||||
context: err.context,
|
||||
errno: err.errno,
|
||||
message: OAuthErrors.toInterpolatedMessage(err, this._translator),
|
||||
message: err.errorModule.toInterpolatedMessage(err, this._translator),
|
||||
namespace: err.namespace,
|
||||
param: err.param
|
||||
});
|
||||
|
|
|
@ -260,7 +260,7 @@ define(function (require, exports, module) {
|
|||
try {
|
||||
if (this.is(err, 'INVALID_PARAMETER')) {
|
||||
return {
|
||||
param: err.validation.keys
|
||||
param: err.param || err.validation.keys
|
||||
};
|
||||
} else if (this.is(err, 'MISSING_PARAMETER')) {
|
||||
return {
|
||||
|
|
|
@ -128,6 +128,34 @@ define(function (require, exports, module) {
|
|||
return err;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create an INVALID_PARAMETER error. The returned
|
||||
* error will contain a `param` key with the parameter
|
||||
* name
|
||||
*
|
||||
* @param {String} paramName
|
||||
* @returns {Error}
|
||||
*/
|
||||
toInvalidParameterError: function (paramName) {
|
||||
var err = this.toError('INVALID_PARAMETER');
|
||||
err.param = paramName;
|
||||
return err;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a MISSING_PARAMETER error. The returned
|
||||
* error will contain a `param` key with the parameter
|
||||
* name
|
||||
*
|
||||
* @param {String} paramName
|
||||
* @returns {Error}
|
||||
*/
|
||||
toMissingParameterError: function (paramName) {
|
||||
var err = this.toError('MISSING_PARAMETER');
|
||||
err.param = paramName;
|
||||
return err;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if an error is of the given type
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Transform and validate data. Uses the [VAT
|
||||
* library](https://github.com/shane-tomlinson/vat) to
|
||||
* do the heavy lifting.
|
||||
*
|
||||
* If a required field is missing from the data, a
|
||||
* `MISSING_ERROR` error is generated, with the error's
|
||||
* `param` field set to the missing field's name.
|
||||
*
|
||||
* If a field does not pass validation, an
|
||||
* `INVALID_PARAMETER` error is generated, with the error's
|
||||
* `param` field set to the invalid field's name.
|
||||
*/
|
||||
|
||||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Vat = require('lib/vat');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Transform and validate `data` using `schema`.
|
||||
*
|
||||
* @param {object} data - data to validate
|
||||
* @param {object} schema - schema that can be passed to the validator
|
||||
* @param {object} Errors - Errors module used to create errors
|
||||
* @returns {object} validation/transformation results
|
||||
*/
|
||||
transformUsingSchema: function (data, schema, Errors) {
|
||||
var result = Vat.validate(data, schema);
|
||||
var error = result.error;
|
||||
if (error instanceof ReferenceError) {
|
||||
throw Errors.toMissingParameterError(error.key);
|
||||
} else if (error instanceof TypeError) {
|
||||
throw Errors.toInvalidParameterError(error.key);
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
};
|
||||
});
|
|
@ -34,6 +34,9 @@ define(function (require, exports, module) {
|
|||
// * http://tools.ietf.org/html/rfc5321#section-4.5.3.1.1
|
||||
var emailRegex = /^[\w.!#$%&’*+/=?^`{|}~-]{1,64}@[a-z\d](?:[a-z\d-]{0,253}[a-z\d])?(?:\.[a-z\d](?:[a-z\d-]{0,253}[a-z\d])?)+$/i;
|
||||
|
||||
// A Base64 encoded JWT
|
||||
var BASE64_JWT = /^(?:[a-zA-Z0-9-_]+[=]{0,2}\.){2}[a-zA-Z0-9-_]+[=]{0,2}$/;
|
||||
|
||||
var self = {
|
||||
/**
|
||||
* Check if an email address is valid
|
||||
|
@ -233,7 +236,7 @@ define(function (require, exports, module) {
|
|||
},
|
||||
|
||||
/**
|
||||
* Check if the verification redirect value is valid
|
||||
* Check if the verification redirect value is valid.
|
||||
*
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
|
@ -245,6 +248,16 @@ define(function (require, exports, module) {
|
|||
];
|
||||
|
||||
return _.contains(valid, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a JSON Web Token (JWT) is valid.
|
||||
*
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isBase64JwtValid: function isJwtValid(value) {
|
||||
return BASE64_JWT.test(value);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/* 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/. */
|
||||
|
||||
// Do some validation.
|
||||
|
||||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Validate = require('lib/validate');
|
||||
var Vat = require('vat');
|
||||
|
||||
Vat.register('base64jwt', Vat.string().test(Validate.isBase64JwtValid));
|
||||
Vat.register('hex', Vat.string().test(Validate.isHexValid));
|
||||
Vat.register('uri', Vat.string().test(Validate.isUriValid));
|
||||
Vat.register('url', Vat.string().test(Validate.isUrlValid));
|
||||
Vat.register('urn', Vat.string().test(Validate.isUrnValid));
|
||||
|
||||
return Vat;
|
||||
});
|
||||
|
|
@ -10,12 +10,18 @@
|
|||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var _ = require('underscore');
|
||||
var AuthErrors = require('lib/auth-errors');
|
||||
var Backbone = require('backbone');
|
||||
var Cocktail = require('cocktail');
|
||||
var NullBehavior = require('views/behaviors/null');
|
||||
var p = require('lib/promise');
|
||||
var SameBrowserVerificationModel = require('models/verification/same-browser');
|
||||
var SearchParamMixin = require('models/mixins/search-param');
|
||||
var Vat = require('lib/vat');
|
||||
|
||||
var QUERY_PARAMETER_SCHEMA = {
|
||||
automatedBrowser: Vat.boolean()
|
||||
};
|
||||
|
||||
var BaseAuthenticationBroker = Backbone.Model.extend({
|
||||
type: 'base',
|
||||
|
@ -85,7 +91,7 @@ define(function (require, exports, module) {
|
|||
return p()
|
||||
.then(function () {
|
||||
self._isForceAuth = self._isForceAuthUrl();
|
||||
self.importBooleanSearchParam('automatedBrowser');
|
||||
self.importSearchParamsUsingSchema(QUERY_PARAMETER_SCHEMA, AuthErrors);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -409,7 +415,10 @@ define(function (require, exports, module) {
|
|||
verificationInfo.clear();
|
||||
}
|
||||
|
||||
_.extend(BaseAuthenticationBroker.prototype, SearchParamMixin);
|
||||
Cocktail.mixin(
|
||||
BaseAuthenticationBroker,
|
||||
SearchParamMixin
|
||||
);
|
||||
|
||||
module.exports = BaseAuthenticationBroker;
|
||||
});
|
||||
|
|
|
@ -11,13 +11,20 @@ define(function (require, exports, module) {
|
|||
'use strict';
|
||||
|
||||
var _ = require('underscore');
|
||||
var Cocktail = require('cocktail');
|
||||
var OAuthErrors = require('lib/oauth-errors');
|
||||
var ChannelMixin = require('models/auth_brokers/mixins/channel');
|
||||
var OAuthAuthenticationBroker = require('models/auth_brokers/oauth');
|
||||
var p = require('lib/promise');
|
||||
var Vat = require('lib/vat');
|
||||
var WebChannel = require('lib/channels/web');
|
||||
|
||||
var proto = OAuthAuthenticationBroker.prototype;
|
||||
|
||||
var QUERY_PARAMETER_SCHEMA = {
|
||||
webChannelId: Vat.string()
|
||||
};
|
||||
|
||||
var WebChannelAuthenticationBroker = OAuthAuthenticationBroker.extend({
|
||||
type: 'web-channel',
|
||||
defaults: _.extend({}, proto.defaults, {
|
||||
|
@ -207,7 +214,7 @@ define(function (require, exports, module) {
|
|||
},
|
||||
|
||||
_setupSigninSignupFlow: function () {
|
||||
this.importSearchParam('webChannelId');
|
||||
this.importSearchParamsUsingSchema(QUERY_PARAMETER_SCHEMA, OAuthErrors);
|
||||
},
|
||||
|
||||
_setupVerificationFlow: function () {
|
||||
|
@ -223,6 +230,10 @@ define(function (require, exports, module) {
|
|||
}
|
||||
});
|
||||
|
||||
_.extend(WebChannelAuthenticationBroker.prototype, ChannelMixin);
|
||||
Cocktail.mixin(
|
||||
WebChannelAuthenticationBroker,
|
||||
ChannelMixin
|
||||
);
|
||||
|
||||
module.exports = WebChannelAuthenticationBroker;
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Transform = require('lib/transform');
|
||||
var Url = require('lib/url');
|
||||
|
||||
module.exports = {
|
||||
|
@ -16,75 +17,43 @@ define(function (require, exports, module) {
|
|||
* Get a value from the URL search parameter
|
||||
*
|
||||
* @param {String} paramName - name of the search parameter to get
|
||||
* @returns {String}
|
||||
*/
|
||||
getSearchParam: function (paramName) {
|
||||
return Url.searchParam(paramName, this.window.location.search);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a value based on a value in window.location.search. Only updates
|
||||
* model if parameter exists in window.location.search.
|
||||
* Get values from the URL search parameters.
|
||||
*
|
||||
* @param {String} paramName - name of the search parameter
|
||||
* @param {String} [modelName] - name to set in model. If not specified,
|
||||
* use the same value as `paramName`
|
||||
* @param {array of Strings} paramNames - name of the search parameters
|
||||
* to get
|
||||
* @returns {Object}
|
||||
*/
|
||||
importSearchParam: function (paramName, modelName) {
|
||||
modelName = modelName || paramName;
|
||||
|
||||
var value = this.getSearchParam(paramName);
|
||||
if (typeof value !== 'undefined') {
|
||||
this.set(modelName, value);
|
||||
}
|
||||
getSearchParams: function (paramNames) {
|
||||
return Url.searchParams(this.window.location.search, paramNames);
|
||||
},
|
||||
|
||||
/**
|
||||
* Import a boolean search parameter. Search parameter must be `true`
|
||||
* nor `false` or model item will not be set.
|
||||
* Import search parameters defined in the schema. Parameters are
|
||||
* transformed and validated based on the rules defined in the `schema`.
|
||||
*
|
||||
* @param {String} paramName - name of the search parameter
|
||||
* @param {String} [modelName] - name to set in model. If not specified,
|
||||
* use the same value as `paramName`
|
||||
* @param {Errors} Errors - corresponding Errors object
|
||||
* @throws {error}
|
||||
* @param {Object} schema - schema used to define data to import
|
||||
* and validate against
|
||||
* @param {Object} Errors - errors object used to generate errors
|
||||
* @throws
|
||||
* If a required field is missing from the data, a
|
||||
* `MISSING_ERROR` error is generated, with the error's
|
||||
* `param` field set to the missing field's name.
|
||||
*
|
||||
* If a field does not pass validation, an
|
||||
* `INVALID_PARAMETER` error is generated, with the error's
|
||||
* `param` field set to the invalid field's name.
|
||||
*/
|
||||
importBooleanSearchParam: function (paramName, modelName, Errors) {
|
||||
modelName = modelName || paramName;
|
||||
var self = this;
|
||||
|
||||
var textValue = self.getSearchParam(paramName);
|
||||
if (typeof textValue !== 'undefined') {
|
||||
if (textValue === 'true') {
|
||||
self.set(modelName, true);
|
||||
} else if (textValue === 'false') {
|
||||
self.set(modelName, false);
|
||||
} else {
|
||||
var err = Errors.toError('INVALID_PARAMETER');
|
||||
err.param = paramName;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a value based on a value in window.location.search. Throws error
|
||||
* if paramName param is not in window.location.search.
|
||||
*
|
||||
* Throws Error mapped to `MISSING_PARAMETER` in Errors object.
|
||||
*
|
||||
* @param {string} paramName - name of the search parameter
|
||||
* @param {string} modelName - name to set in model
|
||||
* @param {Errors} Errors - corresponding Errors object
|
||||
* @throws {error}
|
||||
*/
|
||||
importRequiredSearchParam: function (paramName, modelName, Errors) {
|
||||
var self = this;
|
||||
self.importSearchParam(paramName, modelName);
|
||||
if (! self.has(modelName || paramName)) {
|
||||
var err = Errors.toError('MISSING_PARAMETER');
|
||||
err.param = paramName;
|
||||
throw err;
|
||||
}
|
||||
importSearchParamsUsingSchema: function (schema, Errors) {
|
||||
var params = this.getSearchParams(Object.keys(schema));
|
||||
var result = Transform.transformUsingSchema(params, schema, Errors);
|
||||
this.set(result);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -14,11 +14,52 @@ define(function (require, exports, module) {
|
|||
var OAuthErrors = require('lib/oauth-errors');
|
||||
var Relier = require('models/reliers/relier');
|
||||
var RelierKeys = require('lib/relier-keys');
|
||||
var Transform = require('lib/transform');
|
||||
var Url = require('lib/url');
|
||||
var Validate = require('lib/validate');
|
||||
var Vat = require('lib/vat');
|
||||
|
||||
var RELIER_FIELDS_IN_RESUME_TOKEN = ['state', 'verificationRedirect'];
|
||||
|
||||
/*eslint-disable camelcase*/
|
||||
var CLIENT_INFO_SCHEMA = {
|
||||
id: Vat.hex().required().renameTo('clientId'),
|
||||
image_uri: Vat.url().allow('').renameTo('imageUri'),
|
||||
name: Vat.string().required().min(1).renameTo('serviceName'),
|
||||
privacy_uri: Vat.url().allow('').renameTo('privacyUri'),
|
||||
redirect_uri: Vat.uri().required().renameTo('redirectUri'),
|
||||
terms_uri: Vat.url().allow('').renameTo('termsUri'),
|
||||
trusted: Vat.boolean().required()
|
||||
};
|
||||
|
||||
var SIGNIN_SIGNUP_QUERY_PARAM_SCHEMA = {
|
||||
access_type: Vat.string().test(Validate.isAccessTypeValid).renameTo('accessType'),
|
||||
client_id: Vat.hex().required().renameTo('clientId'),
|
||||
keys: Vat.boolean(),
|
||||
prompt: Vat.string().test(Validate.isPromptValid),
|
||||
redirectTo: Vat.uri(),
|
||||
redirect_uri: Vat.uri().renameTo('redirectUri'),
|
||||
scope: Vat.string().required().min(1),
|
||||
service: Vat.string(),
|
||||
state: Vat.string(),
|
||||
verification_redirect: Vat.string().test(Validate.isVerificationRedirectValid).renameTo('verificationRedirect')
|
||||
};
|
||||
|
||||
var VERIFICATION_INFO_SCHEMA = {
|
||||
access_type: Vat.string().test(Validate.isAccessTypeValid).renameTo('accessType'),
|
||||
action: Vat.string().min(1),
|
||||
client_id: Vat.hex().required().renameTo('clientId'),
|
||||
keys: Vat.boolean(),
|
||||
prompt: Vat.string().test(Validate.isPromptValid),
|
||||
redirect_uri: Vat.uri().renameTo('redirectUri'),
|
||||
// scopes are optional when verifying, user could be verifying in a 2nd browser
|
||||
scope: Vat.string().min(1),
|
||||
service: Vat.string().min(1),
|
||||
state: Vat.string().min(1)
|
||||
};
|
||||
/*eslint-enable camelcase*/
|
||||
|
||||
|
||||
var OAuthRelier = Relier.extend({
|
||||
defaults: _.extend({}, Relier.prototype.defaults, {
|
||||
accessType: null,
|
||||
|
@ -74,16 +115,6 @@ define(function (require, exports, module) {
|
|||
return self._setupOAuthRPInfo();
|
||||
})
|
||||
.then(function () {
|
||||
if (! self.get('clientId')) {
|
||||
throw toMissingParameterError('client_id');
|
||||
} else if (! self.get('redirectUri')) {
|
||||
// Guard against a possible empty redirect_uri
|
||||
// sent by the server. redirect_uri is not required
|
||||
// by the OAuth server, but if it's missing, we redirect
|
||||
// to ourselves. See issue #3452
|
||||
throw toMissingParameterError('redirect_uri');
|
||||
}
|
||||
|
||||
if (self.has('scope')) {
|
||||
// normalization depends on `trusted` field set in
|
||||
// setupOAuthRPInfo.
|
||||
|
@ -106,7 +137,7 @@ define(function (require, exports, module) {
|
|||
}
|
||||
|
||||
if (! permissions.length) {
|
||||
throw toInvalidParameterError('scope');
|
||||
throw OAuthErrors.toInvalidParameterError('scope');
|
||||
}
|
||||
|
||||
this.set('scope', permissions.join(' '));
|
||||
|
@ -150,90 +181,17 @@ define(function (require, exports, module) {
|
|||
};
|
||||
}
|
||||
|
||||
this._validateOAuthValues(resumeObj);
|
||||
var result = Transform.transformUsingSchema(
|
||||
resumeObj, VERIFICATION_INFO_SCHEMA, OAuthErrors);
|
||||
|
||||
self.set({
|
||||
accessType: resumeObj.access_type,
|
||||
clientId: resumeObj.client_id,
|
||||
keys: resumeObj.keys,
|
||||
redirectUri: resumeObj.redirect_uri,
|
||||
scope: resumeObj.scope,
|
||||
state: resumeObj.state
|
||||
});
|
||||
self.set(result);
|
||||
},
|
||||
|
||||
_setupSignInSignUpFlow: function () {
|
||||
var self = this;
|
||||
/*eslint-disable camelcase*/
|
||||
var oauthOptions = {
|
||||
access_type: self.getSearchParam('access_type'),
|
||||
client_id: self.getSearchParam('client_id'),
|
||||
prompt: self.getSearchParam('prompt'),
|
||||
redirectTo: self.getSearchParam('redirectTo'),
|
||||
redirect_uri: self.getSearchParam('redirect_uri'),
|
||||
verification_redirect: self.getSearchParam('verification_redirect'),
|
||||
};
|
||||
/*eslint-enable camelcase*/
|
||||
|
||||
this._validateOAuthValues(oauthOptions);
|
||||
|
||||
// params listed in:
|
||||
// https://github.com/mozilla/fxa-oauth-server/blob/master/docs/api.md#post-v1authorization
|
||||
self.importSearchParam('access_type', 'accessType');
|
||||
self.importRequiredSearchParam('client_id', 'clientId', OAuthErrors);
|
||||
self.importBooleanSearchParam('keys', 'keys', OAuthErrors);
|
||||
self.importSearchParam('prompt');
|
||||
self.importSearchParam('redirectTo');
|
||||
self.importSearchParam('redirect_uri', 'redirectUri');
|
||||
self.importRequiredSearchParam('scope', 'scope', OAuthErrors);
|
||||
self.importSearchParam('state');
|
||||
self.importSearchParam('verification_redirect', 'verificationRedirect');
|
||||
},
|
||||
|
||||
_validateOAuthValues: function (options) {
|
||||
var accessType = options.access_type;
|
||||
var clientId = options.client_id;
|
||||
var prompt = options.prompt;
|
||||
var redirectUri = options.redirect_uri;
|
||||
var redirectTo = options.redirectTo;
|
||||
var termsUri = options.terms_uri;
|
||||
var privacyUri = options.privacy_uri;
|
||||
var verificationRedirect = options.verification_redirect;
|
||||
|
||||
if (isDefined(accessType) && ! Validate.isAccessTypeValid(accessType)) {
|
||||
throw toInvalidParameterError('access_type');
|
||||
}
|
||||
|
||||
if (isDefined(clientId) && ! Validate.isHexValid(clientId)) {
|
||||
throw toInvalidParameterError('client_id');
|
||||
}
|
||||
|
||||
// privacyUri is allowed to be ``
|
||||
if (privacyUri && ! Validate.isUriValid(privacyUri)) {
|
||||
throw toInvalidParameterError('privacy_uri');
|
||||
}
|
||||
|
||||
if (isDefined(prompt) && ! Validate.isPromptValid(prompt)) {
|
||||
throw toInvalidParameterError('prompt');
|
||||
}
|
||||
|
||||
if (isDefined(redirectTo) && ! Validate.isUriValid(redirectTo)) {
|
||||
throw toInvalidParameterError('redirectTo');
|
||||
}
|
||||
|
||||
if (isDefined(redirectUri) && ! Validate.isUriValid(redirectUri)) {
|
||||
throw toInvalidParameterError('redirect_uri');
|
||||
}
|
||||
|
||||
// termsUri is allowed to be ``
|
||||
if (termsUri && ! Validate.isUriValid(termsUri)) {
|
||||
throw toInvalidParameterError('terms_uri');
|
||||
}
|
||||
|
||||
if (isDefined(verificationRedirect) &&
|
||||
! Validate.isVerificationRedirectValid(verificationRedirect)) {
|
||||
throw toInvalidParameterError('verification_redirect');
|
||||
}
|
||||
this.importSearchParamsUsingSchema(
|
||||
SIGNIN_SIGNUP_QUERY_PARAM_SCHEMA, OAuthErrors);
|
||||
},
|
||||
|
||||
_setupOAuthRPInfo: function () {
|
||||
|
@ -242,15 +200,9 @@ define(function (require, exports, module) {
|
|||
|
||||
return self._oAuthClient.getClientInfo(clientId)
|
||||
.then(function (serviceInfo) {
|
||||
self.set('serviceName', serviceInfo.name);
|
||||
|
||||
self._validateOAuthValues(serviceInfo);
|
||||
|
||||
// server version always takes precedent over the search parameter
|
||||
self.set('redirectUri', serviceInfo.redirect_uri);
|
||||
self.set('termsUri', serviceInfo.terms_uri);
|
||||
self.set('privacyUri', serviceInfo.privacy_uri);
|
||||
self.set('trusted', serviceInfo.trusted);
|
||||
var result = Transform.transformUsingSchema(
|
||||
serviceInfo, CLIENT_INFO_SCHEMA, OAuthErrors);
|
||||
self.set(result);
|
||||
self.set('origin', Url.getOrigin(serviceInfo.redirect_uri));
|
||||
}, function (err) {
|
||||
// the server returns an invalid request parameter for an
|
||||
|
@ -327,23 +279,5 @@ define(function (require, exports, module) {
|
|||
return _.intersection(permissions, Constants.OAUTH_UNTRUSTED_ALLOWED_PERMISSIONS);
|
||||
}
|
||||
|
||||
function toInvalidParameterError(param) {
|
||||
return toOAuthParameterError('INVALID_PARAMETER', param);
|
||||
}
|
||||
|
||||
function toMissingParameterError(param) {
|
||||
return toOAuthParameterError('MISSING_PARAMETER', param);
|
||||
}
|
||||
|
||||
function toOAuthParameterError(type, param) {
|
||||
var err = OAuthErrors.toError(type);
|
||||
err.param = param;
|
||||
return err;
|
||||
}
|
||||
|
||||
function isDefined(value) {
|
||||
return ! _.isUndefined(value);
|
||||
}
|
||||
|
||||
module.exports = OAuthRelier;
|
||||
});
|
||||
|
|
|
@ -13,31 +13,61 @@
|
|||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var AuthErrors = require('lib/auth-errors');
|
||||
var BaseRelier = require('models/reliers/base');
|
||||
var Cocktail = require('cocktail');
|
||||
var Constants = require('lib/constants');
|
||||
var p = require('lib/promise');
|
||||
var ResumeTokenMixin = require('models/mixins/resume-token');
|
||||
var SearchParamMixin = require('models/mixins/search-param');
|
||||
var Vat = require('lib/vat');
|
||||
|
||||
var RELIER_FIELDS_IN_RESUME_TOKEN = [
|
||||
'utmTerm',
|
||||
'utmSource',
|
||||
'utmMedium',
|
||||
'utmContent',
|
||||
'utmCampaign',
|
||||
'campaign',
|
||||
'entrypoint'
|
||||
'entrypoint',
|
||||
'utmCampaign',
|
||||
'utmContent',
|
||||
'utmMedium',
|
||||
'utmSource',
|
||||
'utmTerm'
|
||||
];
|
||||
|
||||
/*eslint-disable camelcase*/
|
||||
var QUERY_PARAMETER_SCHEMA = {
|
||||
campaign: Vat.string(),
|
||||
// `email` will be further validated by views to
|
||||
// show the appropriate error message
|
||||
email: Vat.string().allow(Constants.DISALLOW_CACHED_CREDENTIALS),
|
||||
// FxDesktop declares both `entryPoint` (capital P) and
|
||||
// `entrypoint` (lowcase p). Normalize to `entrypoint`.
|
||||
entryPoint: Vat.string(),
|
||||
entrypoint: Vat.string(),
|
||||
migration: Vat.string().valid(Constants.AMO_MIGRATION, Constants.SYNC11_MIGRATION),
|
||||
preVerifyToken: Vat.base64jwt(),
|
||||
service: Vat.string(),
|
||||
setting: Vat.string(),
|
||||
// `uid` will be further validated by the views to
|
||||
// show the appropriate error message
|
||||
uid: Vat.string(),
|
||||
utm_campaign: Vat.string().renameTo('utmCampaign'),
|
||||
utm_content: Vat.string().renameTo('utmContent'),
|
||||
utm_medium: Vat.string().renameTo('utmMedium'),
|
||||
utm_source: Vat.string().renameTo('utmSource'),
|
||||
utm_term: Vat.string().renameTo('utmTerm')
|
||||
};
|
||||
/*eslint-enable camelcase*/
|
||||
|
||||
var Relier = BaseRelier.extend({
|
||||
defaults: {
|
||||
allowCachedCredentials: true,
|
||||
campaign: null,
|
||||
email: null,
|
||||
entrypoint: null,
|
||||
migration: null,
|
||||
preVerifyToken: null,
|
||||
service: null,
|
||||
setting: null,
|
||||
uid: null,
|
||||
utmCampaign: null,
|
||||
utmContent: null,
|
||||
utmMedium: null,
|
||||
|
@ -75,36 +105,20 @@ define(function (require, exports, module) {
|
|||
// query parameters and server provided data override
|
||||
// resume provided data.
|
||||
self.populateFromStringifiedResumeToken(self.getSearchParam('resume'));
|
||||
// TODO - validate data coming from the resume token
|
||||
|
||||
self.importSearchParam('service');
|
||||
self.importSearchParam('preVerifyToken');
|
||||
self.importSearchParam('uid');
|
||||
self.importSearchParam('setting');
|
||||
self.importSearchParam('entrypoint');
|
||||
if (! self.has('entrypoint')) {
|
||||
// FxDesktop declares both `entryPoint` (capital P) and
|
||||
// `entrypoint` (lowcase p). Normalize to `entrypoint`.
|
||||
self.importSearchParam('entryPoint', 'entrypoint');
|
||||
self.importSearchParamsUsingSchema(QUERY_PARAMETER_SCHEMA, AuthErrors);
|
||||
|
||||
// FxDesktop declares both `entryPoint` (capital P) and
|
||||
// `entrypoint` (lowcase p). Normalize to `entrypoint`.
|
||||
if (self.has('entryPoint') && ! self.has('entrypoint')) {
|
||||
self.set('entrypoint', self.get('entryPoint'));
|
||||
}
|
||||
self.importSearchParam('campaign');
|
||||
|
||||
self.importSearchParam('utm_campaign', 'utmCampaign');
|
||||
self.importSearchParam('utm_content', 'utmContent');
|
||||
self.importSearchParam('utm_medium', 'utmMedium');
|
||||
self.importSearchParam('utm_source', 'utmSource');
|
||||
self.importSearchParam('utm_term', 'utmTerm');
|
||||
|
||||
// A relier can indicate they do not want to allow
|
||||
// cached credentials if they set email === 'blank'
|
||||
var email = self.getSearchParam('email');
|
||||
if (email === Constants.DISALLOW_CACHED_CREDENTIALS) {
|
||||
if (self.get('email') === Constants.DISALLOW_CACHED_CREDENTIALS) {
|
||||
self.unset('email');
|
||||
self.set('allowCachedCredentials', false);
|
||||
} else {
|
||||
self.importSearchParam('email');
|
||||
}
|
||||
|
||||
self.importSearchParam('migration');
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -13,14 +13,24 @@
|
|||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var AuthErrors = require('lib/auth-errors');
|
||||
var _ = require('underscore');
|
||||
var Relier = require('models/reliers/relier');
|
||||
var ServiceNameTranslator = require('lib/service-name');
|
||||
var Vat = require('lib/vat');
|
||||
|
||||
/*eslint-disable camelcase*/
|
||||
var QUERY_PARAMETER_SCHEMA = {
|
||||
// context is not available when verifying.
|
||||
context: Vat.string().min(1),
|
||||
customizeSync: Vat.boolean()
|
||||
};
|
||||
/*eslint-enable camelcase*/
|
||||
|
||||
var SyncRelier = Relier.extend({
|
||||
defaults: _.extend({}, Relier.prototype.defaults, {
|
||||
context: null,
|
||||
migration: null
|
||||
customizeSync: false
|
||||
}),
|
||||
|
||||
initialize: function (options) {
|
||||
|
@ -35,19 +45,7 @@ define(function (require, exports, module) {
|
|||
var self = this;
|
||||
return Relier.prototype.fetch.call(self)
|
||||
.then(function () {
|
||||
self.importSearchParam('context');
|
||||
|
||||
try {
|
||||
self.importBooleanSearchParam('customizeSync');
|
||||
} catch (e) {
|
||||
// ignore it for now.
|
||||
// TODO - handle the error whenever startup error handling is
|
||||
// complete - see #1982. This includes logging the error.
|
||||
// Use something like:
|
||||
// var err = AuthErrors.toError('INVALID_PARAMETER')
|
||||
// err.param = 'customizeSync';
|
||||
// throw err;
|
||||
}
|
||||
self.importSearchParamsUsingSchema(QUERY_PARAMETER_SCHEMA, AuthErrors);
|
||||
|
||||
self._setupServiceName();
|
||||
});
|
||||
|
|
|
@ -48,6 +48,7 @@ require.config({
|
|||
text: '../bower_components/requirejs-text/text',
|
||||
underscore: '../bower_components/underscore/underscore',
|
||||
uuid: '../bower_components/node-uuid/uuid',
|
||||
vat: '../bower_components/vat/vat',
|
||||
webrtc: '../bower_components/webrtc-adapter/adapter'
|
||||
},
|
||||
shim: {
|
||||
|
|
|
@ -89,6 +89,18 @@ define(function (require, exports, module) {
|
|||
return email.split('@')[0];
|
||||
}
|
||||
|
||||
function getValueLabel(value) {
|
||||
if (_.isUndefined(value)) {
|
||||
return 'not set';
|
||||
} else if (value === '') {
|
||||
return 'empty';
|
||||
} else if (/^\s+$/.test(value)) {
|
||||
return 'whitespace only';
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function indexOfEvent(metrics, eventName) {
|
||||
var events = metrics.getFilteredData().events;
|
||||
|
||||
|
@ -145,6 +157,7 @@ define(function (require, exports, module) {
|
|||
createEmail: createEmail,
|
||||
createRandomHexString: createRandomHexString,
|
||||
emailToUser: emailToUser,
|
||||
getValueLabel: getValueLabel,
|
||||
indexOfEvent: indexOfEvent,
|
||||
isErrorLogged: isErrorLogged,
|
||||
isEventLogged: isEventLogged,
|
||||
|
|
|
@ -536,19 +536,19 @@ define(function (require, exports, module) {
|
|||
});
|
||||
|
||||
describe('_getErrorPage', function () {
|
||||
it('returns BAD_REQUEST_PAGE for a missing OAuth parameter', function () {
|
||||
var errorUrl = appStart._getErrorPage(OAuthErrors.toError('MISSING_PARAMETER'));
|
||||
assert.include(errorUrl, Constants.BAD_REQUEST_PAGE);
|
||||
});
|
||||
var badRequestPageErrors = [
|
||||
AuthErrors.toError('INVALID_PARAMETER'),
|
||||
AuthErrors.toError('MISSING_PARAMETER'),
|
||||
OAuthErrors.toError('INVALID_PARAMETER'),
|
||||
OAuthErrors.toError('MISSING_PARAMETER'),
|
||||
OAuthErrors.toError('UNKNOWN_CLIENT')
|
||||
];
|
||||
|
||||
it('returns BAD_REQUEST_PAGE for an invalid OAuth parameter', function () {
|
||||
var errorUrl = appStart._getErrorPage(OAuthErrors.toError('INVALID_PARAMETER'));
|
||||
assert.include(errorUrl, Constants.BAD_REQUEST_PAGE);
|
||||
});
|
||||
|
||||
it('returns BAD_REQUEST_PAGE for an unknown OAuth client', function () {
|
||||
var errorUrl = appStart._getErrorPage(OAuthErrors.toError('UNKNOWN_CLIENT'));
|
||||
assert.include(errorUrl, Constants.BAD_REQUEST_PAGE);
|
||||
badRequestPageErrors.forEach(function (err) {
|
||||
it('redirects to BAD_REQUEST_PAGE for ' + err.message, function () {
|
||||
var errorUrl = appStart._getErrorPage(OAuthErrors.toError('MISSING_PARAMETER'));
|
||||
assert.include(errorUrl, Constants.BAD_REQUEST_PAGE);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns INTERNAL_ERROR_PAGE by default', function () {
|
||||
|
|
|
@ -32,6 +32,33 @@ define(function (require, exports, module) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('toInvalidParameterError', function () {
|
||||
var err;
|
||||
|
||||
before(function () {
|
||||
err = AuthErrors.toInvalidParameterError('param name', AuthErrors);
|
||||
});
|
||||
|
||||
it('creates an INVALID_PARAMTER Error', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
assert.equal(err.param, 'param name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toMissingParameterError', function () {
|
||||
var err;
|
||||
|
||||
before(function () {
|
||||
err = AuthErrors.toMissingParameterError('param name', AuthErrors);
|
||||
});
|
||||
|
||||
it('creates an MISSING_PARAMTER Error', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'MISSING_PARAMETER'));
|
||||
assert.equal(err.param, 'param name');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('toMessage', function () {
|
||||
it('converts a code to a message', function () {
|
||||
assert.equal(AuthErrors.toMessage(102), 'Unknown account');
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/* 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/. */
|
||||
|
||||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var AuthErrors = require('lib/auth-errors');
|
||||
var chai = require('chai');
|
||||
var Transform = require('lib/transform');
|
||||
var Vat = require('lib/vat');
|
||||
|
||||
var assert = chai.assert;
|
||||
|
||||
describe('lib/transform', function () {
|
||||
describe('transformUsingSchema', function () {
|
||||
describe('with a missing parameter', function () {
|
||||
var err;
|
||||
|
||||
before(function () {
|
||||
var schema = {
|
||||
optional: Vat.any(),
|
||||
required: Vat.any().required()
|
||||
};
|
||||
|
||||
try {
|
||||
Transform.transformUsingSchema({}, schema, AuthErrors);
|
||||
} catch (_err) {
|
||||
err = _err;
|
||||
}
|
||||
});
|
||||
|
||||
it('throws a `MISSING_PARAMETER` error', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'MISSING_PARAMETER'));
|
||||
assert.equal(err.param, 'required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an invalid parameter', function () {
|
||||
var err;
|
||||
|
||||
before(function () {
|
||||
var schema = {
|
||||
numeric: Vat.number()
|
||||
};
|
||||
|
||||
try {
|
||||
Transform.transformUsingSchema({
|
||||
numeric: 'a'
|
||||
}, schema, AuthErrors);
|
||||
} catch (_err) {
|
||||
err = _err;
|
||||
}
|
||||
});
|
||||
|
||||
it('throws a `INVALID_PARAMETER` error', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
assert.equal(err.param, 'numeric');
|
||||
});
|
||||
});
|
||||
|
||||
describe('valid', function () {
|
||||
var result;
|
||||
|
||||
before(function () {
|
||||
var schema = {
|
||||
numeric: Vat.number(),
|
||||
optional: Vat.any(),
|
||||
required: Vat.any().required()
|
||||
};
|
||||
|
||||
result = Transform.transformUsingSchema({
|
||||
numeric: 123,
|
||||
required: true,
|
||||
}, schema, AuthErrors);
|
||||
});
|
||||
|
||||
it('succeeds', function () {
|
||||
assert.deepEqual(result, {
|
||||
numeric: 123,
|
||||
required: true
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -447,5 +447,25 @@ define(function (require, exports, module) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBase64JwtValid', function () {
|
||||
it('is defined', function () {
|
||||
assert.isFunction(Validate.isBase64JwtValid);
|
||||
});
|
||||
|
||||
var invalidTypes = ['', 'word.asdf=', 'crazyType'];
|
||||
invalidTypes.forEach( function (item) {
|
||||
it('returns false for ' + item, function () {
|
||||
assert.isFalse(Validate.isBase64JwtValid(item));
|
||||
});
|
||||
});
|
||||
|
||||
var validTypes = ['abcd-=.asdfbc==.asdf90154-_=='];
|
||||
validTypes.forEach( function (item) {
|
||||
it('returns true for ' + item, function () {
|
||||
assert.isTrue(Validate.isBase64JwtValid(item));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ define(function (require, exports, module) {
|
|||
var Cocktail = require('cocktail');
|
||||
var SearchParamMixin = require('models/mixins/search-param');
|
||||
var TestHelpers = require('../../../lib/helpers');
|
||||
var Vat = require('vat');
|
||||
var WindowMock = require('../../../mocks/window');
|
||||
|
||||
var assert = chai.assert;
|
||||
|
@ -45,150 +46,67 @@ define(function (require, exports, module) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('importSearchParam', function () {
|
||||
it('imports the value of a search parameter, onto the model', function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
searchParam: 'value'
|
||||
});
|
||||
describe('importSearchParamsUsingSchema', function () {
|
||||
var schema = {
|
||||
optional: Vat.string().optional(),
|
||||
required: Vat.string().valid('value').required()
|
||||
};
|
||||
|
||||
model.importSearchParam('searchParam');
|
||||
assert.equal(model.get('searchParam'), 'value');
|
||||
|
||||
model.importSearchParam('notAvailable');
|
||||
assert.isUndefined(model.get('notAvailable'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('importBooleanSearchParam', function () {
|
||||
var err;
|
||||
|
||||
function importExpectFailure(searchParams, sourceName, destName) {
|
||||
windowMock.location.search = TestHelpers.toSearchString(searchParams);
|
||||
try {
|
||||
model.importBooleanSearchParam(sourceName, destName, AuthErrors);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
}
|
||||
|
||||
it('sets value to the boolean `true` if search param is `true`', function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
expectBoolean: true
|
||||
});
|
||||
|
||||
model.importBooleanSearchParam('expectBoolean');
|
||||
assert.isTrue(model.get('expectBoolean'));
|
||||
});
|
||||
|
||||
it('sets value to the boolean `false` if search param is `false`', function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
expectBoolean: false
|
||||
});
|
||||
|
||||
model.importBooleanSearchParam('expectBoolean');
|
||||
assert.isFalse(model.get('expectBoolean'));
|
||||
});
|
||||
|
||||
describe('value is not boolean', function () {
|
||||
describe('passes validation', function () {
|
||||
beforeEach(function () {
|
||||
importExpectFailure({ expectBoolean: 'not a boolean' }, 'expectBoolean');
|
||||
windowMock.location.search =
|
||||
TestHelpers.toSearchString({ ignored: true, required: 'value' });
|
||||
model.importSearchParamsUsingSchema(schema, AuthErrors);
|
||||
});
|
||||
|
||||
it('errors correctly', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
it('imports fields in the schema that have values', function () {
|
||||
assert.equal(model.get('required'), 'value');
|
||||
});
|
||||
|
||||
it('does not import optional fields in the schema w/o values', function () {
|
||||
assert.isFalse(model.has('optional'));
|
||||
});
|
||||
|
||||
it('ignores fields not in the schema', function () {
|
||||
assert.isFalse(model.has('ignored'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('value is empty', function () {
|
||||
beforeEach(function () {
|
||||
importExpectFailure({ expectBoolean: '' }, 'expectBoolean');
|
||||
});
|
||||
describe('does not pass validation', function () {
|
||||
var err;
|
||||
|
||||
it('errors correctly', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('value is a space', function () {
|
||||
beforeEach(function () {
|
||||
importExpectFailure({ expectBoolean: ' ' }, 'expectBoolean');
|
||||
});
|
||||
|
||||
it('errors correctly', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('importRequiredSearchParam', function () {
|
||||
var err;
|
||||
|
||||
function importExpectFailure(searchParams, sourceName, destName) {
|
||||
windowMock.location.search = TestHelpers.toSearchString(searchParams);
|
||||
try {
|
||||
model.importRequiredSearchParam(sourceName, destName, AuthErrors);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
}
|
||||
|
||||
describe('missing', function () {
|
||||
beforeEach(function () {
|
||||
importExpectFailure({}, 'searchParam');
|
||||
});
|
||||
|
||||
it('errors correctly', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'MISSING_PARAMETER'));
|
||||
assert.equal(err.param, 'searchParam');
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty', function () {
|
||||
beforeEach(function () {
|
||||
importExpectFailure({searchParam: ''}, 'searchParam');
|
||||
});
|
||||
|
||||
it('errors correctly', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'MISSING_PARAMETER'));
|
||||
assert.equal(err.param, 'searchParam');
|
||||
});
|
||||
});
|
||||
|
||||
describe('space', function () {
|
||||
beforeEach(function () {
|
||||
importExpectFailure({searchParam: ' '}, 'searchParam');
|
||||
});
|
||||
|
||||
it('errors correctly', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'MISSING_PARAMETER'));
|
||||
assert.equal(err.param, 'searchParam');
|
||||
});
|
||||
});
|
||||
|
||||
describe('available', function () {
|
||||
describe('without destName', function () {
|
||||
describe('missing data', function () {
|
||||
beforeEach(function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({ searchParam: 'value'});
|
||||
model.importRequiredSearchParam('searchParam', 'searchParam', AuthErrors);
|
||||
windowMock.location.search = TestHelpers.toSearchString({});
|
||||
try {
|
||||
model.importSearchParamsUsingSchema(schema, AuthErrors);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
});
|
||||
|
||||
it('imports the value', function () {
|
||||
assert.equal(model.get('searchParam'), 'value');
|
||||
it('throws a MISSING_PARAMETER error', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'MISSING_PARAMETER'));
|
||||
assert.equal(err.param, 'required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with destName', function () {
|
||||
describe('invalid data', function () {
|
||||
beforeEach(function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({ searchParam: 'value'});
|
||||
model.importRequiredSearchParam('searchParam', 'key2', AuthErrors);
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
required: 'invalid'
|
||||
});
|
||||
|
||||
try {
|
||||
model.importSearchParamsUsingSchema(schema, AuthErrors);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
});
|
||||
|
||||
it('does not import to `sourceName`', function () {
|
||||
assert.isFalse(model.has('searchParam'));
|
||||
});
|
||||
|
||||
it('imports the value to `destName`', function () {
|
||||
assert.equal(model.get('key2'), 'value');
|
||||
it('throws a INVALID_PARAMETER', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
assert.equal(err.param, 'required');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@ define(function (require, exports, module) {
|
|||
|
||||
/*eslint-disable camelcase */
|
||||
var assert = chai.assert;
|
||||
var getValueLabel = TestHelpers.getValueLabel;
|
||||
|
||||
describe('models/reliers/oauth', function () {
|
||||
var err;
|
||||
|
@ -34,7 +35,8 @@ define(function (require, exports, module) {
|
|||
var ACCESS_TYPE = 'offline';
|
||||
var ACTION = 'signup';
|
||||
var CLIENT_ID = 'dcdb5ae7add825d2';
|
||||
var PREVERIFY_TOKEN = 'abigtoken';
|
||||
var CLIENT_IMAGE_URI = 'https://mozorg.cdn.mozilla.net/media/img/firefox/new/header-firefox.pngx';
|
||||
var PREVERIFY_TOKEN = 'a=.big==.token==';
|
||||
var PROMPT = Constants.OAUTH_PROMPT_CONSENT;
|
||||
var REDIRECT_URI = 'http://redirect.here';
|
||||
var SCOPE = 'profile:email profile:uid';
|
||||
|
@ -157,7 +159,7 @@ define(function (require, exports, module) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('parameter validation', function () {
|
||||
describe('query parameter validation', function () {
|
||||
describe('access_type', function () {
|
||||
var validValues = [undefined, 'offline', 'online'];
|
||||
testValidQueryParams('access_type', validValues, 'accessType', validValues);
|
||||
|
@ -253,14 +255,6 @@ define(function (require, exports, module) {
|
|||
testValidQueryParams('keys', validValues, 'keys', expectedValues);
|
||||
});
|
||||
|
||||
describe('privacy_uri', function () {
|
||||
var validValues = ['', PRIVACY_URI];
|
||||
testValidClientInfoValues('privacy_uri', validValues, 'privacyUri', validValues);
|
||||
|
||||
var invalidValues = [' ', 'not-a-url'];
|
||||
testInvalidClientInfoValues('privacy_uri', invalidValues);
|
||||
});
|
||||
|
||||
describe('prompt', function () {
|
||||
var invalidValues = ['', ' ', 'invalid'];
|
||||
testInvalidQueryParams('prompt', invalidValues);
|
||||
|
@ -278,10 +272,6 @@ define(function (require, exports, module) {
|
|||
});
|
||||
|
||||
describe('redirect_uri', function () {
|
||||
describe('is missing on the server', function () {
|
||||
testMissingClientInfoValue('redirect_uri');
|
||||
});
|
||||
|
||||
var validQueryParamValues = [undefined, REDIRECT_URI];
|
||||
// redirectUri will always be loaded from the server
|
||||
var expectedValues = [SERVER_REDIRECT_URI, SERVER_REDIRECT_URI];
|
||||
|
@ -289,9 +279,6 @@ define(function (require, exports, module) {
|
|||
|
||||
var invalidQueryParamValues = ['', ' ', 'not-a-url'];
|
||||
testInvalidQueryParams('redirect_uri', invalidQueryParamValues);
|
||||
|
||||
var invalidClientInfoValues = ['', ' '];
|
||||
testInvalidClientInfoValues('redirect_uri', invalidClientInfoValues);
|
||||
});
|
||||
|
||||
describe('scope', function () {
|
||||
|
@ -336,14 +323,6 @@ define(function (require, exports, module) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('terms_uri', function () {
|
||||
var invalidValues = [' ', 'not-a-url'];
|
||||
testInvalidClientInfoValues('terms_uri', invalidValues);
|
||||
|
||||
var validValues = ['', TERMS_URI];
|
||||
testValidClientInfoValues('terms_uri', validValues, 'termsUri', validValues);
|
||||
});
|
||||
|
||||
describe('verification_redirect', function () {
|
||||
var invalidValues = ['', ' ', 'invalid'];
|
||||
testInvalidQueryParams('verification_redirect', invalidValues);
|
||||
|
@ -352,37 +331,94 @@ define(function (require, exports, module) {
|
|||
var expectedValues = ['no', 'no', 'always'];
|
||||
testValidQueryParams('verification_redirect', validValues, 'verificationRedirect', expectedValues);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('isTrusted', function () {
|
||||
describe('client info validation', function () {
|
||||
|
||||
describe('image_uri', function () {
|
||||
// leading & trailing whitespace will be trimmed
|
||||
var validValues = ['', ' ', CLIENT_IMAGE_URI, ' ' + CLIENT_IMAGE_URI];
|
||||
var expectedValues = ['', '', CLIENT_IMAGE_URI, CLIENT_IMAGE_URI];
|
||||
testValidClientInfoValues(
|
||||
'image_uri', validValues, 'imageUri', expectedValues);
|
||||
|
||||
var invalidValues = ['not-a-url'];
|
||||
testInvalidClientInfoValues('image_uri', invalidValues);
|
||||
});
|
||||
|
||||
describe('name', function () {
|
||||
var validValues = ['client name'];
|
||||
testValidClientInfoValues('name', validValues, 'serviceName', validValues);
|
||||
|
||||
var invalidValues = ['', ' '];
|
||||
testInvalidClientInfoValues('name', invalidValues);
|
||||
});
|
||||
|
||||
describe('privacy_uri', function () {
|
||||
var validValues = ['', ' ', PRIVACY_URI, PRIVACY_URI + ' '];
|
||||
var expectedValues = ['', '', PRIVACY_URI, PRIVACY_URI];
|
||||
testValidClientInfoValues(
|
||||
'privacy_uri', validValues, 'privacyUri', expectedValues);
|
||||
|
||||
var invalidValues = ['not-a-url'];
|
||||
testInvalidClientInfoValues('privacy_uri', invalidValues);
|
||||
});
|
||||
|
||||
describe('redirect_uri', function () {
|
||||
describe('is missing on the server', function () {
|
||||
testMissingClientInfoValue('redirect_uri');
|
||||
});
|
||||
|
||||
var invalidClientInfoValues = ['', ' '];
|
||||
testInvalidClientInfoValues('redirect_uri', invalidClientInfoValues);
|
||||
});
|
||||
|
||||
describe('terms_uri', function () {
|
||||
var invalidValues = ['not-a-url'];
|
||||
testInvalidClientInfoValues('terms_uri', invalidValues);
|
||||
|
||||
var validValues = ['', ' ', TERMS_URI, ' ' + TERMS_URI];
|
||||
var expectedValues = ['', '', TERMS_URI, TERMS_URI];
|
||||
testValidClientInfoValues('terms_uri', validValues, 'termsUri', expectedValues);
|
||||
});
|
||||
|
||||
describe('trusted', function () {
|
||||
var validValues = ['true', true, 'false', false];
|
||||
var expected = [true, true, false, false];
|
||||
testValidClientInfoValues('trusted', validValues, 'trusted', expected);
|
||||
var invalidValues = ['', 'not-a-boolean'];
|
||||
testInvalidClientInfoValues('trusted', invalidValues);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTrusted', function () {
|
||||
beforeEach(function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
client_id: CLIENT_ID,
|
||||
scope: SCOPE
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `trusted` is true', function () {
|
||||
beforeEach(function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
client_id: CLIENT_ID,
|
||||
scope: SCOPE
|
||||
});
|
||||
isTrusted = true;
|
||||
return relier.fetch();
|
||||
});
|
||||
|
||||
describe('when `trusted` is true', function () {
|
||||
beforeEach(function () {
|
||||
isTrusted = true;
|
||||
return relier.fetch();
|
||||
});
|
||||
it('returns `true`', function () {
|
||||
assert.isTrue(relier.isTrusted());
|
||||
});
|
||||
});
|
||||
|
||||
it('returns `true`', function () {
|
||||
assert.isTrue(relier.isTrusted());
|
||||
});
|
||||
describe('when `trusted` is false', function () {
|
||||
beforeEach(function () {
|
||||
isTrusted = false;
|
||||
return relier.fetch();
|
||||
});
|
||||
|
||||
describe('when `trusted` is false', function () {
|
||||
beforeEach(function () {
|
||||
isTrusted = false;
|
||||
return relier.fetch();
|
||||
});
|
||||
|
||||
it('returns `false`', function () {
|
||||
assert.isFalse(relier.isTrusted());
|
||||
});
|
||||
it('returns `false`', function () {
|
||||
assert.isFalse(relier.isTrusted());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -581,6 +617,7 @@ define(function (require, exports, module) {
|
|||
|
||||
sinon.stub(oAuthClient, 'getClientInfo', function () {
|
||||
var clientInfo = {
|
||||
id: CLIENT_ID,
|
||||
name: SERVICE_NAME,
|
||||
privacy_uri: PRIVACY_URI,
|
||||
redirect_uri: SERVER_REDIRECT_URI,
|
||||
|
@ -774,18 +811,6 @@ define(function (require, exports, module) {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getValueLabel(value) {
|
||||
if (_.isUndefined(value)) {
|
||||
return 'not set';
|
||||
} else if (value === '') {
|
||||
return 'empty';
|
||||
} else if (/^\s+$/.test(value)) {
|
||||
return 'whitespace only';
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var _ = require('underscore');
|
||||
var AuthErrors = require('lib/auth-errors');
|
||||
var chai = require('chai');
|
||||
var Constants = require('lib/constants');
|
||||
var Relier = require('models/reliers/relier');
|
||||
|
@ -13,18 +15,19 @@ define(function (require, exports, module) {
|
|||
var WindowMock = require('../../../mocks/window');
|
||||
|
||||
var assert = chai.assert;
|
||||
var getValueLabel = TestHelpers.getValueLabel;
|
||||
|
||||
describe('models/reliers/relier', function () {
|
||||
var relier;
|
||||
var windowMock;
|
||||
|
||||
var SERVICE = 'service';
|
||||
var PREVERIFY_TOKEN = 'abigtoken';
|
||||
var EMAIL = 'email@moz.org';
|
||||
var UID = 'uid';
|
||||
var ENTRYPOINT = 'preferences';
|
||||
var CAMPAIGN = 'fennec';
|
||||
var EMAIL = 'email@moz.org';
|
||||
var ENTRYPOINT = 'preferences';
|
||||
var PREVERIFY_TOKEN = 'a=.big==.token==';
|
||||
var SERVICE = 'sync';
|
||||
var SETTING = 'avatar';
|
||||
var UID = TestHelpers.createRandomHexString(Constants.UID_LENGTH);
|
||||
var UTM_CAMPAIGN = 'utm_campaign';
|
||||
var UTM_CONTENT = 'utm_content';
|
||||
var UTM_MEDIUM = 'utm_medium';
|
||||
|
@ -119,6 +122,46 @@ define(function (require, exports, module) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('preVerifyToken', function () {
|
||||
describe('invalid', function () {
|
||||
var invalidTokens = ['', ' ', 'invalid token'];
|
||||
invalidTokens.forEach(function (value) {
|
||||
describe(getValueLabel(value), function () {
|
||||
testInvalidQueryParam('preVerifyToken', value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('valid', function () {
|
||||
var validTokens = [undefined, PREVERIFY_TOKEN];
|
||||
validTokens.forEach(function (value) {
|
||||
describe(getValueLabel(value), function () {
|
||||
testValidQueryParam('preVerifyToken', value, 'preVerifyToken', value);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('migration', function () {
|
||||
describe('invalid', function () {
|
||||
var invalidMigrations = ['', ' ', 'invalid migration'];
|
||||
invalidMigrations.forEach(function (token) {
|
||||
describe(getValueLabel(token), function () {
|
||||
testInvalidQueryParam('migration', token);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('valid', function () {
|
||||
var validMigrations = [undefined, Constants.AMO_MIGRATION, Constants.SYNC11_MIGRATION];
|
||||
validMigrations.forEach(function (value) {
|
||||
describe(getValueLabel(value), function () {
|
||||
testValidQueryParam('migration', value, 'migration', value);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isOAuth', function () {
|
||||
it('returns `false`', function () {
|
||||
assert.isFalse(relier.isOAuth());
|
||||
|
@ -225,6 +268,56 @@ define(function (require, exports, module) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
function testInvalidQueryParam(paramName, value) {
|
||||
var err;
|
||||
|
||||
beforeEach(function () {
|
||||
var params = {};
|
||||
|
||||
if (! _.isUndefined(value)) {
|
||||
params[paramName] = value;
|
||||
} else {
|
||||
delete params[paramName];
|
||||
}
|
||||
|
||||
windowMock.location.search = TestHelpers.toSearchString(params);
|
||||
|
||||
return relier.fetch()
|
||||
.then(assert.fail, function (_err) {
|
||||
err = _err;
|
||||
});
|
||||
});
|
||||
|
||||
it('errors correctly', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
assert.equal(err.param, paramName);
|
||||
});
|
||||
}
|
||||
|
||||
function testValidQueryParam(paramName, paramValue, modelName, expectedValue) {
|
||||
beforeEach(function () {
|
||||
var params = {};
|
||||
|
||||
if (! _.isUndefined(paramValue)) {
|
||||
params[paramName] = paramValue;
|
||||
} else {
|
||||
delete params[paramName];
|
||||
}
|
||||
|
||||
windowMock.location.search = TestHelpers.toSearchString(params);
|
||||
|
||||
return relier.fetch();
|
||||
});
|
||||
|
||||
it('is successful', function () {
|
||||
if (_.isUndefined(expectedValue)) {
|
||||
assert.isFalse(relier.has(modelName));
|
||||
} else {
|
||||
assert.equal(relier.get(modelName), expectedValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
define(function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var AuthErrors = require('lib/auth-errors');
|
||||
var chai = require('chai');
|
||||
var Relier = require('models/reliers/sync');
|
||||
var TestHelpers = require('../../../lib/helpers');
|
||||
|
@ -14,13 +15,25 @@ define(function (require, exports, module) {
|
|||
var assert = chai.assert;
|
||||
|
||||
describe('models/reliers/sync', function () {
|
||||
var windowMock;
|
||||
var translator;
|
||||
var err;
|
||||
var relier;
|
||||
var translator;
|
||||
var windowMock;
|
||||
|
||||
var CONTEXT = 'fx_desktop_v1';
|
||||
var SYNC_MIGRATION = 'sync11';
|
||||
var SYNC_SERVICE = 'sync';
|
||||
|
||||
function fetchExpectError () {
|
||||
return relier.fetch()
|
||||
.then(assert.fail, function (_err) {
|
||||
err = _err;
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
windowMock = new WindowMock();
|
||||
translator = new Translator('en-US', ['en-US']);
|
||||
windowMock = new WindowMock();
|
||||
|
||||
relier = new Relier({
|
||||
translator: translator,
|
||||
|
@ -31,35 +44,134 @@ define(function (require, exports, module) {
|
|||
describe('fetch', function () {
|
||||
it('populates model from the search parameters', function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
context: 'fx_desktop_v1',
|
||||
context: CONTEXT,
|
||||
customizeSync: 'true',
|
||||
migration: 'sync11',
|
||||
service: 'sync'
|
||||
migration: SYNC_MIGRATION,
|
||||
service: SYNC_SERVICE
|
||||
});
|
||||
|
||||
return relier.fetch()
|
||||
.then(function () {
|
||||
assert.equal(relier.get('context'), 'fx_desktop_v1');
|
||||
assert.equal(relier.get('migration'), 'sync11');
|
||||
assert.equal(relier.get('service'), 'sync');
|
||||
assert.equal(relier.get('context'), CONTEXT);
|
||||
assert.equal(relier.get('migration'), SYNC_MIGRATION);
|
||||
assert.equal(relier.get('service'), SYNC_SERVICE);
|
||||
assert.isTrue(relier.get('customizeSync'));
|
||||
});
|
||||
});
|
||||
|
||||
it('does not throw if `customizeSync` is not a boolean', function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
customizeSync: 'not a boolean'
|
||||
describe('context query parameter', function () {
|
||||
describe('missing', function () {
|
||||
beforeEach(function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({});
|
||||
|
||||
return relier.fetch();
|
||||
});
|
||||
|
||||
it('succeeds', function () {
|
||||
// it's OK
|
||||
assert.isFalse(relier.has('context'));
|
||||
});
|
||||
});
|
||||
|
||||
return relier.fetch()
|
||||
.then(function () {
|
||||
assert.isFalse(relier.has('customizeSync'));
|
||||
describe('emtpy', function () {
|
||||
beforeEach(function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
context: ''
|
||||
});
|
||||
|
||||
return fetchExpectError();
|
||||
});
|
||||
|
||||
it('errors correctly', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
assert.equal(err.param, 'context');
|
||||
});
|
||||
});
|
||||
|
||||
describe('whitepsace', function () {
|
||||
beforeEach(function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
context: ' '
|
||||
});
|
||||
|
||||
return fetchExpectError();
|
||||
});
|
||||
|
||||
it('errors correctly', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
assert.equal(err.param, 'context');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('customizeSync query parameter', function () {
|
||||
describe('missing', function () {
|
||||
beforeEach(function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
context: CONTEXT
|
||||
});
|
||||
|
||||
return relier.fetch();
|
||||
});
|
||||
|
||||
it('succeeds', function () {
|
||||
assert.isFalse(relier.get('customizeSync'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('emtpy', function () {
|
||||
beforeEach(function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
context: CONTEXT,
|
||||
customizeSync: ''
|
||||
});
|
||||
|
||||
return fetchExpectError();
|
||||
});
|
||||
|
||||
it('errors correctly', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
assert.equal(err.param, 'customizeSync');
|
||||
});
|
||||
});
|
||||
|
||||
describe('whitepsace', function () {
|
||||
beforeEach(function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
context: CONTEXT,
|
||||
customizeSync: ' '
|
||||
});
|
||||
|
||||
return fetchExpectError();
|
||||
});
|
||||
|
||||
it('errors correctly', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
assert.equal(err.param, 'customizeSync');
|
||||
});
|
||||
});
|
||||
|
||||
describe('not a boolean', function () {
|
||||
beforeEach(function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
context: CONTEXT,
|
||||
customizeSync: 'not a boolean'
|
||||
});
|
||||
|
||||
return fetchExpectError();
|
||||
});
|
||||
|
||||
it('errors correctly', function () {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
|
||||
assert.equal(err.param, 'customizeSync');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('translates `service` to `serviceName`', function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
service: 'sync'
|
||||
context: CONTEXT,
|
||||
service: SYNC_SERVICE
|
||||
});
|
||||
|
||||
return relier.fetch()
|
||||
|
@ -78,6 +190,7 @@ define(function (require, exports, module) {
|
|||
describe('isCustomizeSyncChecked', function () {
|
||||
it('returns true if `customizeSync=true`', function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
context: CONTEXT,
|
||||
customizeSync: 'true'
|
||||
});
|
||||
|
||||
|
@ -89,6 +202,7 @@ define(function (require, exports, module) {
|
|||
|
||||
it('returns false if `customizeSync=false`', function () {
|
||||
windowMock.location.search = TestHelpers.toSearchString({
|
||||
context: CONTEXT,
|
||||
customizeSync: 'false'
|
||||
});
|
||||
|
||||
|
|
|
@ -61,6 +61,7 @@ function (Translator, Session) {
|
|||
'../tests/spec/lib/height-observer',
|
||||
'../tests/spec/lib/config-loader',
|
||||
'../tests/spec/lib/require-on-demand',
|
||||
'../tests/spec/lib/transform',
|
||||
'../tests/spec/head/startup-styles',
|
||||
'../tests/spec/views/app',
|
||||
'../tests/spec/views/base',
|
||||
|
|
|
@ -36,7 +36,8 @@
|
|||
"speed-trap": "0.0.5",
|
||||
"tos-pp": "https://github.com/mozilla/legal-docs.git",
|
||||
"underscore": "1.8.3",
|
||||
"webrtc-adapter": "0.2.5"
|
||||
"webrtc-adapter": "0.2.5",
|
||||
"vat": "0.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"moment": "2.8.3"
|
||||
|
|
Загрузка…
Ссылка в новой задаче