Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to manually checking transaction state #27

Merged
merged 16 commits into from
Mar 7, 2017
2 changes: 2 additions & 0 deletions lib/errors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ exports.NotEnrolledError = require('./not_enrolled_error');
exports.AuthMethodDisabledError = require('./auth_method_disabled_error');
exports.EnrollmentMethodDisabledError = require('./enrollment_method_disabled_error');
exports.InvalidEnrollmentError = require('./invalid_enrollment_error');
exports.InvalidStateError = require('./invalid_state_error');
exports.UnexpectedInputError = require('./unexpected_input_error');
16 changes: 16 additions & 0 deletions lib/errors/invalid_state_error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';

var object = require('../utils/object');
var GuardianError = require('./guardian_error');

function InvalidStateError(message) {
GuardianError.call(this, {
message: message,
errorCode: 'invalid_state'
});
}

InvalidStateError.prototype = object.create(GuardianError.prototype);
InvalidStateError.prototype.contructor = InvalidStateError;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contructor...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch


module.exports = InvalidStateError;
16 changes: 16 additions & 0 deletions lib/errors/unexpected_input_error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';

var object = require('../utils/object');
var GuardianError = require('./guardian_error');

function UnexpectedInputError(message) {
GuardianError.call(this, {
message: message,
errorCode: 'unexpected_input'
});
}

UnexpectedInputError.prototype = object.create(GuardianError.prototype);
UnexpectedInputError.prototype.contructor = UnexpectedInputError;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contructor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch


module.exports = UnexpectedInputError;
19 changes: 17 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ var auth0Ticket = require('./utils/auth0_ticket');
var httpClient = require('./utils/http_client');
var transactionFactory = require('./transaction/factory');
var clientFactory = require('./utils/client_factory');

var apiTransport = {
polling: 'polling',
manual: 'polling',
socket: 'socket'
};

/**
* @public
*
Expand Down Expand Up @@ -81,7 +88,9 @@ auth0GuardianJS.prototype.start = function start(callback) {

self.httpClient.post('/api/start-flow',
self.credentials,
{ state_transport: self.transport },
// TODO: polling is not a good name for api state checking since
// it could be polling or manual checking
{ state_transport: apiTransport[self.transport] },
function startTransaction(err, txLegacyData) {
if (err) {
callback(err);
Expand Down Expand Up @@ -144,14 +153,20 @@ auth0GuardianJS.prototype.start = function start(callback) {
*/

