Skip to content

Commit

Permalink
Merge pull request #1282 from code-corps/1265-github-state-service
Browse files Browse the repository at this point in the history
Add github-state service for generating and validating the connect state parameter
  • Loading branch information
begedin committed May 18, 2017
2 parents 6048419 + 5a53a31 commit 0a88bce
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 0 deletions.
90 changes: 90 additions & 0 deletions app/services/github-state.js
Original file line number Diff line number Diff line change
@@ -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;
}
});
38 changes: 38 additions & 0 deletions tests/unit/services/github-state-test.js
Original file line number Diff line number Diff line change
@@ -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.');
});

0 comments on commit 0a88bce

Please sign in to comment.