Skip to content
This repository was archived by the owner on Apr 3, 2019. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions db-server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@ function createServer(db) {
op(req => db.consumeSigninCode(req.params.code))
)

api.post('/account/:id/recoveryCodes',
op((req) => {
return db.replaceRecoveryCodes(req.params.id, req.body.count)
})
)

api.post('/account/:id/recoveryCodes/:code',
op((req) => db.consumeRecoveryCode(req.params.id, req.params.code))
)

api.get(
'/',
function (req, res, next) {
Expand Down
97 changes: 97 additions & 0 deletions db-server/test/backend/db_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -1985,6 +1985,103 @@ module.exports = function (config, DB) {
})
})

describe('recovery codes', () => {
let account
beforeEach(() => {
account = createAccount()
account.emailVerified = true
return db.createAccount(account.uid, account)
})

it('should fail to generate for unknown user', () => {
return db.replaceRecoveryCodes(hex16(), 2)
.then(assert.fail, (err) => {
assert.equal(err.errno, 116, 'correct errno, not found')
})
})

const codeLengthTest = [0, 4, 8]
codeLengthTest.forEach((num) => {
it('should generate ' + num + ' recovery codes', () => {
return db.replaceRecoveryCodes(account.uid, num)
.then((codes) => {
assert.equal(codes.length, num, 'correct number of codes')
}, (err) => {
assert.equal(err.errno, 116, 'correct errno, not found')
})
})
})

it('should replace recovery codes', () => {
let firstCodes
return db.replaceRecoveryCodes(account.uid, 2)
.then((codes) => {
firstCodes = codes
assert.equal(firstCodes.length, 2, 'correct number of codes')

return db.replaceRecoveryCodes(account.uid, 3)
})
.then((codes) => {
assert.equal(codes.length, 3, 'correct number of codes')
assert.notDeepEqual(codes, firstCodes, 'codes are different')
})
})

describe('should consume recovery codes', () => {
let recoveryCodes
beforeEach(() => {
return db.replaceRecoveryCodes(account.uid, 2)
.then((codes) => {
recoveryCodes = codes
assert.equal(recoveryCodes.length, 2, 'correct number of recovery codes')
})
})

it('should fail to consume recovery code with unknown uid', () => {
return db.consumeRecoveryCode(hex16(), 'recoverycodez')
.then(assert.fail, (err) => {
assert.equal(err.errno, 116, 'correct errno, not found')
})
})

it('should fail to consume recovery code with unknown code', () => {
return db.replaceRecoveryCodes(account.uid, 3)
.then(() => {
return db.consumeRecoveryCode(account.uid, 'notvalidcode')
.then(assert.fail, (err) => {
assert.equal(err.errno, 116, 'correct errno, unknown recovery code')
})
})
})

it('should fail to consume code twice', () => {
return db.consumeRecoveryCode(account.uid, recoveryCodes[0])
.then((result) => {
assert.equal(result.remaining, 1, 'correct number of remaining codes')

// Should fail to consume code twice
return db.consumeRecoveryCode(account.uid, recoveryCodes[0])
.then(assert.fail, (err) => {
assert.equal(err.errno, 116, 'correct errno, unknown recovery code')
})
})
})

it('should consume code', () => {
return db.consumeRecoveryCode(account.uid, recoveryCodes[0])
.then((result) => {
assert.equal(result.remaining, 1, 'correct number of remaining codes')

return db.consumeRecoveryCode(account.uid, recoveryCodes[1])
.then((result) => {
assert.equal(result.remaining, 0, 'correct number of remaining codes')
})
})
})
})
})


after(() => db.close())
})
}
56 changes: 55 additions & 1 deletion db-server/test/backend/remote.js
Original file line number Diff line number Diff line change
Expand Up @@ -1627,7 +1627,7 @@ module.exports = function(cfg, makeServer) {
.then((res) => respOkEmpty(res))
})

