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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,12 @@

**Added**
- Serialize and resume to guardianJs transactions [\#24](https://github.com/auth0/auth0-guardian.js/pull/24) ([joseluisdiaz](https://github.com/joseluisdiaz))

## [v1.0.0](https://github.com/auth0/auth0-guardian.js/tree/v0.4.0) (2017-03-01)
[Full Changelog](https://github.com/auth0/auth0-guardian.js/compare/v1.0.0...v0.4.0)

**Added**
- Support manually checking server side state [\#27](https://github.com/auth0/auth0-guardian.js/pull/27) ([dafortune](https://github.com/dafortune)):
* Manual transaction state checking: call `transaction.getState` to get the state without relying on an open websocket or automatic polling.
* Know the result of otp code validation (SMS / TOTP) without relying on a socket.
* Allow to confirm the enrollment after serializing the transaction.
306 changes: 306 additions & 0 deletions docs/server-side.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
# Using Guardian JS Client from server side

You can use Guardian Client from server side, by default Guardian Client from server side
works exactly like in the browser. Meaning, it opens a web socket connection through socket.io
and uses it to interact with the server exactly as in the client side.

However, for certain use cases it is better not to have this automatic behavior, in particular,
you might want to manually check the state of the transaction and avoid not necessary external
connections or use a polling approach instead of the classic socket-based approach.

## Using manual transaction state check instead of websocket or polling

To manually check the state of the transaction you need to configure guardian JS
not to use a socket client, you can do so by setting `stateCheckingMechanism` to `manual` in
the configuration.

Keep in mind, that by doing so you will stop getting automatic events, i.e. you won't receive
`enrollment-complete` and `auth-response` events and their handlers won't be executed automatically
when the transaction state changes on Guardian servers.

Enrollment is the only valid scenario for server side usage.

### Serializing a transaction
Serializing a transaction allows you to save an active transaction and resume it later if it is not
expired, it is specially suitable for server-side usage were you might want to save a transaction
once the request finishes and resume it in a follow up request.

The following method will create a new transaction if a ticket is available or try to resume it
from session.

```js
function getGuardianTransaction(req, options, cb) {
options = options || {};

if (options.ticket) {
// Start a new transaction if a ticket is available

return void guardianJSBuilder({
serviceUrl: process.env.GUARDIAN_URL,
ticket: options.ticket,

issuer: {
label: env.GUARDIAN_ISSUER_LABEL,
name: env.GUARDIAN_ISSUER_NAME
},

accountLabel: req.user.email,

stateCheckingMechanism: 'manual'
}).start(cb);
}

if (req.session.mfaTx) {
// Resume a transaction if it is available in session

return void guardianJSBuilder.resume({ stateCheckingMechanism: 'manual' }, req.session.mfaTx, cb);
}

cb(new HttpError(403, 'no_session_active', 'There is no valid mfa transaction active, nor ticket provided'));
}
```

### Enrollment example 1: supports all methods using manual state checking mechanism

```js
// WARNING 1: This is an advanced example, before using it consider if the
// hosted-page option does not match you use case, they are easier to implement
Copy link

Choose a reason for hiding this comment

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

s/you/your/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is your environment not yours enrollment. Good catch :)

// and better suited for most use cases.
//
// WARNING 2: POST Methods require CSRF protection, don't forget to add them or
// you will be at risk of CSRF attacks, this example left them out for simplicity
// reasons and because there are many different ways to add them that
// are really specific to your environment.

'use strict'

const guardianJSBuilder = require('auth0-guardian-js');

router.post('/api/mfa/guardian/start', function(req, res, next) {
async.waterfall([
cb => auth0api.createGuardianEnrollmentTicket(req.user.id, cb),

(ticket, cb) => getGuardianTransaction(req, { ticket: req.body.ticket }, (err, transaction) => {
if (err) { return void cb(err); }

cb(null, transaction);
})
], (err, transaction) => {
if (err) { return void next(err); }

req.session.mfaTx = transaction.serialize();

res.sendStatus(204);
});
});

router.post('/api/mfa/guardian/enroll',

// Validates input information this middleware could be removed since
// guardian-js will make many of the validations though it is not recommended
validateEnrollInput,

function(req, res, next) {
async.waterfall([
(cb) => getGuardianTransaction(req, null, (err, transaction) => {
if (err) { return void cb(err); }

cb(null, transaction);
}),

(transaction, cb) => transaction.enroll(
req.body.method,
req.body.data,
(err, enrollment) => cb(err, transaction, enrollment)
)
], (err, transaction, enrollment) => {
if (err) {
return void next(err);
}

req.session.mfaTx = transaction.serialize();

res.json({
method: req.body.method,
uri: enrollment.getUri()
});
});
});

router.post('/api/mfa/guardian/confirm',

// Validates input information this middleware could be removed since
// guardian-js will make many of the validations though it is not recommended
validateConfirmInput,

function(req, res, next) {
getGuardianTransaction(req, null, (err, transaction) => {
transaction.getEnrollmentConfirmationStep().confirm({ otpCode: req.body.otpCode }, (err, data) => {
if (err) {
return void next(err);
}

req.session.mfaTx = transaction.serialize();

res.sendStatus(204);
});
})
});

// Poll this endpoint to get transaction state
router.post('/api/mfa/guardian/state', function(req, res, next) {
getGuardianTransaction(req, null, (err, transaction) => {
if (err) {
next(err);
}

transaction.getState(function(err, state) {
if (err) { return void next(err); }

if (state.enrollment) {
req.session.mfaTx = null;
}

res.json({
enrolled: !!state.enrollment
});
});
});
});
```

### Enrollment example 2: supports otp and sms methods no polling needed

```js
// WARNING 1: This is an advanced example, before using it consider if the
// hosted-page option does not match you use case, they are easier to implement
// and better suited for most use cases.
//
// WARNING 2: POST Methods require CSRF protection, don't forget to add them or
// you will be at risk of CSRF attacks, this example left them out for simplicity
// reasons and because there are many different ways to add them that
// are really specific to your enrollment.

'use strict'

const guardianJSBuilder = require('auth0-guardian-js');

router.post('/api/mfa/guardian/enroll',

// Validates input information this middleware could be removed since
// guardian-js will make many of the validations though it is not recommended
validateEnrollInput,

function(req, res, next) {
async.waterfall([
(cb) => getGuardianTransaction(req, { ticket: req.body.ticket }, (err, transaction) => {
if (err) { return void cb(err); }

cb(null, transaction);
}),

(transaction, cb) => transaction.enroll(
req.body.method,
req.body.data,
(err, enrollment) => cb(err, transaction, enrollment)
)
], (err, transaction, enrollment) => {
if (err) {
return void next(err);
}

req.session.mfaTx = transaction.serialize();

res.json({
method: req.body.method,
uri: enrollment.getUri()
});
});
});

router.post('/api/mfa/guardian/confirm',

// Validates input information this middleware could be removed since
// guardian-js will make many of the validations though it is not recommended
validateConfirmInput,

function(req, res, next) {
getGuardianTransaction(req, null, (err, transaction) => {
transaction.getEnrollmentConfirmationStep().confirm({ otpCode: req.body.otpCode }, (err, data) => {
if (err) {
return void next(err);
}

req.session.mfaTx = null;

res.sendStatus(204);
});
})
});
});
```

### Discussion: Sockets / polling vs manual transaction state checking
TL;DR You may want to use manual transaction state checking when you don't want
to keep an open socket nor poll the service for changes and you are not using
push notifications or you want to control the checking interval on you own.

#### Sockets / polling
Keeping an open socket (or polling) server side involves certain complexities you will have
to deal with, on the one hand, every socket is an open connection to handle, also
a socket is inherently connected to a single node, which will need to keep a transaction
"in memory" to handle the events once they arrive (probably sending them
back to the browser client) this means that if that particular node fails the whole
transaction will fail. You can deal with that in many different ways:

- using sticky sessions: this ensures that all your connections for a given
user/browser are handled by the same node, but if the node fails, the whole
transaction is aborted, also sticky sessions are difficult to scale under certain
scenarios.

- by serializing and reusing the transaction in different nodes but ensuring
that only one node is listening to the events, that way the load can be
distributed across your nodes, but only one node will handle the transaction-state
change events, meaning that if that node fails the whole transaction is aborted.

- by serializing and reusing the transaction in different nodes but letting all
of them open a socket (or poll) for events. This way, even if a node fails the other
nodes can still respond and the transaction won't be aborted. This is probably the
most complex scenario, since more than one node will receive the events you need to
make sure the handlers are idempotent meaning that executing them more than once
over the same input has the same effect as executing them only once.

All of these scenarios involve an "in memory" transaction in at least one of your
nodes.

#### Manual transaction state checking
Keeping an open socket or polling might not be the right solution for all the
scenarios, sometimes you might be able to guess fairly well (based on external
conditions) when an certain transaction might have succeed and you might want
to check only in such case: for example, if you don't want to use push notifications
but instead just OTP / SMS, you know that once you send the otp code and it has been
received by the server the transaction is succeed or rejected. Also, even if you
use push notifications, you might want to avoid automatic polling and control transaction
state check based on external conditions, for example, letting the client control
the polling rhythm by calling a transaction-state check endpoint under certain limits.

For such cases you can manually check the state of the transaction, this combined with
transaction serialization might help you solve complex scenarios and in particular
the ones described above:

- You don't need to keep a transaction in memory until transaction timeout:
simply serialize it and store in your preferred db, when you are ready to check
the state deserialize it and check its state as many times as you want.

- You can reuse the same transaction in different nodes: you don't need session
stickiness, if you have the serialized transaction in a common source of truth for
all your nodes different nodes can handle the load for a particular transaction simply
deserializing it and checking its state.

- You can respond synchronously under certain conditions: if you don't need push
notifications you know that once your otp code has been received by the server
your transaction is either accepted or rejected and so you can respond synchronously
to otp verification calls.



2 changes: 1 addition & 1 deletion lib/errors/already_enrolled_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ function AlreadyEnrolledError() {
}

AlreadyEnrolledError.prototype = object.create(GuardianError.prototype);
AlreadyEnrolledError.prototype.contructor = AlreadyEnrolledError;
AlreadyEnrolledError.prototype.constructor = AlreadyEnrolledError;

module.exports = AlreadyEnrolledError;
2 changes: 1 addition & 1 deletion lib/errors/auth_method_disabled_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ function AuthMethodDisabledError(method) {
}

AuthMethodDisabledError.prototype = object.create(GuardianError.prototype);
AuthMethodDisabledError.prototype.contructor = AuthMethodDisabledError;
AuthMethodDisabledError.prototype.constructor = AuthMethodDisabledError;

module.exports = AuthMethodDisabledError;
2 changes: 1 addition & 1 deletion lib/errors/credentials_expired_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ function CredentialsExpiredError() {
}

CredentialsExpiredError.prototype = object.create(GuardianError.prototype);
CredentialsExpiredError.prototype.contructor = CredentialsExpiredError;
CredentialsExpiredError.prototype.constructor = CredentialsExpiredError;

module.exports = CredentialsExpiredError;
2 changes: 1 addition & 1 deletion lib/errors/enrollment_method_disabled_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ function EnrollmentMethodDisabledError(method) {
}

EnrollmentMethodDisabledError.prototype = object.create(GuardianError.prototype);
EnrollmentMethodDisabledError.prototype.contructor = EnrollmentMethodDisabledError;
EnrollmentMethodDisabledError.prototype.constructor = EnrollmentMethodDisabledError;

module.exports = EnrollmentMethodDisabledError;
2 changes: 1 addition & 1 deletion lib/errors/field_required_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ function FieldRequiredError(field) {
}

FieldRequiredError.prototype = object.create(GuardianError.prototype);
FieldRequiredError.prototype.contructor = FieldRequiredError;
FieldRequiredError.prototype.constructor = FieldRequiredError;

module.exports = FieldRequiredError;
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');
2 changes: 1 addition & 1 deletion lib/errors/invalid_enrollment_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ function InvalidEnrollmentError() {
}

InvalidEnrollmentError.prototype = object.create(GuardianError.prototype);
InvalidEnrollmentError.prototype.contructor = InvalidEnrollmentError;
InvalidEnrollmentError.prototype.constructor = InvalidEnrollmentError;

module.exports = InvalidEnrollmentError;
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.constructor = InvalidStateError;

module.exports = InvalidStateError;
2 changes: 1 addition & 1 deletion lib/errors/method_not_found_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ function MethodNotFoundError(method) {
}

MethodNotFoundError.prototype = object.create(GuardianError.prototype);
MethodNotFoundError.prototype.contructor = MethodNotFoundError;
MethodNotFoundError.prototype.constructor = MethodNotFoundError;

module.exports = MethodNotFoundError;
Loading