Skip to content
This repository was archived by the owner on Apr 3, 2019. It is now read-only.

feat(codes): add support for verifying token short code#287

Merged
vladikoff merged 2 commits intomasterfrom
feature.signincodes
Nov 27, 2017
Merged

feat(codes): add support for verifying token short code#287
vladikoff merged 2 commits intomasterfrom
feature.signincodes

Conversation

@vbudhram
Copy link
Contributor

@vbudhram vbudhram commented Nov 20, 2017

This PR exposes new APIs to create and verify sessionTokens using a short code. Couple notes

  • Each code has an expiration date
  • Codes are stored as hashes in database
  • Support for resending (aka regenerating) a code to come in later PR.
  • New endpoint verifyTokensShortCode used to explicitly verify shortCodes.

Connects with mozilla/fxa-auth-server#2218

Feature doc: https://docs.google.com/document/d/1xQtCGBJ9XZuF1q9faZV3YIiOnxomQMHKiTP62YSf3v8/edit

@mozilla/fxa-devs r?

@vbudhram vbudhram added the WIP label Nov 20, 2017
@vbudhram vbudhram self-assigned this Nov 20, 2017
@ghost ghost added the waffle:active label Nov 20, 2017
@vbudhram vbudhram force-pushed the feature.signincodes branch from 2bc32ba to bb4a7d7 Compare November 21, 2017 03:01
@vbudhram vbudhram changed the title feat(codes): Add tokenVerification shortCode feat(codes): add support for verifying token short code Nov 21, 2017
@vbudhram vbudhram force-pushed the feature.signincodes branch 2 times, most recently from aa15106 to c867338 Compare November 21, 2017 14:31
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.

Thanks @vbudhram. This broadly makes sense to me, although I admit I'm wary of the growing complexity around token verification in general. I left a bunch of smaller nits for your consideration.

docs/API.md Outdated

* sessionToken : data, uid, createdAt, uaBrowser, uaBrowserVersion, uaOS, uaOSVersion, uaDeviceType,
uaFormFactor, mustVerify, tokenVerificationId
uaFormFactor, mustVerify, tokenVerificationId, shortCode, shortCodeExpiresAt
Copy link
Contributor

Choose a reason for hiding this comment

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

naming nit: I wonder if we should include the word "verification" somewhere in the name of this thing, to make it more clear what it's for.

Copy link
Contributor

Choose a reason for hiding this comment

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

(that may be less of a concern if it's only for internal use by this repo, and is return returned to the auth-server)

api.get('/sessionToken/:id/verified', withIdAndBody(db.sessionTokenWithVerificationStatus))
api.get('/keyFetchToken/:id/verified', withIdAndBody(db.keyFetchTokenWithVerificationStatus))
api.post('/tokens/:id/verify', withIdAndBody(db.verifyTokens))
api.post('/tokens/:shortCode/verifyShortCode', op(req => db.verifyTokensShortCode(req.params.shortCode, { uid: req.body.uid } )))
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the withIdAndBody helper not usable here? It seems a little odd to have this route as the only one that's not using the helper.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea it wasn't useable. Using that would create a buffer instead of sending the string. This was based off how signinCodes do it.

})
})

describe('db.verifyTokensShortCodes', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: "ShortCodes" plural in the name here doesn't seem quite right, should it be singular?

ALGORITHM = INPLACE LOCK=NONE;

ALTER TABLE `unverifiedTokens`
ADD COLUMN `shortCodeExpiresAt` BIGINT UNSIGNED DEFAULT NULL,
Copy link
Contributor

Choose a reason for hiding this comment

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

You should be able to add both of these columns in a single ALTER TABLE statement, which will be easier for ops to run.


-- ALTER TABLE `unverifiedTokens`
-- DROP COLUMN `shortCodeExpiresAt` BIGINT UNSIGNED,
-- ALGORITHM = INPLACE, LOCK = NONE;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also drop the unverifiedTokens_shortCodeHash index here, potentially before removing the columns in question?

IF @updateCount > 0 THEN
SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 2101, MESSAGE_TEXT = 'Expired shortcode.';
END IF;
END IF;
Copy link
Contributor

Choose a reason for hiding this comment

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

If we expect the common case to be using a valid, unexpired shortCode, then we should optimize for doing a single read in that case. I think we can achieve that by having the first query include the expiry check, like:

SELECT tokenVerificationId INTO @tokenVerificationId FROM unverifiedTokens WHERE uid = uidArg AND shortCodeHash = shortCodeHashArg and shortCodeExpiresAt >= (UNIX_TIMESTAMP(NOW(3)) * 1000)

And then, if we didn't find one, we can whether it exists but is expired with a separate query like above.

docs/API.md Outdated
d.callbackAuthKey AS deviceCallbackAuthKey,
d.callbackIsExpired AS deviceCallbackIsExpired,
ut.mustVerify, ut.tokenVerificationId
ut.mustVerify, ut.tokenVerificationId, ut.shortCode, ut.shortCodeExpiresAt
Copy link
Contributor

Choose a reason for hiding this comment

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

Likewise here, this returns the shortCodeHash.

SET @tokenVerificationId = NULL;
SELECT tokenVerificationId INTO @tokenVerificationId FROM unverifiedTokens WHERE uid = uidArg AND shortCodeHash = shortCodeHashArg;

IF @tokenVerificationId IS NOT NULL THEN
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if @tokenVerificationId is NULL here? IIUC control flow will fall through and execute the UPDATE securityEvents below, which will set verified = true for all security events where tokenVerificationId is NULL. I think that's OK because such events will already have verified=true anyway, but it might be safer to bail out early if we don't find any matching tokenVerificationId.

IN `uidArg` BINARY(16)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
Copy link
Contributor

Choose a reason for hiding this comment

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

This exit-handler logic is only necessary if you're going to use a transaction, which actually it might be worth doing here anyway because we're modifying multiple tables.


CREATE INDEX `unverifiedTokens_shortCodeHash`
ON `unverifiedTokens` (`shortCodeHash`)
ALGORITHM = INPLACE LOCK=NONE;
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if it's worth making this a unique index on (shortCodeHash, uid) in order to prevent the accidental creation of duplicate shortcodes for different login sessions by the same user. That should never happen in theory, but sometimes it's worth ensuring that theory and practice will match :-)