auth0GuardianJS.resume = function resume(options, transactionState, callback) {
var txId = jwtToken(transactionState.transactionToken).getDecoded().txid;
var transactionTokenObject = jwtToken(transactionState.transactionToken);
var txId = transactionTokenObject.getDecoded().txid;

// create httpClient/socketClient
var httpClientInstance = object.get(options, 'dependencies.httpClient',
httpClient(transactionState.baseUrl, txId));

var transport = options.transport || 'socket';

if (transactionTokenObject.isExpired()) {
asyncHelpers.setImmediate(callback, new errors.CredentialsExpiredError());
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3


var socketClient = clientFactory.create({
serviceUrl: transactionState.baseUrl,
transport: transport,
Expand Down
23 changes: 22 additions & 1 deletion lib/transaction/auth_verification_step.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,16 @@ authVerificationStep.prototype.getMethod = function getMethod() {
return this.strategy.method;
};

authVerificationStep.prototype.verify = function verify(data) {
/**
* @param {object} data
* @param {function} [acceptedCallback] Triggered once the data has been
* accepted by the service provider, the first arguments indicates if it
* was valid
*/
authVerificationStep.prototype.verify = function verify(data, acceptedCallback) {
// eslint-disable-next-line no-param-reassign
acceptedCallback = acceptedCallback || function noop() {};

var self = this;

// TODO Move this to the transaction
Expand Down Expand Up @@ -57,10 +66,16 @@ authVerificationStep.prototype.verify = function verify(data) {
verificationPayload = verificationPayload || {};

if (err) {
acceptedCallback(err);
done(err);
return;
}

// On the one hand we trigger the callback since the data has been
// accepted by the service provider, on the other hand
// we still need to wait for (loginOrRejectTask) to trigger the event.
// loginOrRejectTask might never be triggered in case of transport=manual
acceptedCallback();
done(null, {
// New recovery code if needed (recover)
recoveryCode: verificationPayload.recoveryCode
Expand Down Expand Up @@ -88,4 +103,10 @@ authVerificationStep.prototype.verify = function verify(data) {
});
};

authVerificationStep.prototype.serialize = function serialize() {
var self = this;

return { method: self.getMethod() };
};

module.exports = authVerificationStep;
24 changes: 22 additions & 2 deletions lib/transaction/enrollment_confirmation_step.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,18 @@ enrollmentConfirmationStep.prototype.getData = function getData() {
return this.strategy.getData();
};

enrollmentConfirmationStep.prototype.confirm = function confirm(data) {
enrollmentConfirmationStep.prototype.getMethod = function getMethod() {
return this.strategy.method;
};

/**
* @param {object} data
* @param {function} [acceptedCallback] Triggered once the data has been accepted by
* the service provider, the first argument indicates if it was valid
*/
enrollmentConfirmationStep.prototype.confirm = function confirm(data, acceptedCallback) {
// eslint-disable-next-line no-param-reassign
acceptedCallback = acceptedCallback || function noop() {};
var self = this;

// TODO Move this to the transaction
Expand All @@ -44,7 +55,10 @@ enrollmentConfirmationStep.prototype.confirm = function confirm(data) {
};

var confirmTask = function confirmTask(done) {
self.strategy.confirm(data, done);
self.strategy.confirm(data, function confirmationDataAccepted(err, result) {
acceptedCallback(err, { recoveryCode: self.enrollmentAttempt.getRecoveryCode() });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

passing the recovery code here don't quite convince me, but, how are you going to get the recovery code otherwise... making self.enrollmentAttempt.getRecoveryCode() public seems like opening the door too much considering that we are going to change how enrollmentAttempt works.

done(err, result);
});
};

asyncHelpers.all([
Expand Down Expand Up @@ -73,4 +87,10 @@ enrollmentConfirmationStep.prototype.confirm = function confirm(data) {
});
};

enrollmentConfirmationStep.prototype.serialize = function serialize() {
var self = this;

return { method: self.getMethod() };
};

module.exports = enrollmentConfirmationStep;
10 changes: 9 additions & 1 deletion lib/transaction/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ exports.fromStartFlow = function buildTransactionFromStartFlow(data, options) {
* @param {EventEmitter} options.transactionEventsReceiver
* Receiver for transaction events; it will receive backend related transaction events
*
* @param {object} transactionState.authVerificationStep
* @param {otp|push|sms} transactionState.authVerificationStep.method
*
* @param {object} transactionState.enrollmentConfirmationStep
* @param {otp|push|sms} transactionState.enrollmentConfirmationStep.method
*
* @param {HttpClient} options.HttpClient
*
* @returns {Transaction}
Expand All @@ -63,7 +69,9 @@ exports.fromTransactionState = function fromTransactionState(transactionState, o
transactionToken: jwtToken(transactionState.transactionToken),
enrollments: object.map(transactionState.enrollments, enrollment),
availableEnrollmentMethods: transactionState.availableEnrollmentMethods,
availableAuthenticationMethods: transactionState.availableAuthenticationMethods
availableAuthenticationMethods: transactionState.availableAuthenticationMethods,
authVerificationStep: transactionState.authVerificationStep,
enrollmentConfirmationStep: transactionState.enrollmentConfirmationStep
};

if (transactionState.enrollmentAttempt) {
Expand Down
110 changes: 109 additions & 1 deletion lib/transaction/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ var otpAuthStrategy = require('../auth_strategies/otp_auth_strategy');
var eventSequencer = require('../utils/event_sequencer');
var eventListenerHub = require('../utils/event_listener_hub');

var VALID_METHODS = ['otp', 'sms', 'push'];

/**
* @public
*
Expand All @@ -35,6 +37,12 @@ var eventListenerHub = require('../utils/event_listener_hub');
* @param {array.<string>} data.availableEnrollmentMethods
* @param {JWTToken} data.transactionToken
*
* @param {object} [data.authVerificationStep]
* @param {otp|push|sms} data.authVerificationStep.method
*
* @param {object} [data.enrollmentConfirmationStep]
* @param {otp|push|sms} data.enrollmentConfirmationStep.method
*
* @param {EventEmitter} options.transactionEventsReceiver
* Receiver for transaction events; it will receive backend related transaction events
* @param {HttpClient} options.HttpClient
Expand Down Expand Up @@ -133,11 +141,80 @@ function transaction(data, options) {
self.eventSequencer.emit('error', err);
});

self.authVerificationStep = data.authVerificationStep;
self.enrollmentConfirmationStep = data.enrollmentConfirmationStep;

if (data.enrollmentConfirmationStep && data.enrollmentConfirmationStep.method) {
self.enrollmentConfirmationStep = self.buildEnrollmentConfirmationStep(
data.enrollmentConfirmationStep);
}

if (data.authVerificationStep && data.authVerificationStep.method) {
self.authVerificationStep = self.buildAuthVerificationStep(
data.authVerificationStep);
}

return self;
}

transaction.prototype = object.create(EventEmitter.prototype);

/**
* @param {object} data
* @param {otp|push|sms} data.method
*/
transaction.prototype.buildAuthVerificationStep = function buildAuthVerificationStep(data) {
var self = this;

if (typeof data !== 'object') {
throw new errors.UnexpectedInputError('Expected data to be an object');
}

if (typeof VALID_METHODS.indexOf(data.method) < 0) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add test for invalid method in the serialized transaction

throw new errors.UnexpectedInputError('Expected data.method to be one of ' +
VALID_METHODS.join(', ') + ' but found ' + data.method);
}

if (!self.isEnrolled()) {
throw new errors.InvalidStateError('Expected user to be enrolled');
}

return authVerificationStep(self.authStrategies[data.method], {
loginCompleteHub: self.loginCompleteHub,
loginRejectedHub: self.loginRejectedHub,
transaction: self
});
};

/**
* @param {object} data
* @param {otp|push|sms} data.method
*/
transaction.prototype.buildEnrollmentConfirmationStep =
function buildEnrollmentConfirmationStep(data) {
var self = this;

if (typeof data !== 'object') {
throw new errors.UnexpectedInputError('Expected data to be an object');
}

if (typeof VALID_METHODS.indexOf(data.method) < 0) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add test for invalid method in the serialized transaction

throw new errors.UnexpectedInputError('Expected data.method to be one of ' +
VALID_METHODS.join(', ') + ' but found ' + data.method);
}

if (!self.enrollmentAttempt) {
throw new errors.InvalidStateError('Expected enrollment attempt to be present: ' +
'try calling .enroll method first');
}
return enrollmentConfirmationStep({
strategy: self.enrollmentStrategies[data.method],
enrollmentAttempt: self.enrollmentAttempt,
enrollmentCompleteHub: self.enrollmentCompleteHub,
transaction: self
});
};

/**
* @Public
*
Expand All @@ -148,18 +225,27 @@ transaction.prototype = object.create(EventEmitter.prototype);
*
*/
transaction.prototype.serialize = function serialize() {
var self = this;

function enrollmentSerialize(enrollment) {
return enrollment.serialize();
}

return {
var data = {
transactionToken: this.transactionToken.getToken(),
enrollmentAttempt: this.enrollmentAttempt ? this.enrollmentAttempt.serialize() : undefined,
enrollments: object.map(this.enrollments, enrollmentSerialize),
baseUrl: this.httpClient.getBaseUrl(),
availableEnrollmentMethods: this.availableEnrollmentMethods,
availableAuthenticationMethods: this.availableAuthenticationMethods
};

data.authVerificationStep = self.authVerificationStep
? self.authVerificationStep.serialize() : null;
data.enrollmentConfirmationStep = self.enrollmentConfirmationStep
? self.enrollmentConfirmationStep.serialize() : null;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move 236, to:

data.enrollmentAttempt: this.enrollmentAttempt ? this.enrollmentAttempt.serialize() : undefined

return data;
};


Expand Down Expand Up @@ -220,6 +306,8 @@ transaction.prototype.enroll = function enroll(method, data, callback) {
enrollmentAttempt: self.enrollmentAttempt
});

self.enrollmentConfirmationStep = confirmationStep;

confirmationStep.once('enrollment-complete', function onEnrollmentComplete(payload) {
self.addEnrollment(payload.enrollment);
self.eventSequencer.emit('enrollment-complete', payload);
Expand Down Expand Up @@ -414,8 +502,28 @@ transaction.prototype.requestStrategyAuth = function requestStrategyAuth(strateg
self.eventSequencer.emit('error', error);
});

self.authVerificationStep = verificationStep;

callback(null, verificationStep);
});
};

transaction.prototype.getAuthVerficationStep = function getAuthVerficationStep() {
Copy link
Contributor Author

@dafortune dafortune Mar 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be easily converted in @nikolaseu proposal of adding the method verifyAuth to the transaction, just call getAuthVerficationStep().verify(...) on that method.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in "getAuthVerficationStep()", missing "i" in verification

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if (!self.authVerificationStep) {
throw new errors.InvalidStateError('cannot get enrollment confirmation ' +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistent capitalization -- "Cannot get..."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

'step, you must call .requestAuth first');
}

return self.authVerificationStep;
};

transaction.prototype.getEnrollmentConfirmationStep = function getEnrollmentConfirmationStep() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be easily converted in @nikolaseu proposal of adding the method confirmEnrollment to the transaction, just call getEnrollmentConfirmationStep().confirm(...) on that method.

if (!self.enrollmentConfirmationStep) {
throw new errors.InvalidStateError('cannot get enrollment confirmation step, ' +
'you must call .enroll first');
}

return self.enrollmentConfirmationStep;
};

module.exports = transaction;
3 changes: 3 additions & 0 deletions lib/utils/client_factory.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
var polling = require('./polling_client');
var socketio = require('./socket_client');
var nullClient = require('./null_client');

exports.create = function create(options) {
var serviceUrl = options.serviceUrl;
Expand All @@ -13,6 +14,8 @@ exports.create = function create(options) {

if (transport === 'polling') {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to replace this by @thoper proposed name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We support both, internally I keep using transport not to spread the changes.

return polling(serviceUrl, { httpClient: httpClient });
} else if (transport === 'manual') {
return nullClient();
}

// default socket
Expand Down
Loading