-
Notifications
You must be signed in to change notification settings - Fork 39
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
dafortune
merged 16 commits into
auth0:master
from
dafortune:allow-manual-transaction-checking
Mar 7, 2017
Merged
Changes from 15 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
588d883
Added null client
dafortune a241a1d
Add serialization and behavior changes
dafortune e5bab1a
Add tests for callbacks
dafortune b29b3c2
Add tests for resume and enrollmentCompletion/VerificationStep
dafortune 06c4ce7
Add tests for serialization
dafortune d77033d
Fix issue with enrollment
dafortune 8fed2bf
Add emitted error ignored
dafortune 2e3b535
Rename stateCheckingMechanism
dafortune e1f0822
Add docs
dafortune e649bff
Fix noop issue
dafortune 8294dc7
Add tests
dafortune 0015778
Fix casing
dafortune 6a3832f
Fix typo
dafortune 2291036
Fix docs typo
dafortune 0d6d662
Fix doc typo
dafortune 6d9b19e
Style fixes
dafortune File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
// 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. | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/you/your/
There was a problem hiding this comment.
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 :)