diff --git a/app/services/github-state.js b/app/services/github-state.js new file mode 100644 index 000000000..c9dd5857d --- /dev/null +++ b/app/services/github-state.js @@ -0,0 +1,90 @@ +import Ember from 'ember'; + +const { get, Service, inject: { service } } = Ember; + +/** + * Converts an integer to a hex string and returns the last two characters as + * a string. + * + * @method integerToHex + * @param {Integer} integer An integer to convert + * @return {String} A hexadecimal string of length 2. + */ +function integerToHex(integer) { + return (`0${integer.toString(16)}`.substr(-2)); +} + +/** + * Generates a randomized array of decimals, converts them to hexadecimal values + * and joins into a string in order to output a random string of specified + * length. + * + * @method generateRandomString + * @param {Integer} length The length of the random string to generate + * @return {String} A random string of the specified length + */ +function generateRandomString(length = 40) { + let unsignedInt8Array = new window.Uint8Array(length / 2); + let randomizedArray = window.crypto.getRandomValues(unsignedInt8Array); + return Array.from(randomizedArray).map(integerToHex).join(''); +} + +/** + * Service used to protect against cross site request forgery during the github + * connect process. The two methods, `generate()` and `validate(state)` are + * served to initialy generate and then later validate a `state` string used + * in the process. + * + * @class GithubStateService + * @module code-corps-ember/services/github-state + * @extends Ember.Service + * @uses SessionService + * @public + */ +export default Service.extend({ + /** + * We use the injected session service to store the `state` in a way that + * persists across tabs + * + * @property session + * @type Ember.Service + * @private + */ + session: service(), + + /** + * Generates and returns a random string. The string is also stored into the + * user's session. + * + * This string can then be used as a `state` variable when navigating to + * github's connect URL. Github will then return it upon succesful approval, + * so it can be validated using the sibling `validate(state)` function. + * + * @method generate + * @return {String} A randomly generated string. The string is also stored in the user's session. + * @public + */ + generate() { + let githubState = generateRandomString(); + // session service overrides `set`, so we need to use it directly + get(this, 'session').set('data.githubState', githubState); + return githubState; + }, + + /** + * Validates a state string. + * + * The string will be compared against another string, generated by the + * sibling `generate()` function and stored into the current user's session, + * to determine if it's valid or not. + * + * @method validate + * @param {String} stateToCheck The string to check if valid + * @return {Boolean} True if the provided string is also the one stored in the user's session, false otherwise. + * @public + */ + validate(stateToCheck) { + let state = get(this, 'session.data.githubState'); + return state && state === stateToCheck; + } +}); diff --git a/tests/unit/services/github-state-test.js b/tests/unit/services/github-state-test.js new file mode 100644 index 000000000..419aff10c --- /dev/null +++ b/tests/unit/services/github-state-test.js @@ -0,0 +1,38 @@ +import { moduleFor, test } from 'ember-qunit'; +import Test from 'ember-simple-auth/authenticators/test'; +import setupSession from 'ember-simple-auth/initializers/setup-session'; +import setupSessionService from 'ember-simple-auth/initializers/setup-session-service'; + +moduleFor('service:github-state', 'Unit | Service | github state', { + needs: ['service:session'], + beforeEach() { + this.register('authenticator:test', Test); + setupSession(this.registry); + setupSessionService(this.registry); + } +}); + +test('validates a generated "state" as correct', function(assert) { + assert.expect(1); + + let service = this.subject(); + let state = service.generate(); + + assert.ok(service.validate(state), 'Validation passed.'); +}); + +test('validates some random "state" as incorrect', function(assert) { + assert.expect(1); + + let state = 'some random string'; + let service = this.subject(); + assert.notOk(service.validate(state), 'Validation failed.'); +}); + +test('validates undefined and null as incorrect, even though stored state is also undefined', function(assert) { + assert.expect(2); + + let service = this.subject(); + assert.notOk(service.validate(undefined), 'Validation failed for undefined.'); + assert.notOk(service.validate(null), 'Validation failed for null.'); +});