├── .eslintrc ├── .gitignore ├── .jscsrc ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── client ├── .eslintrc ├── FxAccountClient.js └── lib │ ├── credentials.js │ ├── errors.js │ ├── hawk.js │ ├── hawkCredentials.js │ ├── hkdf.js │ ├── metricsContext.js │ ├── pbkdf2.js │ └── request.js ├── node ├── amd-loader.js └── index.js ├── package.json ├── tasks ├── buildcontrol.js ├── bump.js ├── bytesize.js ├── changelog.js ├── clean.js ├── copyright.js ├── eslint.js ├── intern.js ├── open.js ├── sjcl.js ├── watch.js ├── webpack.js └── yuidoc.js ├── tests ├── .eslintrc ├── addons │ ├── accountHelper.js │ ├── environment.js │ ├── node-client.js │ ├── restmail.js │ └── sinon.js ├── all.js ├── ci │ ├── install-tunnel.sh │ └── travis-auth-server-test.sh ├── examples │ ├── example.html │ └── proxy.js ├── intern.js ├── intern_browser.js ├── intern_native_node.js ├── intern_sauce.js ├── lib │ ├── account.js │ ├── certificateSign.js │ ├── credentials.js │ ├── device.js │ ├── emails.js │ ├── errors.js │ ├── hawkCredentials.js │ ├── headerLang.js │ ├── hkdf.js │ ├── init.js │ ├── metricsContext.js │ ├── misc.js │ ├── passwordChange.js │ ├── push-constants.js │ ├── recoveryCodes.js │ ├── recoveryEmail.js │ ├── recoveryKeys.js │ ├── request.js │ ├── session.js │ ├── signIn.js │ ├── signUp.js │ ├── signinCodes.js │ ├── sms.js │ ├── tokenCodes.js │ ├── totp.js │ ├── unbundle.js │ ├── uriVersion.js │ └── verifyCode.js └── mocks │ ├── errors.js │ └── request.js └── webpack.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "fxa", 3 | "env": { 4 | "amd": true, 5 | "node": true 6 | }, 7 | "rules": { 8 | "no-path-concat": 0, 9 | "strict": 0 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | /components 4 | /docs 5 | /fxa-auth-server 6 | sauce_connect.log* 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowKeywords": ["eval"], 3 | "disallowKeywordsOnNewLine": ["else"], 4 | "requireSpaceBeforeBinaryOperators": ["?", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 5 | "disallowMultipleLineStrings": true, 6 | "requireSpaceAfterBinaryOperators": ["?", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 7 | "disallowSpaceAfterObjectKeys": true, 8 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-"], 9 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 10 | "maximumLineLength": 420, 11 | "requireCapitalizedConstructors": true, 12 | "requireLineFeedAtFileEnd": true, 13 | "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return"], 14 | "validateIndentation": 2, 15 | "validateLineBreaks": "LF", 16 | "jsDoc": { 17 | "checkAnnotations": "jsdoc3", 18 | "checkTypes": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | services: 4 | - memcached 5 | - redis-server 6 | env: 7 | global: 8 | # Sauce Labs are OK with this and it is currently necessary to expose this information for testing pull requests; 9 | # please get your own free key if you want to test yourself 10 | - SAUCE_USERNAME: fxa-client 11 | - SAUCE_ACCESS_KEY: 863203af-38fd-4f1d-9332-adc8f60f157b 12 | - CXX: g++-4.8 13 | matrix: 14 | - SERVER=mock 15 | - SERVER=local 16 | 17 | sudo: false 18 | 19 | notifications: 20 | irc: 21 | channels: 22 | - "irc.mozilla.org#fxa-bots" 23 | use_notice: false 24 | skip_join: false 25 | on_success: change 26 | on_failure: change 27 | 28 | node_js: 29 | - "8" 30 | 31 | addons: 32 | apt: 33 | sources: 34 | - ubuntu-toolchain-r-test 35 | packages: 36 | - g++-4.8 37 | 38 | # blacklist 39 | branches: 40 | except: 41 | - release 42 | - /^.*-docs$/ 43 | 44 | install: 45 | - npm run-script setup 46 | - tests/ci/install-tunnel.sh 47 | script: 48 | - if [ $SERVER == "mock" ]; then grunt travis; fi 49 | - if [ $SERVER == "local" ]; then ./tests/ci/travis-auth-server-test.sh; fi 50 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Vlad Filippov 2 | Zach Carter 3 | Peter deHaan 4 | Glen Mailer 5 | Brian Warner 6 | Shane Tomlinson 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ### 0.1.30 (2015-07-20) 3 | 4 | 5 | #### Bug Fixes 6 | 7 | * **client:** throw harder on bad client init ([c3b06b4d](https://github.com/mozilla/fxa-js-client/commit/c3b06b4df48ef01910c2c98fee01b676e0ea58af)) 8 | * **tests:** fix account.js tests ([c0cc0d59](https://github.com/mozilla/fxa-js-client/commit/c0cc0d5915589366f6e6af01259065f70975eb60)) 9 | 10 | 11 | 12 | ### 0.1.29 (2015-06-10) 13 | 14 | 15 | #### Bug Fixes 16 | 17 | * **docs:** include keys for signUp ([dff42d0d](https://github.com/mozilla/fxa-js-client/commit/dff42d0d185524cfccf02a67af0e4c696875e54c), closes [#140](https://github.com/mozilla/fxa-js-client/issues/140)) 18 | * **tests:** remove account devices, add unlock verify code ([13c1b836](https://github.com/mozilla/fxa-js-client/commit/13c1b836ffe9ad5cdde43a4a809d83d33795ce75), closes [#151](https://github.com/mozilla/fxa-js-client/issues/151)) 19 | 20 | 21 | #### Features 22 | 23 | * **client:** 24 | * Pass along `reason` in `signIn` ([b33b1d53](https://github.com/mozilla/fxa-js-client/commit/b33b1d53d8b49e1e069c733d897a063309346194)) 25 | * signIn can now pass along a `service` option. ([0188dbb2](https://github.com/mozilla/fxa-js-client/commit/0188dbb233286cefd3145f53b39e8279ad0c6e40)) 26 | 27 | 28 | 29 | ### 0.1.28 (2015-02-12) 30 | 31 | 32 | #### Features 33 | 34 | * **client:** Add account unlock functionality. ([2f8e642c](https://github.com/mozilla/fxa-js-client/commit/2f8e642c3600e29fedd3913b60e417f376593754)) 35 | 36 | 37 | 38 | ### 0.1.27 (2014-12-09) 39 | 40 | 41 | #### Bug Fixes 42 | 43 | * **docs:** fixes typo for certificateSign ([ab22f068](https://github.com/mozilla/fxa-js-client/commit/ab22f0682bae8a70768562fd9f3b6057243f3475)) 44 | 45 | 46 | #### Features 47 | 48 | * **client:** Return `unwrapBKey` in `signUp` if `keys=true` is specified. ([1cd19e52](https://github.com/mozilla/fxa-js-client/commit/1cd19e52feb188905ae41c5d66e540fa2b1aee5b)) 49 | 50 | 51 | 52 | ### 0.1.26 (2014-09-23) 53 | 54 | 55 | #### Bug Fixes 56 | 57 | * **request:** return an error object when the response is an HTML error page ([38a25556](https://github.com/mozilla/fxa-js-client/commit/38a25556001c2afcc9f9e87901964bca04bca624)) 58 | 59 | 60 | 61 | ### 0.1.25 (2014-09-15) 62 | 63 | 64 | #### Features 65 | 66 | * **client:** Pass along the `resume` parameter to the auth-server ([07cff4ec](https://github.com/mozilla/fxa-js-client/commit/07cff4ec9568f2243400755dbed7ce4c077aa02b)) 67 | 68 | 69 | 70 | ### 0.1.24 (2014-09-03) 71 | 72 | 73 | #### Bug Fixes 74 | 75 | * **tests:** use a locale that is supported by the auth-mailer for header tests ([2e13d22e](https://github.com/mozilla/fxa-js-client/commit/2e13d22e30751b8cea836fe5585a696fdbb79149)) 76 | 77 | 78 | #### Features 79 | 80 | * **client:** signUp accepts a `preVerifyToken` option. ([35b4b232](https://github.com/mozilla/fxa-js-client/commit/35b4b2326a452520efb7901ae53411f1b42baabe)) 81 | 82 | 83 | 84 | ### 0.1.23 (2014-06-12) 85 | 86 | 87 | 88 | ### 0.1.22 (2014-06-11) 89 | 90 | 91 | 92 | ### 0.1.20 (2014-05-16) 93 | 94 | 95 | #### Bug Fixes 96 | 97 | * **xhr:** make the default payload null ([83666223](https://github.com/mozilla/fxa-js-client/commit/83666223b6fdf4c6993bb4fefce9f0d63c6b38d4)) 98 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines to fxa-js-client 2 | 3 | Anyone is welcome to help with Firefox Accounts. Feel free to get in touch with other community members on IRC, the 4 | mailing list or through issues here on GitHub. 5 | 6 | - IRC: `#fxa` on `irc.mozilla.org` 7 | - Mailing list: 8 | - and of course, [the issues list](https://github.com/mozilla/fxa-js-client/issues) 9 | 10 | ## Bug Reports ## 11 | 12 | You can file issues here on GitHub. Please try to include as much information as you can and under what conditions 13 | you saw the issue. 14 | 15 | ## Sending Pull Requests ## 16 | 17 | Patches should be submitted as pull requests (PR). 18 | 19 | Before submitting a PR: 20 | - Your code must run and pass all the automated tests before you submit your PR for review. "Work in progress" pull requests are allowed to be submitted, but should be clearly labeled as such and should not be merged until all tests pass and the code has been reviews. 21 | - Run `grunt lint` to make sure your code passes linting. 22 | - Run `npm test` to make sure all tests still pass. 23 | - Your patch should include new tests that cover your changes. It is your and your reviewer's responsibility to ensure your patch includes adequate tests. 24 | 25 | When submitting a PR: 26 | - You agree to license your code under the project's open source license ([MPL 2.0](/LICENSE)). 27 | - Base your branch off the current `master` (see below for an example workflow). 28 | - Add both your code and new tests if relevant. 29 | - Run `grunt lint` and `npm test` to make sure your code passes linting and tests. 30 | - Please do not include merge commits in pull requests; include only commits with the new relevant code. 31 | 32 | See the main [README.md](/README.md) for information on prerequisites, installing, running and testing. 33 | 34 | ## Code Review ## 35 | 36 | This project is production Mozilla code and subject to our [engineering practices and quality standards](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Committing_Rules_and_Responsibilities). Every patch must be peer reviewed. This project is part of the [Firefox Accounts module](https://wiki.mozilla.org/Modules/Other#Firefox_Accounts), and your patch must be reviewed by one of the listed module owners or peers. 37 | 38 | ## Build Library 39 | 40 | Note: Java is required to build the library due to a custom SJCL build. 41 | 42 | ``` 43 | npm run-script setup 44 | npm start 45 | ``` 46 | 47 | The `build` directory should have `fxa-client.js` and `fxa-client.min.js`. 48 | 49 | 50 | ## Development 51 | 52 | `grunt build` - builds the regular and minified version of the library 53 | 54 | `grunt dev` - builds the library, runs eslint, shows library size, runs tests, watches for changes 55 | 56 | `grunt debug` - builds the regular library, runs test, watches for changes. Helpful when you are debugging. 57 | 58 | `grunt release` - will prepare a new release of this library with the version in `package.json`. 59 | It will create or update the repositories in `build` and `docs`. If the version in `package.json` has not changed, 60 | then the tagging will be skipped. 61 | 62 | 63 | ### SJCL Notes 64 | 65 | Currently [SJCL](http://bitwiseshiftleft.github.io/sjcl/) is built with `./configure --without-random --without-ocb2 --without-gcm --without-ccm`. 66 | Adjust this if you need other SJCL features. 67 | 68 | 69 | ## Testing 70 | 71 | This library uses [The Intern](https://github.com/theintern/intern/wiki) testing framework. 72 | 73 | `npm test` - run local tests via Node.js 74 | 75 | `npm run test-local` - run the tests against the local fxa-auth-server 76 | 77 | `grunt intern:browser` - Locally run tests in Selenium (Requires `java -jar selenium-server-standalone-2.37.0.jar`). 78 | 79 | `grunt intern:sauce` - Run tests on SauceLabs. 80 | 81 | ## Git Commit Guidelines 82 | 83 | We loosely follow the [Angular commit guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#type) of `(): ` where `type` must be one of: 84 | 85 | * **feat**: A new feature 86 | * **fix**: A bug fix 87 | * **docs**: Documentation only changes 88 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing 89 | semi-colons, etc) 90 | * **refactor**: A code change that neither fixes a bug or adds a feature 91 | * **perf**: A code change that improves performance 92 | * **test**: Adding missing tests 93 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation 94 | generation 95 | 96 | ## Documentation 97 | 98 | Running `grunt doc` will create a `docs` directory, browse the documentation by opening `docs/index.html`. 99 | 100 | Write documentation using [YUIDoc Syntax](http://yui.github.io/yuidoc/syntax/). 101 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | // load all grunt tasks matching the `grunt-*` pattern 7 | require('load-grunt-tasks')(grunt); 8 | // load the Intern tasks 9 | grunt.loadNpmTasks('intern-geezer'); 10 | 11 | var pkg = grunt.file.readJSON('package.json'); 12 | 13 | grunt.initConfig({ 14 | pkg: pkg, 15 | pkgReadOnly: pkg 16 | }); 17 | 18 | // load local Grunt tasks 19 | grunt.loadTasks('tasks'); 20 | 21 | grunt.registerTask('build', 22 | 'Build client', 23 | ['clean', 'lint', 'webpack:app', 'bytesize']); 24 | 25 | grunt.registerTask('test', 26 | 'Run tests via node', 27 | ['intern:node', 'intern:native_node']); 28 | 29 | grunt.registerTask('lint', 30 | 'Alias for eslint', 31 | ['eslint']); 32 | 33 | grunt.registerTask('default', 34 | ['build']); 35 | 36 | grunt.registerTask('release', 37 | ['build', 'bump-only', 'conventionalChangelog', 'bump-commit', 'yuidoc', 'buildcontrol']); 38 | 39 | grunt.registerTask('dev', 40 | ['watch:dev']); 41 | 42 | grunt.registerTask('debug', 43 | ['watch:debug']); 44 | 45 | grunt.registerTask('doc', 46 | 'Create client documentation using YUIDoc', 47 | ['yuidoc', 'open']); 48 | 49 | grunt.registerTask('travis', 50 | 'Test runner task for Travis CI', 51 | ['build', 'intern:node', 'intern:sauce']); 52 | }; 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository has been migrated to https://github.com/mozilla/fxa/tree/master/packages/fxa-js-client 2 | 3 | Please file issues and open pull requests against https://github.com/mozilla/fxa 4 | -------------------------------------------------------------------------------- /client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": false 5 | }, 6 | "rules": { 7 | "no-console": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client/lib/credentials.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | define(['./request', 'sjcl', './hkdf', './pbkdf2'], function (Request, sjcl, hkdf, pbkdf2) { 5 | 'use strict'; 6 | 7 | // Key wrapping and stretching configuration. 8 | var NAMESPACE = 'identity.mozilla.com/picl/v1/'; 9 | var PBKDF2_ROUNDS = 1000; 10 | var STRETCHED_PASS_LENGTH_BYTES = 32 * 8; 11 | 12 | var HKDF_SALT = sjcl.codec.hex.toBits('00'); 13 | var HKDF_LENGTH = 32; 14 | 15 | /** 16 | * Key Wrapping with a name 17 | * 18 | * @method kw 19 | * @static 20 | * @param {String} name The name of the salt 21 | * @return {bitArray} the salt combination with the namespace 22 | */ 23 | function kw(name) { 24 | return sjcl.codec.utf8String.toBits(NAMESPACE + name); 25 | } 26 | 27 | /** 28 | * Key Wrapping with a name and an email 29 | * 30 | * @method kwe 31 | * @static 32 | * @param {String} name The name of the salt 33 | * @param {String} email The email of the user. 34 | * @return {bitArray} the salt combination with the namespace 35 | */ 36 | function kwe(name, email) { 37 | return sjcl.codec.utf8String.toBits(NAMESPACE + name + ':' + email); 38 | } 39 | 40 | /** 41 | * @class credentials 42 | * @constructor 43 | */ 44 | return { 45 | /** 46 | * Setup credentials 47 | * 48 | * @method setup 49 | * @param {String} emailInput 50 | * @param {String} passwordInput 51 | * @return {Promise} A promise that will be fulfilled with `result` of generated credentials 52 | */ 53 | setup: function (emailInput, passwordInput) { 54 | var result = {}; 55 | var email = kwe('quickStretch', emailInput); 56 | var password = sjcl.codec.utf8String.toBits(passwordInput); 57 | 58 | result.emailUTF8 = emailInput; 59 | result.passwordUTF8 = passwordInput; 60 | 61 | return pbkdf2.derive(password, email, PBKDF2_ROUNDS, STRETCHED_PASS_LENGTH_BYTES) 62 | .then( 63 | function (quickStretchedPW) { 64 | result.quickStretchedPW = quickStretchedPW; 65 | 66 | return hkdf(quickStretchedPW, kw('authPW'), HKDF_SALT, HKDF_LENGTH) 67 | .then( 68 | function (authPW) { 69 | result.authPW = authPW; 70 | 71 | return hkdf(quickStretchedPW, kw('unwrapBkey'), HKDF_SALT, HKDF_LENGTH); 72 | } 73 | ); 74 | } 75 | ) 76 | .then( 77 | function (unwrapBKey) { 78 | result.unwrapBKey = unwrapBKey; 79 | return result; 80 | } 81 | ); 82 | }, 83 | /** 84 | * Wrap 85 | * 86 | * @method wrap 87 | * @param {bitArray} bitArray1 88 | * @param {bitArray} bitArray2 89 | * @return {bitArray} wrap result of the two bitArrays 90 | */ 91 | xor: function (bitArray1, bitArray2) { 92 | var result = []; 93 | 94 | for (var i = 0; i < bitArray1.length; i++) { 95 | result[i] = bitArray1[i] ^ bitArray2[i]; 96 | } 97 | 98 | return result; 99 | }, 100 | /** 101 | * Unbundle the WrapKB 102 | * @param {String} key Bundle Key in hex 103 | * @param {String} bundle Key bundle in hex 104 | * @returns {*} 105 | */ 106 | unbundleKeyFetchResponse: function (key, bundle) { 107 | var self = this; 108 | var bitBundle = sjcl.codec.hex.toBits(bundle); 109 | 110 | return this.deriveBundleKeys(key, 'account/keys') 111 | .then( 112 | function (keys) { 113 | var ciphertext = sjcl.bitArray.bitSlice(bitBundle, 0, 8 * 64); 114 | var expectedHmac = sjcl.bitArray.bitSlice(bitBundle, 8 * -32); 115 | var hmac = new sjcl.misc.hmac(keys.hmacKey, sjcl.hash.sha256); 116 | hmac.update(ciphertext); 117 | 118 | if (!sjcl.bitArray.equal(hmac.digest(), expectedHmac)) { 119 | throw new Error('Bad HMac'); 120 | } 121 | 122 | var keyAWrapB = self.xor(sjcl.bitArray.bitSlice(bitBundle, 0, 8 * 64), keys.xorKey); 123 | 124 | return { 125 | kA: sjcl.codec.hex.fromBits(sjcl.bitArray.bitSlice(keyAWrapB, 0, 8 * 32)), 126 | wrapKB: sjcl.codec.hex.fromBits(sjcl.bitArray.bitSlice(keyAWrapB, 8 * 32)) 127 | }; 128 | } 129 | ); 130 | }, 131 | /** 132 | * Derive the HMAC and XOR keys required to encrypt a given size of payload. 133 | * @param {String} key Hex Bundle Key 134 | * @param {String} keyInfo Bundle Key Info 135 | * @returns {Object} hmacKey, xorKey 136 | */ 137 | deriveBundleKeys: function(key, keyInfo) { 138 | var bitKeyInfo = kw(keyInfo); 139 | var salt = sjcl.codec.hex.toBits(''); 140 | key = sjcl.codec.hex.toBits(key); 141 | 142 | return hkdf(key, bitKeyInfo, salt, 3 * 32) 143 | .then( 144 | function (keyMaterial) { 145 | 146 | return { 147 | hmacKey: sjcl.bitArray.bitSlice(keyMaterial, 0, 8 * 32), 148 | xorKey: sjcl.bitArray.bitSlice(keyMaterial, 8 * 32) 149 | }; 150 | } 151 | ); 152 | } 153 | }; 154 | 155 | }); 156 | -------------------------------------------------------------------------------- /client/lib/errors.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | define([], function () { 5 | return { 6 | INVALID_TIMESTAMP: 111, 7 | INCORRECT_EMAIL_CASE: 120 8 | }; 9 | }); 10 | -------------------------------------------------------------------------------- /client/lib/hawk.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | define(['sjcl'], function (sjcl) { 5 | 'use strict'; 6 | 7 | /* 8 | HTTP Hawk Authentication Scheme 9 | Copyright (c) 2012-2013, Eran Hammer 10 | MIT Licensed 11 | */ 12 | 13 | 14 | // Declare namespace 15 | 16 | var hawk = {}; 17 | 18 | hawk.client = { 19 | 20 | // Generate an Authorization header for a given request 21 | 22 | /* 23 | uri: 'http://example.com/resource?a=b' 24 | method: HTTP verb (e.g. 'GET', 'POST') 25 | options: { 26 | 27 | // Required 28 | 29 | credentials: { 30 | id: 'dh37fgj492je', 31 | key: 'aoijedoaijsdlaksjdl', 32 | algorithm: 'sha256' // 'sha1', 'sha256' 33 | }, 34 | 35 | // Optional 36 | 37 | ext: 'application-specific', // Application specific data sent via the ext attribute 38 | timestamp: Date.now() / 1000, // A pre-calculated timestamp in seconds 39 | nonce: '2334f34f', // A pre-generated nonce 40 | localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided) 41 | payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided) 42 | contentType: 'application/json', // Payload content-type (ignored if hash provided) 43 | hash: 'U4MKKSmiVxk37JCCrAVIjV=', // Pre-calculated payload hash 44 | app: '24s23423f34dx', // Oz application id 45 | dlg: '234sz34tww3sd' // Oz delegated-by application id 46 | } 47 | */ 48 | 49 | header: function (uri, method, options) { 50 | /*eslint complexity: [2, 21] */ 51 | var result = { 52 | field: '', 53 | artifacts: {} 54 | }; 55 | 56 | // Validate inputs 57 | 58 | if (!uri || (typeof uri !== 'string' && typeof uri !== 'object') || 59 | !method || typeof method !== 'string' || 60 | !options || typeof options !== 'object') { 61 | 62 | result.err = 'Invalid argument type'; 63 | return result; 64 | } 65 | 66 | // Application time 67 | 68 | var timestamp = options.timestamp || Math.floor((hawk.utils.now() + (options.localtimeOffsetMsec || 0)) / 1000); 69 | 70 | // Validate credentials 71 | 72 | var credentials = options.credentials; 73 | if (!credentials || 74 | !credentials.id || 75 | !credentials.key || 76 | !credentials.algorithm) { 77 | 78 | result.err = 'Invalid credential object'; 79 | return result; 80 | } 81 | 82 | if (hawk.utils.baseIndexOf(hawk.crypto.algorithms, credentials.algorithm) === -1) { 83 | result.err = 'Unknown algorithm'; 84 | return result; 85 | } 86 | 87 | // Parse URI 88 | 89 | if (typeof uri === 'string') { 90 | uri = hawk.utils.parseUri(uri); 91 | } 92 | 93 | // Calculate signature 94 | 95 | var artifacts = { 96 | ts: timestamp, 97 | nonce: options.nonce || hawk.utils.randomString(6), 98 | method: method, 99 | resource: uri.relative, 100 | host: uri.hostname, 101 | port: uri.port, 102 | hash: options.hash, 103 | ext: options.ext, 104 | app: options.app, 105 | dlg: options.dlg 106 | }; 107 | 108 | result.artifacts = artifacts; 109 | 110 | // Calculate payload hash 111 | 112 | if (!artifacts.hash && 113 | options.hasOwnProperty('payload')) { 114 | 115 | artifacts.hash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType); 116 | } 117 | 118 | var mac = hawk.crypto.calculateMac('header', credentials, artifacts); 119 | 120 | // Construct header 121 | 122 | var hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''; // Other falsey values allowed 123 | var header = 'Hawk id="' + credentials.id + 124 | '", ts="' + artifacts.ts + 125 | '", nonce="' + artifacts.nonce + 126 | (artifacts.hash ? '", hash="' + artifacts.hash : '') + 127 | (hasExt ? '", ext="' + hawk.utils.escapeHeaderAttribute(artifacts.ext) : '') + 128 | '", mac="' + mac + '"'; 129 | 130 | if (artifacts.app) { 131 | header += ', app="' + artifacts.app + 132 | (artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"'; 133 | } 134 | 135 | result.field = header; 136 | 137 | return result; 138 | }, 139 | 140 | 141 | // Validate server response 142 | 143 | /* 144 | request: object created via 'new XMLHttpRequest()' after response received 145 | artifacts: object recieved from header().artifacts 146 | options: { 147 | payload: optional payload received 148 | required: specifies if a Server-Authorization header is required. Defaults to 'false' 149 | } 150 | */ 151 | 152 | authenticate: function (request, credentials, artifacts, options) { 153 | 154 | options = options || {}; 155 | 156 | if (request.getResponseHeader('www-authenticate')) { 157 | 158 | // Parse HTTP WWW-Authenticate header 159 | 160 | var attrsAuth = hawk.utils.parseAuthorizationHeader(request.getResponseHeader('www-authenticate'), ['ts', 'tsm', 'error']); 161 | if (!attrsAuth) { 162 | return false; 163 | } 164 | 165 | if (attrsAuth.ts) { 166 | var tsm = hawk.crypto.calculateTsMac(attrsAuth.ts, credentials); 167 | if (tsm !== attrsAuth.tsm) { 168 | return false; 169 | } 170 | 171 | hawk.utils.setNtpOffset(attrsAuth.ts - Math.floor((new Date()).getTime() / 1000)); // Keep offset at 1 second precision 172 | } 173 | } 174 | 175 | // Parse HTTP Server-Authorization header 176 | 177 | if (!request.getResponseHeader('server-authorization') && 178 | !options.required) { 179 | 180 | return true; 181 | } 182 | 183 | var attributes = hawk.utils.parseAuthorizationHeader(request.getResponseHeader('server-authorization'), ['mac', 'ext', 'hash']); 184 | if (!attributes) { 185 | return false; 186 | } 187 | 188 | var modArtifacts = { 189 | ts: artifacts.ts, 190 | nonce: artifacts.nonce, 191 | method: artifacts.method, 192 | resource: artifacts.resource, 193 | host: artifacts.host, 194 | port: artifacts.port, 195 | hash: attributes.hash, 196 | ext: attributes.ext, 197 | app: artifacts.app, 198 | dlg: artifacts.dlg 199 | }; 200 | 201 | var mac = hawk.crypto.calculateMac('response', credentials, modArtifacts); 202 | if (mac !== attributes.mac) { 203 | return false; 204 | } 205 | 206 | if (!options.hasOwnProperty('payload')) { 207 | return true; 208 | } 209 | 210 | if (!attributes.hash) { 211 | return false; 212 | } 213 | 214 | var calculatedHash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, request.getResponseHeader('content-type')); 215 | return (calculatedHash === attributes.hash); 216 | }, 217 | 218 | message: function (host, port, message, options) { 219 | 220 | // Validate inputs 221 | 222 | if (!host || typeof host !== 'string' || 223 | !port || typeof port !== 'number' || 224 | message === null || message === undefined || typeof message !== 'string' || 225 | !options || typeof options !== 'object') { 226 | 227 | return null; 228 | } 229 | 230 | // Application time 231 | 232 | var timestamp = options.timestamp || Math.floor((hawk.utils.now() + (options.localtimeOffsetMsec || 0)) / 1000); 233 | 234 | // Validate credentials 235 | 236 | var credentials = options.credentials; 237 | if (!credentials || 238 | !credentials.id || 239 | !credentials.key || 240 | !credentials.algorithm) { 241 | 242 | // Invalid credential object 243 | return null; 244 | } 245 | 246 | if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) { 247 | return null; 248 | } 249 | 250 | // Calculate signature 251 | 252 | var artifacts = { 253 | ts: timestamp, 254 | nonce: options.nonce || hawk.utils.randomString(6), 255 | host: host, 256 | port: port, 257 | hash: hawk.crypto.calculatePayloadHash(message, credentials.algorithm) 258 | }; 259 | 260 | // Construct authorization 261 | 262 | var result = { 263 | id: credentials.id, 264 | ts: artifacts.ts, 265 | nonce: artifacts.nonce, 266 | hash: artifacts.hash, 267 | mac: hawk.crypto.calculateMac('message', credentials, artifacts) 268 | }; 269 | 270 | return result; 271 | }, 272 | 273 | authenticateTimestamp: function (message, credentials, updateClock) { // updateClock defaults to true 274 | 275 | var tsm = hawk.crypto.calculateTsMac(message.ts, credentials); 276 | if (tsm !== message.tsm) { 277 | return false; 278 | } 279 | 280 | if (updateClock !== false) { 281 | hawk.utils.setNtpOffset(message.ts - Math.floor((new Date()).getTime() / 1000)); // Keep offset at 1 second precision 282 | } 283 | 284 | return true; 285 | } 286 | }; 287 | 288 | 289 | hawk.crypto = { 290 | 291 | headerVersion: '1', 292 | 293 | algorithms: ['sha1', 'sha256'], 294 | 295 | calculateMac: function (type, credentials, options) { 296 | var normalized = hawk.crypto.generateNormalizedString(type, options); 297 | var hmac = new sjcl.misc.hmac(credentials.key, sjcl.hash.sha256); 298 | hmac.update(normalized); 299 | 300 | return sjcl.codec.base64.fromBits(hmac.digest()); 301 | }, 302 | 303 | generateNormalizedString: function (type, options) { 304 | 305 | var normalized = 'hawk.' + hawk.crypto.headerVersion + '.' + type + '\n' + 306 | options.ts + '\n' + 307 | options.nonce + '\n' + 308 | (options.method || '').toUpperCase() + '\n' + 309 | (options.resource || '') + '\n' + 310 | options.host.toLowerCase() + '\n' + 311 | options.port + '\n' + 312 | (options.hash || '') + '\n'; 313 | 314 | if (options.ext) { 315 | normalized += options.ext.replace('\\', '\\\\').replace('\n', '\\n'); 316 | } 317 | 318 | normalized += '\n'; 319 | 320 | if (options.app) { 321 | normalized += options.app + '\n' + 322 | (options.dlg || '') + '\n'; 323 | } 324 | 325 | return normalized; 326 | }, 327 | 328 | calculatePayloadHash: function (payload, algorithm, contentType) { 329 | var hash = new sjcl.hash.sha256(); 330 | hash.update('hawk.' + hawk.crypto.headerVersion + '.payload\n') 331 | .update(hawk.utils.parseContentType(contentType) + '\n') 332 | .update(payload || '') 333 | .update('\n'); 334 | 335 | return sjcl.codec.base64.fromBits(hash.finalize()); 336 | }, 337 | 338 | calculateTsMac: function (ts, credentials) { 339 | var hmac = new sjcl.misc.hmac(credentials.key, sjcl.hash.sha256); 340 | hmac.update('hawk.' + hawk.crypto.headerVersion + '.ts\n' + ts + '\n'); 341 | 342 | return sjcl.codec.base64.fromBits(hmac.digest()); 343 | } 344 | }; 345 | 346 | 347 | hawk.utils = { 348 | 349 | storage: { // localStorage compatible interface 350 | _cache: {}, 351 | setItem: function (key, value) { 352 | 353 | hawk.utils.storage._cache[key] = value; 354 | }, 355 | getItem: function (key) { 356 | 357 | return hawk.utils.storage._cache[key]; 358 | } 359 | }, 360 | 361 | setStorage: function (storage) { 362 | 363 | var ntpOffset = hawk.utils.getNtpOffset() || 0; 364 | hawk.utils.storage = storage; 365 | hawk.utils.setNtpOffset(ntpOffset); 366 | }, 367 | 368 | setNtpOffset: function (offset) { 369 | 370 | try { 371 | hawk.utils.storage.setItem('hawk_ntp_offset', offset); 372 | } 373 | catch (err) { 374 | console.error('[hawk] could not write to storage.'); 375 | console.error(err); 376 | } 377 | }, 378 | 379 | getNtpOffset: function () { 380 | 381 | return parseInt(hawk.utils.storage.getItem('hawk_ntp_offset') || '0', 10); 382 | }, 383 | 384 | now: function () { 385 | 386 | return (new Date()).getTime() + hawk.utils.getNtpOffset(); 387 | }, 388 | 389 | escapeHeaderAttribute: function (attribute) { 390 | 391 | return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"'); 392 | }, 393 | 394 | parseContentType: function (header) { 395 | 396 | if (!header) { 397 | return ''; 398 | } 399 | 400 | return header.split(';')[0].replace(/^\s+|\s+$/g, '').toLowerCase(); 401 | }, 402 | 403 | parseAuthorizationHeader: function (header, keys) { 404 | 405 | if (!header) { 406 | return null; 407 | } 408 | 409 | var headerParts = header.match(/^(\w+)(?:\s+(.*))?$/); // Header: scheme[ something] 410 | if (!headerParts) { 411 | return null; 412 | } 413 | 414 | var scheme = headerParts[1]; 415 | if (scheme.toLowerCase() !== 'hawk') { 416 | return null; 417 | } 418 | 419 | var attributesString = headerParts[2]; 420 | if (!attributesString) { 421 | return null; 422 | } 423 | 424 | var attributes = {}; 425 | var verify = attributesString.replace(/(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, function ($0, $1, $2) { 426 | 427 | // Check valid attribute names 428 | 429 | if (keys.indexOf($1) === -1) { 430 | return; 431 | } 432 | 433 | // Allowed attribute value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9 434 | 435 | if ($2.match(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/) === null) { 436 | return; 437 | } 438 | 439 | // Check for duplicates 440 | 441 | if (attributes.hasOwnProperty($1)) { 442 | return; 443 | } 444 | 445 | attributes[$1] = $2; 446 | return ''; 447 | }); 448 | 449 | if (verify !== '') { 450 | return null; 451 | } 452 | 453 | return attributes; 454 | }, 455 | 456 | randomString: function (size) { 457 | 458 | var randomSource = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 459 | var len = randomSource.length; 460 | 461 | var result = []; 462 | for (var i = 0; i < size; ++i) { 463 | result[i] = randomSource[Math.floor(Math.random() * len)]; 464 | } 465 | 466 | return result.join(''); 467 | }, 468 | 469 | baseIndexOf: function(array, value, fromIndex) { 470 | var index = (fromIndex || 0) - 1, 471 | length = array ? array.length : 0; 472 | 473 | while (++index < length) { 474 | if (array[index] === value) { 475 | return index; 476 | } 477 | } 478 | return -1; 479 | }, 480 | 481 | parseUri: function (input) { 482 | 483 | // Based on: parseURI 1.2.2 484 | // http://blog.stevenlevithan.com/archives/parseuri 485 | // (c) Steven Levithan 486 | // MIT License 487 | 488 | var keys = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'hostname', 'port', 'resource', 'relative', 'pathname', 'directory', 'file', 'query', 'fragment']; 489 | 490 | var uriRegex = /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?(((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?)(?:#(.*))?)/; 491 | var uriByNumber = uriRegex.exec(input); 492 | var uri = {}; 493 | 494 | var i = 15; 495 | while (i--) { 496 | uri[keys[i]] = uriByNumber[i] || ''; 497 | } 498 | 499 | if (uri.port === null || 500 | uri.port === '') { 501 | 502 | uri.port = (uri.protocol.toLowerCase() === 'http' ? '80' : (uri.protocol.toLowerCase() === 'https' ? '443' : '')); 503 | } 504 | 505 | return uri; 506 | } 507 | }; 508 | 509 | 510 | return hawk; 511 | }); 512 | -------------------------------------------------------------------------------- /client/lib/hawkCredentials.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | define(['sjcl', './hkdf'], function (sjcl, hkdf) { 5 | 'use strict'; 6 | 7 | var PREFIX_NAME = 'identity.mozilla.com/picl/v1/'; 8 | var bitSlice = sjcl.bitArray.bitSlice; 9 | var salt = sjcl.codec.hex.toBits(''); 10 | 11 | /** 12 | * @class hawkCredentials 13 | * @method deriveHawkCredentials 14 | * @param {String} tokenHex 15 | * @param {String} context 16 | * @param {int} size 17 | * @returns {Promise} 18 | */ 19 | function deriveHawkCredentials(tokenHex, context, size) { 20 | var token = sjcl.codec.hex.toBits(tokenHex); 21 | var info = sjcl.codec.utf8String.toBits(PREFIX_NAME + context); 22 | 23 | return hkdf(token, info, salt, size || 3 * 32) 24 | .then(function(out) { 25 | var authKey = bitSlice(out, 8 * 32, 8 * 64); 26 | var bundleKey = bitSlice(out, 8 * 64); 27 | 28 | return { 29 | algorithm: 'sha256', 30 | id: sjcl.codec.hex.fromBits(bitSlice(out, 0, 8 * 32)), 31 | key: authKey, 32 | bundleKey: bundleKey 33 | }; 34 | }); 35 | } 36 | 37 | return deriveHawkCredentials; 38 | }); 39 | -------------------------------------------------------------------------------- /client/lib/hkdf.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | define(['sjcl'], function (sjcl) { 5 | 'use strict'; 6 | 7 | /** 8 | * hkdf - The HMAC-based Key Derivation Function 9 | * based on https://github.com/mozilla/node-hkdf 10 | * 11 | * @class hkdf 12 | * @param {bitArray} ikm Initial keying material 13 | * @param {bitArray} info Key derivation data 14 | * @param {bitArray} salt Salt 15 | * @param {integer} length Length of the derived key in bytes 16 | * @return promise object- It will resolve with `output` data 17 | */ 18 | function hkdf(ikm, info, salt, length) { 19 | 20 | var mac = new sjcl.misc.hmac(salt, sjcl.hash.sha256); 21 | mac.update(ikm); 22 | 23 | // compute the PRK 24 | var prk = mac.digest(); 25 | 26 | // hash length is 32 because only sjcl.hash.sha256 is used at this moment 27 | var hashLength = 32; 28 | var num_blocks = Math.ceil(length / hashLength); 29 | var prev = sjcl.codec.hex.toBits(''); 30 | var output = ''; 31 | 32 | for (var i = 0; i < num_blocks; i++) { 33 | var hmac = new sjcl.misc.hmac(prk, sjcl.hash.sha256); 34 | 35 | var input = sjcl.bitArray.concat( 36 | sjcl.bitArray.concat(prev, info), 37 | sjcl.codec.utf8String.toBits((String.fromCharCode(i + 1))) 38 | ); 39 | 40 | hmac.update(input); 41 | 42 | prev = hmac.digest(); 43 | output += sjcl.codec.hex.fromBits(prev); 44 | } 45 | 46 | var truncated = sjcl.bitArray.clamp(sjcl.codec.hex.toBits(output), length * 8); 47 | 48 | return Promise.resolve(truncated); 49 | } 50 | 51 | return hkdf; 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /client/lib/metricsContext.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // This module does the handling for the metrics context 6 | // activity event metadata. 7 | 8 | define([], function () { 9 | 'use strict'; 10 | 11 | return { 12 | marshall: function (data) { 13 | return { 14 | deviceId: data.deviceId, 15 | entrypoint: data.entrypoint, 16 | flowId: data.flowId, 17 | flowBeginTime: data.flowBeginTime, 18 | utmCampaign: data.utmCampaign, 19 | utmContent: data.utmContent, 20 | utmMedium: data.utmMedium, 21 | utmSource: data.utmSource, 22 | utmTerm: data.utmTerm 23 | }; 24 | } 25 | }; 26 | }); 27 | -------------------------------------------------------------------------------- /client/lib/pbkdf2.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | define(['sjcl'], function (sjcl, P) { 5 | 'use strict'; 6 | 7 | /** 8 | * @class pbkdf2 9 | * @constructor 10 | */ 11 | var pbkdf2 = { 12 | /** 13 | * @method derive 14 | * @param {bitArray} input The password hex buffer. 15 | * @param {bitArray} salt The salt string buffer. 16 | * @return {int} iterations the derived key bit array. 17 | */ 18 | derive: function(input, salt, iterations, len) { 19 | var result = sjcl.misc.pbkdf2(input, salt, iterations, len, sjcl.misc.hmac); 20 | return Promise.resolve(result); 21 | } 22 | }; 23 | 24 | return pbkdf2; 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /client/lib/request.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | define(['./hawk', './errors'], function (hawk, ERRORS) { 5 | 'use strict'; 6 | /* global XMLHttpRequest */ 7 | 8 | /** 9 | * @class Request 10 | * @constructor 11 | * @param {String} baseUri Base URI 12 | * @param {Object} xhr XMLHttpRequest constructor 13 | * @param {Object} [options={}] Options 14 | * @param {Number} [options.localtimeOffsetMsec] 15 | * Local time offset with the remote auth server's clock 16 | */ 17 | function Request (baseUri, xhr, options) { 18 | if (!options) { 19 | options = {}; 20 | } 21 | this.baseUri = baseUri; 22 | this._localtimeOffsetMsec = options.localtimeOffsetMsec; 23 | this.xhr = xhr || XMLHttpRequest; 24 | this.timeout = options.timeout || 30 * 1000; 25 | } 26 | 27 | /** 28 | * @method send 29 | * @param {String} path Request path 30 | * @param {String} method HTTP Method 31 | * @param {Object} credentials HAWK Headers 32 | * @param {Object} jsonPayload JSON Payload 33 | * @param {Object} [options={}] Options 34 | * @param {String} [options.retrying] 35 | * Flag indicating if the request is a retry 36 | * @param {Array} [options.headers] 37 | * A set of extra headers to add to the request 38 | * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request 39 | */ 40 | Request.prototype.send = function request(path, method, credentials, jsonPayload, options) { 41 | /*eslint complexity: [2, 8] */ 42 | var xhr = new this.xhr(); 43 | var uri = this.baseUri + path; 44 | var payload = null; 45 | var self = this; 46 | options = options || {}; 47 | 48 | if (jsonPayload) { 49 | payload = JSON.stringify(jsonPayload); 50 | } 51 | 52 | try { 53 | xhr.open(method, uri); 54 | } catch (e) { 55 | return Promise.reject({ error: 'Unknown error', message: e.toString(), errno: 999 }); 56 | } 57 | 58 | return new Promise(function (resolve, reject) { 59 | xhr.timeout = self.timeout; 60 | 61 | xhr.onreadystatechange = function() { 62 | if (xhr.readyState === 4) { 63 | var result = xhr.responseText; 64 | try { 65 | result = JSON.parse(xhr.responseText); 66 | } catch (e) { } 67 | 68 | if (result.errno) { 69 | // Try to recover from a timeskew error and not already tried 70 | if (result.errno === ERRORS.INVALID_TIMESTAMP && !options.retrying) { 71 | var serverTime = result.serverTime; 72 | self._localtimeOffsetMsec = (serverTime * 1000) - new Date().getTime(); 73 | 74 | // add to options that the request is retrying 75 | options.retrying = true; 76 | 77 | return self.send(path, method, credentials, jsonPayload, options) 78 | .then(resolve, reject); 79 | 80 | } else { 81 | return reject(result); 82 | } 83 | } 84 | 85 | if (typeof xhr.status === 'undefined' || xhr.status !== 200) { 86 | if (result.length === 0) { 87 | return reject({ error: 'Timeout error', errno: 999 }); 88 | } else { 89 | return reject({ error: 'Unknown error', message: result, errno: 999, code: xhr.status }); 90 | } 91 | } 92 | 93 | resolve(result); 94 | } 95 | }; 96 | 97 | // calculate Hawk header if credentials are supplied 98 | if (credentials) { 99 | var hawkHeader = hawk.client.header(uri, method, { 100 | credentials: credentials, 101 | payload: payload, 102 | contentType: 'application/json', 103 | localtimeOffsetMsec: self._localtimeOffsetMsec || 0 104 | }); 105 | xhr.setRequestHeader('authorization', hawkHeader.field); 106 | } 107 | 108 | xhr.setRequestHeader('Content-Type', 'application/json'); 109 | 110 | if (options && options.headers) { 111 | // set extra headers for this request 112 | for (var header in options.headers) { 113 | xhr.setRequestHeader(header, options.headers[header]); 114 | } 115 | } 116 | 117 | xhr.send(payload); 118 | }); 119 | }; 120 | 121 | return Request; 122 | 123 | }); 124 | -------------------------------------------------------------------------------- /node/amd-loader.js: -------------------------------------------------------------------------------- 1 | // This is intended to be the simplest possible AMD shim that works 2 | // It is not intended a general AMD loader just enough to load this package 3 | // This relies on the fact that Node.js require() is synchronous. 4 | // It attempts to let the node.js module loader do as much work as possible 5 | // Also provides a way to replace modules with api compatible counterparts 6 | 7 | var path = require('path'); 8 | 9 | module.exports = function amdload(absoluteFilename, map) { 10 | // Store this so we can put it back later. 11 | var oldDefine = global.define; 12 | 13 | map = map || {}; 14 | var loaded = {}, dirs = [], exported; 15 | 16 | /** 17 | * These two functions operate as a pair 18 | */ 19 | var amdrequire = function amdrequire(filepath) { 20 | // Return real node modules if we have them mapped 21 | if (filepath in map) { 22 | return require(map[filepath]); 23 | } 24 | 25 | // Resolve target against 'current working directory' 26 | var fullpath = path.resolve(dirs[0], filepath); 27 | 28 | if (!loaded[fullpath]) { 29 | // Put current operation on stack 30 | dirs.unshift(path.dirname(fullpath)); 31 | 32 | // setup fake define and delegate to real require() 33 | global.define = define; 34 | 35 | require(fullpath); 36 | 37 | // Capture and store exported module 38 | loaded[fullpath] = exported; 39 | exported = null; 40 | 41 | // Restore previous define() state 42 | if (oldDefine) { 43 | global.define = define; 44 | } else { 45 | delete global.define; 46 | } 47 | 48 | // return to cwd from before define 49 | dirs.shift(); 50 | } 51 | 52 | // return value captured by define() 53 | return loaded[fullpath]; 54 | }; 55 | var define = function define(deps, factory) { 56 | // Load all dependencies 57 | var modules = deps.map(amdrequire); 58 | // Capture the exported value 59 | exported = factory.apply(null, modules); 60 | }; 61 | define.amd = true; 62 | 63 | return amdrequire(absoluteFilename); 64 | }; 65 | -------------------------------------------------------------------------------- /node/index.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var amd = require('./amd-loader'); 4 | 5 | var map = { 6 | 'es6-promise': 'es6-promise', 7 | sjcl: 'sjcl' 8 | }; 9 | 10 | var FxAccountClient = amd(__dirname + '/../client/FxAccountClient.js', map); 11 | 12 | function NodeFxAccountClient(uri, config) { 13 | if (!(this instanceof FxAccountClient)) { 14 | return new NodeFxAccountClient(uri, config); 15 | } 16 | 17 | if (typeof uri !== 'string') { 18 | config = uri || {}; 19 | uri = config.uri; 20 | } 21 | if (typeof config === 'undefined') { 22 | config = {}; 23 | } 24 | 25 | if (!config.xhr) { 26 | config.xhr = require('xhr2'); 27 | } 28 | 29 | FxAccountClient.call(this, uri, config); 30 | } 31 | 32 | NodeFxAccountClient.VERSION = FxAccountClient.VERSION; 33 | 34 | module.exports = NodeFxAccountClient; 35 | util.inherits(NodeFxAccountClient, FxAccountClient); 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fxa-js-client", 3 | "version": "1.0.8", 4 | "description": "Web client that talks to the Firefox Accounts API server", 5 | "author": "Mozilla", 6 | "license": "MPL-2.0", 7 | "scripts": { 8 | "start": "grunt", 9 | "test": "grunt test", 10 | "test-local": "intern-client config=tests/intern auth_server=LOCAL", 11 | "setup": "npm install && grunt sjcl", 12 | "contributors": "git shortlog -s | cut -c8- | sort -f > CONTRIBUTORS.md" 13 | }, 14 | "main": "node/index.js", 15 | "files": [ 16 | "node/", 17 | "client/", 18 | "LICENSE" 19 | ], 20 | "readmeFilename": "README.md", 21 | "homepage": "https://github.com/mozilla/fxa-js-client", 22 | "engines": { 23 | "node": ">=8" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/mozilla/fxa-js-client.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/mozilla/fxa-js-client/issues" 31 | }, 32 | "dependencies": { 33 | "es6-promise": "4.1.1", 34 | "sjcl": "git://github.com/bitwiseshiftleft/sjcl.git#a03ea8e", 35 | "xhr2": "0.0.7" 36 | }, 37 | "devDependencies": { 38 | "eslint-config-fxa": "1.8.1", 39 | "grunt": "0.4.2", 40 | "grunt-build-control": "git://github.com/robwierzbowski/grunt-build-control#274952", 41 | "grunt-bump": "0.3.1", 42 | "grunt-bytesize": "0.1.1", 43 | "grunt-cli": "0.1.13", 44 | "grunt-contrib-clean": "0.6.0", 45 | "grunt-contrib-watch": "0.6.1", 46 | "grunt-contrib-yuidoc": "0.9.0", 47 | "grunt-conventional-changelog": "3.0.0", 48 | "grunt-copyright": "0.2.0", 49 | "grunt-eslint": "16.0.0", 50 | "grunt-open": "0.2.2", 51 | "grunt-webpack": "3.0.2", 52 | "http-proxy": "1.11.1", 53 | "intern-geezer": "2.2.3", 54 | "jscs-jsdoc": "1.1.0", 55 | "load-grunt-tasks": "3.2.0", 56 | "otplib": "7.1.0", 57 | "webpack": "3.10.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tasks/buildcontrol.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | 'use strict'; 7 | 8 | grunt.config('buildcontrol', { 9 | options: { 10 | commit: true, 11 | push: true, 12 | remote: 'git@github.com:mozilla/fxa-js-client.git' 13 | }, 14 | release: { 15 | options: { 16 | branch: 'release', 17 | dir: 'build', 18 | tag: '<%= pkg.version %>' 19 | } 20 | }, 21 | docs: { 22 | options: { 23 | branch: 'gh-pages', 24 | dir: 'docs', 25 | tag: 'docs-<%= pkg.version %>' 26 | } 27 | } 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /tasks/bump.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | 'use strict'; 7 | 8 | grunt.config('bump', { 9 | options: { 10 | files: ['package.json'], 11 | updateConfigs: ['pkg'], 12 | push: true, 13 | pushTo: 'git@github.com:mozilla/fxa-js-client.git update-master', 14 | commitMessage: 'source-%VERSION%', 15 | tagName: 'source-%VERSION%', 16 | // commit all modified files 17 | commitFiles: ['-a'], 18 | commit: true 19 | } 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /tasks/bytesize.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | 'use strict'; 7 | 8 | grunt.config('bytesize', { 9 | all: { 10 | src: ['build/fxa-client.js', 'build/fxa-client.min.js'] 11 | } 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /tasks/changelog.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | 'use strict'; 7 | 8 | grunt.config('conventionalChangelog', { 9 | options: { 10 | from: 'source-<%= pkgReadOnly.version %>' 11 | } 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /tasks/clean.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | 'use strict'; 7 | 8 | grunt.config('clean', { 9 | build: ['build'] 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /tasks/copyright.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | 'use strict'; 7 | 8 | grunt.config('copyright', { 9 | app: { 10 | options: { 11 | pattern: /This Source Code Form is subject to the terms of the Mozilla Public/ 12 | }, 13 | src: [ 14 | '**/*.js', 15 | '!tests/addons/sinon.js', 16 | '!build/**', 17 | '!node_modules/**', 18 | '!docs/**' 19 | ] 20 | } 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /tasks/eslint.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | grunt.config('eslint', { 7 | app: { 8 | eslintrc: '.eslintrc', 9 | src: ['Gruntfile.js', 'tasks/*.js', 'config/**/*.js', 'node/**/*.js'] 10 | }, 11 | client: { 12 | options: {eslintrc: 'client/.eslintrc'}, 13 | src: ['client/*.js', 'client/lib/**/*'] 14 | }, 15 | test: { 16 | options: { 17 | eslintrc: 'tests/.eslintrc' 18 | }, 19 | src: [ 20 | 'tests/**/*.js', 21 | '!tests/addons/sinon.js' 22 | ] 23 | } 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /tasks/intern.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | 'use strict'; 7 | 8 | grunt.config('intern', { 9 | node: { 10 | options: { 11 | config: 'tests/intern', 12 | reporters: ['console'], 13 | suites: ['tests/all'] 14 | } 15 | }, 16 | native_node: { 17 | options: { 18 | config: 'tests/intern_native_node', 19 | reporters: ['console'], 20 | suites: ['tests/all'] 21 | } 22 | }, 23 | // local browser 24 | browser: { 25 | options: { 26 | runType: 'runner', 27 | config: 'tests/intern_browser', 28 | suites: ['tests/all'] 29 | } 30 | }, 31 | sauce: { 32 | options: { 33 | runType: 'runner', 34 | config: 'tests/intern_sauce', 35 | suites: ['tests/all'], 36 | sauceUsername: 'fxa-client', 37 | sauceAccessKey: '863203af-38fd-4f1d-9332-adc8f60f157b' 38 | } 39 | } 40 | }); 41 | }; 42 | 43 | 44 | -------------------------------------------------------------------------------- /tasks/open.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | 'use strict'; 7 | 8 | grunt.config('open', { 9 | dev: { 10 | path: 'docs/index.html' 11 | } 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /tasks/sjcl.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | var fs = require('fs'); 7 | var exec = require('child_process').exec; 8 | 9 | grunt.registerTask('sjcl', 'Build the SJCL library', function () { 10 | var done = this.async(); 11 | var configSJCL = './configure --without-random --without-ocb2 --without-gcm --without-ccm && make'; 12 | var src = 'core_closure.js'; 13 | var dist = 'sjcl.js'; 14 | 15 | process.chdir('node_modules/sjcl/'); 16 | 17 | exec(configSJCL, 18 | function (error, stdout, stderr) { 19 | grunt.log.write(stdout); 20 | if (stderr) { 21 | grunt.log.warn(stderr); 22 | } 23 | 24 | var sjclBower = fs.readFileSync(src); 25 | var sjclAmd = 'define([], function () {' + sjclBower + ' return sjcl; });'; 26 | fs.writeFileSync(dist, sjclAmd); 27 | 28 | process.chdir('../..'); 29 | done(); 30 | }); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /tasks/watch.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | 'use strict'; 7 | 8 | grunt.config('watch', { 9 | dev: { 10 | options: { 11 | atBegin: true 12 | }, 13 | files: ['Gruntfile.js', 'client/**/*.js', 'tests/**/*.js'], 14 | tasks: ['build', 'intern:node'] 15 | }, 16 | debug: { 17 | options: { 18 | atBegin: true 19 | }, 20 | files: ['Gruntfile.js', 'client/**/*.js', 'tests/**/*.js'], 21 | tasks: ['webpack:app', 'intern:node'] 22 | } 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /tasks/webpack.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | const webpackConfig = require('../webpack.config'); 7 | 8 | grunt.config('webpack', { 9 | options: { 10 | stats: true 11 | }, 12 | app: webpackConfig, 13 | watch: Object.assign({ watch: true }, webpackConfig) 14 | }); 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /tasks/yuidoc.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | 'use strict'; 7 | 8 | grunt.config('yuidoc', { 9 | compile: { 10 | name: '<%= pkg.name %>', 11 | description: '<%= pkg.description %>', 12 | version: '<%= pkg.version %>', 13 | options: { 14 | paths: 'client/', 15 | outdir: 'docs/' 16 | } 17 | } 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "no-with": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/addons/accountHelper.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'tests/mocks/request' 7 | ], function (RequestMocks) { 8 | 'use strict'; 9 | 10 | function AccountHelper(client, mail, respond) { 11 | this.client = client; 12 | this.mail = mail; 13 | this.respond = respond; 14 | } 15 | AccountHelper.prototype.newVerifiedAccount = function (options) { 16 | var username = 'testHelp1'; 17 | var domain = '@restmail.net'; 18 | 19 | if (options && options.domain) { 20 | domain = options.domain; 21 | } 22 | 23 | if (options && options.username) { 24 | username = options.username; 25 | } 26 | 27 | var user = username + new Date().getTime(); 28 | var email = user + domain; 29 | var password = 'iliketurtles'; 30 | var respond = this.respond; 31 | var mail = this.mail; 32 | var client = this.client; 33 | var uid; 34 | var result = { 35 | input: { 36 | user: user, 37 | email: email, 38 | password: password 39 | } 40 | }; 41 | 42 | return respond(client.signUp(email, password), RequestMocks.signUp) 43 | .then(function (res) { 44 | uid = res.uid; 45 | result.signUp = res; 46 | 47 | return respond(mail.wait(user), RequestMocks.mail); 48 | }) 49 | .then(function (emails) { 50 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 51 | 52 | return respond(client.verifyCode(uid, code), RequestMocks.verifyCode); 53 | }) 54 | 55 | .then(function (res) { 56 | result.verifyCode = res; 57 | 58 | return respond(client.signIn(email, password, {keys: true}), RequestMocks.signInWithKeys); 59 | }) 60 | .then(function(res) { 61 | result.signIn = res; 62 | 63 | return result; 64 | }); 65 | }; 66 | 67 | AccountHelper.prototype.newUnverifiedAccount = function (options) { 68 | var username = 'testHelp2'; 69 | var domain = '@restmail.net'; 70 | 71 | if (options && options.domain) { 72 | domain = options.domain; 73 | } 74 | 75 | if (options && options.username) { 76 | username = options.username; 77 | } 78 | 79 | var user = username + new Date().getTime(); 80 | var email = user + domain; 81 | var password = 'iliketurtles'; 82 | var respond = this.respond; 83 | var client = this.client; 84 | var result = { 85 | input: { 86 | user: user, 87 | email: email, 88 | password: password 89 | } 90 | }; 91 | 92 | return respond(client.signUp(email, password), RequestMocks.signUp) 93 | .then(function (res) { 94 | result.signUp = res; 95 | 96 | return respond(client.signIn(email, password, {keys: true}), RequestMocks.signInWithKeys); 97 | }).then(function(res) { 98 | result.signIn = res; 99 | 100 | return result; 101 | }); 102 | 103 | }; 104 | 105 | return AccountHelper; 106 | }); 107 | -------------------------------------------------------------------------------- /tests/addons/environment.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'tests/intern', 7 | 'intern/node_modules/dojo/has!host-node?intern/node_modules/dojo/node!xhr2', 8 | 'tests/addons/sinon', 9 | 'client/FxAccountClient', 10 | 'tests/addons/restmail', 11 | 'tests/addons/accountHelper', 12 | 'tests/mocks/request', 13 | 'tests/mocks/errors' 14 | ], function (config, XHR, Sinon, FxAccountClient, Restmail, AccountHelper, RequestMocks, ErrorMocks) { 15 | 16 | function Environment() { 17 | var self = this; 18 | this.authServerUrl = config.AUTH_SERVER_URL || 'http://127.0.0.1:9000'; 19 | // if 'auth_server' is part of the Intern arguments then using a remote server 20 | this.useRemoteServer = !!config.AUTH_SERVER_URL; 21 | this.mailServerUrl = this.authServerUrl.match(/^http:\/\/127/) ? 22 | 'http://127.0.0.1:9001' : 23 | 'http://restmail.net'; 24 | 25 | if (this.useRemoteServer) { 26 | this.xhr = XHR.XMLHttpRequest; 27 | // respond is a noop because we are using real XHR in this case 28 | this.respond = noop; 29 | 30 | } else { 31 | this.requests = []; 32 | this.responses = []; 33 | // switch to the fake XHR 34 | this.xhr = Sinon.useFakeXMLHttpRequest(); 35 | this.xhr.onCreate = function (xhr) { 36 | if (self.requests.length < self.responses.length) { 37 | var mock = self.responses[self.requests.length]; 38 | setTimeout(function() { 39 | xhr.respond(mock.status, mock.headers, mock.body); 40 | }, 0); 41 | } 42 | self.requests.push(xhr); 43 | }; 44 | // respond calls a fake XHR response using SinonJS 45 | this.respond = function (returnValue, mock) { 46 | if (arguments.length < 2) { 47 | mock = returnValue; 48 | returnValue = null; 49 | } 50 | if (typeof mock === 'undefined') { 51 | console.log('Mock does not exist!'); 52 | } 53 | // this has to be here to work in IE 54 | setTimeout(function () { 55 | if (self.responses.length < self.requests.length) { 56 | self.requests[self.responses.length].respond(mock.status, mock.headers, mock.body); 57 | } 58 | self.responses.push(mock); 59 | }, 0); 60 | return returnValue; 61 | }; 62 | } 63 | // initialize a new FxA Client 64 | this.client = new FxAccountClient(this.authServerUrl, { xhr: this.xhr }); 65 | // setup Restmail, 66 | this.mail = new Restmail(this.mailServerUrl, this.xhr); 67 | // account helper takes care of new verified and unverified accounts 68 | this.accountHelper = new AccountHelper(this.client, this.mail, this.respond); 69 | this.ErrorMocks = ErrorMocks; 70 | this.RequestMocks = RequestMocks; 71 | } 72 | 73 | function noop(val) { return val; } 74 | 75 | return Environment; 76 | }); 77 | -------------------------------------------------------------------------------- /tests/addons/node-client.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern/node_modules/dojo/node!../../node/index' 7 | ], function (FxAccountClient) { 8 | 9 | return FxAccountClient; 10 | }); 11 | -------------------------------------------------------------------------------- /tests/addons/restmail.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'client/lib/request' 7 | ], function (Request) { 8 | 'use strict'; 9 | 10 | function Restmail(server, xhr) { 11 | this.request = new Request(server, xhr); 12 | } 13 | 14 | // utility function that waits for a restmail email to arrive 15 | Restmail.prototype.wait = function (user, number) { 16 | var self = this; 17 | 18 | if (!number) number = 1; //eslint-disable-line curly 19 | console.log('Waiting for email...'); 20 | 21 | return this.request.send('/mail/' + user, 'GET') 22 | .then(function (result) { 23 | if (result.length === number) { 24 | return result; 25 | } else { 26 | return new Promise(function (resolve, reject) { 27 | setTimeout(function () { 28 | self.wait(user, number) 29 | .then(resolve, reject); 30 | }, 1000); 31 | }); 32 | } 33 | }); 34 | }; 35 | 36 | return Restmail; 37 | }); 38 | -------------------------------------------------------------------------------- /tests/all.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'tests/lib/account', 7 | 'tests/lib/certificateSign', 8 | 'tests/lib/credentials', 9 | 'tests/lib/device', 10 | 'tests/lib/emails', 11 | 'tests/lib/errors', 12 | 'tests/lib/hawkCredentials', 13 | 'tests/lib/headerLang', 14 | 'tests/lib/hkdf', 15 | 'tests/lib/init', 16 | 'tests/lib/metricsContext', 17 | 'tests/lib/misc', 18 | 'tests/lib/passwordChange', 19 | 'tests/lib/recoveryCodes', 20 | 'tests/lib/recoveryKeys', 21 | 'tests/lib/recoveryEmail', 22 | 'tests/lib/request', 23 | 'tests/lib/session', 24 | 'tests/lib/signIn', 25 | 'tests/lib/signinCodes', 26 | 'tests/lib/signUp', 27 | 'tests/lib/totp', 28 | 'tests/lib/tokenCodes', 29 | 'tests/lib/sms', 30 | 'tests/lib/unbundle', 31 | 'tests/lib/uriVersion', 32 | 'tests/lib/verifyCode' 33 | ], function () {}); 34 | -------------------------------------------------------------------------------- /tests/ci/install-tunnel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | wget https://saucelabs.com/downloads/sc-4.4.5-linux.tar.gz -O /tmp/sc-4.4.5-linux.tar.gz 4 | cd /tmp 5 | tar xvf /tmp/sc-4.4.5-linux.tar.gz 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/ci/travis-auth-server-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | # Install and start the auth server 4 | git clone https://github.com/mozilla/fxa-auth-server.git 5 | cd fxa-auth-server && npm i 6 | SECONDARY_EMAIL_ENABLED=true SIGNIN_CONFIRMATION_ENABLED=true SIGNIN_CONFIRMATION_FORCE_EMAIL_REGEX="^confirm.*@restmail\\.net$" SIGNIN_UNBLOCK_ALLOWED_EMAILS="^block.*@restmail\\.net$" SIGNIN_UNBLOCK_FORCED_EMAILS="^block.*@restmail\\.net$" npm start & 7 | cd .. 8 | sleep 10 9 | 10 | # Run the tests against the local auth server 11 | npm run test-local 12 | -------------------------------------------------------------------------------- /tests/examples/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 |

fxa-js-client tester

20 | 21 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 | 41 | 45 | 46 |
47 | 48 |
49 | 50 | 51 |
52 | 56 | 57 | 58 |
59 | 60 |
61 | 62 |
63 | 64 | 65 | 69 | 73 | 74 | 75 | 76 |
77 | 78 |
79 | 80 |
81 | 85 | 89 | 90 | 91 |
92 |
93 | See Console.... 94 | 95 | 96 | 97 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /tests/examples/proxy.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /** 6 | * Auth Proxy tester for better IE debugging. 7 | * 8 | * Run using: node tests/examples/proxy.js 9 | */ 10 | var http = require('http'); 11 | var fs = require('fs'); 12 | var httpProxy = require('http-proxy'); 13 | 14 | var proxy = httpProxy.createProxyServer(); 15 | var port = 9133; 16 | var targetAuthServer = 'http://127.0.0.1:9000'; 17 | 18 | http.createServer(function (req, res) { 19 | 20 | if (req.url === '/example.html') { 21 | 22 | res.end(fs.readFileSync('tests/examples/example.html')); 23 | } else if (req.url === '/build/fxa-client.js') { 24 | 25 | res.end(fs.readFileSync('build/fxa-client.js')); 26 | } else { 27 | 28 | proxy.web(req, res, { 29 | target: targetAuthServer 30 | }); 31 | } 32 | 33 | }).listen(port); 34 | 35 | console.log('Starting proxy on', port, 'targeting', targetAuthServer, 'fxa-auth-server'); 36 | -------------------------------------------------------------------------------- /tests/intern.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // Learn more about configuring this file at . 6 | // These default settings work OK for most people. The options that *must* be changed below are the 7 | // packages, suites, excludeInstrumentation, and (if you want functional tests) functionalSuites. 8 | define(['intern/lib/args'], function (args) { 9 | 10 | // define a server to run against 11 | var server; 12 | 13 | // if 'auth_server' in the Intern args 14 | if (args.auth_server) { 15 | server = args.auth_server; 16 | if (server === 'LOCAL') { 17 | server = 'http://127.0.0.1:9000'; 18 | } 19 | 20 | if (server === 'LATEST') { 21 | server = 'https://latest.dev.lcip.org/auth'; 22 | } 23 | 24 | if (server === 'STABLE') { 25 | server = 'https://stable.dev.lcip.org/auth'; 26 | } 27 | 28 | console.log('Running against ' + server); 29 | } else { 30 | console.log('Running with mocks...'); 31 | } 32 | 33 | return { 34 | loader: { 35 | // Packages that should be registered with the loader in each testing environment 36 | packages: [ { name: 'fxa-js-client', location: 'client' } ], 37 | map: { 38 | '*': { 39 | 'es6-promise': 'node_modules/es6-promise/dist/es6-promise', 40 | sjcl: 'node_modules/sjcl/sjcl' 41 | } 42 | } 43 | }, 44 | 45 | suites: [ 'tests/all' ], 46 | functionalSuites: [ ], 47 | AUTH_SERVER_URL: server, 48 | 49 | excludeInstrumentation: /./ 50 | 51 | }; 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /tests/intern_browser.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | './intern' 7 | ], function (intern) { 8 | intern.proxyPort = 9090; 9 | intern.proxyUrl = 'http://localhost:9090/'; 10 | 11 | intern.useSauceConnect = false; 12 | 13 | intern.webdriver = { 14 | host: 'localhost', 15 | port: 4444 16 | }; 17 | 18 | intern.capabilities = { 19 | 'selenium-version': '2.39.0' 20 | }; 21 | 22 | intern.environments = [ 23 | { browserName: 'firefox', version: '25' } 24 | ]; 25 | 26 | return intern; 27 | }); 28 | -------------------------------------------------------------------------------- /tests/intern_native_node.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | './intern' 7 | ], function (intern, FxAccountClient) { 8 | 9 | var map = intern.loader.map['*']; 10 | map['client/FxAccountClient'] = 'tests/addons/node-client'; 11 | 12 | return intern; 13 | }); 14 | -------------------------------------------------------------------------------- /tests/intern_sauce.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | './intern' 7 | ], function (intern) { 8 | intern.proxyPort = 9090; 9 | intern.proxyUrl = 'http://localhost:9090/'; 10 | 11 | intern.useSauceConnect = true; 12 | intern.maxConcurrency = 3; 13 | 14 | intern.tunnel = 'SauceLabsTunnel'; 15 | intern.tunnelOptions = { 16 | directory: '/tmp/sc-4.4.5-linux/bin', 17 | executable: './sc' 18 | }; 19 | 20 | intern.webdriver = { 21 | host: 'localhost', 22 | port: 4445 23 | }; 24 | 25 | intern.capabilities = { 26 | 'build': '1', 27 | }; 28 | 29 | intern.environments = [ 30 | { browserName: 'firefox', version: [ '45' ], platform: [ 'Windows 7', 'Linux' ] }, 31 | { browserName: 'firefox', version: [ '56' ], platform: [ 'Windows 7' ] }, // Sauce only supports Fx 56 on Windows/Mac 32 | { browserName: 'internet explorer', version: [ '10', '11' ], platform: [ 'Windows 7' ] }, 33 | { browserName: 'chrome' } 34 | ]; 35 | 36 | console.log('SAUCE', intern.proxyUrl); 37 | 38 | return intern; 39 | }); 40 | -------------------------------------------------------------------------------- /tests/lib/certificateSign.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment' 9 | ], function (tdd, assert, Environment) { 10 | 11 | with (tdd) { 12 | suite('certificateSign', function () { 13 | var accountHelper; 14 | var respond; 15 | var client; 16 | var RequestMocks; 17 | 18 | beforeEach(function () { 19 | var env = new Environment(); 20 | accountHelper = env.accountHelper; 21 | respond = env.respond; 22 | client = env.client; 23 | RequestMocks = env.RequestMocks; 24 | }); 25 | 26 | test('#basic', function () { 27 | 28 | return accountHelper.newVerifiedAccount() 29 | .then(function (account) { 30 | var publicKey = { 31 | algorithm: 'RS', 32 | n: '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123', 33 | e: '65537' 34 | }; 35 | var duration = 86400000; 36 | 37 | return respond(client.certificateSign(account.signIn.sessionToken, publicKey, duration), RequestMocks.certificateSign); 38 | }) 39 | .then( 40 | function(res) { 41 | assert.property(res, 'cert', 'got cert'); 42 | }, 43 | assert.notOk 44 | ); 45 | }); 46 | 47 | test('#with service option', function () { 48 | return accountHelper.newVerifiedAccount() 49 | .then(function (account) { 50 | var publicKey = { 51 | algorithm: 'RS', 52 | n: '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123', 53 | e: '65537' 54 | }; 55 | var duration = 86400000; 56 | 57 | return respond( 58 | client.certificateSign(account.signIn.sessionToken, publicKey, duration, { 59 | service: 'wibble' 60 | }), 61 | RequestMocks.certificateSign 62 | ); 63 | }) 64 | .then( 65 | function(res) { 66 | assert.ok(res); 67 | }, 68 | assert.notOk 69 | ); 70 | }); 71 | }); 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /tests/lib/credentials.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'node_modules/sjcl/sjcl', 9 | 'client/lib/credentials' 10 | ], function (tdd, assert, sjcl, credentials) { 11 | with (tdd) { 12 | suite('credentials', function () { 13 | test('#client stretch-KDF vectors', function () { 14 | var email = sjcl.codec.utf8String.fromBits(sjcl.codec.hex.toBits('616e6472c3a9406578616d706c652e6f7267')); 15 | var password = sjcl.codec.utf8String.fromBits(sjcl.codec.hex.toBits('70c3a4737377c3b67264')); 16 | 17 | return credentials.setup(email, password) 18 | .then( 19 | function(result) { 20 | var quickStretchedPW = sjcl.codec.hex.fromBits(result.quickStretchedPW); 21 | var authPW = sjcl.codec.hex.fromBits(result.authPW); 22 | var unwrapBKey = sjcl.codec.hex.fromBits(result.unwrapBKey); 23 | 24 | assert.equal(quickStretchedPW, 'e4e8889bd8bd61ad6de6b95c059d56e7b50dacdaf62bd84644af7e2add84345d', '== quickStretchedPW is equal'); 25 | assert.equal(authPW, '247b675ffb4c46310bc87e26d712153abe5e1c90ef00a4784594f97ef54f2375', '== authPW is equal'); 26 | assert.equal(unwrapBKey, 'de6a2648b78284fcb9ffa81ba95803309cfba7af583c01a8a1a63e567234dd28', '== unwrapBkey is equal'); 27 | }, 28 | assert.notOk 29 | ); 30 | }); 31 | 32 | test('#wrap', function () { 33 | var bit1 = sjcl.codec.hex.toBits('c347de41c8a409c17b5b88e4985e1cd10585bb79b4a80d5e576eaf97cd1277fc'); 34 | var bit2 = sjcl.codec.hex.toBits('3afd383d9bc1857318f24c5f293af62254f0476f0aaacfb929c61b534d0b5075'); 35 | var result = credentials.xor(bit1, bit2); 36 | 37 | assert.equal(sjcl.codec.hex.fromBits(result), 'f9bae67c53658cb263a9c4bbb164eaf35175fc16be02c2e77ea8b4c480192789', '== wrap worked correctly'); 38 | }); 39 | }); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /tests/lib/device.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment', 9 | 'tests/lib/push-constants' 10 | ], function (tdd, assert, Environment, PushTestConstants) { 11 | 12 | var DEVICE_CALLBACK = PushTestConstants.DEVICE_CALLBACK; 13 | var DEVICE_NAME = PushTestConstants.DEVICE_NAME; 14 | var DEVICE_NAME_2 = PushTestConstants.DEVICE_NAME_2; 15 | var DEVICE_TYPE = PushTestConstants.DEVICE_TYPE; 16 | 17 | with (tdd) { 18 | suite('device', function () { 19 | var accountHelper; 20 | var respond; 21 | var client; 22 | var RequestMocks; 23 | 24 | beforeEach(function () { 25 | var env = new Environment(); 26 | accountHelper = env.accountHelper; 27 | respond = env.respond; 28 | client = env.client; 29 | RequestMocks = env.RequestMocks; 30 | }); 31 | 32 | test('#register', function () { 33 | 34 | return accountHelper.newVerifiedAccount() 35 | .then(function (account) { 36 | 37 | return respond(client.deviceRegister( 38 | account.signIn.sessionToken, 39 | DEVICE_NAME, 40 | DEVICE_TYPE, 41 | { 42 | deviceCallback: DEVICE_CALLBACK 43 | } 44 | ), RequestMocks.deviceRegister); 45 | }) 46 | .then( 47 | function(res) { 48 | assert.ok(res.id); 49 | assert.equal(res.name, DEVICE_NAME); 50 | assert.equal(res.pushCallback, DEVICE_CALLBACK); 51 | assert.equal(res.type, DEVICE_TYPE); 52 | }, 53 | function (err) { 54 | console.log(err); 55 | assert.notOk(); 56 | } 57 | ); 58 | }); 59 | 60 | test('#update', function () { 61 | 62 | return accountHelper.newVerifiedAccount() 63 | .then(function (account) { 64 | 65 | return respond(client.deviceRegister( 66 | account.signIn.sessionToken, 67 | DEVICE_NAME, 68 | DEVICE_TYPE, 69 | { 70 | deviceCallback: DEVICE_CALLBACK 71 | } 72 | ), RequestMocks.deviceRegister) 73 | 74 | .then(function (device) { 75 | 76 | return respond(client.deviceUpdate( 77 | account.signIn.sessionToken, 78 | device.id, 79 | DEVICE_NAME_2, 80 | { 81 | deviceCallback: DEVICE_CALLBACK 82 | } 83 | ), RequestMocks.deviceUpdate); 84 | }); 85 | }) 86 | .then( 87 | function(res) { 88 | assert.ok(res.id); 89 | assert.equal(res.name, DEVICE_NAME_2); 90 | assert.equal(res.pushCallback, DEVICE_CALLBACK); 91 | }, 92 | assert.notOk 93 | ); 94 | }); 95 | 96 | test('#destroy', function () { 97 | 98 | return accountHelper.newVerifiedAccount() 99 | .then(function (account) { 100 | 101 | return respond(client.deviceRegister( 102 | account.signIn.sessionToken, 103 | DEVICE_NAME, 104 | DEVICE_TYPE, 105 | { 106 | deviceCallback: DEVICE_CALLBACK 107 | } 108 | ), RequestMocks.deviceRegister) 109 | 110 | .then(function (device) { 111 | 112 | return respond(client.deviceDestroy( 113 | account.signIn.sessionToken, 114 | device.id 115 | ), RequestMocks.deviceDestroy); 116 | }); 117 | }) 118 | .then( 119 | function(res) { 120 | assert.equal(Object.keys(res), 0); 121 | }, 122 | assert.notOk 123 | ); 124 | }); 125 | 126 | test('#list', function () { 127 | 128 | return accountHelper.newVerifiedAccount() 129 | .then(function (account) { 130 | 131 | return respond(client.deviceRegister( 132 | account.signIn.sessionToken, 133 | DEVICE_NAME, 134 | DEVICE_TYPE, 135 | { 136 | deviceCallback: DEVICE_CALLBACK 137 | } 138 | ), RequestMocks.deviceRegister) 139 | 140 | .then(function (device) { 141 | return respond(client.deviceList(account.signIn.sessionToken), 142 | RequestMocks.deviceList); 143 | }) 144 | 145 | .then(function (devices) { 146 | assert.equal(devices.length, 1); 147 | 148 | var device = devices[0]; 149 | assert.ok(device.id); 150 | assert.equal(device.name, DEVICE_NAME); 151 | assert.equal(device.pushCallback, DEVICE_CALLBACK); 152 | assert.equal(device.type, DEVICE_TYPE); 153 | }); 154 | }); 155 | }); 156 | 157 | }); 158 | } 159 | }); 160 | 161 | -------------------------------------------------------------------------------- /tests/lib/emails.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment' 9 | ], function (tdd, assert, Environment) { 10 | 11 | var user2; 12 | var user2Email; 13 | 14 | with (tdd) { 15 | suite('emails', function () { 16 | var accountHelper; 17 | var respond; 18 | var mail; 19 | var client; 20 | var RequestMocks; 21 | var account; 22 | 23 | beforeEach(function () { 24 | var env = new Environment(); 25 | accountHelper = env.accountHelper; 26 | respond = env.respond; 27 | mail = env.mail; 28 | client = env.client; 29 | RequestMocks = env.RequestMocks; 30 | 31 | user2 = 'anotherEmail' + new Date().getTime(); 32 | user2Email = user2 + '@restmail.net'; 33 | }); 34 | 35 | function recoveryEmailCreate() { 36 | return accountHelper.newVerifiedAccount() 37 | .then( 38 | function (res) { 39 | account = res; 40 | return respond(client.recoveryEmailCreate( 41 | account.signIn.sessionToken, 42 | user2Email 43 | ), RequestMocks.recoveryEmailCreate); 44 | }, 45 | handleError 46 | ); 47 | } 48 | 49 | function handleError(err) { 50 | console.log(err); 51 | assert.notOk(); 52 | } 53 | 54 | test('#recoveryEmailCreate', function () { 55 | return recoveryEmailCreate() 56 | .then( 57 | function (res) { 58 | assert.ok(res); 59 | }, 60 | handleError 61 | ); 62 | }); 63 | 64 | test('#recoveryEmails', function () { 65 | return recoveryEmailCreate() 66 | .then( 67 | function (res) { 68 | assert.ok(res); 69 | return respond(client.recoveryEmails( 70 | account.signIn.sessionToken 71 | ), RequestMocks.recoveryEmailsUnverified); 72 | }, 73 | handleError 74 | ) 75 | .then( 76 | function (res) { 77 | assert.ok(res); 78 | assert.equal(res.length, 2, 'returned two emails'); 79 | assert.equal(res[1].verified, false, 'returned not verified'); 80 | }, 81 | handleError 82 | ); 83 | }); 84 | 85 | test('#verifyCode', function () { 86 | return recoveryEmailCreate() 87 | .then( 88 | function (res) { 89 | assert.ok(res); 90 | 91 | return respond(mail.wait(user2, 1), RequestMocks.mailUnverifiedEmail); 92 | }, 93 | handleError 94 | ) 95 | .then(function (emails) { 96 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 97 | 98 | return respond(client.verifyCode(account.signIn.uid, code, {type: 'secondary'}), RequestMocks.verifyCode); 99 | }) 100 | .then( 101 | function (res) { 102 | assert.ok(res); 103 | 104 | return respond(client.recoveryEmails( 105 | account.signIn.sessionToken 106 | ), RequestMocks.recoveryEmailsVerified); 107 | }, 108 | handleError 109 | ) 110 | .then( 111 | function (res) { 112 | assert.ok(res); 113 | assert.equal(res.length, 2, 'returned one email'); 114 | assert.equal(res[1].verified, true, 'returned not verified'); 115 | }, 116 | handleError 117 | ); 118 | }); 119 | 120 | test('#recoveryEmailDestroy', function () { 121 | return recoveryEmailCreate() 122 | .then( 123 | function (res) { 124 | assert.ok(res); 125 | 126 | return respond(client.recoveryEmails( 127 | account.signIn.sessionToken 128 | ), RequestMocks.recoveryEmailsUnverified); 129 | }, 130 | handleError 131 | ) 132 | .then( 133 | function (res) { 134 | assert.ok(res); 135 | assert.equal(res.length, 2, 'returned two email'); 136 | assert.equal(res[1].verified, false, 'returned not verified'); 137 | 138 | return respond(client.recoveryEmailDestroy( 139 | account.signIn.sessionToken, 140 | user2Email 141 | ), RequestMocks.recoveryEmailDestroy); 142 | }, 143 | handleError 144 | ) 145 | .then( 146 | function (res) { 147 | assert.ok(res); 148 | 149 | return respond(client.recoveryEmails( 150 | account.signIn.sessionToken 151 | ), RequestMocks.recoveryEmails); 152 | }, 153 | handleError 154 | ) 155 | .then( 156 | function (res) { 157 | assert.ok(res); 158 | assert.equal(res.length, 1, 'returned one email'); 159 | }, 160 | handleError 161 | ); 162 | }); 163 | 164 | test('#recoveryEmailSetPrimaryEmail', function () { 165 | return recoveryEmailCreate() 166 | .then( 167 | function (res) { 168 | assert.ok(res); 169 | 170 | return respond(mail.wait(user2, 1), RequestMocks.mailUnverifiedEmail); 171 | }, 172 | handleError 173 | ) 174 | .then(function (emails) { 175 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 176 | 177 | return respond(client.verifyCode(account.signIn.uid, code, {type: 'secondary'}), RequestMocks.verifyCode); 178 | }) 179 | .then( 180 | function (res) { 181 | assert.ok(res); 182 | 183 | return respond(client.recoveryEmailSetPrimaryEmail( 184 | account.signIn.sessionToken, 185 | user2Email 186 | ), RequestMocks.recoveryEmailSetPrimaryEmail); 187 | }, 188 | handleError 189 | ) 190 | .then( 191 | function (res) { 192 | assert.ok(res); 193 | 194 | return respond(client.recoveryEmails( 195 | account.signIn.sessionToken 196 | ), RequestMocks.recoveryEmailsSetPrimaryVerified); 197 | }, 198 | handleError 199 | ) 200 | .then( 201 | function (res) { 202 | assert.ok(res); 203 | assert.equal(res.length, 2, 'returned two emails'); 204 | 205 | assert.equal(true, res[0].email.indexOf('anotherEmail') > -1, 'returned correct primary email'); 206 | assert.equal(res[0].verified, true, 'returned verified'); 207 | assert.equal(res[0].isPrimary, true, 'returned isPrimary true'); 208 | 209 | assert.equal(res[1].verified, true, 'returned verified'); 210 | assert.equal(res[1].isPrimary, false, 'returned isPrimary false'); 211 | }, 212 | handleError 213 | ); 214 | }); 215 | }); 216 | } 217 | }); 218 | 219 | -------------------------------------------------------------------------------- /tests/lib/errors.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment' 9 | ], function (tdd, assert, Environment) { 10 | 11 | with (tdd) { 12 | suite('errors', function () { 13 | var accountHelper; 14 | var respond; 15 | var client; 16 | var ErrorMocks; 17 | 18 | beforeEach(function () { 19 | var env = new Environment(); 20 | accountHelper = env.accountHelper; 21 | respond = env.respond; 22 | client = env.client; 23 | ErrorMocks = env.ErrorMocks; 24 | }); 25 | 26 | test('#accountUnverified', function () { 27 | 28 | return accountHelper.newUnverifiedAccount() 29 | .then(function (account) { 30 | var pk = {algorithm: 'RS', n: 'x', e: 'y'}; 31 | var duration = 1000; 32 | 33 | return respond(client.certificateSign(account.signIn.sessionToken, pk, duration), ErrorMocks.accountUnverified); 34 | }) 35 | .then( 36 | function () { 37 | assert.fail(); 38 | }, 39 | function(error) { 40 | assert.equal(error.code, 400); 41 | assert.equal(error.errno, 104); 42 | } 43 | ); 44 | }); 45 | 46 | test('#invalidVerificationCode', function () { 47 | 48 | return accountHelper.newUnverifiedAccount() 49 | .then(function (account) { 50 | return respond(client.verifyCode(account.signUp.uid, 'eb531a64deb628b2baeaceaa8762abf0'), ErrorMocks.invalidVerification); 51 | }) 52 | .then( 53 | function () { 54 | assert.fail(); 55 | }, 56 | function(error) { 57 | assert.equal(error.code, 400); 58 | assert.equal(error.errno, 105); 59 | } 60 | ); 61 | }); 62 | }); 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /tests/lib/hawkCredentials.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'node_modules/sjcl/sjcl', 9 | 'client/lib/hawkCredentials' 10 | ], function (tdd, assert, sjcl, hawkCredentials) { 11 | with (tdd) { 12 | suite('hawkCredentials', function () { 13 | test('#client derive hawk credentials', function () { 14 | var context = 'sessionToken'; 15 | var sessionToken = 'a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf'; 16 | 17 | return hawkCredentials(sessionToken, context, 3 * 32) 18 | .then( 19 | function (result) { 20 | var hmacKey = sjcl.codec.hex.fromBits(result.key); 21 | 22 | assert.equal(hmacKey, '9d8f22998ee7f5798b887042466b72d53e56ab0c094388bf65831f702d2febc0', '== hmacKey is equal'); 23 | assert.equal(result.id, 'c0a29dcf46174973da1378696e4c82ae10f723cf4f4d9f75e39f4ae3851595ab', '== id is equal'); 24 | }, 25 | assert.notOk 26 | ); 27 | }); 28 | }); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /tests/lib/headerLang.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment' 9 | ], function (tdd, assert, Environment) { 10 | 11 | with (tdd) { 12 | suite('headerLanguage', function () { 13 | var accountHelper; 14 | var respond; 15 | var client; 16 | var mail; 17 | var RequestMocks; 18 | 19 | beforeEach(function () { 20 | var env = new Environment(); 21 | accountHelper = env.accountHelper; 22 | respond = env.respond; 23 | RequestMocks = env.RequestMocks; 24 | client = env.client; 25 | mail = env.mail; 26 | }); 27 | 28 | test('#signUp', function () { 29 | var user = 'test' + new Date().getTime(); 30 | var email = user + '@restmail.net'; 31 | var password = 'iliketurtles'; 32 | var opts = { 33 | lang: 'zh-cn;' 34 | }; 35 | 36 | return respond(client.signUp(email, password, opts), RequestMocks.signUp) 37 | .then(function (res) { 38 | assert.ok(res.uid); 39 | return respond(mail.wait(user), RequestMocks.mailSignUpLang); 40 | }) 41 | .then( 42 | function (emails) { 43 | assert.property(emails[0], 'headers'); 44 | assert.equal(emails[0].headers['content-language'], 'zh-CN'); 45 | }, 46 | assert.notOk 47 | ); 48 | }); 49 | 50 | test('#passwordForgotSendCode', function () { 51 | var account; 52 | var passwordForgotToken; 53 | var opts = { 54 | lang: 'zh-CN', 55 | service: 'sync' 56 | }; 57 | 58 | return accountHelper.newUnverifiedAccount() 59 | .then(function (acc) { 60 | account = acc; 61 | 62 | return respond(client.passwordForgotSendCode(account.input.email, opts), RequestMocks.passwordForgotSendCode); 63 | }) 64 | .then(function (result) { 65 | passwordForgotToken = result.passwordForgotToken; 66 | assert.ok(passwordForgotToken, 'passwordForgotToken is returned'); 67 | 68 | return respond(mail.wait(account.input.user, 3), RequestMocks.resetMailLang); 69 | }) 70 | .then( 71 | function (emails) { 72 | assert.property(emails[2], 'headers'); 73 | assert.equal(emails[2].headers['content-language'], 'zh-CN'); 74 | }, 75 | assert.notOk 76 | ); 77 | }); 78 | 79 | test('#recoveryEmailResendCode', function () { 80 | var user; 81 | var opts = { 82 | lang: 'zh-CN' 83 | }; 84 | 85 | return accountHelper.newUnverifiedAccount() 86 | .then(function (account) { 87 | user = account.input.user; 88 | 89 | return respond(client.recoveryEmailResendCode(account.signIn.sessionToken, opts), RequestMocks.recoveryEmailResendCode); 90 | }) 91 | .then( 92 | function(res) { 93 | assert.ok(res); 94 | 95 | return respond(mail.wait(user, 3), RequestMocks.resetMailLang); 96 | }) 97 | .then( 98 | function (emails) { 99 | assert.property(emails[2], 'headers'); 100 | assert.equal(emails[2].headers['content-language'], 'zh-CN'); 101 | }, 102 | assert.notOk 103 | ); 104 | }); 105 | 106 | }); 107 | } 108 | }); 109 | -------------------------------------------------------------------------------- /tests/lib/hkdf.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'node_modules/sjcl/sjcl', 9 | 'client/lib/hkdf' 10 | ], function (tdd, assert, sjcl, hkdf) { 11 | with (tdd) { 12 | 13 | // test vectors from RFC5869 14 | suite('hkdf', function () { 15 | 16 | test('#vector 1', function () { 17 | 18 | var ikm = sjcl.codec.hex.toBits('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'); 19 | var salt = sjcl.codec.hex.toBits('000102030405060708090a0b0c'); 20 | var info = sjcl.codec.hex.toBits('f0f1f2f3f4f5f6f7f8f9'); 21 | 22 | return hkdf(ikm, info, salt, 42) 23 | .then( 24 | function (result) { 25 | assert.equal(sjcl.codec.hex.fromBits(result).length, 84); 26 | assert.equal(sjcl.codec.hex.fromBits(result), '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865'); 27 | }, 28 | assert.notOk 29 | ); 30 | }); 31 | 32 | test('#vector 2', function () { 33 | 34 | var ikm = sjcl.codec.hex.toBits('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'); 35 | var salt = sjcl.codec.hex.toBits(''); 36 | var info = sjcl.codec.hex.toBits(''); 37 | 38 | return hkdf(ikm, info, salt, 42) 39 | .then( 40 | function (result) { 41 | assert.equal(sjcl.codec.hex.fromBits(result).length, 84); 42 | assert.equal(sjcl.codec.hex.fromBits(result), '8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8'); 43 | }, 44 | assert.notOk 45 | ); 46 | }); 47 | 48 | test('#vector 3', function () { 49 | 50 | var ikm = sjcl.codec.hex.toBits('4a9cbe5ae7190a7bb7cc54d5d84f5e4ba743904f8a764933b72f10260067375a'); 51 | var salt = sjcl.codec.hex.toBits(''); 52 | var info = sjcl.codec.utf8String.toBits('identity.mozilla.com/picl/v1/keyFetchToken'); 53 | 54 | return hkdf(ikm, info, salt, 3 * 32) 55 | .then( 56 | function (result) { 57 | assert.equal(sjcl.codec.hex.fromBits(result), 'f4df04ffb79db35e94e4881719a6f145f9206e8efea17fc9f02a5ce09cbfac1e829a935f34111d75e0d16b7aa178e2766759eedb6f623c0babd2abcfea82bc12af75f6aa543a8ba7e0a029f87c785c4af0ad03889f7437f735b5256a88fc73fd'); 58 | }, 59 | assert.notOk 60 | ); 61 | }); 62 | 63 | test('#vector 4', function () { 64 | 65 | var ikm = sjcl.codec.hex.toBits('ba0a107dab60f3b065ff7a642d14fe824fbd71bc5c99087e9e172a1abd1634f1'); 66 | var salt = sjcl.codec.hex.toBits(''); 67 | var info = sjcl.codec.utf8String.toBits('identity.mozilla.com/picl/v1/account/keys'); 68 | 69 | return hkdf(ikm, info, salt, 3 * 32) 70 | .then( 71 | function (result) { 72 | assert.equal(sjcl.codec.hex.fromBits(result), '17ab463653a94c9a6419b48781930edefe500395e3b4e7879a2be1599975702285de16c3218a126404668bf9b7acfb6ce2b7e03c8889047ba48b8b854c6d8beb3ae100e145ca6d69cb519a872a83af788771954455716143bc08225ea8644d85'); 73 | }, 74 | assert.notOk 75 | ); 76 | }); 77 | 78 | }); 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /tests/lib/init.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'client/FxAccountClient' 9 | ], function (tdd, assert, FxAccountClient) { 10 | 11 | with (tdd) { 12 | suite('init', function () { 13 | test('#should error if no options set', function () { 14 | try { 15 | void new FxAccountClient(); 16 | } catch (e) { 17 | assert.isDefined(e.message); 18 | } 19 | }); 20 | 21 | test('#should catch undefined parameters for the url', function () { 22 | try { 23 | void new FxAccountClient(undefined, {}); 24 | } catch (e) { 25 | assert.isDefined(e.message); 26 | } 27 | }); 28 | }); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /tests/lib/metricsContext.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'client/lib/metricsContext' 9 | ], function (t, assert, metricsContext) { 10 | 'use strict'; 11 | 12 | t.suite('metricsContext', function () { 13 | t.test('interface is correct', function () { 14 | assert.isObject(metricsContext); 15 | assert.lengthOf(Object.keys(metricsContext), 1); 16 | assert.isFunction(metricsContext.marshall); 17 | }); 18 | 19 | t.test('marshall returns correct data', function () { 20 | var input = { 21 | context: 'fx_desktop_v3', 22 | deviceId: '0123456789abcdef0123456789abcdef', 23 | entrypoint: 'menupanel', 24 | flowBeginTime: 1479815991573, 25 | flowId: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 26 | migration: 'sync11', 27 | service: 'sync', 28 | utmCampaign: 'foo', 29 | utmContent: 'bar', 30 | utmMedium: 'baz', 31 | utmSource: 'qux', 32 | utmTerm: 'wibble' 33 | }; 34 | 35 | assert.deepEqual(metricsContext.marshall(input), { 36 | deviceId: input.deviceId, 37 | entrypoint: 'menupanel', 38 | flowBeginTime: input.flowBeginTime, 39 | flowId: input.flowId, 40 | utmCampaign: 'foo', 41 | utmContent: 'bar', 42 | utmMedium: 'baz', 43 | utmSource: 'qux', 44 | utmTerm: 'wibble' 45 | }); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/lib/misc.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment' 9 | ], function (tdd, assert, Environment) { 10 | 11 | with (tdd) { 12 | suite('misc', function () { 13 | var respond; 14 | var client; 15 | var RequestMocks; 16 | 17 | beforeEach(function () { 18 | var env = new Environment(); 19 | respond = env.respond; 20 | client = env.client; 21 | RequestMocks = env.RequestMocks; 22 | }); 23 | 24 | test('#getRandomBytes', function () { 25 | 26 | return respond(client.getRandomBytes(), RequestMocks.getRandomBytes) 27 | .then( 28 | function(res) { 29 | assert.property(res, 'data'); 30 | }, 31 | assert.notOk 32 | ); 33 | }); 34 | 35 | test('_required', function () { 36 | assert.doesNotThrow(function () { 37 | client._required(true, 'true_boolean'); 38 | client._required(false, 'false_boolean'); 39 | client._required('string', 'string'); 40 | client._required({ hasValue: true }, 'object_with_value'); 41 | client._required(1, 'number'); 42 | client._required(0, 'zero'); 43 | }); 44 | 45 | assert.throws(function () { 46 | client._required('', 'empty_string'); 47 | }); 48 | 49 | assert.throws(function () { 50 | client._required({}, 'empty_object'); 51 | }); 52 | 53 | assert.throws(function () { 54 | client._required(null, 'null'); 55 | }); 56 | }); 57 | }); 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /tests/lib/passwordChange.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'sjcl', 9 | 'client/lib/credentials', 10 | 'tests/addons/environment' 11 | ], function (tdd, assert, sjcl, credentials, Environment) { 12 | 13 | with (tdd) { 14 | suite('passwordChange', function () { 15 | var accountHelper; 16 | var respond; 17 | var mail; 18 | var client; 19 | var RequestMocks; 20 | var ErrorMocks; 21 | var requests; 22 | 23 | beforeEach(function () { 24 | var env = new Environment(); 25 | accountHelper = env.accountHelper; 26 | respond = env.respond; 27 | mail = env.mail; 28 | client = env.client; 29 | RequestMocks = env.RequestMocks; 30 | ErrorMocks = env.ErrorMocks; 31 | requests = env.requests; 32 | }); 33 | 34 | test('#basic', function () { 35 | var user = 'test7' + new Date().getTime(); 36 | var email = user + '@restmail.net'; 37 | var password = 'iliketurtles'; 38 | var newPassword = 'ilikefoxes'; 39 | var kB; 40 | var newUnwrapBKey; 41 | var oldCreds; 42 | var uid; 43 | var account; 44 | 45 | // newUnwrapBKey from email+newpassword. The submitted newWrapKB 46 | // should equal (kB XOR newUnwrapBKey). This way we don't need to 47 | // know what the server will return for wrapKB: handy, since 48 | // sometimes we're using a mock (with a fixed response), but 49 | // sometimes we're using a real server (which randomly creates 50 | // wrapKB) 51 | 52 | return credentials.setup(email, newPassword) 53 | .then(function (newCreds) { 54 | newUnwrapBKey = sjcl.codec.hex.fromBits(newCreds.unwrapBKey); 55 | return respond(client.signUp(email, password), RequestMocks.signUp); 56 | }) 57 | .then(function (result) { 58 | uid = result.uid; 59 | 60 | return respond(mail.wait(user), RequestMocks.mail); 61 | }) 62 | .then(function (emails) { 63 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 64 | 65 | return respond(client.verifyCode(uid, code), RequestMocks.verifyCode); 66 | }) 67 | .then(function() { 68 | return respond(client.signIn(email, password, {keys: true}), RequestMocks.signInWithKeys); 69 | }) 70 | .then(function(result) { 71 | account = result; 72 | }) 73 | .then(function () { 74 | return respond(client.accountKeys(account.keyFetchToken, account.unwrapBKey), RequestMocks.accountKeys); 75 | }) 76 | .then(function(keys) { 77 | kB = keys.kB; 78 | }) 79 | .then(function () { 80 | return respond(client._passwordChangeStart(email, password), RequestMocks.passwordChangeStart); 81 | }) 82 | .then(function (credentials) { 83 | oldCreds = credentials; 84 | assert.equal(credentials.emailToHashWith, email); 85 | 86 | return respond(client._passwordChangeKeys(oldCreds), RequestMocks.accountKeys); 87 | }) 88 | .then(function (keys) { 89 | 90 | return respond(client._passwordChangeFinish(email, newPassword, oldCreds, keys, { keys: false }), RequestMocks.passwordChangeFinish); 91 | }) 92 | .then(function (result) { 93 | // currently only available for mocked requests (issue #103) 94 | if (requests) { 95 | var req = requests[requests.length - 1]; 96 | var args = JSON.parse(req.requestBody); 97 | var expectedNewWrapKB = sjcl.codec.hex.fromBits( 98 | credentials.xor(sjcl.codec.hex.toBits(kB), 99 | sjcl.codec.hex.toBits(newUnwrapBKey))); 100 | assert.equal(args.wrapKb, expectedNewWrapKB); 101 | } 102 | assert.notProperty(result, 'keyFetchToken'); 103 | 104 | return respond(client.signIn(email, newPassword), RequestMocks.signIn); 105 | }) 106 | .then( 107 | function (res) { 108 | assert.property(res, 'sessionToken'); 109 | }, 110 | function (err) { 111 | throw err; 112 | } 113 | ); 114 | }); 115 | 116 | test('#keys', function () { 117 | var user = 'test7' + new Date().getTime(); 118 | var email = user + '@restmail.net'; 119 | var password = 'iliketurtles'; 120 | var newPassword = 'ilikefoxes'; 121 | var kB; 122 | var newUnwrapBKey; 123 | var oldCreds; 124 | var sessionToken; 125 | var uid; 126 | var account; 127 | 128 | // newUnwrapBKey from email+newpassword. The submitted newWrapKB 129 | // should equal (kB XOR newUnwrapBKey). This way we don't need to 130 | // know what the server will return for wrapKB: handy, since 131 | // sometimes we're using a mock (with a fixed response), but 132 | // sometimes we're using a real server (which randomly creates 133 | // wrapKB) 134 | 135 | return credentials.setup(email, newPassword) 136 | .then(function (newCreds) { 137 | newUnwrapBKey = sjcl.codec.hex.fromBits(newCreds.unwrapBKey); 138 | return respond(client.signUp(email, password), RequestMocks.signUp); 139 | }) 140 | .then(function (result) { 141 | uid = result.uid; 142 | 143 | return respond(mail.wait(user), RequestMocks.mail); 144 | }) 145 | .then(function (emails) { 146 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 147 | 148 | return respond(client.verifyCode(uid, code), RequestMocks.verifyCode); 149 | }) 150 | .then(function() { 151 | return respond(client.signIn(email, password, {keys: true}), RequestMocks.signInWithKeys); 152 | }) 153 | .then(function(result) { 154 | sessionToken = result.sessionToken; 155 | account = result; 156 | }) 157 | .then(function () { 158 | 159 | return respond(client.accountKeys(account.keyFetchToken, account.unwrapBKey), RequestMocks.accountKeys); 160 | }) 161 | .then(function(keys) { 162 | kB = keys.kB; 163 | }) 164 | .then(function () { 165 | return respond(client._passwordChangeStart(email, password), RequestMocks.passwordChangeStart); 166 | }) 167 | .then(function (credentials) { 168 | oldCreds = credentials; 169 | assert.equal(credentials.emailToHashWith, email); 170 | 171 | return respond(client._passwordChangeKeys(oldCreds), RequestMocks.accountKeys); 172 | }) 173 | .then(function (keys) { 174 | 175 | return respond(client._passwordChangeFinish(email, newPassword, oldCreds, keys, { keys: true, sessionToken: sessionToken }), RequestMocks.passwordChangeFinishKeys); 176 | }) 177 | .then(function (result) { 178 | // currently only available for mocked requests (issue #103) 179 | if (requests) { 180 | var req = requests[requests.length - 1]; 181 | var args = JSON.parse(req.requestBody); 182 | var expectedNewWrapKB = sjcl.codec.hex.fromBits( 183 | credentials.xor(sjcl.codec.hex.toBits(kB), 184 | sjcl.codec.hex.toBits(newUnwrapBKey))); 185 | assert.equal(args.wrapKb, expectedNewWrapKB); 186 | } 187 | assert.property(result, 'sessionToken'); 188 | assert.property(result, 'keyFetchToken'); 189 | assert.property(result, 'unwrapBKey'); 190 | assert.isTrue(result.verified); 191 | 192 | return respond(client.signIn(email, newPassword), RequestMocks.signIn); 193 | }) 194 | .then( 195 | function (res) { 196 | assert.property(res, 'sessionToken'); 197 | }, 198 | function (err) { 199 | throw err; 200 | } 201 | ); 202 | }); 203 | 204 | test('#with incorrect case', function () { 205 | var newPassword = 'ilikefoxes'; 206 | var account; 207 | var oldCreds; 208 | 209 | return accountHelper.newVerifiedAccount() 210 | .then(function (acc) { 211 | account = acc; 212 | var incorrectCaseEmail = account.input.email.charAt(0).toUpperCase() + account.input.email.slice(1); 213 | 214 | return respond(client._passwordChangeStart(incorrectCaseEmail, account.input.password), RequestMocks.passwordChangeStart); 215 | }) 216 | .then(function (credentials) { 217 | 218 | oldCreds = credentials; 219 | 220 | return respond(client._passwordChangeKeys(oldCreds), RequestMocks.accountKeys); 221 | }) 222 | .then(function (keys) { 223 | 224 | return respond(client._passwordChangeFinish(account.input.email, newPassword, oldCreds, keys), RequestMocks.passwordChangeFinish); 225 | }) 226 | .then(function (result) { 227 | assert.ok(result, '{}'); 228 | 229 | return respond(client.signIn(account.input.email, newPassword), RequestMocks.signIn); 230 | }) 231 | .then( 232 | function (res) { 233 | assert.property(res, 'sessionToken'); 234 | }, 235 | function (err) { 236 | throw err; 237 | } 238 | ); 239 | }); 240 | 241 | test('#with incorrect case with skipCaseError', function () { 242 | var account; 243 | 244 | return accountHelper.newVerifiedAccount() 245 | .then(function (acc) { 246 | account = acc; 247 | var incorrectCaseEmail = account.input.email.charAt(0).toUpperCase() + account.input.email.slice(1); 248 | 249 | return respond(client._passwordChangeStart(incorrectCaseEmail, account.input.password, {skipCaseError: true}), 250 | ErrorMocks.incorrectEmailCase); 251 | }) 252 | .then( 253 | function () { 254 | assert.fail(); 255 | }, 256 | function (res) { 257 | assert.equal(res.code, 400); 258 | assert.equal(res.errno, 120); 259 | } 260 | ); 261 | }); 262 | 263 | /** 264 | * Changing the Password failure 265 | */ 266 | test('#changeFailure', function () { 267 | var user = 'test8' + new Date().getTime(); 268 | var email = user + '@restmail.net'; 269 | var password = 'iliketurtles'; 270 | var newPassword = 'ilikefoxes'; 271 | var wrongPassword = '12345678'; 272 | var uid; 273 | var oldCreds; 274 | 275 | return respond(client.signUp(email, password), RequestMocks.signUp) 276 | .then(function (result) { 277 | uid = result.uid; 278 | 279 | return respond(mail.wait(user), RequestMocks.mail); 280 | }) 281 | .then(function (emails) { 282 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 283 | 284 | return respond(client.verifyCode(uid, code), RequestMocks.verifyCode); 285 | }) 286 | .then(function () { 287 | return respond(client._passwordChangeStart(email, password), RequestMocks.passwordChangeStart); 288 | }) 289 | .then(function (credentials) { 290 | oldCreds = credentials; 291 | assert.equal(credentials.emailToHashWith, email); 292 | return respond(client._passwordChangeKeys(oldCreds), RequestMocks.accountKeys); 293 | }) 294 | .then(function (keys) { 295 | 296 | return respond(client._passwordChangeFinish(email, newPassword, oldCreds, keys), RequestMocks.passwordChangeFinish); 297 | }) 298 | .then(function (result) { 299 | assert.ok(result); 300 | 301 | return respond(client.signIn(email, wrongPassword), ErrorMocks.accountIncorrectPassword); 302 | }) 303 | .then( 304 | function () { 305 | assert.fail(); 306 | }, 307 | function (error) { 308 | assert.ok(error); 309 | assert.equal(error.message, 'Incorrect password', '== Password is incorrect'); 310 | assert.equal(error.code, 400, '== Correct status code'); 311 | } 312 | ); 313 | }); 314 | }); 315 | } 316 | }); 317 | -------------------------------------------------------------------------------- /tests/lib/push-constants.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([], function () { 6 | return { 7 | DEVICE_CALLBACK: 'https://updates.push.services.mozilla.com/update/abcdef01234567890abcdefabcdef01234567890abcdef', 8 | DEVICE_ID: '0f7aa00356e5416e82b3bef7bc409eef', 9 | DEVICE_NAME: 'My Phone', 10 | DEVICE_NAME_2: 'My Android Phone', 11 | DEVICE_PUBLIC_KEY: 'BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc', 12 | DEVICE_AUTH_KEY: 'GSsIiaD2Mr83iPqwFNK4rw', 13 | DEVICE_TYPE: 'mobile' 14 | }; 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /tests/lib/recoveryCodes.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment', 9 | 'tests/addons/sinon', 10 | 'node_modules/otplib/otplib-browser' 11 | ], function (tdd, assert, Environment, sinon, otplib) { 12 | 13 | with (tdd) { 14 | suite('recovery codes', function () { 15 | var account; 16 | var accountHelper; 17 | var respond; 18 | var client; 19 | var RequestMocks; 20 | var env; 21 | var xhr; 22 | var xhrOpen; 23 | var xhrSend; 24 | var recoveryCodes; 25 | var metricsContext; 26 | 27 | beforeEach(function () { 28 | env = new Environment(); 29 | accountHelper = env.accountHelper; 30 | respond = env.respond; 31 | client = env.client; 32 | RequestMocks = env.RequestMocks; 33 | metricsContext = { 34 | flowBeginTime: Date.now(), 35 | flowId: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' 36 | }; 37 | 38 | return accountHelper.newVerifiedAccount() 39 | .then(function (newAccount) { 40 | account = newAccount; 41 | return respond(client.createTotpToken(account.signIn.sessionToken), RequestMocks.createTotpToken); 42 | }) 43 | .then(function (res) { 44 | assert.ok(res.qrCodeUrl, 'should return QR code data encoded url'); 45 | assert.ok(res.secret, 'should return secret that is encoded in url'); 46 | 47 | var authenticator = new otplib.authenticator.Authenticator(); 48 | authenticator.options = otplib.authenticator.options; 49 | 50 | var code = authenticator.generate(res.secret); 51 | return respond(client.verifyTotpCode(account.signIn.sessionToken, code), RequestMocks.verifyTotpCodeTrueEnableToken); 52 | }) 53 | .then(function (res) { 54 | assert.equal(res.recoveryCodes.length, 8, 'should return recovery codes'); 55 | recoveryCodes = res.recoveryCodes; 56 | 57 | xhr = env.xhr; 58 | xhrOpen = sinon.spy(xhr.prototype, 'open'); 59 | xhrSend = sinon.spy(xhr.prototype, 'send'); 60 | }); 61 | }); 62 | 63 | afterEach(function () { 64 | xhrOpen.restore(); 65 | xhrSend.restore(); 66 | }); 67 | 68 | test('#consumeRecoveryCode - fails for invalid code', function () { 69 | return respond(client.consumeRecoveryCode(account.signIn.sessionToken, '00000000'), RequestMocks.consumeRecoveryCodeInvalidCode) 70 | .then(assert.fail, function (err) { 71 | assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); 72 | assert.include(xhrOpen.args[0][1], '/session/verify/recoveryCode', 'path is correct'); 73 | assert.equal(err.errno, 156, 'invalid recovery code errno'); 74 | }); 75 | }); 76 | 77 | test('#consumeRecoveryCode - consumes valid code', function () { 78 | var code = recoveryCodes[0]; 79 | return respond(client.consumeRecoveryCode(account.signIn.sessionToken, code, {metricsContext: metricsContext}), RequestMocks.consumeRecoveryCodeSuccess) 80 | .then(function (res) { 81 | assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); 82 | assert.include(xhrOpen.args[0][1], '/session/verify/recoveryCode', 'path is correct'); 83 | var sentData = JSON.parse(xhrSend.args[0][0]); 84 | assert.lengthOf(Object.keys(sentData), 1); 85 | assert.equal(sentData.code, code, 'code is correct'); 86 | 87 | assert.equal(res.remaining, 7, 'correct remaining recovery codes'); 88 | }); 89 | }); 90 | 91 | test('#replaceRecoveryCodes - replaces current recovery codes', function () { 92 | return respond(client.replaceRecoveryCodes(account.signIn.sessionToken), RequestMocks.replaceRecoveryCodesSuccessNew) 93 | .then(function (res) { 94 | assert.equal(xhrOpen.args[0][0], 'GET', 'method is correct'); 95 | assert.include(xhrOpen.args[0][1], '/recoveryCodes', 'path is correct'); 96 | 97 | assert.equal(res.recoveryCodes.length, 8, 'should return recovery codes'); 98 | assert.notDeepEqual(res.recoveryCodes, recoveryCodes, 'should not be the same codes'); 99 | }); 100 | }); 101 | }); 102 | } 103 | }); 104 | -------------------------------------------------------------------------------- /tests/lib/recoveryEmail.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment' 9 | ], function (tdd, assert, Environment) { 10 | 11 | with (tdd) { 12 | suite('recoveryEmail', function () { 13 | var accountHelper; 14 | var respond; 15 | var mail; 16 | var client; 17 | var RequestMocks; 18 | 19 | beforeEach(function () { 20 | var env = new Environment(); 21 | accountHelper = env.accountHelper; 22 | respond = env.respond; 23 | mail = env.mail; 24 | client = env.client; 25 | RequestMocks = env.RequestMocks; 26 | }); 27 | 28 | test('#recoveryEmail - recoveryEmailResendCode', function () { 29 | var user; 30 | 31 | return accountHelper.newUnverifiedAccount() 32 | .then(function (account) { 33 | user = account.input.user; 34 | 35 | return respond(client.recoveryEmailResendCode(account.signIn.sessionToken), RequestMocks.recoveryEmailResendCode); 36 | }) 37 | .then( 38 | function(res) { 39 | assert.ok(res); 40 | 41 | return respond(mail.wait(user, 3), RequestMocks.resetMailrecoveryEmailResendCode); 42 | }) 43 | .then( 44 | function (emails) { 45 | // second email, the code is resent. 46 | var code = emails[2].html.match(/code=([A-Za-z0-9]+)/)[1]; 47 | assert.ok(code, 'code is returned'); 48 | }, 49 | assert.notOk 50 | ); 51 | }); 52 | 53 | test('#recoveryEmailResendCode with service, redirectTo, type, and resume', function () { 54 | var user; 55 | var opts = { 56 | service: 'sync', 57 | redirectTo: 'https://sync.127.0.0.1/after_reset', 58 | resume: 'resumejwt', 59 | type: 'upgradeSession' 60 | }; 61 | 62 | return accountHelper.newUnverifiedAccount() 63 | .then(function (account) { 64 | user = account.input.user; 65 | 66 | return respond(client.recoveryEmailResendCode(account.signIn.sessionToken, opts), RequestMocks.recoveryEmailResendCode); 67 | }) 68 | .then( 69 | function(res) { 70 | assert.ok(res); 71 | 72 | return respond(mail.wait(user, 3), RequestMocks.resetMailWithServiceAndRedirectNoSignup); 73 | }) 74 | .then( 75 | function (emails) { 76 | // second email, the code is resent. 77 | var code = emails[2].html.match(/code=([A-Za-z0-9]+)/); 78 | assert.ok(code, 'code found'); 79 | var service = emails[2].html.match(/service=([A-Za-z0-9]+)/); 80 | assert.ok(service, 'service found'); 81 | var redirectTo = emails[2].html.match(/redirectTo=([A-Za-z0-9]+)/); 82 | assert.ok(redirectTo, 'redirectTo found'); 83 | var resume = emails[2].html.match(/resume=([A-Za-z0-9]+)/); 84 | assert.ok(resume, 'resume found'); 85 | 86 | assert.ok(code[1], 'code is returned'); 87 | assert.equal(service[1], 'sync', 'service is returned'); 88 | assert.equal(redirectTo[1], 'https', 'redirectTo is returned'); 89 | assert.equal(resume[1], 'resumejwt', 'resume is returned'); 90 | }, 91 | assert.notOk 92 | ); 93 | }); 94 | 95 | }); 96 | } 97 | }); 98 | -------------------------------------------------------------------------------- /tests/lib/recoveryKeys.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment', 9 | 'tests/addons/sinon' 10 | ], function (tdd, assert, Environment, sinon) { 11 | 12 | with (tdd) { 13 | suite('recovery key', function () { 14 | var account; 15 | var accountHelper; 16 | var respond; 17 | var client; 18 | var email; 19 | var RequestMocks; 20 | var env; 21 | var xhr; 22 | var xhrOpen; 23 | var xhrSend; 24 | var keys; 25 | var passwordForgotToken; 26 | var accountResetToken; 27 | var mail; 28 | var newPassword = '~(_8^(I)'; 29 | var recoveryKeyId = 'edc243a821582ee9e979583be9989ee7'; 30 | var bundle = 'eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIiwia2lkIjoiODE4NDIwZjBkYTU4ZDIwZjZhZTR' + 31 | 'kMmM5YmVhYjkyNTEifQ..D29EXHp8ubLvftaZ.xHJd2Nl2Uco2RyywYPLkUU7fHpgO2FztY12Zjpq1ffiyLRIUcQVfmiNC6aMiHB' + 32 | 'l7Hp-lXEbb5mR1uXHrTH9iRXEBVaAfyf9KEAWOukWGVSH8EaOkr7cfu2Yr0K93Ec8glsssjiKp8NGB8VKTUJ-lmBv2cIrG68V4eTUVDo' + 33 | 'DhMbXhrF-Mv4JNeh338pPeatTnyg.Ow2bhEYWxzxfSPMxVwKmSA'; 34 | 35 | beforeEach(function () { 36 | env = new Environment(); 37 | accountHelper = env.accountHelper; 38 | respond = env.respond; 39 | client = env.client; 40 | RequestMocks = env.RequestMocks; 41 | mail = env.mail; 42 | 43 | return accountHelper.newVerifiedAccount() 44 | .then(function (newAccount) { 45 | account = newAccount; 46 | email = account.input.email; 47 | return respond(client.accountKeys(account.signIn.keyFetchToken, account.signIn.unwrapBKey), RequestMocks.accountKeys); 48 | }) 49 | .then(function (result) { 50 | keys = result; 51 | xhr = env.xhr; 52 | xhrOpen = sinon.spy(xhr.prototype, 'open'); 53 | xhrSend = sinon.spy(xhr.prototype, 'send'); 54 | }); 55 | }); 56 | 57 | afterEach(function () { 58 | xhrOpen.restore(); 59 | xhrSend.restore(); 60 | }); 61 | 62 | test('#can create and get a recovery key that can be used to reset an account', function () { 63 | return respond(client.createRecoveryKey(account.signIn.sessionToken, recoveryKeyId, bundle), RequestMocks.createRecoveryKey) 64 | .then(function (res) { 65 | assert.ok(res); 66 | return respond(client.passwordForgotSendCode(email), RequestMocks.passwordForgotSendCode); 67 | }) 68 | .then(function (result) { 69 | passwordForgotToken = result.passwordForgotToken; 70 | assert.ok(passwordForgotToken, 'passwordForgotToken is returned'); 71 | 72 | return respond(mail.wait(account.input.user, 4), RequestMocks.resetMailpasswordForgotRecoveryKey); 73 | }) 74 | .then(function (emails) { 75 | var code = emails[3].html.match(/code=([A-Za-z0-9]+)/)[1]; 76 | assert.ok(code, 'code is returned: ' + code); 77 | 78 | return respond(client.passwordForgotVerifyCode(code, passwordForgotToken, {accountResetWithRecoveryKey: true}), RequestMocks.passwordForgotVerifyCode); 79 | }) 80 | .then(function(result) { 81 | accountResetToken = result.accountResetToken; 82 | assert.ok(accountResetToken, 'accountResetToken is returned'); 83 | 84 | assert.equal(xhrOpen.args[3][0], 'POST', 'method is correct'); 85 | assert.include(xhrOpen.args[3][1], '/password/forgot/verify_code', 'path is correct'); 86 | var sentData = JSON.parse(xhrSend.args[3][0]); 87 | assert.equal(Object.keys(sentData).length, 2); 88 | assert.equal(sentData.accountResetWithRecoveryKey, true, 'param set'); 89 | return respond(client.getRecoveryKey(accountResetToken, recoveryKeyId), RequestMocks.getRecoveryKey); 90 | }) 91 | .then(function (res) { 92 | assert.equal(xhrOpen.args[4][0], 'GET', 'method is correct'); 93 | assert.include(xhrOpen.args[4][1], '/recoveryKey/' + recoveryKeyId, 'path is correct'); 94 | assert.ok(res.recoveryData, 'contains recovery data'); 95 | 96 | var options = { 97 | keys: true, 98 | metricsContext: { 99 | deviceId: '0123456789abcdef0123456789abcdef', 100 | flowBeginTime: 1480615985437, 101 | flowId: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 102 | utmCampaign: 'mock-campaign', 103 | utmContent: 'mock-content', 104 | utmMedium: 'mock-medium', 105 | utmSource: 'mock-source', 106 | utmTerm: 'mock-term' 107 | }, 108 | sessionToken: true 109 | }; 110 | return respond(client.resetPasswordWithRecoveryKey(accountResetToken, email, newPassword, recoveryKeyId, keys, options), RequestMocks.accountReset); 111 | }) 112 | .then(function (res) { 113 | assert.ok(res.keyFetchToken); 114 | assert.ok(res.sessionToken); 115 | assert.ok(res.unwrapBKey); 116 | assert.ok(res.uid); 117 | 118 | // Attempt to login with new password and retrieve keys 119 | return respond(client.signIn(email, newPassword, {keys: true}), RequestMocks.signInWithKeys); 120 | }) 121 | .then(function (res) { 122 | return respond(client.accountKeys(res.keyFetchToken, res.unwrapBKey), RequestMocks.accountKeys); 123 | }) 124 | .then(function (res) { 125 | if (!env.useRemoteServer) { 126 | assert.ok(res.kB, 'kB exists'); 127 | } else { 128 | assert.equal(res.kB, keys.kB, 'kB is equal to original kB'); 129 | } 130 | }); 131 | }); 132 | 133 | test('#can create and delete recovery key', function () { 134 | return respond(client.createRecoveryKey(account.signIn.sessionToken, recoveryKeyId, bundle), RequestMocks.createRecoveryKey) 135 | .then(function (res) { 136 | assert.ok(res); 137 | return respond(client.deleteRecoveryKey(account.signIn.sessionToken), RequestMocks.deleteRecoveryKey); 138 | }) 139 | .then(function (res) { 140 | assert.ok(res); 141 | assert.equal(xhrOpen.args[1][0], 'DELETE', 'method is correct'); 142 | assert.include(xhrOpen.args[1][1], '/recoveryKey', 'path is correct'); 143 | }); 144 | }); 145 | 146 | test('#can check if recovery exist using sessionToken', function () { 147 | return respond(client.recoveryKeyExists(account.signIn.sessionToken), RequestMocks.recoveryKeyExistsFalse) 148 | .then(function (res) { 149 | assert.equal(res.exists, false, 'recovery key does not exist'); 150 | assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); 151 | assert.include(xhrOpen.args[0][1], '/recoveryKey/exists', 'path is correct'); 152 | return respond(client.createRecoveryKey(account.signIn.sessionToken, recoveryKeyId, bundle), RequestMocks.createRecoveryKey); 153 | }) 154 | .then(function (res) { 155 | assert.ok(res); 156 | return respond(client.recoveryKeyExists(account.signIn.sessionToken), RequestMocks.recoveryKeyExistsTrue); 157 | }) 158 | .then(function (res) { 159 | assert.equal(res.exists, true, 'recovery key exists'); 160 | assert.equal(xhrOpen.args[2][0], 'POST', 'method is correct'); 161 | assert.include(xhrOpen.args[2][1], '/recoveryKey/exists', 'path is correct'); 162 | }); 163 | }); 164 | 165 | test('#can check if recovery exist using email', function () { 166 | return respond(client.recoveryKeyExists(undefined, account.input.email), RequestMocks.recoveryKeyExistsFalse) 167 | .then(function (res) { 168 | assert.equal(res.exists, false, 'recovery key does not exist'); 169 | assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); 170 | assert.include(xhrOpen.args[0][1], '/recoveryKey/exists', 'path is correct'); 171 | 172 | return respond(client.createRecoveryKey(account.signIn.sessionToken, recoveryKeyId, bundle), RequestMocks.createRecoveryKey); 173 | }) 174 | .then(function (res) { 175 | assert.ok(res); 176 | return respond(client.recoveryKeyExists(undefined, account.input.email), RequestMocks.recoveryKeyExistsTrue); 177 | }) 178 | .then(function (res) { 179 | assert.equal(res.exists, true, 'recovery key exists'); 180 | assert.equal(xhrOpen.args[2][0], 'POST', 'method is correct'); 181 | assert.include(xhrOpen.args[2][1], '/recoveryKey/exists', 'path is correct'); 182 | }); 183 | }); 184 | }); 185 | } 186 | }); 187 | -------------------------------------------------------------------------------- /tests/lib/request.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'tests/addons/sinon', 7 | 'intern!tdd', 8 | 'intern/chai!assert', 9 | 'tests/addons/environment', 10 | 'client/lib/request', 11 | 'tests/mocks/errors' 12 | ], function (sinon, tdd, assert, Environment, Request, ErrorMocks) { 13 | with (tdd) { 14 | suite('request module', function () { 15 | var RequestMocks; 16 | var request; 17 | var env; 18 | 19 | beforeEach(function () { 20 | env = new Environment(); 21 | RequestMocks = env.RequestMocks; 22 | request = new Request(env.authServerUrl, env.xhr); 23 | }); 24 | 25 | test('#heartbeat', function () { 26 | var heartbeatRequest = env.respond(request.send('/__heartbeat__', 'GET'), RequestMocks.heartbeat) 27 | .then( 28 | function (res) { 29 | assert.ok(res); 30 | }, 31 | assert.notOk 32 | ); 33 | 34 | return heartbeatRequest; 35 | }); 36 | 37 | test('#error', function () { 38 | request = new Request('http://', env.xhr); 39 | 40 | request.send('/', 'GET') 41 | .then( 42 | assert.notOk, 43 | function () { 44 | assert.ok(true); 45 | } 46 | ); 47 | 48 | }); 49 | 50 | test('#timeout', function () { 51 | request = new Request('http://google.com:81', env.xhr, { timeout: 200 }); 52 | 53 | var timeoutRequest = env.respond(request.send('/', 'GET'), ErrorMocks.timeout); 54 | 55 | return timeoutRequest.then( 56 | assert.notOk, 57 | function (err) { 58 | assert.equal(err.error, 'Timeout error'); 59 | } 60 | ); 61 | }); 62 | 63 | test('#bad response format error', function () { 64 | request = new Request('http://example.com/', env.xhr); 65 | 66 | // Trigger an error response that's in HTML 67 | var response = env.respond(request.send('/nonexistent', 'GET'), ErrorMocks.badResponseFormat); 68 | 69 | return response.then( 70 | assert.notOk, 71 | function (err) { 72 | assert.equal(err.error, 'Unknown error'); 73 | } 74 | ); 75 | }); 76 | 77 | test('#ensure is usable', function () { 78 | request = new Request('http://google.com:81', env.xhr, { timeout: 200 }); 79 | sinon.stub(env.xhr.prototype, 'open').throws(); 80 | 81 | return env.respond(request.send('/__heartbeat__', 'GET'), RequestMocks.heartbeat) 82 | .then( 83 | null, 84 | function (err) { 85 | assert.ok(err); 86 | env.xhr.prototype.open.restore(); 87 | } 88 | ); 89 | }); 90 | }); 91 | } 92 | }); 93 | -------------------------------------------------------------------------------- /tests/lib/session.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment', 9 | 'tests/addons/sinon' 10 | ], function (tdd, assert, Environment, sinon) { 11 | 12 | with (tdd) { 13 | suite('session', function () { 14 | var accountHelper; 15 | var respond; 16 | var requests; 17 | var client; 18 | var RequestMocks; 19 | var ErrorMocks; 20 | var xhr; 21 | 22 | beforeEach(function () { 23 | var env = new Environment(); 24 | accountHelper = env.accountHelper; 25 | respond = env.respond; 26 | requests = env.requests; 27 | client = env.client; 28 | RequestMocks = env.RequestMocks; 29 | ErrorMocks = env.ErrorMocks; 30 | xhr = env.xhr; 31 | sinon.spy(xhr.prototype, 'open'); 32 | sinon.spy(xhr.prototype, 'send'); 33 | }); 34 | 35 | afterEach(function () { 36 | xhr.prototype.open.restore(); 37 | xhr.prototype.send.restore(); 38 | }); 39 | 40 | test('#destroy', function () { 41 | 42 | return accountHelper.newVerifiedAccount() 43 | .then(function (account) { 44 | 45 | return respond(client.sessionDestroy(account.signIn.sessionToken), RequestMocks.sessionDestroy); 46 | }) 47 | .then( 48 | function(res) { 49 | assert.ok(res, 'got response'); 50 | }, 51 | assert.notOk 52 | ); 53 | }); 54 | 55 | test('#status', function () { 56 | 57 | return accountHelper.newVerifiedAccount() 58 | .then(function (account) { 59 | 60 | return respond(client.sessionStatus(account.signIn.sessionToken), RequestMocks.sessionStatus); 61 | }) 62 | .then( 63 | function(res) { 64 | assert.isNotNull(res); 65 | }, 66 | assert.notOk 67 | ); 68 | }); 69 | 70 | test('#status error with a false token', function () { 71 | 72 | return accountHelper.newVerifiedAccount() 73 | .then(function () { 74 | var fakeToken = 'e838790265a45f6ee1130070d57d67d9bb20953706f73af0e34b0d4d92f10000'; 75 | 76 | return respond(client.passwordForgotStatus(fakeToken), ErrorMocks.invalidAuthToken); 77 | }) 78 | .then( 79 | assert.notOk, 80 | function (err) { 81 | assert.equal(err.code, 401); 82 | assert.equal(err.errno, 110); 83 | } 84 | ); 85 | }); 86 | 87 | test('#sessions', function () { 88 | 89 | return accountHelper.newVerifiedAccount() 90 | .then(function (account) { 91 | return respond(client.sessions(account.signIn.sessionToken), RequestMocks.sessions); 92 | }) 93 | .then( 94 | function (res) { 95 | assert.equal(res.length, 2); 96 | var s = res[0]; 97 | assert.ok(s.id); 98 | assert.ok(s.deviceType); 99 | assert.equal(s.isDevice, false); 100 | assert.ok(s.lastAccessTime); 101 | assert.ok(s.lastAccessTimeFormatted); 102 | }, 103 | assert.notOk 104 | ); 105 | }); 106 | 107 | 108 | test('#sessions error', function () { 109 | 110 | return accountHelper.newVerifiedAccount() 111 | .then(function (account) { 112 | var fakeToken = 'e838790265a45f6ee1130070d57d67d9bb20953706f73af0e34b0d4d92f10000'; 113 | 114 | return respond(client.sessions(fakeToken), ErrorMocks.invalidAuthToken); 115 | }) 116 | .then( 117 | assert.notOk, 118 | function (err) { 119 | assert.equal(err.code, 401); 120 | assert.equal(err.errno, 110); 121 | } 122 | ); 123 | }); 124 | 125 | test('#reauth', function () { 126 | 127 | return accountHelper.newVerifiedAccount() 128 | .then(function (account) { 129 | var email = account.input.email; 130 | var password = account.input.password; 131 | 132 | return respond(client.sessionReauth(account.signIn.sessionToken, email, password), RequestMocks.sessionReauth) 133 | .then( 134 | function(res) { 135 | assert.ok(res.uid); 136 | assert.ok(res.verified); 137 | assert.ok(res.authAt); 138 | assert.notOk(res.keyFetchToken); 139 | assert.notOk(res.unwrapBKey); 140 | 141 | var args = xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; 142 | assert.equal(args[0], 'POST'); 143 | assert.include(args[1], '/session/reauth'); 144 | 145 | var payload = JSON.parse(xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0]); 146 | assert.equal(Object.keys(payload).length, 2); 147 | assert.equal(payload.email, email); 148 | assert.equal(payload.authPW.length, 64); 149 | }, 150 | assert.notOk 151 | ); 152 | }); 153 | }); 154 | 155 | test('#reauth with keys', function () { 156 | 157 | return accountHelper.newVerifiedAccount() 158 | .then(function (account) { 159 | var email = account.input.email; 160 | var password = account.input.password; 161 | 162 | return respond(client.sessionReauth(account.signIn.sessionToken, email, password, {keys: true}), RequestMocks.sessionReauthWithKeys) 163 | .then( 164 | function (res) { 165 | assert.ok(res.uid); 166 | assert.ok(res.verified); 167 | assert.ok(res.authAt); 168 | assert.ok(res.keyFetchToken); 169 | assert.ok(res.unwrapBKey); 170 | 171 | var args = xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; 172 | assert.equal(args[0], 'POST'); 173 | assert.include(args[1], '/session/reauth?keys=true'); 174 | 175 | var payload = JSON.parse(xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0]); 176 | assert.equal(Object.keys(payload).length, 2); 177 | assert.equal(payload.email, email); 178 | assert.equal(payload.authPW.length, 64); 179 | }, 180 | assert.notOk 181 | ); 182 | }); 183 | }); 184 | 185 | test('#reauth with incorrect password', function () { 186 | 187 | return accountHelper.newVerifiedAccount() 188 | .then(function (account) { 189 | var email = account.input.email; 190 | var password = 'incorrect password'; 191 | 192 | return respond(client.sessionReauth(account.signIn.sessionToken, email, password), ErrorMocks.accountIncorrectPassword) 193 | .then( 194 | function () { 195 | assert.fail(); 196 | }, 197 | function (res) { 198 | assert.equal(res.code, 400); 199 | assert.equal(res.errno, 103); 200 | } 201 | ); 202 | }); 203 | }); 204 | 205 | test('#reauth with incorrect email case', function () { 206 | 207 | return accountHelper.newVerifiedAccount() 208 | .then(function (account) { 209 | var numSetupRequests = requests ? requests.length : null; 210 | var sessionToken = account.signIn.sessionToken; 211 | var incorrectCaseEmail = account.input.email.charAt(0).toUpperCase() + account.input.email.slice(1); 212 | var password = account.input.password; 213 | 214 | respond(ErrorMocks.incorrectEmailCase); 215 | return respond(client.sessionReauth(sessionToken, incorrectCaseEmail, password), RequestMocks.sessionReauth) 216 | .then( 217 | function (res) { 218 | assert.property(res, 'uid'); 219 | assert.property(res, 'verified'); 220 | assert.property(res, 'authAt'); 221 | 222 | if (requests) { 223 | assert.equal(requests.length - numSetupRequests, 2); 224 | } 225 | 226 | var args = xhr.prototype.open.args[xhr.prototype.open.args.length - 2]; 227 | assert.equal(args[0], 'POST'); 228 | assert.include(args[1], '/session/reauth'); 229 | 230 | var payload = JSON.parse(xhr.prototype.send.args[xhr.prototype.send.args.length - 2][0]); 231 | assert.equal(Object.keys(payload).length, 2); 232 | assert.equal(payload.email, incorrectCaseEmail); 233 | assert.equal(payload.authPW.length, 64); 234 | 235 | args = xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; 236 | assert.equal(args[0], 'POST'); 237 | assert.include(args[1], '/session/reauth'); 238 | 239 | payload = JSON.parse(xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0]); 240 | assert.equal(Object.keys(payload).length, 3); 241 | assert.notEqual(payload.email, incorrectCaseEmail); 242 | assert.equal(payload.originalLoginEmail, incorrectCaseEmail); 243 | assert.equal(payload.authPW.length, 64); 244 | }, 245 | assert.notOk 246 | ); 247 | }); 248 | }); 249 | 250 | test('#reauth with incorrect email case with skipCaseError', function () { 251 | 252 | return accountHelper.newVerifiedAccount() 253 | .then(function (account) { 254 | var numSetupRequests = requests ? requests.length : null; 255 | var sessionToken = account.signIn.sessionToken; 256 | var incorrectCaseEmail = account.input.email.charAt(0).toUpperCase() + account.input.email.slice(1); 257 | var password = account.input.password; 258 | 259 | return respond(client.sessionReauth(sessionToken, incorrectCaseEmail, password, {skipCaseError: true}), ErrorMocks.incorrectEmailCase) 260 | .then( 261 | function () { 262 | assert.fail(); 263 | }, 264 | function (res) { 265 | assert.equal(res.code, 400); 266 | assert.equal(res.errno, 120); 267 | 268 | if (requests) { 269 | assert.equal(requests.length - numSetupRequests, 1); 270 | } 271 | 272 | var args = xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; 273 | assert.equal(args[0], 'POST'); 274 | assert.include(args[1], '/session/reauth'); 275 | 276 | var payload = JSON.parse(xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0]); 277 | assert.equal(Object.keys(payload).length, 2); 278 | assert.equal(payload.email, incorrectCaseEmail); 279 | assert.equal(payload.authPW.length, 64); 280 | } 281 | ); 282 | }); 283 | }); 284 | 285 | test('#reauth with all the options', function () { 286 | 287 | return accountHelper.newVerifiedAccount() 288 | .then(function (account) { 289 | var sessionToken = account.signIn.sessionToken; 290 | var email = account.input.email; 291 | var password = account.input.password; 292 | var options = { 293 | keys: true, 294 | metricsContext: { 295 | entrypoint: 'mock-entrypoint', 296 | utmCampaign: 'mock-utm-campaign', 297 | utmContent: 'mock-utm-content', 298 | utmMedium: 'mock-utm-medium', 299 | utmSource: 'mock-utm-source', 300 | utmTerm: 'mock-utm-term' 301 | }, 302 | originalLoginEmail: email.toUpperCase(), 303 | reason: 'password_change', 304 | redirectTo: 'http://127.0.0.1', 305 | resume: 'RESUME_TOKEN', 306 | service: 'sync', 307 | verificationMethod: 'email-2fa' 308 | }; 309 | 310 | return respond(client.sessionReauth(sessionToken, email, password, options), RequestMocks.sessionReauthWithKeys) 311 | .then( 312 | function (res) { 313 | assert.ok(res.uid); 314 | assert.ok(res.verified); 315 | assert.ok(res.authAt); 316 | assert.ok(res.keyFetchToken); 317 | assert.ok(res.unwrapBKey); 318 | 319 | var args = xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; 320 | assert.equal(args[0], 'POST'); 321 | assert.include(args[1], '/session/reauth?keys=true'); 322 | 323 | var payload = JSON.parse(xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0]); 324 | assert.equal(Object.keys(payload).length, 9); 325 | assert.equal(payload.email, email); 326 | assert.equal(payload.authPW.length, 64); 327 | assert.deepEqual(payload.metricsContext, options.metricsContext); 328 | assert.equal(payload.originalLoginEmail, options.originalLoginEmail); 329 | assert.equal(payload.reason, options.reason); 330 | assert.equal(payload.redirectTo, options.redirectTo); 331 | assert.equal(payload.resume, options.resume); 332 | assert.equal(payload.service, options.service); 333 | assert.equal(payload.verificationMethod, options.verificationMethod); 334 | } 335 | ); 336 | }); 337 | }); 338 | }); 339 | } 340 | }); 341 | -------------------------------------------------------------------------------- /tests/lib/signIn.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment', 9 | 'tests/lib/push-constants' 10 | ], function (tdd, assert, Environment, PushTestConstants) { 11 | 12 | with (tdd) { 13 | suite('signIn', function () { 14 | var ErrorMocks; 15 | var RequestMocks; 16 | var accountHelper; 17 | var client; 18 | var mail; 19 | var respond; 20 | var requests; 21 | 22 | beforeEach(function () { 23 | var env = new Environment(); 24 | ErrorMocks = env.ErrorMocks; 25 | RequestMocks = env.RequestMocks; 26 | accountHelper = env.accountHelper; 27 | client = env.client; 28 | mail = env.mail; 29 | respond = env.respond; 30 | requests = env.requests; 31 | }); 32 | 33 | test('#basic', function () { 34 | var email = 'test' + new Date().getTime() + '@restmail.net'; 35 | var password = 'iliketurtles'; 36 | 37 | return respond(client.signUp(email, password), RequestMocks.signUp) 38 | .then(function () { 39 | 40 | return respond(client.signIn(email, password), RequestMocks.signIn); 41 | }) 42 | .then( 43 | function (res) { 44 | assert.ok(res.sessionToken); 45 | }, 46 | assert.notOk 47 | ); 48 | }); 49 | 50 | test('#with keys', function () { 51 | var email = 'test' + new Date().getTime() + '@restmail.net'; 52 | var password = 'iliketurtles'; 53 | 54 | return respond(client.signUp(email, password), RequestMocks.signUp) 55 | .then(function (res) { 56 | return respond(client.signIn(email, password, {keys: true}), RequestMocks.signInWithKeys); 57 | }) 58 | .then( 59 | function (res) { 60 | assert.ok(res.sessionToken); 61 | assert.ok(res.keyFetchToken); 62 | assert.ok(res.unwrapBKey); 63 | }, 64 | assert.notOk 65 | ); 66 | }); 67 | 68 | test('#with service', function () { 69 | var email = 'test' + new Date().getTime() + '@restmail.net'; 70 | var password = 'iliketurtles'; 71 | 72 | return respond(client.signUp(email, password), RequestMocks.signUp) 73 | .then(function () { 74 | return respond(client.signIn(email, password, {service: 'sync'}), RequestMocks.signIn); 75 | }); 76 | }); 77 | 78 | test('#with reason', function () { 79 | var email = 'test' + new Date().getTime() + '@restmail.net'; 80 | var password = 'iliketurtles'; 81 | 82 | return respond(client.signUp(email, password), RequestMocks.signUp) 83 | .then(function () { 84 | return respond(client.signIn(email, password, {reason: 'password_change'}), RequestMocks.signIn); 85 | }); 86 | }); 87 | 88 | test('#with Sync/redirectTo', function () { 89 | var user = 'confirm' + new Date().getTime(); 90 | var email = user + '@restmail.net'; 91 | var password = 'iliketurtles'; 92 | var opts = { 93 | keys: true, 94 | metricsContext: { 95 | context: 'fx_desktop_v2' 96 | }, 97 | redirectTo: 'http://sync.127.0.0.1/after_reset', 98 | service: 'sync' 99 | }; 100 | 101 | return respond(client.signUp(email, password, { preVerified: true }), RequestMocks.signUp) 102 | .then(function () { 103 | 104 | return respond(client.signIn(email, password, opts), RequestMocks.signIn); 105 | }) 106 | .then(function (res) { 107 | assert.ok(res.uid); 108 | return respond(mail.wait(user), RequestMocks.mailServiceAndRedirect); 109 | }) 110 | .then( 111 | function (emails) { 112 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 113 | var redirectTo = emails[0].html.match(/redirectTo=([A-Za-z0-9]+)/)[1]; 114 | 115 | assert.ok(code, 'code is returned'); 116 | assert.ok(redirectTo, 'redirectTo is returned'); 117 | 118 | }, 119 | assert.notOk 120 | ); 121 | }); 122 | 123 | test('#with Sync/resume', function () { 124 | var user = 'confirm' + new Date().getTime(); 125 | var email = user + '@restmail.net'; 126 | var password = 'iliketurtles'; 127 | var opts = { 128 | keys: true, 129 | metricsContext: { 130 | context: 'fx_desktop_v2' 131 | }, 132 | redirectTo: 'http://sync.127.0.0.1/after_reset', 133 | resume: 'resumejwt', 134 | service: 'sync' 135 | }; 136 | 137 | return respond(client.signUp(email, password, { preVerified: true }), RequestMocks.signUp) 138 | .then(function () { 139 | return respond(client.signIn(email, password, opts), RequestMocks.signIn); 140 | }) 141 | .then(function (res) { 142 | assert.ok(res.uid); 143 | return respond(mail.wait(user), RequestMocks.mailServiceAndRedirect); 144 | }) 145 | .then( 146 | function (emails) { 147 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 148 | var resume = emails[0].html.match(/resume=([A-Za-z0-9]+)/)[1]; 149 | 150 | assert.ok(code, 'code is returned'); 151 | assert.ok(resume, 'resume is returned'); 152 | 153 | }, 154 | assert.notOk 155 | ); 156 | }); 157 | 158 | 159 | test('#incorrect email case', function () { 160 | 161 | return accountHelper.newVerifiedAccount() 162 | .then(function (account) { 163 | var numSetupRequests = requests ? requests.length : null; 164 | var incorrectCaseEmail = account.input.email.charAt(0).toUpperCase() + account.input.email.slice(1); 165 | 166 | respond(ErrorMocks.incorrectEmailCase); 167 | return respond(client.signIn(incorrectCaseEmail, account.input.password), RequestMocks.signIn) 168 | .then( 169 | function (res) { 170 | assert.property(res, 'sessionToken'); 171 | if (requests) { 172 | assert.equal(requests.length - numSetupRequests, 2); 173 | } 174 | }, 175 | assert.notOk 176 | ); 177 | }); 178 | }); 179 | 180 | test('#incorrect email case with skipCaseError', function () { 181 | 182 | return accountHelper.newVerifiedAccount() 183 | .then(function (account) { 184 | var numSetupRequests = requests ? requests.length : null; 185 | var incorrectCaseEmail = account.input.email.charAt(0).toUpperCase() + account.input.email.slice(1); 186 | 187 | return respond(client.signIn(incorrectCaseEmail, account.input.password, {skipCaseError: true}), ErrorMocks.incorrectEmailCase) 188 | .then( 189 | function () { 190 | assert.fail(); 191 | }, 192 | function (res) { 193 | assert.equal(res.code, 400); 194 | assert.equal(res.errno, 120); 195 | if (requests) { 196 | assert.equal(requests.length - numSetupRequests, 1); 197 | } 198 | } 199 | ); 200 | }); 201 | }); 202 | 203 | test('#incorrectPassword', function () { 204 | 205 | return accountHelper.newVerifiedAccount() 206 | .then(function (account) { 207 | return respond(client.signIn(account.input.email, 'wrong password'), ErrorMocks.accountIncorrectPassword); 208 | }) 209 | .then( 210 | function () { 211 | assert.fail(); 212 | }, 213 | function (res) { 214 | assert.equal(res.code, 400); 215 | assert.equal(res.errno, 103); 216 | } 217 | ); 218 | }); 219 | 220 | test('#with metricsContext metadata', function () { 221 | var email = 'test' + new Date().getTime() + '@restmail.net'; 222 | var password = 'iliketurtles'; 223 | 224 | return respond(client.signUp(email, password), RequestMocks.signUp) 225 | .then(function () { 226 | return respond( 227 | client.signIn(email, password, { 228 | metricsContext: {}, 229 | reason: 'signin' 230 | }), 231 | RequestMocks.signIn 232 | ); 233 | }) 234 | .then( 235 | function (resp) { 236 | assert.ok(resp); 237 | }, 238 | assert.notOk 239 | ); 240 | }); 241 | }); 242 | } 243 | }); 244 | -------------------------------------------------------------------------------- /tests/lib/signUp.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment', 9 | 'tests/addons/sinon' 10 | ], function (tdd, assert, Environment, sinon) { 11 | 12 | with (tdd) { 13 | suite('signUp', function () { 14 | var accountHelper; 15 | var respond; 16 | var mail; 17 | var client; 18 | var RequestMocks; 19 | var ErrorMocks; 20 | var xhr; 21 | var xhrOpen; 22 | var xhrSend; 23 | 24 | beforeEach(function () { 25 | var env = new Environment(); 26 | accountHelper = env.accountHelper; 27 | respond = env.respond; 28 | mail = env.mail; 29 | client = env.client; 30 | RequestMocks = env.RequestMocks; 31 | ErrorMocks = env.ErrorMocks; 32 | xhr = env.xhr; 33 | xhrOpen = sinon.spy(xhr.prototype, 'open'); 34 | xhrSend = sinon.spy(xhr.prototype, 'send'); 35 | }); 36 | 37 | afterEach(function () { 38 | xhrOpen.restore(); 39 | xhrSend.restore(); 40 | }); 41 | 42 | test('#basic', function () { 43 | var email = 'test' + new Date().getTime() + '@restmail.net'; 44 | var password = 'iliketurtles'; 45 | 46 | return respond(client.signUp(email, password), RequestMocks.signUp) 47 | .then( 48 | function (res) { 49 | assert.property(res, 'uid', 'uid should be returned on signUp'); 50 | assert.property(res, 'sessionToken', 'sessionToken should be returned on signUp'); 51 | assert.notProperty(res, 'keyFetchToken', 'keyFetchToken should not be returned on signUp'); 52 | 53 | assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); 54 | assert.include(xhrOpen.args[0][1], '/account/create', 'path is correct'); 55 | var sentData = JSON.parse(xhrSend.args[0][0]); 56 | assert.equal(Object.keys(sentData).length, 2); 57 | assert.equal(sentData.email, email, 'email is correct'); 58 | assert.equal(sentData.authPW.length, 64, 'length of authPW'); 59 | }, 60 | assert.notOk 61 | ); 62 | }); 63 | 64 | test('#withKeys', function () { 65 | var email = 'test' + new Date().getTime() + '@restmail.net'; 66 | var password = 'iliketurtles'; 67 | var opts = { 68 | keys: true 69 | }; 70 | 71 | return respond(client.signUp(email, password, opts), RequestMocks.signUpKeys) 72 | .then( 73 | function (res) { 74 | assert.property(res, 'uid', 'uid should be returned on signUp'); 75 | assert.property(res, 'sessionToken', 'sessionToken should be returned on signUp'); 76 | assert.property(res, 'keyFetchToken', 'keyFetchToken should be returned on signUp'); 77 | assert.property(res, 'unwrapBKey', 'unwrapBKey should be returned on signUp'); 78 | 79 | assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); 80 | assert.include(xhrOpen.args[0][1], '/account/create?keys=true', 'path is correct'); 81 | }, 82 | assert.notOk 83 | ); 84 | }); 85 | 86 | test('#create account with service, redirectTo, and resume', function () { 87 | var user = 'test' + new Date().getTime(); 88 | var email = user + '@restmail.net'; 89 | var password = 'iliketurtles'; 90 | var opts = { 91 | service: 'sync', 92 | redirectTo: 'https://sync.127.0.0.1/after_reset', 93 | resume: 'resumejwt' 94 | }; 95 | 96 | return respond(client.signUp(email, password, opts), RequestMocks.signUp) 97 | .then(function (res) { 98 | assert.ok(res.uid); 99 | return respond(mail.wait(user), RequestMocks.mailServiceAndRedirect); 100 | }) 101 | .then( 102 | function (emails) { 103 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 104 | var service = emails[0].html.match(/service=([A-Za-z0-9]+)/)[1]; 105 | var redirectTo = emails[0].html.match(/redirectTo=([A-Za-z0-9]+)/)[1]; 106 | var resume = emails[0].html.match(/resume=([A-Za-z0-9]+)/)[1]; 107 | 108 | assert.ok(code, 'code is returned'); 109 | assert.ok(service, 'service is returned'); 110 | assert.ok(redirectTo, 'redirectTo is returned'); 111 | assert.ok(resume, 'resume is returned'); 112 | 113 | assert.include(xhrOpen.args[0][1], '/account/create', 'path is correct'); 114 | var sentData = JSON.parse(xhrSend.args[0][0]); 115 | assert.equal(Object.keys(sentData).length, 5); 116 | assert.equal(sentData.email, email, 'email is correct'); 117 | assert.equal(sentData.authPW.length, 64, 'length of authPW'); 118 | assert.equal(sentData.service, opts.service); 119 | assert.equal(sentData.resume, opts.resume); 120 | assert.equal(sentData.redirectTo, opts.redirectTo); 121 | }, 122 | assert.notOk 123 | ); 124 | }); 125 | 126 | test('#withService', function () { 127 | var user = 'test' + new Date().getTime(); 128 | var email = user + '@restmail.net'; 129 | var password = 'iliketurtles'; 130 | var opts = { 131 | service: 'sync' 132 | }; 133 | 134 | return respond(client.signUp(email, password, opts), RequestMocks.signUp) 135 | .then(function (res) { 136 | assert.ok(res.uid); 137 | return respond(mail.wait(user), RequestMocks.mailServiceAndRedirect); 138 | }) 139 | .then( 140 | function (emails) { 141 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 142 | var service = emails[0].html.match(/service=([A-Za-z0-9]+)/)[1]; 143 | 144 | assert.ok(code, 'code is returned'); 145 | assert.ok(service, 'service is returned'); 146 | }, 147 | assert.notOk 148 | ); 149 | }); 150 | 151 | test('#withRedirectTo', function () { 152 | var user = 'test' + new Date().getTime(); 153 | var email = user + '@restmail.net'; 154 | var password = 'iliketurtles'; 155 | var opts = { 156 | redirectTo: 'http://sync.127.0.0.1/after_reset' 157 | }; 158 | 159 | return respond(client.signUp(email, password, opts), RequestMocks.signUp) 160 | .then(function (res) { 161 | assert.ok(res.uid); 162 | return respond(mail.wait(user), RequestMocks.mailServiceAndRedirect); 163 | }) 164 | .then( 165 | function (emails) { 166 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 167 | var redirectTo = emails[0].html.match(/redirectTo=([A-Za-z0-9]+)/)[1]; 168 | 169 | assert.ok(code, 'code is returned'); 170 | assert.ok(redirectTo, 'redirectTo is returned'); 171 | 172 | }, 173 | assert.notOk 174 | ); 175 | }); 176 | 177 | test('#withResume', function () { 178 | var user = 'test' + new Date().getTime(); 179 | var email = user + '@restmail.net'; 180 | var password = 'iliketurtles'; 181 | var opts = { 182 | resume: 'resumejwt' 183 | }; 184 | 185 | return respond(client.signUp(email, password, opts), RequestMocks.signUp) 186 | .then(function (res) { 187 | assert.ok(res.uid); 188 | return respond(mail.wait(user), RequestMocks.mailServiceAndRedirect); 189 | }) 190 | .then( 191 | function (emails) { 192 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 193 | var resume = emails[0].html.match(/resume=([A-Za-z0-9]+)/)[1]; 194 | 195 | assert.ok(code, 'code is returned'); 196 | assert.ok(resume, 'resume is returned'); 197 | 198 | }, 199 | assert.notOk 200 | ); 201 | }); 202 | 203 | test('#preVerified', function () { 204 | var email = 'test' + new Date().getTime() + '@restmail.net'; 205 | var password = 'iliketurtles'; 206 | var opts = { 207 | preVerified: true 208 | }; 209 | 210 | return respond(client.signUp(email, password, opts), RequestMocks.signUp) 211 | .then(function (res) { 212 | assert.ok(res.uid); 213 | 214 | return respond(client.signIn(email, password), RequestMocks.signIn); 215 | }) 216 | .then(function(res) { 217 | assert.equal(res.verified, true, '== account is verified'); 218 | }); 219 | }); 220 | 221 | test('#accountExists', function () { 222 | return accountHelper.newVerifiedAccount() 223 | .then(function (account) { 224 | return respond(client.signUp(account.input.email, 'somepass'), ErrorMocks.accountExists); 225 | }) 226 | .then( 227 | function (res) { 228 | assert.fail(); 229 | }, 230 | function (err) { 231 | assert.equal(err.code, 400); 232 | assert.equal(err.errno, 101); 233 | } 234 | ); 235 | }); 236 | 237 | test('#with metricsContext metadata', function () { 238 | var email = 'test' + new Date().getTime() + '@restmail.net'; 239 | var password = 'iliketurtles'; 240 | 241 | return respond( 242 | client.signUp(email, password, { 243 | metricsContext: { 244 | deviceId: '0123456789abcdef0123456789abcdef', 245 | flowId: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 246 | flowBeginTime: Date.now(), 247 | utmCampaign: 'mock-campaign', 248 | utmContent: 'mock-content', 249 | utmMedium: 'mock-medium', 250 | utmSource: 'mock-source', 251 | utmTerm: 'mock-term', 252 | forbiddenProperty: 666 253 | } 254 | }), 255 | RequestMocks.signUp 256 | ) 257 | .then( 258 | function (resp) { 259 | assert.ok(resp); 260 | }, 261 | assert.notOk 262 | ); 263 | }); 264 | }); 265 | } 266 | }); 267 | -------------------------------------------------------------------------------- /tests/lib/signinCodes.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var SIGNIN_CODE = '123456-_'; 6 | var FLOW_ID = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; 7 | var FLOW_BEGIN_TIME = Date.now(); 8 | 9 | define([ 10 | 'intern!tdd', 11 | 'intern/chai!assert', 12 | 'tests/addons/environment' 13 | ], function (tdd, assert, Environment) { 14 | var env = new Environment(); 15 | 16 | with (tdd) { 17 | suite('signinCodes', function () { 18 | var respond; 19 | var client; 20 | var RequestMocks; 21 | 22 | beforeEach(function () { 23 | env = new Environment(); 24 | respond = env.respond; 25 | client = env.client; 26 | RequestMocks = env.RequestMocks; 27 | }); 28 | 29 | if (env.useRemoteServer) { 30 | // This test is intended to run against a local auth-server. To test 31 | // against a mock auth-server would be pointless for this assertion. 32 | test('consumeSigninCode with invalid signinCode', function () { 33 | return client.consumeSigninCode(SIGNIN_CODE, FLOW_ID, FLOW_BEGIN_TIME) 34 | .then(function () { 35 | assert.fail('client.consumeSigninCode should reject if signinCode is invalid'); 36 | }, function (err) { 37 | assert.ok(err, 'client.consumeSigninCode should return an error'); 38 | assert.equal(err.code, 400, 'client.consumeSigninCode should return a 400 response'); 39 | assert.equal(err.errno, 146, 'client.consumeSigninCode should return errno 146'); 40 | }); 41 | }); 42 | } else { 43 | // This test is intended to run against a mock auth-server. To test 44 | // against a local auth-server, we'd need to know a valid signinCode. 45 | test('consumeSigninCode', function () { 46 | return respond(client.consumeSigninCode(SIGNIN_CODE, FLOW_ID, FLOW_BEGIN_TIME), RequestMocks.consumeSigninCode) 47 | .then(assert.ok, assert.fail); 48 | }); 49 | } 50 | 51 | 52 | test('consumeSigninCode with missing code', function () { 53 | return client.consumeSigninCode(null, FLOW_ID, FLOW_BEGIN_TIME) 54 | .then(function () { 55 | assert.fail('client.consumeSigninCode should reject if code is missing'); 56 | }, function (err) { 57 | assert.equal(err.message, 'Missing code'); 58 | }); 59 | }); 60 | 61 | test('consumeSigninCode with missing flowId', function () { 62 | return client.consumeSigninCode(SIGNIN_CODE, null, FLOW_BEGIN_TIME) 63 | .then(function () { 64 | assert.fail('client.consumeSigninCode should reject if flowId is missing'); 65 | }, function (err) { 66 | assert.equal(err.message, 'Missing flowId'); 67 | }); 68 | }); 69 | 70 | test('consumeSigninCode with missing flowBeginTime', function () { 71 | return client.consumeSigninCode(SIGNIN_CODE, FLOW_ID, null) 72 | .then(function () { 73 | assert.fail('client.consumeSigninCode should reject if flowBeginTime is missing'); 74 | }, function (err) { 75 | assert.equal(err.message, 'Missing flowBeginTime'); 76 | }); 77 | }); 78 | }); 79 | } 80 | }); 81 | 82 | -------------------------------------------------------------------------------- /tests/lib/sms.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment', 9 | 'tests/lib/push-constants' 10 | ], function (tdd, assert, Environment) { 11 | 12 | // These tests are intended to run against a mock auth-server. To test 13 | // against a local auth-server, you will need to have it correctly 14 | // configured to send sms and specify a real phone number here. 15 | var env = new Environment(); 16 | if (env.useRemoteServer) { 17 | return; 18 | } 19 | 20 | var PHONE_NUMBER = '+14071234567'; 21 | var MESSAGE_ID = 1; 22 | 23 | with (tdd) { 24 | suite('sms', function () { 25 | var accountHelper; 26 | var respond; 27 | var client; 28 | var RequestMocks; 29 | 30 | beforeEach(function () { 31 | env = new Environment(); 32 | accountHelper = env.accountHelper; 33 | respond = env.respond; 34 | client = env.client; 35 | RequestMocks = env.RequestMocks; 36 | }); 37 | 38 | test('#send connect device', function () { 39 | 40 | return accountHelper.newVerifiedAccount() 41 | .then(function (account) { 42 | 43 | return respond(client.sendSms( 44 | account.signIn.sessionToken, 45 | PHONE_NUMBER, 46 | MESSAGE_ID 47 | ), RequestMocks.sendSmsConnectDevice); 48 | }) 49 | .then( 50 | function (resp) { 51 | assert.ok(resp); 52 | }, 53 | assert.notOk 54 | ); 55 | }); 56 | 57 | test('status', function () { 58 | return accountHelper.newVerifiedAccount() 59 | .then( 60 | function (account) { 61 | return respond( 62 | client.smsStatus(account.signIn.sessionToken), 63 | RequestMocks.smsStatus 64 | ); 65 | } 66 | ) 67 | .then( 68 | function (resp) { 69 | assert.ok(resp); 70 | assert.ok(resp.ok); 71 | assert.ok(resp.country); 72 | }, 73 | assert.notOk 74 | ); 75 | }); 76 | 77 | test('status with country', function () { 78 | return accountHelper.newVerifiedAccount() 79 | .then( 80 | function (account) { 81 | return respond( 82 | client.smsStatus(account.signIn.sessionToken, { country: 'RO' }), 83 | RequestMocks.smsStatus 84 | ); 85 | } 86 | ) 87 | .then( 88 | function (resp) { 89 | assert.ok(resp); 90 | assert.ok(resp.ok); 91 | assert.ok(resp.country, 'RO'); 92 | }, 93 | assert.notOk 94 | ); 95 | }); 96 | }); 97 | } 98 | }); 99 | 100 | -------------------------------------------------------------------------------- /tests/lib/tokenCodes.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment' 9 | ], function (tdd, assert, Environment) { 10 | 11 | with (tdd) { 12 | suite('tokenCodes', function () { 13 | var account; 14 | var accountHelper; 15 | var respond; 16 | var client; 17 | var mail; 18 | var RequestMocks; 19 | var env = new Environment(); 20 | 21 | beforeEach(function () { 22 | env = new Environment(); 23 | accountHelper = env.accountHelper; 24 | respond = env.respond; 25 | client = env.client; 26 | mail = env.mail; 27 | RequestMocks = env.RequestMocks; 28 | return accountHelper.newVerifiedAccount({username: 'confirm.' + Date.now()}) 29 | .then(function (newAccount) { 30 | account = newAccount; 31 | }); 32 | }); 33 | 34 | if (env.useRemoteServer) { 35 | // This test is intended to run against a local auth-server. To test 36 | // against a mock auth-server would be pointless for this assertion. 37 | test('verify session with invalid tokenCode', function () { 38 | var opts = {verificationMethod: 'email-2fa', keys:true}; 39 | return respond(client.signIn(account.input.email, account.input.password, opts), RequestMocks.signInWithVerificationMethodEmail2faResponse) 40 | .then(function (res) { 41 | assert.equal(res.verificationMethod, 'email-2fa', 'should return correct verificationMethod'); 42 | assert.equal(res.verificationReason, 'login', 'should return correct verificationReason'); 43 | return respond(mail.wait(account.input.user, 3), RequestMocks.signInWithVerificationMethodEmail2faCode); 44 | }) 45 | .then(function (emails) { 46 | // should contain token code 47 | var code = emails[2].headers['x-signin-verify-code']; 48 | code = code === '000000' ? '000001' : '000000'; 49 | return client.verifyTokenCode(account.signIn.sessionToken, account.signIn.uid, code); 50 | }) 51 | .then(function () { 52 | assert.fail('should reject if tokenCode is invalid'); 53 | }, function (err) { 54 | assert.ok(err, 'should return an error'); 55 | assert.equal(err.code, 400, 'should return a 400 response'); 56 | assert.equal(err.errno, 152, 'should return errno 152'); 57 | }); 58 | }); 59 | } 60 | 61 | test('#verify session with valid tokenCode', function () { 62 | var code; 63 | var opts = {verificationMethod: 'email-2fa', keys:true}; 64 | return respond(client.signIn(account.input.email, account.input.password, opts), RequestMocks.signInWithVerificationMethodEmail2faResponse) 65 | .then(function (res) { 66 | assert.equal(res.verificationMethod, 'email-2fa', 'should return correct verificationMethod'); 67 | assert.equal(res.verificationReason, 'login', 'should return correct verificationReason'); 68 | return respond(mail.wait(account.input.user, 3), RequestMocks.signInWithVerificationMethodEmail2faCode); 69 | }) 70 | .then(function (emails) { 71 | // should contain token code 72 | code = emails[2].headers['x-signin-verify-code']; 73 | assert.ok(code, 'code is returned'); 74 | return respond(client.verifyTokenCode(account.signIn.sessionToken, account.signIn.uid, code), RequestMocks.sessionVerifyTokenCodeSuccess); 75 | }, assert.notOk) 76 | .then(function (res) { 77 | assert.ok(res, 'res is ok'); 78 | }, assert.notOk); 79 | }); 80 | }); 81 | } 82 | }); 83 | -------------------------------------------------------------------------------- /tests/lib/totp.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment', 9 | 'tests/addons/sinon', 10 | 'node_modules/otplib/otplib-browser' 11 | ], function (tdd, assert, Environment, sinon, otplib) { 12 | 13 | with (tdd) { 14 | suite('totp', function () { 15 | var authenticator; 16 | var account; 17 | var accountHelper; 18 | var respond; 19 | var client; 20 | var RequestMocks; 21 | var env; 22 | var xhr; 23 | var xhrOpen; 24 | var xhrSend; 25 | var secret; 26 | var opts = { 27 | metricsContext: { 28 | flowBeginTime: Date.now(), 29 | flowId: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' 30 | }, 31 | service: 'sync' 32 | }; 33 | 34 | beforeEach(function () { 35 | env = new Environment(); 36 | accountHelper = env.accountHelper; 37 | respond = env.respond; 38 | client = env.client; 39 | RequestMocks = env.RequestMocks; 40 | 41 | return accountHelper.newVerifiedAccount() 42 | .then(function (newAccount) { 43 | account = newAccount; 44 | return respond(client.createTotpToken(account.signIn.sessionToken), RequestMocks.createTotpToken); 45 | }) 46 | .then(function (res) { 47 | assert.ok(res.qrCodeUrl, 'should return QR code data encoded url'); 48 | assert.ok(res.secret, 'should return secret that is encoded in url'); 49 | 50 | // Create a new authenticator instance with shared options 51 | authenticator = new otplib.authenticator.Authenticator(); 52 | authenticator.options = otplib.authenticator.options; 53 | secret = res.secret; 54 | 55 | xhr = env.xhr; 56 | xhrOpen = sinon.spy(xhr.prototype, 'open'); 57 | xhrSend = sinon.spy(xhr.prototype, 'send'); 58 | }); 59 | }); 60 | 61 | afterEach(function () { 62 | xhrOpen.restore(); 63 | xhrSend.restore(); 64 | }); 65 | 66 | test('#createTotpToken - fails if already exists', function () { 67 | return respond(client.createTotpToken(account.signIn.sessionToken), RequestMocks.createTotpTokenDuplicate) 68 | .then(assert.fail, function (err) { 69 | assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); 70 | assert.include(xhrOpen.args[0][1], '/totp/create', 'path is correct'); 71 | assert.equal(err.errno, 154, 'token already exists for account errno'); 72 | }); 73 | }); 74 | 75 | test('#deleteTotpToken', function () { 76 | return respond(client.deleteTotpToken(account.signIn.sessionToken), RequestMocks.deleteTotpToken) 77 | .then(function (res) { 78 | assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); 79 | assert.include(xhrOpen.args[0][1], '/totp/destroy', 'path is correct'); 80 | assert.ok(res, 'should return empty response'); 81 | }); 82 | }); 83 | 84 | test('#checkTotpTokenExists - does not exist returns false', function () { 85 | return accountHelper.newVerifiedAccount() 86 | .then(function (newAccount) { 87 | return respond(client.checkTotpTokenExists(newAccount.signIn.sessionToken), RequestMocks.checkTotpTokenExistsFalse) 88 | .then(function (res) { 89 | assert.equal(xhrOpen.args[4][0], 'GET', 'method is correct'); 90 | assert.include(xhrOpen.args[4][1], '/totp/exists', 'path is correct'); 91 | assert.equal(res.exists, false); 92 | }); 93 | }); 94 | }); 95 | 96 | test('#checkTotpTokenExists - created token but not verified returns false', function () { 97 | return respond(client.checkTotpTokenExists(account.signIn.sessionToken), RequestMocks.checkTotpTokenExistsFalse) 98 | .then(function (res) { 99 | assert.equal(xhrOpen.args[0][0], 'GET', 'method is correct'); 100 | assert.include(xhrOpen.args[0][1], '/totp/exists', 'path is correct'); 101 | assert.equal(res.exists, false); 102 | }); 103 | }); 104 | 105 | test('#checkTotpTokenExists - verified token returns true', function () { 106 | var code = authenticator.generate(secret); 107 | return respond(client.verifyTotpCode(account.signIn.sessionToken, code), RequestMocks.verifyTotpCodeTrue) 108 | .then(function (res) { 109 | assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); 110 | assert.include(xhrOpen.args[0][1], '/session/verify/totp', 'path is correct'); 111 | var sentData = JSON.parse(xhrSend.args[0][0]); 112 | assert.equal(Object.keys(sentData).length, 1); 113 | assert.equal(sentData.code, code, 'code is correct'); 114 | 115 | assert.equal(res.success, true); 116 | return respond(client.checkTotpTokenExists(account.signIn.sessionToken), RequestMocks.checkTotpTokenExistsTrue) 117 | .then(function (res) { 118 | assert.equal(xhrOpen.args[1][0], 'GET', 'method is correct'); 119 | assert.include(xhrOpen.args[1][1], '/totp/exists', 'path is correct'); 120 | assert.equal(res.exists, true); 121 | }); 122 | }); 123 | }); 124 | 125 | test('#verifyTotpCode - succeeds for valid code', function () { 126 | var code = authenticator.generate(secret); 127 | return respond(client.verifyTotpCode(account.signIn.sessionToken, code, opts), RequestMocks.verifyTotpCodeTrue) 128 | .then(function (res) { 129 | assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); 130 | assert.include(xhrOpen.args[0][1], '/session/verify/totp', 'path is correct'); 131 | var sentData = JSON.parse(xhrSend.args[0][0]); 132 | assert.lengthOf(Object.keys(sentData), 2); 133 | assert.equal(sentData.code, code, 'code is correct'); 134 | assert.equal(sentData.service, opts.service, 'service is correct'); 135 | 136 | 137 | assert.equal(res.success, true); 138 | }); 139 | }); 140 | 141 | test('#verifyTotpCode - fails for invalid code', function () { 142 | var code = authenticator.generate(secret) === '000000' ? '000001' : '000000'; 143 | return respond(client.verifyTotpCode(account.signIn.sessionToken, code, opts), RequestMocks.verifyTotpCodeFalse) 144 | .then(function (res) { 145 | assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); 146 | assert.include(xhrOpen.args[0][1], '/session/verify/totp', 'path is correct'); 147 | var sentData = JSON.parse(xhrSend.args[0][0]); 148 | assert.lengthOf(Object.keys(sentData), 2); 149 | assert.equal(sentData.code, code, 'code is correct'); 150 | assert.equal(sentData.service, opts.service, 'service is correct'); 151 | 152 | assert.equal(res.success, false); 153 | }); 154 | }); 155 | }); 156 | } 157 | }); 158 | -------------------------------------------------------------------------------- /tests/lib/unbundle.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'node_modules/sjcl/sjcl', 9 | 'client/lib/credentials' 10 | ], function (tdd, assert, sjcl, credentials) { 11 | with (tdd) { 12 | 13 | suite('unbundle', function () { 14 | 15 | test('#vector 1', function () { 16 | // credentials.unbundleKeyFetchResponse(bundleKey, 'account/keys', payload.bundle); 17 | // Vectors generated from fxa-auth-server 18 | var bundleKey = 'ba0a107dab60f3b065ff7a642d14fe824fbd71bc5c99087e9e172a1abd1634f1'; 19 | var keyInfo = 'account/keys'; 20 | var bundle = 'e47eb17e487eb4495e79846d5e0c16ea51ef51ff5ef59cd8f626f95f572ec64dcc7b97fcbc0d0ece0cc93dbe6ac84974066830280ccacf5de13a8460524238cf543edfc5027aabeddc107e9fd429a25ce6f5d94917f2a6435380ee5f11353814'; 21 | var bitBundle = sjcl.codec.hex.toBits(bundle); 22 | 23 | return credentials.deriveBundleKeys(bundleKey, keyInfo) 24 | .then( 25 | function (keys) { 26 | assert.equal(sjcl.codec.hex.fromBits(keys.hmacKey), '17ab463653a94c9a6419b48781930edefe500395e3b4e7879a2be15999757022', '== hmacKey equal'); 27 | assert.equal(sjcl.codec.hex.fromBits(keys.xorKey), '85de16c3218a126404668bf9b7acfb6ce2b7e03c8889047ba48b8b854c6d8beb3ae100e145ca6d69cb519a872a83af788771954455716143bc08225ea8644d85', '== xorKey equal'); 28 | 29 | var keyAWrapB = credentials.xor(sjcl.bitArray.bitSlice(bitBundle, 0, 8 * 64), keys.xorKey); 30 | assert.equal(sjcl.codec.hex.fromBits(keyAWrapB), '61a0a7bd69f4a62d5a1f0f94e9a0ed86b358b1c3d67c98a352ad72da1b434da6f69a971df9c763a7c798a739404be60c8119a56c59bbae1e5d32a63efa26754a', '== xorBuffers equal'); 31 | var keyObj = { 32 | kA: sjcl.codec.hex.fromBits(sjcl.bitArray.bitSlice(keyAWrapB, 0, 8 * 32)), 33 | wrapKB: sjcl.codec.hex.fromBits(sjcl.bitArray.bitSlice(keyAWrapB, 8 * 32, 8 * 64)) 34 | }; 35 | 36 | return keyObj; 37 | } 38 | ).then( 39 | function(result) { 40 | assert.equal(result.kA, '61a0a7bd69f4a62d5a1f0f94e9a0ed86b358b1c3d67c98a352ad72da1b434da6', '== kA equal'); 41 | assert.equal(result.wrapKB, 'f69a971df9c763a7c798a739404be60c8119a56c59bbae1e5d32a63efa26754a', '== wrapKB equal'); 42 | } 43 | ); 44 | }); 45 | 46 | test('#vector 2', function () { 47 | 48 | var bundleKey = 'dedd009a8275a4f672bb4b41e14a117812c0b2f400c85fa058e0293f3f45726a'; 49 | var bundle = 'df4717238a738501bd2ad8f7114ef193ea69751a40108149bfb88a5643a8d683a1e75b705d4db135130f0896dbac0819ab7d54334e0cd4f9c945e0a7ada91899756cedf4384be404844050270310bc2b396f100eeda0c7b428cfe77c40a873ae'; 50 | return credentials.unbundleKeyFetchResponse(bundleKey, bundle) 51 | .then( 52 | function(result) { 53 | assert.equal(result.kA, '939282904b808c6003ea31aeb14bc766d2ab70ba7dcaa54f820efcf4762b9619', '== kA equal'); 54 | assert.equal(result.wrapKB, '849ac9f71643ace46dcdd384633ec1bffe565852806ee2f859c3eba7fafeafec', '== wrapKB equal'); 55 | } 56 | ); 57 | }); 58 | 59 | }); 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /tests/lib/uriVersion.js: -------------------------------------------------------------------------------- 1 | 2 | /* This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | define([ 7 | 'intern!tdd', 8 | 'intern/chai!assert', 9 | 'client/FxAccountClient' 10 | ], function (tdd, assert, FxAccountClient) { 11 | 12 | var xhr = function () {}; 13 | var serverUri = 'https://mock.server'; 14 | var VERSION = FxAccountClient.VERSION; 15 | 16 | with (tdd) { 17 | suite('fxa client', function () { 18 | test('#version appended to uri when not present', function () { 19 | var client = new FxAccountClient(serverUri, { xhr: xhr }); 20 | assert.equal(serverUri + '/' + VERSION, client.request.baseUri); 21 | }); 22 | 23 | test('#version not appended to uri when already present', function () { 24 | var uri = serverUri + '/' + VERSION; 25 | var client = new FxAccountClient(uri, { xhr: xhr }); 26 | assert.equal(uri, client.request.baseUri); 27 | }); 28 | }); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /tests/lib/verifyCode.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([ 6 | 'intern!tdd', 7 | 'intern/chai!assert', 8 | 'tests/addons/environment', 9 | 'tests/addons/sinon' 10 | ], function (tdd, assert, Environment, sinon) { 11 | 12 | with (tdd) { 13 | suite('verifyCode', function () { 14 | var respond; 15 | var mail; 16 | var client; 17 | var RequestMocks; 18 | var xhr; 19 | var xhrOpen; 20 | var xhrSend; 21 | 22 | beforeEach(function () { 23 | var env = new Environment(); 24 | respond = env.respond; 25 | mail = env.mail; 26 | client = env.client; 27 | RequestMocks = env.RequestMocks; 28 | xhr = env.xhr; 29 | xhrOpen = sinon.spy(xhr.prototype, 'open'); 30 | xhrSend = sinon.spy(xhr.prototype, 'send'); 31 | }); 32 | 33 | afterEach(function () { 34 | xhrOpen.restore(); 35 | xhrSend.restore(); 36 | }); 37 | 38 | test('#verifyEmail', function () { 39 | var user = 'test3' + new Date().getTime(); 40 | var email = user + '@restmail.net'; 41 | var password = 'iliketurtles'; 42 | var uid; 43 | 44 | return respond(client.signUp(email, password), RequestMocks.signUp) 45 | .then(function (result) { 46 | uid = result.uid; 47 | assert.ok(uid, 'uid is returned'); 48 | 49 | return respond(mail.wait(user), RequestMocks.mail); 50 | }) 51 | .then(function (emails) { 52 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 53 | assert.ok(code, 'code is returned'); 54 | 55 | return respond(client.verifyCode(uid, code), RequestMocks.verifyCode); 56 | }) 57 | .then( 58 | function (result) { 59 | assert.ok(result); 60 | }, 61 | assert.notOk 62 | ); 63 | }); 64 | 65 | test('#verifyEmailCheckStatus', function () { 66 | var user = 'test4' + new Date().getTime(); 67 | var email = user + '@restmail.net'; 68 | var password = 'iliketurtles'; 69 | var uid; 70 | var sessionToken; 71 | 72 | return respond(client.signUp(email, password), RequestMocks.signUp) 73 | .then(function (result) { 74 | uid = result.uid; 75 | assert.ok(uid, 'uid is returned'); 76 | 77 | return respond(client.signIn(email, password), RequestMocks.signIn); 78 | }) 79 | .then(function (result) { 80 | assert.ok(result.sessionToken, 'sessionToken is returned'); 81 | sessionToken = result.sessionToken; 82 | 83 | return respond(client.recoveryEmailStatus(sessionToken), 84 | RequestMocks.recoveryEmailUnverified); 85 | }) 86 | .then(function (result) { 87 | assert.equal(result.verified, false, 'Email should not be verified.'); 88 | 89 | return respond(mail.wait(user, 2), RequestMocks.mailUnverifiedSignin); 90 | }) 91 | .then(function (emails) { 92 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 93 | assert.ok(code, 'code is returned: ' + code); 94 | 95 | return respond(client.verifyCode(uid, code), 96 | RequestMocks.verifyCode); 97 | }) 98 | .then(function (result) { 99 | 100 | return respond(client.recoveryEmailStatus(sessionToken), 101 | RequestMocks.recoveryEmailVerified); 102 | }) 103 | .then( 104 | function (result) { 105 | assert.equal(result.verified, true, 'Email should be verified.'); 106 | }, 107 | assert.notOk 108 | ); 109 | }); 110 | 111 | test('#verifyEmail with service param', function () { 112 | var user = 'test5' + new Date().getTime(); 113 | var email = user + '@restmail.net'; 114 | var password = 'iliketurtles'; 115 | var uid; 116 | 117 | return respond(client.signUp(email, password), RequestMocks.signUp) 118 | .then(function (result) { 119 | uid = result.uid; 120 | assert.ok(uid, 'uid is returned'); 121 | 122 | return respond(mail.wait(user), RequestMocks.mail); 123 | }) 124 | .then(function (emails) { 125 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 126 | assert.ok(code, 'code is returned'); 127 | 128 | return respond(client.verifyCode(uid, code, { service: 'sync' }), 129 | RequestMocks.verifyCode); 130 | }) 131 | .then( 132 | function (result) { 133 | assert.ok(result); 134 | }, 135 | assert.notOk 136 | ); 137 | }); 138 | 139 | test('#verifyEmail with reminder param', function () { 140 | var user = 'test6' + new Date().getTime(); 141 | var email = user + '@restmail.net'; 142 | var password = 'iliketurtles'; 143 | var uid; 144 | 145 | return respond(client.signUp(email, password), RequestMocks.signUp) 146 | .then(function (result) { 147 | uid = result.uid; 148 | assert.ok(uid, 'uid is returned'); 149 | 150 | return respond(mail.wait(user), RequestMocks.mail); 151 | }) 152 | .then(function (emails) { 153 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 154 | assert.ok(code, 'code is returned'); 155 | 156 | return respond(client.verifyCode(uid, code, { reminder: 'first' }), 157 | RequestMocks.verifyCode); 158 | }) 159 | .then( 160 | function (result) { 161 | assert.ok(result); 162 | }, 163 | assert.notOk 164 | ); 165 | }); 166 | 167 | test('#verifyEmail with marketingOptIn param', function () { 168 | var user = 'test7' + new Date().getTime(); 169 | var email = user + '@restmail.net'; 170 | var password = 'iliketurtles'; 171 | var uid; 172 | 173 | return respond(client.signUp(email, password), RequestMocks.signUp) 174 | .then(function (result) { 175 | uid = result.uid; 176 | assert.ok(uid, 'uid is returned'); 177 | 178 | return respond(mail.wait(user), RequestMocks.mail); 179 | }) 180 | .then(function (emails) { 181 | var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; 182 | assert.ok(code, 'code is returned'); 183 | 184 | return respond(client.verifyCode(uid, code, { marketingOptIn: true }), 185 | RequestMocks.verifyCode); 186 | }) 187 | .then( 188 | function (result) { 189 | assert.ok(result); 190 | assert.equal(xhrOpen.args[2][0], 'POST', 'method is correct'); 191 | assert.include(xhrOpen.args[2][1], '/recovery_email/verify_code', 'path is correct'); 192 | var sentData = JSON.parse(xhrSend.args[2][0]); 193 | assert.equal(sentData.marketingOptIn, true); 194 | }, 195 | assert.notOk 196 | ); 197 | }); 198 | }); 199 | } 200 | }); 201 | -------------------------------------------------------------------------------- /tests/mocks/errors.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | define([], function () { 6 | return { 7 | // status code 400, errno 101: attempt to create an account that already exists 8 | accountExists: { 9 | status: 400, 10 | headers: {}, 11 | body: '{"code":400, "errno": 101}' 12 | }, 13 | // status code 400, errno 102: attempt to access an account that does not exist 14 | accountDoesNotExist: { 15 | status: 400, 16 | headers: {}, 17 | body: '{"code":400, "errno": 102}' 18 | }, 19 | // status code 400, errno 103: incorrect password 20 | accountIncorrectPassword: { 21 | status: 400, 22 | headers: {}, 23 | body: '{"code":400, "errno": 103, "message":"Incorrect password"}' 24 | }, 25 | // status code 400, errno 104: attempt to operate on an unverified account 26 | accountUnverified: { 27 | status: 400, 28 | headers: {}, 29 | body: '{"code":400, "errno": 104}' 30 | }, 31 | // status code 400, errno 105: invalid verification code 32 | invalidVerification: { 33 | status: 400, 34 | headers: {}, 35 | body: '{"code":400, "errno": 105}' 36 | }, 37 | // status code 400, errno 106: request body was not valid json 38 | invalidJson: { 39 | status: 400, 40 | headers: {}, 41 | body: '{"code":400, "errno": 106}' 42 | }, 43 | // status code 400, errno 107: request body contains invalid parameters 44 | requestInvalidParams: { 45 | status: 400, 46 | headers: {}, 47 | body: '{"code":400, "errno": 107}' 48 | }, 49 | // status code 400, errno 107: request body contains invalid parameters 50 | requestMissingParams: { 51 | status: 400, 52 | headers: {}, 53 | body: '{"code":400, "errno": 108}' 54 | }, 55 | // status code 401, errno 109: invalid request signature 56 | invalidRequestSignature: { 57 | status: 401, 58 | headers: {}, 59 | body: '{"code":401, "errno": 109}' 60 | }, 61 | // status code 401, errno 110: invalid authentication token 62 | invalidAuthToken: { 63 | status: 401, 64 | headers: {}, 65 | body: '{"code":401, "errno": 110}' 66 | }, 67 | // status code 401, errno 111: invalid authentication timestamp 68 | invalidAuthTimestamp: { 69 | status: 401, 70 | headers: {}, 71 | body: '{"code":401, "errno": 111}' 72 | }, 73 | // status code 411, errno 112: content-length header was not provided 74 | missingContentLength: { 75 | status: 411, 76 | headers: {}, 77 | body: '{"code":411, "errno": 112}' 78 | }, 79 | // status code 413, errno 113: request body too large 80 | requestTooLarge: { 81 | status: 413, 82 | headers: {}, 83 | body: '{"code":413, "errno": 113}' 84 | }, 85 | // status code 429, errno 114: client has sent too many requests (see backoff protocol) 86 | sentTooManyRequests: { 87 | status: 429, 88 | headers: {}, 89 | body: '{"code":429, "errno": 114}' 90 | }, 91 | // status code 429, errno 115: invalid authentication nonce 92 | invalidAuthNonce: { 93 | status: 401, 94 | headers: {}, 95 | body: '{"code":401, "errno": 115}' 96 | }, 97 | // status code 410, errno 116: endpoint is no longer supported 98 | endpointNotSupported: { 99 | status: 410, 100 | headers: {}, 101 | body: '{"code":410, "errno": 116}' 102 | }, 103 | // status code 400, errno 117: incorrect login method for this account 104 | incorrectLoginMethod: { 105 | status: 400, 106 | headers: {}, 107 | body: '{"code":400, "errno": 117}' 108 | }, 109 | // status code 400, errno 118: incorrect key retrieval method for this account 110 | incorrectKeyMethod: { 111 | status: 400, 112 | headers: {}, 113 | body: '{"code":400, "errno": 118}' 114 | }, 115 | // status code 400, errno 119: incorrect API version for this account 116 | incorrectAPIVersion: { 117 | status: 400, 118 | headers: {}, 119 | body: '{"code":400, "errno": 119}' 120 | }, 121 | // status code 400, errno 120: incorrect email case 122 | incorrectEmailCase: { 123 | status: 400, 124 | headers: {}, 125 | body: '{"code":400, "errno": 120, "email": "a@b.com"}' 126 | }, 127 | // status code 503, errno 201: service temporarily unavailable to due high load (see backoff protocol) 128 | temporarilyUnavailable: { 129 | status: 503, 130 | headers: {}, 131 | body: '{"code":503, "errno": 201}' 132 | }, 133 | // any status code, errno 999: unknown error 134 | unknownError: { 135 | status: 400, 136 | headers: {}, 137 | body: '{"code":400, "errno": 999}' 138 | }, 139 | timeout: { 140 | status: 400, 141 | headers: {}, 142 | body: '' 143 | }, 144 | badResponseFormat: { 145 | status: 404, 146 | headers: {}, 147 | body: 'Something is wrong.' 148 | }, 149 | signInBlocked: { 150 | status: 429, 151 | headers: {}, 152 | body: JSON.stringify({ 153 | code: 429, 154 | errno: 125, 155 | verificationMethod: 'email-captcha', 156 | verificationReason: 'login' 157 | }) 158 | }, 159 | signInInvalidUnblockCode: { 160 | status: 400, 161 | body: '{"code":400, "errno": 127}' 162 | } 163 | }; 164 | }); 165 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /* eslint-disable */ 6 | const path = require('path'); 7 | const webpack = require('webpack'); 8 | 9 | module.exports = { 10 | context: path.resolve(__dirname), 11 | entry: { 12 | 'fxa-client': './client/FxAccountClient', 13 | 'fxa-client.min': './client/FxAccountClient', 14 | }, 15 | 16 | output: { 17 | filename: '[name].js', 18 | library: 'FxAccountClient', 19 | libraryTarget: 'umd', 20 | path: path.resolve(__dirname, 'build'), 21 | publicPath: '/' 22 | }, 23 | 24 | resolve: { 25 | extensions: ['.js'], 26 | modules: [ 27 | path.resolve(__dirname, 'client') 28 | ], 29 | alias: { 30 | 'es6-promise': path.resolve(__dirname, 'node_modules/es6-promise/dist/es6-promise'), 31 | sjcl: path.resolve(__dirname, 'node_modules/sjcl/sjcl') 32 | } 33 | }, 34 | 35 | plugins: [ 36 | new webpack.optimize.UglifyJsPlugin({ 37 | include: /\.min\.js$/, 38 | output: { 39 | comments: false 40 | }, 41 | compress: { 42 | unsafe_comps: true, 43 | properties: true, 44 | keep_fargs: false, 45 | pure_getters: true, 46 | collapse_vars: true, 47 | unsafe: true, 48 | warnings: false, 49 | screw_ie8: true, 50 | sequences: true, 51 | dead_code: true, 52 | drop_debugger: true, 53 | comparisons: true, 54 | conditionals: true, 55 | evaluate: true, 56 | booleans: true, 57 | loops: true, 58 | unused: true, 59 | hoist_funs: true, 60 | if_return: true, 61 | join_vars: true, 62 | cascade: true, 63 | drop_console: true 64 | }, 65 | sourceMap: true 66 | }) 67 | ], 68 | 69 | node: { 70 | global: true, 71 | process: false, 72 | Buffer: false, 73 | __filename: false, 74 | __dirname: false, 75 | setImmediate: false 76 | }, 77 | 78 | module: {}, 79 | stats: { colors: true }, 80 | 81 | // See https://webpack.js.org/configuration/devtool/ to 82 | // configure source maps to personal preferences. 83 | devtool: 'source-map' 84 | }; 85 | /* eslint-enable */ 86 | --------------------------------------------------------------------------------