it('set session verification method', () => {
it('set session verification method - totp-2fa', () => {
const verifyOptions = {
verificationMethod: 'totp-2fa',
}
Expand All @@ -1643,6 +1643,60 @@ module.exports = function(cfg, makeServer) {
})
})

it('set session verification method - recovery-code', () => {
const verifyOptions = {
verificationMethod: 'recovery-code',
}
return client.postThen('/tokens/' + user.sessionTokenId + '/verifyWithMethod', verifyOptions)
.then((res) => {
respOkEmpty(res)
return client.getThen('/sessionToken/' + user.sessionTokenId + '/device')
})
.then((sessionToken) => {
sessionToken = sessionToken.obj
assert.equal(sessionToken.verificationMethod, 3, 'verificationMethod set')
assert.ok(sessionToken.verifiedAt, 'verifiedAt set')
})
})
})

describe('recovery codes', () => {
let user
beforeEach(() => {
user = fake.newUserDataHex()
return client.putThen('/account/' + user.accountId, user.account)
.then((r) => {
respOkEmpty(r)
})
})

it('should generate new recovery codes', () => {
return client.postThen('/account/' + user.accountId + '/recoveryCodes', {count: 8})
.then((res) => {
const codes = res.obj
assert.equal(codes.length, 8, 'correct number of codes')
})
})

it('should fail to consume unknown recovery code', () => {
return client.postThen('/account/' + user.accountId + '/recoveryCodes/' + '12345678')
.then(assert.fail, (err) => {
testNotFound(err)
})
})

it('should consume recovery code', () => {
return client.postThen('/account/' + user.accountId + '/recoveryCodes', {count: 8})
.then((res) => {
const codes = res.obj
assert.equal(codes.length, 8, 'correct number of codes')
return client.postThen('/account/' + user.accountId + '/recoveryCodes/' + codes[0])
})
.then((res) => {
const result = res.obj
assert.equal(result.remaining, 7, 'correct number of remaining codes')
})
})
})

after(() => server.close())
Expand Down
90 changes: 90 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ The following datatypes are used throughout this document:
* totpToken : `GET /totp/:id`
* deleteTotpToken : `DEL /totp/:id`
* updateTotpToken : `POST /totp/:id/update`
* Recovery codes:
* replaceRecoveryCodes : `POST /account/:id/recoveryCodes`
* consumeRecoveryCode : `POST /account/:id/recoveryCodes/:code`

## Ping : `GET /`

Expand Down Expand Up @@ -2066,3 +2069,90 @@ Content-Length: 2
* Conditions: if something goes wrong on the server
* Content-Type : `application/json`
* Body : `{"code":"InternalError","message":"..."}`

## replaceRecoveryCodes : `GET /account/:uid/recoveryCodes`

Replaces a users current recovery codes with new ones.

### Example

```
curl \
-v \
-X POST \
-H "Content-Type: application/json" \
-d '{
"count" : 8
}'
http://localhost:8000/account/1234567890ab/recoveryCodes
```

### Request

* Method : `POST`
* Path : `/account/<uid>/recoveryCodes`
* `uid` : hex
*

### Response

```
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 2

["code1", "code2"]
```

* Status Code : `200 OK`
* Content-Type : `application/json`
* Body : `["code1", "code2"]`
* Status Code : `404 Not Found`
* Conditions: if no user found
* 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":"..."}`

## consumeRecoveryCode : `POST /account/:uid/recoveryCodes/:code`

Consumes a recovery code.

### Example

```
curl \
-v \
-X POST \
-H "Content-Type: application/json" \
http://localhost:8000/account/1234567890ab/recoveryCodes/1123
```

### Request

* Method : `POST`
* Path : `/account/<uid>/recoveryCodes/<code>`
* `uid` : hex
* `code`: hex

### Response

```
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 2

{"remaining" : 1}
```

* Status Code : `200 OK`
* Content-Type : `application/json`
* Body : `{"remaining" : 1}`
* Status Code : `404 Not Found`
* Conditions: if no user found or code not found
* 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":"..."}`
Loading