@vbudhram vbudhram force-pushed the feature.signincodes branch from 664dd10 to 6851f4e Compare November 22, 2017 20:43
@vbudhram
Copy link
Contributor Author

@rfk Thanks for review! Updated based on feedback

  • Opted to use tokenVerificationCode, tokenVerificationCodeHash, tokenVerificationCodeExpiresAt instead of shortCode because I think it describes it a little better (configurable code length, stored as hash)
  • Optimized SQL to check for valid code first

Review at your liesure.

lib/db/mem.js Outdated
mustVerify: !! sessionToken.mustVerify,
uid: sessionToken.uid
tokenVerificationId: sessionToken.tokenVerificationId,
tokenVerificationCodeHash: sessionToken.tokenVerificationCode,
Copy link
Contributor

Choose a reason for hiding this comment

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

IIUC, this is not actually hashing the tokenVerificationCode, so the field name here is wrong. Should it hash it, for consistency with the mysql version?

lib/db/mem.js Outdated
}

// Is code expired?
if (t.tokenVerificationCodeHash.toString('hex') === tokenVerificationCode) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Likewise here, it looks slightly off the compare a field named tokenVerificationCodeHash to a variable named tokenVerificationCode, we should either do the hashing or rename the field.

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.

A couple of nits, but this LGTM!

ADD COLUMN `tokenVerificationCodeExpiresAt` BIGINT UNSIGNED DEFAULT NULL,
ALGORITHM = INPLACE, LOCK = NONE;

CREATE INDEX `unverifiedTokens_tokenVerificationCodeHash_uid`
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you intend to make this a CREATE UNIQUE INDEX? If not, I don't think it needs to contain the uid column at all.

AND tokenVerificationCodeExpiresAt >= (UNIX_TIMESTAMP(NOW(3)) * 1000);

IF @tokenVerificationId IS NULL THEN
SET @updateCount = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

tiny nit: I don't really understand what the name "updateCount" is supposed to mean here; maybe "expiredCount" or something to make it clearer?

Copy link
Contributor

@vladikoff vladikoff left a comment

Choose a reason for hiding this comment

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

Some feedback

api.get('/sessionToken/:id/verified', withIdAndBody(db.sessionTokenWithVerificationStatus))
api.get('/keyFetchToken/:id/verified', withIdAndBody(db.keyFetchTokenWithVerificationStatus))
api.post('/tokens/:id/verify', withIdAndBody(db.verifyTokens))
api.post('/tokens/:code/verifyCode', op(req => db.verifyTokenCode(req.params.code, { uid: req.body.uid } )))
Copy link
Contributor

Choose a reason for hiding this comment

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

should this use something like withParams ?

lib/db/mem.js Outdated

Memory.prototype.verifyTokenCode = function (tokenVerificationCode, accountData) {
const uid = accountData.uid.toString('hex')
tokenVerificationCode = tokenVerificationCode.toString('hex')
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: rename this to tokenVerificationCodeHex ?

lib/db/mysql.js Outdated
MySql.prototype.verifyTokenCode = function (tokenVerificationCode, accountData) {
return this.readFirstResult(VERIFY_TOKEN_CODE, [createHash(tokenVerificationCode), accountData.uid])
.then(function (result) {
if (result['@rowCount'] === 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't use this @rowCount check anywhere else, is this the only way we can find out if nothing was found?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately, this was the only way I could find that would return the row count from within a transaction. That might also explain why the affectedRows property was not returned.

@@ -0,0 +1,165 @@
ALTER TABLE `unverifiedTokens`
Copy link
Contributor

Choose a reason for hiding this comment

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

is this file missing the boilerplate for SET NAMES, via https://github.com/mozilla/fxa-auth-db-mysql/blob/master/scripts/migration.sh#L35 ?

WHERE t.tokenId = tokenIdArg;
END;

UPDATE dbMetadata SET value = '68' WHERE name = 'schema-patch-level'; No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

missing new line EOF

@vbudhram vbudhram force-pushed the feature.signincodes branch from d0dc563 to 238e6e3 Compare November 27, 2017 19:14
@vbudhram
Copy link
Contributor Author

Made changes @vladikoff mind r?

@vladikoff vladikoff self-requested a review November 27, 2017 21:25
lib/db/util.js Outdated

createHash () {
const hash = crypto.createHash('sha256')
// Use ...rest operator and forEach when we're on node 6
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought we were on node 6 already?

Copy link
Contributor

Choose a reason for hiding this comment

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

@rfk confirm?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I believe we are comfortably on node 6; if this repo is still testing node 4 support, feel free to drop it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated!

@vladikoff vladikoff merged commit ac0b814 into master Nov 27, 2017
@vladikoff
Copy link
Contributor

Thanks @vbudhram !!

@shane-tomlinson shane-tomlinson deleted the feature.signincodes branch April 18, 2018 12:44
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants