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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(async): Refactor auth server routes to async/await #2286

Merged
merged 4 commits into from
Aug 26, 2019

Conversation

jaredhirsch
Copy link
Member

@jaredhirsch jaredhirsch commented Aug 21, 2019

Promises and async/await: an asynchronous adventure

We're going to convert some auth-server routes from promises to async. The hapi route handlers are already async functions, so hopefully this should be pretty straightforward 馃.

Preliminaries

Bluebird (our promise library) is not compatible with async/await: petkaantonov/bluebird#1434

However, the auth route handlers are already async functions, so we might not need to worry about this for today. At least, the tests passed when I made some changes 馃槵

Converting Promise code to async/await code

The await keyword unwraps a Promise, converting a resolved promise into its value, and a rejected promise into an error.

The async keyword wraps a function in a Promise: return value becomes a resolved promise, thrown error becomes a rejected promise.

Remember: you can mix async and Promise code, because async functions return Promises. You don't have to convert a whole chain of functions at once.

Some nice things about async/await

  • Removes promise nesting due to conditional responses to intermediate values

  • Allows sync and async calls to be handled by the same error handling code

  • Escape from the 'pyramid of doom'

  • TODO other nice stuff

Some gotchas:

  • Things get a little tricky with error handling return foo() (returns rejected promise) vs return await foo() (throws). Simpler to understand if you separate out assigning the awaited value and returning the value.

  • If you want to parallelize, don't await serially. Instead, assign the promises and await Promise.all(slowThings).

More at MDN: https://mdn.io/asyncawait

A crude replacement algorithm

When you see a promise chain, turn the function outside the then/catch chain into an async function.

If you have a .catch callback, move the .then and .catch callbacks into a try-catch block.

Then, await the promise instead of chaining to get to the next step.

If a value is passed from promise to promise, assign its awaited value.

If the value of the whole promise chain is returned, return foo().then(bar), split this into assignment and returning the awaited value: const result = await foo().then(bar); return result.

Auth server routes: how deeply nested?

We have a few that are 8+ levels deep:

 ]$ echo "8 levels deep: " && ack "                .then"
8 levels deep: 
emails.js
687:                .then(() => {

recovery-key.js
159:                .then(() => db.accountRecord(email))
160:                .then(result => (uid = result.uid));

utils/signin.js
145:                .then(code => {
153:                .then(() => {
165:                    .then(() => {

sign.js
109:                .then(result => {

password.js
68:                .then(match => {
82:                .then(wrapKb => {
90:                    .then(keyFetchToken => {
95:                        .then(passwordChangeToken => {
251:                .then(() => {

account.js
1212:                .then(accountData => (account = accountData));
1238:                .then(result => (wrapKb = result));
1315:                  .then(result => {

Let's walk through one together

I've started hacking on password.js to open this PR. We can make some more changes together.

image

Do the thing

Grab a file and put your name next to it, let's get to hackin.

Open PRs against this async-conversion branch, not against master 馃憤

  • account.js
  • attached-clients.js (already using async/await)
  • defaults.js
  • devices-and-sessions.js @philbooth, refactor(api): prefer async/await in device/session routes聽#2301
  • emails.js @chenba
  • idp.js (made no promises)
  • index.js (made no promises)
  • oauth.js (already using async/await)
  • password.js @6a68 working on this one
  • recovery-codes.js
  • recovery-key.js
  • security-events.js (already using async/await)
  • session.js
  • sign.js @dannycoates
  • signin-codes.js
  • sms.js
  • subscriptions.js (already using async/await)
  • support.js (already using async/await)
  • token-codes.js
  • totp.js
  • unblock-codes.js PR refactor(unblock-codes): Convert unblock-codes.js to async/await 聽#2292
  • util.js
  • utils/clients.js (made no promises)
  • utils/email.js (made no promises)
  • utils/oauth.js (already using async/await)
  • utils/request_helper.js (made no promises)
  • utils/signin.js
  • utils/signup.js
  • utils/subscriptions.js (already using async/await)
  • utils/totp.js
  • validators.js (made no promises)

@jaredhirsch
Copy link
Member Author

Phil suggested we just land these refactorings as they are completed, since the changes are going to bit rot otherwise. I've copied the checklist into #2305 for future work.

I reviewed the changes other than password.js, could use a review for that one (and any others people have time to glance at).

@jaredhirsch jaredhirsch requested a review from a team August 22, 2019 20:17
@jaredhirsch jaredhirsch changed the title Async/await workshop PR chore(async): Refactor auth server routes to async/await Aug 22, 2019
uid,
};

await mailer.sendPostRemoveTwoStepAuthNotification(
Copy link
Member

Choose a reason for hiding this comment

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

Would be good to have @rfk verify this should be waited on here, as previously the promise was not returned to be waited on.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hrm; to be honest I don't recall. Most of the places where we deliberately don't await a promise have a comment about why. I guess the only reason not to wait here would be to avoid a mail-sending error from "failing" the entire request, making it look like we failed to update the account when we really did. If we're worried about that, I think it would be better to explicitly await and then catch-and-ignore the error.

// cases where the user started creating a token but never completed.
if (!token.verified) {
await db.deleteTotpToken(sessionToken.uid);
}
Copy link
Member

Choose a reason for hiding this comment

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

Lost the else statement here, exists should only be true if the token is verified.

Copy link
Contributor

@rfk rfk left a comment

Choose a reason for hiding this comment

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

This looks OK to me 馃憤

oldAuthPW,
emailRecord.authSalt,
emailRecord.verifierVersion
);
Copy link
Contributor

Choose a reason for hiding this comment

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

I know it was like this already, but I don't understand why we need to create a second Password instance here with same parameters; could we just re-use the value of password from above?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, I can clean this up (and the other spots you called out). Thanks for the suggestions 馃憤

}
});
const devices = await request.app.devices;
devicesToNotify = devices;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we could use devicesToNotify = await request.app.devices directly here rather than using a separate const?

return result;
});
});
const hex = await random.hex(32); // TODO why is this async?
Copy link
Contributor

Choose a reason for hiding this comment

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

Re: the TODO, crypto.randomBytes has an async mode that IIRC is actually worth doing if you're calling it a lot, to avoid janking the event loop.

});
}
const accountData = await db.account(passwordChangeToken.uid);
account = accountData;
Copy link
Contributor

Choose a reason for hiding this comment

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

As above, I don't think we need to intermediate const here.

uid,
};

await mailer.sendPostRemoveTwoStepAuthNotification(
Copy link
Contributor

Choose a reason for hiding this comment

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

Hrm; to be honest I don't recall. Most of the places where we deliberately don't await a promise have a comment about why. I guess the only reason not to wait here would be to avoid a mail-sending error from "failing" the entire request, making it look like we failed to update the account when we really did. If we're worried about that, I think it would be better to explicitly await and then catch-and-ignore the error.

packages/fxa-auth-server/lib/routes/util.js Show resolved Hide resolved
@jaredhirsch jaredhirsch force-pushed the async-conversion branch 2 times, most recently from 86e1038 to a65c70b Compare August 26, 2019 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants