feat(totp): Add totp management api (#299), r=@philbooth

This commit is contained in:
Vijay Budhram 2018-02-12 15:29:55 +00:00 коммит произвёл GitHub
Родитель 6a4fb6771d
Коммит 9b8efcb3c9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 421 добавлений и 3 удалений

Просмотреть файл

@ -154,6 +154,10 @@ function createServer(db) {
api.get('/emailRecord/:id', withIdAndBody(db.emailRecord))
api.head('/emailRecord/:id', withIdAndBody(db.accountExists))
api.get('/totp/:id', withIdAndBody(db.totpToken))
api.del('/totp/:id', withIdAndBody(db.deleteTotpToken))
api.put('/totp/:id', withIdAndBody(db.createTotpToken))
api.get('/__heartbeat__', withIdAndBody(db.ping))
function op(fn) {

Просмотреть файл

@ -1794,6 +1794,49 @@ module.exports = function (config, DB) {
})
describe('Totp handling', () => {
let sharedSecret, epoch
beforeEach(() => {
sharedSecret = crypto.randomBytes(40).toString('hex')
epoch = 0
return db.createTotpToken(accountData.uid, {sharedSecret, epoch})
.then((result) => assert.ok(result, 'token created'))
})
it('should create totp token', () => {
return db.totpToken(accountData.uid)
.then((token) => {
assert.equal(token.sharedSecret, sharedSecret, 'correct sharedSecret')
assert.equal(token.epoch, epoch, 'correct epoch')
})
})
it('should fail to get unknown totp token', () => {
return db.totpToken(newUuid())
.then(assert.fail, (err) => {
assert.equal(err.errno, 116, 'correct errno, not found')
})
})
it('should fail to create second token for same user', () => {
return db.createTotpToken(accountData.uid, {sharedSecret, epoch})
.then(assert.fail, (err) => {
assert.equal(err.errno, 101, 'correct errno, duplicate')
})
})
it('should delete totp token', () => {
return db.deleteTotpToken(accountData.uid)
.then((result) => {
assert.ok(result)
return db.totpToken(accountData.uid)
.then(assert.fail, (err) => {
assert.equal(err.errno, 116, 'correct errno, not found')
})
})
})
})
after(() => db.close())
})
}

Просмотреть файл

@ -1595,6 +1595,38 @@ module.exports = function(cfg, makeServer) {
}
)
describe('totp tokens', () => {
let user
beforeEach(() => {
user = fake.newUserDataHex()
return client.putThen('/account/' + user.accountId, user.account)
.then((r) => {
respOkEmpty(r)
return client.putThen('/totp/' + user.accountId, user.totp)
})
.then((r) => respOkEmpty(r))
})
it('should get totp token', () => {
return client.getThen('/totp/' + user.accountId)
.then((r) => {
const result = r.obj
assert.equal(result.sharedSecret, user.totp.sharedSecret, 'sharedSecret set')
assert.equal(result.epoch, user.totp.epoch, 'epoch set')
})
})
it('should delete totp token', () => {
return client.delThen('/totp/' + user.accountId)
.then((r) => {
respOkEmpty(r)
return client.getThen('/totp/' + user.accountId)
.then(assert.fail, (err) => testNotFound(err))
})
})
})
after(() => server.close())
})

Просмотреть файл

@ -126,6 +126,11 @@ module.exports.newUserDataHex = function() {
}
data.email.normalizedEmail = data.email.email.toLowerCase()
data.totp = {
sharedSecret: hex(10),
epoch: 0
}
return data
}

Просмотреть файл

@ -95,6 +95,10 @@ The following datatypes are used throughout this document:
* Sign-in codes
* createSigninCode : `PUT /signinCodes/:code`
* consumeSigninCode : `POST /signinCodes/:code/consume`
* TOTP resetTokens
* createTotpToken : `PUT /totp/:id`
* totpToken : `GET /totp/:id`
* deleteTotpToken : `DEL /totp/:id`
## Ping : `GET /`
@ -1867,3 +1871,130 @@ Content-Length: 2
* Content-Type : `application/json`
* Body : `{"code":"InternalError","message":"..."}`
## createTotpToken : `PUT /totp/:uid`
Used to create a TOTP token for a user.
### Example
```
curl \
-v \
-X PUT \
-H "Content-Type: application/json" \
-d '{"sharedSecret": "LEVXGTLWMFITC6BSIF2DOQKTIU2WUOKJ", "epoch": 0}' \
http://localhost:8000/totp/1234567890ab
```
### Request
* Method : `PUT`
* Path : `/totp/<uid>
* `uid` : hex
* Params:
* `sharedSecret` : hex10
* `epoch` : epoch
### Response
```
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 2
{}
```
* Status Code : `200 OK`
* Content-Type : `application/json`
* Body : `{}`
* Status Code : `409 Conflict`
* Conditions: if the user already has a TOTP device
* Content-Type : `application/json`
* Body : `{"errno":101,"message":"Record already exists"}`
* Status Code : `500 Internal Server Error`
* Conditions: if something goes wrong on the server
* Content-Type : `application/json`
* Body : `{"code":"InternalError","message":"..."}`
## totpToken : `GET /totp/:uid`
Get the user's TOTP token.
### Example
```
curl \
-v \
-X GET \
-H "Content-Type: application/json" \
http://localhost:8000/totp/1234567890ab
```
### Request
* Method : `GET`
* Path : `/totp/<uid>
* `uid` : hex
### Response
```
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 2
{
"sharedSecret": "LEVXGTLWMFITC6BSIF2DOQKTIU2WUOKJ",
"epoch": 0
}
```
* Status Code : `200 OK`
* Content-Type : `application/json`
* Body : `{"sharedSecret": "LEVXGTLWMFITC6BSIF2DOQKTIU2WUOKJ", "epoch": 0}`
* Status Code : `404 Not Found`
* Conditions: if no TOTP token found for user
* Content-Type : `application/json`
* Status Code : `500 Internal Server Error`
* Conditions: if something goes wrong on the server
* Content-Type : `application/json`
* Body : `{"code":"InternalError","message":"..."}`
## deleteTotpToken : `DEL /totp/:uid`
Delete the user's TOTP token.
### Example
```
curl \
-v \
-X DEL \
-H "Content-Type: application/json" \
http://localhost:8000/totp/1234567890ab
```
### Request
* Method : `DEL`
* Path : `/totp/<uid>
* `uid` : hex
### Response
```
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 2
{}
```
* Status Code : `200 OK`
* Content-Type : `application/json`
* Body : `{}`
* Status Code : `500 Internal Server Error`
* Conditions: if something goes wrong on the server
* Content-Type : `application/json`
* Body : `{"code":"InternalError","message":"..."}`

Просмотреть файл

@ -63,6 +63,10 @@ There are a number of methods that a DB storage backend should implement:
* Signin codes
* .createSigninCode(code, uid, createdAt, flowId)
* .consumeSigninCode(code)
* TOTP
* .createTotpToken(uid, sharedSecret, epoch)
* .totpToken(uid)
* .deleteTotpToken(uid)
* General
* .ping()
* .close()
@ -815,3 +819,58 @@ Returns:
* Rejects with:
* Any error from the underlying storage system (wrapped in `error.wrap()`)
## createTotpToken(uid, sharedSecret, epoch)
Creates a new TOTP token for the user.
Parameters:
* `uid` (Buffer16):
The uid of the owning account
* `sharedSecret` (string):
The shared secret used to generate TOTP code
* `epoch` (number):
The epoch used to generate TOTP code (default 0)
Returns:
* Resolves with:
* An empty object `{}`
* Rejects with:
* Any error from the underlying storage system (wrapped in `error.wrap()`)
* `error.duplicate()` if this user had a token already
## totpToken(uid)
Get's the TOTP token for the user.
Parameters:
* `uid` (Buffer16):
The uid of the owning account
Returns:
* Resolves with:
* An object `{}`
* sharedSecret
* epoch
* Rejects with:
* Any error from the underlying storage system (wrapped in `error.wrap()`)
* `error.duplicate()` if this user had a token already
## deleteTotpToken(uid)
Delete the TOTP token for the user.
Parameters:
* `uid` (Buffer16):
The uid of the owning account
Returns:
* Resolves with:
* An empty object `{}`
* Rejects with:
* Any error from the underlying storage system (wrapped in `error.wrap()`)

Просмотреть файл

@ -25,6 +25,7 @@ var unblockCodes = {}
var emailBounces = {}
var emails = {}
var signinCodes = {}
const totpTokens = {}
var DEVICE_FIELDS = [
'sessionTokenId',
@ -904,6 +905,7 @@ module.exports = function (log, error) {
delete uidByNormalizedEmail[account.normalizedEmail]
delete accounts[uid]
delete totpTokens[uid]
return []
}
)
@ -1221,6 +1223,43 @@ module.exports = function (log, error) {
return P.resolve({})
}
Memory.prototype.createTotpToken = (uid, data) => {
uid = uid.toString('hex')
const totpToken = totpTokens[uid]
if (totpToken) {
return P.reject(error.duplicate())
}
totpTokens[uid] = {
sharedSecret: data.sharedSecret,
epoch: data.epoch || 0
}
return Promise.resolve({})
}
Memory.prototype.totpToken = (uid) => {
uid = uid.toString('hex')
const totpToken = totpTokens[uid]
if (! totpToken) {
return P.reject(error.notFound())
}
return Promise.resolve(totpToken)
}
Memory.prototype.deleteTotpToken = function (uid) {
uid = uid.toString('hex')
delete totpTokens[uid]
return Promise.resolve({})
}
// UTILITY FUNCTIONS
Memory.prototype.ping = function () {

Просмотреть файл

@ -537,9 +537,9 @@ module.exports = function (log, error) {
// DELETE
// Delete : sessionTokens, keyFetchTokens, accountResetTokens, passwordChangeTokens,
// passwordForgotTokens, accounts, devices, unverifiedTokens, emails, signinCodes
// passwordForgotTokens, accounts, devices, unverifiedTokens, emails, signinCodes, totp
// Where : uid = $1
var DELETE_ACCOUNT = 'CALL deleteAccount_13(?)'
var DELETE_ACCOUNT = 'CALL deleteAccount_14(?)'
MySql.prototype.deleteAccount = function (uid) {
return this.write(DELETE_ACCOUNT, [uid])
@ -1285,6 +1285,21 @@ module.exports = function (log, error) {
)
}
const CREATE_TOTP_TOKEN = 'CALL createTotpToken_1(?, ?, ?, ?)'
MySql.prototype.createTotpToken = function (uid, data) {
return this.write(CREATE_TOTP_TOKEN, [uid, data.sharedSecret, data.epoch, Date.now()])
}
const GET_TOTP_TOKEN = 'CALL totpToken_1(?)'
MySql.prototype.totpToken = function (uid) {
return this.readFirstResult(GET_TOTP_TOKEN, [uid])
}
const DELETE_TOTP_TOKEN = 'CALL deleteTotpToken_1(?)'
MySql.prototype.deleteTotpToken = function (uid) {
return this.write(DELETE_TOTP_TOKEN, [uid])
}
return MySql
}

Просмотреть файл

@ -4,4 +4,4 @@
// The expected patch level of the database. Update if you add a new
// patch in the ./schema/ directory.
module.exports.level = 70
module.exports.level = 71

Просмотреть файл

@ -0,0 +1,81 @@
SET NAMES utf8mb4 COLLATE utf8mb4_bin;
CREATE TABLE IF NOT EXISTS totp (
uid BINARY(16) NOT NULL,
sharedSecret VARCHAR(80) NOT NULL,
epoch BIGINT NOT NULL,
createdAt BIGINT UNSIGNED NOT NULL,
UNIQUE KEY (`uid`)
) ENGINE=InnoDB;
CREATE PROCEDURE `createTotpToken_1` (
IN `uidArg` BINARY(16),
IN `sharedSecretArg` VARCHAR(80),
IN `epochArg` BIGINT UNSIGNED,
IN `createdAtArg` BIGINT UNSIGNED
)
BEGIN
INSERT INTO totp(
uid,
sharedSecret,
epoch,
createdAt
)
VALUES(
uidArg,
sharedSecretArg,
epochArg,
createdAtArg
);
END;
CREATE PROCEDURE `totpToken_1` (
IN `uidArg` BINARY(16)
)
BEGIN
SELECT sharedSecret, epoch FROM totp WHERE uid = uidArg;
END;
CREATE PROCEDURE `deleteTotpToken_1` (
IN `uidArg` BINARY(16)
)
BEGIN
DELETE FROM totp WHERE uid = uidArg;
END;
CREATE PROCEDURE `deleteAccount_14` (
IN `uidArg` BINARY(16)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
DELETE FROM sessionTokens WHERE uid = uidArg;
DELETE FROM keyFetchTokens WHERE uid = uidArg;
DELETE FROM accountResetTokens WHERE uid = uidArg;
DELETE FROM passwordChangeTokens WHERE uid = uidArg;
DELETE FROM passwordForgotTokens WHERE uid = uidArg;
DELETE FROM accounts WHERE uid = uidArg;
DELETE FROM devices WHERE uid = uidArg;
DELETE FROM unverifiedTokens WHERE uid = uidArg;
DELETE FROM unblockCodes WHERE uid = uidArg;
DELETE FROM emails WHERE uid = uidArg;
DELETE FROM signinCodes WHERE uid = uidArg;
DELETE FROM totp WHERE uid = uidArg;
COMMIT;
END;
UPDATE dbMetadata SET value = '71' WHERE name = 'schema-patch-level';

Просмотреть файл

@ -0,0 +1,9 @@
-- SET NAMES utf8mb4 COLLATE utf8mb4_bin;
-- DROP PROCEDURE `createTotpToken_1`;
-- DROP PROCEDURE `totpToken_1`;
-- DROP PROCEDURE `deleteTotpToken_1`;
-- DROP PROCEDURE `deleteAccount_14`;
-- UPDATE dbMetadata SET value = '70' WHERE name = 'schema-patch-level';