implemented new /auth/start /session/create
This commit is contained in:
Родитель
6b8296a8f8
Коммит
e9a35fac94
|
@ -190,32 +190,19 @@ ClientApi.prototype.getRandomBytes = function () {
|
|||
)
|
||||
}
|
||||
|
||||
ClientApi.prototype.passwordChangeAuthStart = function (sessionTokenHex) {
|
||||
return tokens.SessionToken.fromHex(sessionTokenHex)
|
||||
ClientApi.prototype.passwordChangeStart = function (authTokenHex) {
|
||||
return tokens.AuthToken.fromHex(authTokenHex)
|
||||
.then(
|
||||
function (token) {
|
||||
return doRequest(
|
||||
'POST',
|
||||
this.origin + '/password/change/auth/start',
|
||||
this.origin + '/password/change/start',
|
||||
token
|
||||
)
|
||||
}.bind(this)
|
||||
)
|
||||
}
|
||||
|
||||
ClientApi.prototype.passwordChangeAuthFinish = function (srpToken, A, M) {
|
||||
return doRequest(
|
||||
'POST',
|
||||
this.origin + '/password/change/auth/finish',
|
||||
null,
|
||||
{
|
||||
srpToken: srpToken,
|
||||
A: A,
|
||||
M: M
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ClientApi.prototype.passwordForgotSendCode = function (email) {
|
||||
return doRequest(
|
||||
'POST',
|
||||
|
@ -239,10 +226,10 @@ ClientApi.prototype.passwordForgotVerifyCode = function (forgotPasswordToken, co
|
|||
)
|
||||
}
|
||||
|
||||
ClientApi.prototype.sessionAuthStart = function (email) {
|
||||
ClientApi.prototype.authStart = function (email) {
|
||||
return doRequest(
|
||||
'POST',
|
||||
this.origin + '/session/auth/start',
|
||||
this.origin + '/auth/start',
|
||||
null,
|
||||
{
|
||||
email: email
|
||||
|
@ -250,10 +237,10 @@ ClientApi.prototype.sessionAuthStart = function (email) {
|
|||
)
|
||||
}
|
||||
|
||||
ClientApi.prototype.sessionAuthFinish = function (srpToken, A, M) {
|
||||
ClientApi.prototype.authFinish = function (srpToken, A, M) {
|
||||
return doRequest(
|
||||
'POST',
|
||||
this.origin + '/session/auth/finish',
|
||||
this.origin + '/auth/finish',
|
||||
null,
|
||||
{
|
||||
srpToken: srpToken,
|
||||
|
@ -263,13 +250,13 @@ ClientApi.prototype.sessionAuthFinish = function (srpToken, A, M) {
|
|||
)
|
||||
}
|
||||
|
||||
ClientApi.prototype.sessionStatus = function (sessionTokenHex) {
|
||||
return tokens.SessionToken.fromHex(sessionTokenHex)
|
||||
ClientApi.prototype.sessionCreate = function (authTokenHex) {
|
||||
return tokens.AuthToken.fromHex(authTokenHex)
|
||||
.then(
|
||||
function (token) {
|
||||
return doRequest(
|
||||
'GET',
|
||||
this.origin + '/session/status',
|
||||
'POST',
|
||||
this.origin + '/session/create',
|
||||
token
|
||||
)
|
||||
}.bind(this)
|
||||
|
|
|
@ -16,6 +16,7 @@ function Client(origin) {
|
|||
this.passwordSalt = null
|
||||
this.srp = null
|
||||
this.email = null
|
||||
this.authToken = null
|
||||
this.sessionToken = null
|
||||
this.accountResetToken = null
|
||||
this.keyFetchToken = null
|
||||
|
@ -27,12 +28,12 @@ Client.Api = ClientApi
|
|||
|
||||
function getAMK(srpSession, email, password) {
|
||||
var a = crypto.randomBytes(32)
|
||||
var g = srp.params[srpSession.srp.N_bits].g
|
||||
var N = srp.params[srpSession.srp.N_bits].N
|
||||
var g = srp.params[2048].g
|
||||
var N = srp.params[2048].N
|
||||
var A = srp.getA(g, a, N)
|
||||
var B = Buffer(srpSession.srp.B, 'hex')
|
||||
var S = srp.client_getS(
|
||||
Buffer(srpSession.srp.s, 'hex'),
|
||||
Buffer(srpSession.srp.salt, 'hex'),
|
||||
Buffer(email),
|
||||
Buffer(password),
|
||||
N,
|
||||
|
@ -134,32 +135,44 @@ Client.prototype.stringify = function () {
|
|||
|
||||
Client.prototype.login = function (callback) {
|
||||
var K = null
|
||||
var p = this.api.sessionAuthStart(this.email)
|
||||
var p = this.api.authStart(this.email)
|
||||
.then(
|
||||
function (srpSession) {
|
||||
var x = getAMK(srpSession, this.email, this.password)
|
||||
K = x.K
|
||||
return this.api.sessionAuthFinish(x.srpToken, x.A, x.M)
|
||||
return this.api.authFinish(x.srpToken, x.A, x.M)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (json) {
|
||||
return AuthBundle.create(K, 'session/auth')
|
||||
return AuthBundle.create(K, 'auth/finish')
|
||||
.then(
|
||||
function (b) {
|
||||
var tokens = b.unbundle(json.bundle)
|
||||
return {
|
||||
keyFetchToken: tokens.keyFetchToken,
|
||||
sessionToken: tokens.otherToken
|
||||
}
|
||||
return b.unbundle(json.bundle)
|
||||
}
|
||||
)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (authToken) {
|
||||
this.authToken = authToken
|
||||
return this.api.sessionCreate(this.authToken)
|
||||
}.bind(this)
|
||||
)
|
||||
.then (
|
||||
function (json) {
|
||||
return tokens.AuthToken.fromHex(this.authToken)
|
||||
.then(
|
||||
function (t) {
|
||||
return t.unbundle(json.bundle)
|
||||
}
|
||||
)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (tokens) {
|
||||
this.sessionToken = tokens.sessionToken
|
||||
this.keyFetchToken = tokens.keyFetchToken
|
||||
this.sessionToken = tokens.sessionToken
|
||||
return tokens
|
||||
}.bind(this)
|
||||
)
|
||||
|
|
|
@ -7,6 +7,7 @@ module.exports = function (P, tokens, RecoveryMethod, db, config, error) {
|
|||
var domain = config.domain
|
||||
var SessionToken = tokens.SessionToken
|
||||
var AccountResetToken = tokens.AccountResetToken
|
||||
var AuthToken = tokens.AuthToken
|
||||
|
||||
function Account() {
|
||||
this.uid = null
|
||||
|
@ -17,6 +18,7 @@ module.exports = function (P, tokens, RecoveryMethod, db, config, error) {
|
|||
this.srp = null
|
||||
this.passwordStretching = null
|
||||
// references
|
||||
this.authTokenId = null
|
||||
this.resetTokenId = null
|
||||
this.sessionTokenIds = null
|
||||
this.recoveryMethodIds = null
|
||||
|
@ -33,6 +35,7 @@ module.exports = function (P, tokens, RecoveryMethod, db, config, error) {
|
|||
a.kA = object.kA
|
||||
a.wrapKb = object.wrapKb
|
||||
a.passwordStretching = object.passwordStretching
|
||||
a.authTokenId = object.authTokenId
|
||||
a.resetTokenId = object.resetTokenId
|
||||
a.sessionTokenIds = object.sessionTokenIds || {}
|
||||
a.recoveryMethodIds = object.recoveryMethodIds || {}
|
||||
|
@ -165,6 +168,19 @@ module.exports = function (P, tokens, RecoveryMethod, db, config, error) {
|
|||
return set()
|
||||
}
|
||||
|
||||
Account.prototype.setAuthToken = function (token) {
|
||||
var set = function () {
|
||||
this.authTokenId = token.id
|
||||
return this.save()
|
||||
}.bind(this)
|
||||
if (this.authTokenId !== null) {
|
||||
return AuthToken
|
||||
.del(this.authTokenId)
|
||||
.then(set)
|
||||
}
|
||||
return set()
|
||||
}
|
||||
|
||||
Account.prototype.deleteSessionToken = function (id) {
|
||||
delete this.sessionTokenIds[id]
|
||||
return SessionToken.del(id)
|
||||
|
@ -241,6 +257,7 @@ module.exports = function (P, tokens, RecoveryMethod, db, config, error) {
|
|||
.then(this.deleteAllRecoveryMethods.bind(this))
|
||||
.then(deleteRecord.bind(null, this.uid))
|
||||
.then(deleteIndex.bind(null, this.email))
|
||||
.then(function () { return {} })
|
||||
}
|
||||
|
||||
return Account
|
||||
|
|
|
@ -6,63 +6,34 @@ module.exports = function (inherits, Bundle, Account, tokens) {
|
|||
|
||||
function AuthBundle() {
|
||||
Bundle.call(this)
|
||||
this.keyFetchToken = null
|
||||
this.authToken = null
|
||||
this.otherToken = null
|
||||
}
|
||||
inherits(AuthBundle, Bundle)
|
||||
|
||||
AuthBundle.create = function (K, type) {
|
||||
return Bundle
|
||||
.hkdf(K, type, null, 3 * 32)
|
||||
.hkdf(K, type, null, 2 * 32)
|
||||
.then(
|
||||
function (key) {
|
||||
var b = new AuthBundle()
|
||||
b.hmacKey = key.slice(0, 32).toString('hex')
|
||||
b.xorKey = key.slice(32, 96).toString('hex')
|
||||
b.xorKey = key.slice(32, 64).toString('hex')
|
||||
return b
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AuthBundle.login = function (K, uid) {
|
||||
return AuthBundle.create(K, 'session/auth')
|
||||
return AuthBundle.create(K, 'auth/finish')
|
||||
.then(
|
||||
function (b) {
|
||||
return tokens.KeyFetchToken
|
||||
.create(uid)
|
||||
.then(function (t) { b.keyFetchToken = t })
|
||||
.then(tokens.SessionToken.create.bind(null, uid))
|
||||
.then(function (t) { b.otherToken = t })
|
||||
return tokens.AuthToken.create(uid)
|
||||
.then(function (t) { b.authToken = t })
|
||||
.then(Account.get.bind(null, uid))
|
||||
.then(
|
||||
function (a) {
|
||||
return a.addSessionToken(b.otherToken)
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function () {
|
||||
return {
|
||||
bundle: b.bundle()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AuthBundle.passwordChange = function (K, uid) {
|
||||
return AuthBundle.create(K, 'password/change')
|
||||
.then(
|
||||
function (b) {
|
||||
return tokens.KeyFetchToken
|
||||
.create(uid)
|
||||
.then(function (t) { b.keyFetchToken = t })
|
||||
.then(tokens.AccountResetToken.create.bind(null, uid))
|
||||
.then(function (t) { b.otherToken = t })
|
||||
.then(Account.get.bind(null, uid))
|
||||
.then(
|
||||
function (a) {
|
||||
return a.setResetToken(b.otherToken)
|
||||
return a.setAuthToken(b.authToken)
|
||||
}
|
||||
)
|
||||
.then(
|
||||
|
@ -78,20 +49,17 @@ module.exports = function (inherits, Bundle, Account, tokens) {
|
|||
|
||||
AuthBundle.prototype.unbundle = function (hex) {
|
||||
var bundle = Buffer(hex, 'hex')
|
||||
var ciphertext = bundle.slice(0, 64)
|
||||
var hmac = bundle.slice(64, 96)
|
||||
var ciphertext = bundle.slice(0, 32)
|
||||
var hmac = bundle.slice(32, 64)
|
||||
if (this.hmac(ciphertext).toString('hex') !== hmac.toString('hex')) {
|
||||
throw new Error('Corrupt Message')
|
||||
}
|
||||
var plaintext = this.xor(ciphertext)
|
||||
return {
|
||||
keyFetchToken: plaintext.slice(0, 32).toString('hex'),
|
||||
otherToken: plaintext.slice(32, 64).toString('hex')
|
||||
}
|
||||
return plaintext.slice(0, 32).toString('hex')
|
||||
}
|
||||
|
||||
AuthBundle.prototype.bundle = function () {
|
||||
return this.bundleHexStrings([this.keyFetchToken.data, this.otherToken.data])
|
||||
return this.bundleHexStrings([this.authToken.data])
|
||||
}
|
||||
|
||||
return AuthBundle
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = function (inherits, Token, db) {
|
||||
|
||||
function AuthToken() {
|
||||
Token.call(this)
|
||||
}
|
||||
inherits(AuthToken, Token)
|
||||
|
||||
AuthToken.hydrate = function (object) {
|
||||
return Token.fill(new AuthToken(), object)
|
||||
}
|
||||
|
||||
AuthToken.create = function (uid) {
|
||||
return Token
|
||||
.randomTokenData('session/create', 5 * 32)
|
||||
.then(
|
||||
function (data) {
|
||||
var key = data[1]
|
||||
var t = new AuthToken()
|
||||
t.uid = uid
|
||||
t.data = data[0].toString('hex')
|
||||
t.id = key.slice(0, 32).toString('hex')
|
||||
t.key = key.slice(32, 64).toString('hex')
|
||||
t.hmacKey = key.slice(64, 96).toString('hex')
|
||||
t.xorKey = key.slice(96, 160).toString('hex')
|
||||
return t.save()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AuthToken.getCredentials = function (id, cb) {
|
||||
AuthToken.get(id)
|
||||
.done(
|
||||
function (token) {
|
||||
cb(null, token)
|
||||
},
|
||||
cb
|
||||
)
|
||||
}
|
||||
|
||||
AuthToken.fromHex = function (string) {
|
||||
return Token.
|
||||
tokenDataFromBytes(
|
||||
'session/create',
|
||||
5 * 32,
|
||||
Buffer(string, 'hex')
|
||||
)
|
||||
.then(
|
||||
function (data) {
|
||||
var key = data[1]
|
||||
var t = new AuthToken()
|
||||
t.data = data[0].toString('hex')
|
||||
t.id = key.slice(0, 32).toString('hex')
|
||||
t.key = key.slice(32, 64).toString('hex')
|
||||
t.hmacKey = key.slice(64, 96).toString('hex')
|
||||
t.xorKey = key.slice(96, 160).toString('hex')
|
||||
return t
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AuthToken.get = function (id) {
|
||||
return db
|
||||
.get(id + '/auth')
|
||||
.then(AuthToken.hydrate)
|
||||
}
|
||||
|
||||
AuthToken.del = function (id) {
|
||||
return db.delete(id + '/auth')
|
||||
}
|
||||
|
||||
AuthToken.prototype.save = function () {
|
||||
var self = this
|
||||
return db.set(this.id + '/auth', this).then(function () { return self })
|
||||
}
|
||||
|
||||
AuthToken.prototype.del = function () {
|
||||
return AuthToken.del(this.id)
|
||||
}
|
||||
|
||||
AuthToken.prototype.bundle = function (keyFetchToken, sessionToken) {
|
||||
return this.bundleHexStrings([keyFetchToken.data, sessionToken.data])
|
||||
}
|
||||
|
||||
AuthToken.prototype.unbundle = function (bundle) {
|
||||
var buffer = Buffer(bundle, 'hex')
|
||||
var ciphertext = buffer.slice(0, 64)
|
||||
var hmac = buffer.slice(64, 96).toString('hex')
|
||||
if(this.hmac(ciphertext).toString('hex') !== hmac) {
|
||||
throw new Error('Unmatching HMAC')
|
||||
}
|
||||
var plaintext = this.xor(ciphertext)
|
||||
return {
|
||||
// TODO: maybe parse the tokens here
|
||||
keyFetchToken: plaintext.slice(0, 32).toString('hex'),
|
||||
sessionToken: plaintext.slice(32, 64).toString('hex')
|
||||
}
|
||||
}
|
||||
|
||||
return AuthToken
|
||||
}
|
|
@ -32,10 +32,16 @@ module.exports = function (config, dbs, mailer) {
|
|||
Token,
|
||||
dbs.store
|
||||
)
|
||||
var AuthToken = require('./auth_token')(
|
||||
inherits,
|
||||
Token,
|
||||
dbs.cache
|
||||
)
|
||||
var tokens = {
|
||||
AccountResetToken: AccountResetToken,
|
||||
KeyFetchToken: KeyFetchToken,
|
||||
SessionToken: SessionToken
|
||||
SessionToken: SessionToken,
|
||||
AuthToken: AuthToken
|
||||
}
|
||||
|
||||
var RecoveryMethod = require('./recovery_method')(
|
||||
|
|
|
@ -16,7 +16,7 @@ module.exports = function (P, uuid, srp, db, error) {
|
|||
this.b = null
|
||||
this.B = null
|
||||
this.K = null
|
||||
this.type = null // 'login' | 'passwordChange'
|
||||
this.passwordStretching = null
|
||||
}
|
||||
|
||||
function srpGenKey() {
|
||||
|
@ -29,7 +29,7 @@ module.exports = function (P, uuid, srp, db, error) {
|
|||
return d.promise
|
||||
}
|
||||
|
||||
SrpSession.create = function (type, account) {
|
||||
SrpSession.create = function (account) {
|
||||
var session = null
|
||||
if (!account) { throw error.unknownAccount() }
|
||||
return srpGenKey()
|
||||
|
@ -44,7 +44,7 @@ module.exports = function (P, uuid, srp, db, error) {
|
|||
session.v = Buffer(account.srp.verifier, 'hex')
|
||||
session.b = b
|
||||
session.B = srp.getB(session.v, session.g, b, session.N, alg)
|
||||
session.type = type
|
||||
session.passwordStretching = account.passwordStretching
|
||||
return session.save()
|
||||
}
|
||||
)
|
||||
|
@ -54,8 +54,8 @@ module.exports = function (P, uuid, srp, db, error) {
|
|||
return db.delete(id + '/srp')
|
||||
}
|
||||
|
||||
SrpSession.start = function (type, account) {
|
||||
return SrpSession.create(type, account)
|
||||
SrpSession.start = function (account) {
|
||||
return SrpSession.create(account)
|
||||
.then(
|
||||
function (session) {
|
||||
return session.clientData()
|
||||
|
@ -105,7 +105,6 @@ module.exports = function (P, uuid, srp, db, error) {
|
|||
s.v = Buffer(object.v, 'hex')
|
||||
s.b = Buffer(object.b, 'hex')
|
||||
s.B = Buffer(object.B, 'hex')
|
||||
s.type = object.type
|
||||
return s
|
||||
}
|
||||
|
||||
|
@ -118,13 +117,10 @@ module.exports = function (P, uuid, srp, db, error) {
|
|||
SrpSession.prototype.clientData = function () {
|
||||
return {
|
||||
srpToken: this.id,
|
||||
stretch: {
|
||||
salt: 'FEED'
|
||||
},
|
||||
passwordStretching: this.passwordStretching,
|
||||
srp: {
|
||||
N_bits: 2048,
|
||||
alg: 'sha256',
|
||||
s: this.s,
|
||||
type: 'SRP-6a/SHA256/2048/v1',
|
||||
salt: this.s,
|
||||
B: this.B.toString('hex')
|
||||
}
|
||||
}
|
||||
|
@ -139,7 +135,7 @@ module.exports = function (P, uuid, srp, db, error) {
|
|||
var A = srp.getA(g, a, N)
|
||||
var B = Buffer(session.srp.B, 'hex')
|
||||
var S = srp.client_getS(
|
||||
Buffer(session.srp.s, 'hex'),
|
||||
Buffer(session.srp.salt, 'hex'),
|
||||
Buffer(email),
|
||||
Buffer(password),
|
||||
N,
|
||||
|
@ -150,7 +146,7 @@ module.exports = function (P, uuid, srp, db, error) {
|
|||
)
|
||||
|
||||
var M = srp.getM(A, B, S, N)
|
||||
var K = srp.getK(S, N, session.srp.alg)
|
||||
var K = srp.getK(S, N, 'sha256')
|
||||
return {
|
||||
A: A.toString('hex'),
|
||||
M: M.toString('hex'),
|
||||
|
@ -169,8 +165,7 @@ module.exports = function (P, uuid, srp, db, error) {
|
|||
s: this.s,
|
||||
v: this.v.toString('hex'),
|
||||
b: this.b.toString('hex'),
|
||||
B: this.B.toString('hex'),
|
||||
type: this.type
|
||||
B: this.B.toString('hex')
|
||||
}).then(function () { return self })
|
||||
}
|
||||
|
||||
|
|
|
@ -304,6 +304,28 @@ module.exports = function (crypto, uuid, isA, error, Account, RecoveryMethod) {
|
|||
.done(reply, reply)
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/account/destroy',
|
||||
config: {
|
||||
auth: {
|
||||
strategy: 'authToken'
|
||||
},
|
||||
tags: ["account"],
|
||||
handler: function accountDestroy(request) {
|
||||
var reply = request.reply.bind(request)
|
||||
var authToken = request.auth.credentials
|
||||
|
||||
authToken.del()
|
||||
.then(
|
||||
function () {
|
||||
return Account.del(authToken.uid)
|
||||
}
|
||||
)
|
||||
.done(reply, reply)
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = function (isA, Account, SrpSession, AuthBundle) {
|
||||
|
||||
const HEX_STRING = /^(?:[a-fA-F0-9]{2})+$/
|
||||
|
||||
var routes = [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/auth/start',
|
||||
config: {
|
||||
description:
|
||||
"Begins an SRP login for the supplied email address, " +
|
||||
"returning the temporary srpToken and parameters for " +
|
||||
"key stretching and the SRP protocol for the client.",
|
||||
tags: ["srp", "account"],
|
||||
handler: function (request) {
|
||||
var reply = request.reply.bind(request)
|
||||
Account.getByEmail(request.payload.email)
|
||||
.then(SrpSession.start.bind(null))
|
||||
.done(reply, reply)
|
||||
},
|
||||
validate: {
|
||||
payload: {
|
||||
email: isA.String().email().required()
|
||||
},
|
||||
response: {
|
||||
schema: {
|
||||
srpToken: isA.String().required(),
|
||||
passwordStretching: isA.Object(),
|
||||
srp: isA.Object({
|
||||
type: isA.String().required(),
|
||||
salt: isA.String().regex(HEX_STRING), // salt
|
||||
B: isA.String().regex(HEX_STRING) // server's public key value
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/auth/finish',
|
||||
config: {
|
||||
description:
|
||||
"Finishes the SRP dance, with the client providing " +
|
||||
"proof-of-knownledge of the password and receiving " +
|
||||
"the bundle encrypted with the authToken.",
|
||||
tags: ["srp", "session"],
|
||||
handler: function (request) {
|
||||
var reply = request.reply.bind(request)
|
||||
SrpSession
|
||||
.finish(request.payload.srpToken, request.payload.A, request.payload.M)
|
||||
.then(
|
||||
function (srpSession) {
|
||||
return AuthBundle.login(srpSession.K, srpSession.uid)
|
||||
}
|
||||
)
|
||||
.done(reply, reply)
|
||||
},
|
||||
validate: {
|
||||
payload: {
|
||||
srpToken: isA.String().required(),
|
||||
A: isA.String().regex(HEX_STRING).required(),
|
||||
M: isA.String().regex(HEX_STRING).required()
|
||||
},
|
||||
response: {
|
||||
schema: {
|
||||
bundle: isA.String().regex(HEX_STRING).required()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
return routes
|
||||
}
|
|
@ -16,16 +16,16 @@ module.exports = function (
|
|||
signer,
|
||||
models
|
||||
) {
|
||||
var srp = require('./srp')(models.SrpSession, models.AuthBundle)
|
||||
|
||||
var auth = require('./auth')(isA, models.Account, models.SrpSession, models.AuthBundle)
|
||||
var defaults = require('./defaults')(P, models.dbs)
|
||||
var idp = require('./idp')(crypto, error, isA, serverPublicKey)
|
||||
var account = require('./account')(crypto, uuid, isA, error, models.Account, models.RecoveryMethod)
|
||||
var password = require('./password')(isA, error, srp, models.Account)
|
||||
var session = require('./session')(srp, isA, error, models.Account)
|
||||
var password = require('./password')(isA, error, models.Account)
|
||||
var session = require('./session')(isA, error, models.Account, models.tokens)
|
||||
var sign = require('./sign')(isA, error, signer, models.Account)
|
||||
|
||||
var routes = defaults.concat(
|
||||
auth,
|
||||
idp,
|
||||
account,
|
||||
password,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = function (isA, error, srp, Account) {
|
||||
module.exports = function (isA, error, Account) {
|
||||
|
||||
const HEX_STRING = /^(?:[a-fA-F0-9]{2})+$/
|
||||
|
||||
|
@ -11,69 +11,6 @@ module.exports = function (isA, error, srp, Account) {
|
|||
}
|
||||
|
||||
var routes = [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/password/change/auth/start',
|
||||
config: {
|
||||
description:
|
||||
"Begins an SRP login for the authenticated user, " +
|
||||
"returning the temporary sessionId and parameters for " +
|
||||
"key stretching and the SRP protocol for the client.",
|
||||
tags: ["srp", "password"],
|
||||
handler: function (request) {
|
||||
Account
|
||||
.get(request.auth.credentials.uid)
|
||||
.done(
|
||||
function (account) {
|
||||
return srp.start('passwordChange', account, request)
|
||||
}
|
||||
)
|
||||
},
|
||||
auth: {
|
||||
strategy: 'sessionToken'
|
||||
},
|
||||
validate: {
|
||||
response: {
|
||||
schema: {
|
||||
srpToken: isA.String().required(),
|
||||
stretch: isA.Object({
|
||||
salt: isA.String()
|
||||
}),
|
||||
srp: isA.Object({
|
||||
N_bits: isA.Number().valid(2048), // number of bits for prime
|
||||
alg: isA.String().valid('sha256'), // hash algorithm (sha256)
|
||||
s: isA.String().regex(HEX_STRING), // salt
|
||||
B: isA.String().regex(HEX_STRING) // server's public key value
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/password/change/auth/finish',
|
||||
handler: srp.finish,
|
||||
config: {
|
||||
description:
|
||||
"Finishes the SRP dance, with the client providing " +
|
||||
"proof-of-knownledge of the password and receiving " +
|
||||
"the bundle encrypted with the shared key.",
|
||||
tags: ["srp", "password"],
|
||||
validate: {
|
||||
payload: {
|
||||
srpToken: isA.String().required(),
|
||||
A: isA.String().regex(HEX_STRING).required(),
|
||||
M: isA.String().regex(HEX_STRING).required()
|
||||
},
|
||||
response: {
|
||||
schema: {
|
||||
bundle: isA.String().regex(HEX_STRING).required()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/password/forgot/send_code',
|
||||
|
|
|
@ -2,86 +2,48 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = function (srp, isA, error, Account) {
|
||||
module.exports = function (isA, error, Account, tokens) {
|
||||
|
||||
const HEX_STRING = /^(?:[a-fA-F0-9]{2})+$/
|
||||
|
||||
var routes = [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/session/auth/start',
|
||||
path: '/session/create',
|
||||
config: {
|
||||
description:
|
||||
"Begins an SRP login for the supplied email address, " +
|
||||
"returning the temporary sessionId and parameters for " +
|
||||
"key stretching and the SRP protocol for the client.",
|
||||
tags: ["srp", "account"],
|
||||
handler: function (request) {
|
||||
Account
|
||||
.getByEmail(request.payload.email)
|
||||
.done(
|
||||
function (account) {
|
||||
return srp.start('login', account, request)
|
||||
}
|
||||
)
|
||||
},
|
||||
validate: {
|
||||
payload: {
|
||||
email: isA.String().email().required()
|
||||
},
|
||||
response: {
|
||||
schema: {
|
||||
srpToken: isA.String().required(),
|
||||
stretch: isA.Object({
|
||||
salt: isA.String()
|
||||
}),
|
||||
srp: isA.Object({
|
||||
N_bits: isA.Number().valid(2048), // number of bits for prime
|
||||
alg: isA.String().valid('sha256'), // hash algorithm (sha256)
|
||||
s: isA.String().regex(HEX_STRING), // salt
|
||||
B: isA.String().regex(HEX_STRING) // server's public key value
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/session/auth/finish',
|
||||
handler: srp.finish,
|
||||
config: {
|
||||
description:
|
||||
"Finishes the SRP dance, with the client providing " +
|
||||
"proof-of-knownledge of the password and receiving " +
|
||||
"the bundle encrypted with the shared key.",
|
||||
tags: ["srp", "session"],
|
||||
validate: {
|
||||
payload: {
|
||||
srpToken: isA.String().required(),
|
||||
A: isA.String().regex(HEX_STRING).required(),
|
||||
M: isA.String().regex(HEX_STRING).required()
|
||||
},
|
||||
response: {
|
||||
schema: {
|
||||
bundle: isA.String().regex(HEX_STRING).required()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/session/status',
|
||||
config: {
|
||||
description: "Check whether a session is still valid.",
|
||||
description: "Creates a new session",
|
||||
tags: ["session"],
|
||||
auth: {
|
||||
strategy: 'sessionToken'
|
||||
strategy: 'authToken'
|
||||
},
|
||||
handler: function (request) {
|
||||
// hapi will take care of the case where the token isn't valid
|
||||
request.reply({})
|
||||
var reply = request.reply.bind(request)
|
||||
var authToken = request.auth.credentials
|
||||
var keyFetchToken = null
|
||||
var sessionToken = null
|
||||
authToken.del()
|
||||
.then(
|
||||
function () {
|
||||
return tokens.KeyFetchToken.create(authToken.uid)
|
||||
}
|
||||
)
|
||||
.then(function (t) { keyFetchToken = t })
|
||||
.then(tokens.SessionToken.create.bind(null, authToken.uid))
|
||||
.then(function (t) { sessionToken = t })
|
||||
.then(Account.get.bind(null, authToken.uid))
|
||||
.then(
|
||||
function (account) {
|
||||
return account.addSessionToken(sessionToken)
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function () {
|
||||
return {
|
||||
bundle: authToken.bundle(keyFetchToken, sessionToken)
|
||||
}
|
||||
}
|
||||
)
|
||||
.done(reply, reply)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -6,8 +6,7 @@ module.exports = function (SrpSession, AuthBundle) {
|
|||
|
||||
function srpStart(type, account, request) {
|
||||
var reply = request.reply.bind(request)
|
||||
SrpSession
|
||||
.start(type, account)
|
||||
SrpSession.start(account)
|
||||
.done(reply, reply)
|
||||
}
|
||||
|
||||
|
@ -17,7 +16,7 @@ module.exports = function (SrpSession, AuthBundle) {
|
|||
.finish(request.payload.srpToken, request.payload.A, request.payload.M)
|
||||
.then(
|
||||
function (srpSession) {
|
||||
return AuthBundle[srpSession.type](srpSession.K, srpSession.uid)
|
||||
return AuthBundle.login(srpSession.K, srpSession.uid)
|
||||
}
|
||||
)
|
||||
.done(reply, reply)
|
||||
|
|
|
@ -22,6 +22,10 @@ module.exports = function (path, Hapi, toobusy) {
|
|||
accountResetToken: {
|
||||
scheme: 'hawk',
|
||||
getCredentialsFunc: tokens.AccountResetToken.getCredentials
|
||||
},
|
||||
authToken: {
|
||||
scheme: 'hawk',
|
||||
getCredentialsFunc: tokens.AuthToken.getCredentials
|
||||
}
|
||||
},
|
||||
cors: true,
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
var test = require('tap').test
|
||||
var crypto = require('crypto')
|
||||
var P = require('p-promise')
|
||||
var config = require('../config').root()
|
||||
|
||||
var dbs = require('../kv')(config)
|
||||
|
||||
var mailer = {
|
||||
sendCode: function () { return P(null) }
|
||||
}
|
||||
|
||||
var models = require('../models')(config, dbs, mailer)
|
||||
var AuthToken = models.tokens.AuthToken
|
||||
|
||||
test(
|
||||
'bundle / unbundle works',
|
||||
function (t) {
|
||||
function end() { t.end() }
|
||||
AuthToken.create('xxx')
|
||||
.then(
|
||||
function (x) {
|
||||
var keyFetchTokenHex = crypto.randomBytes(32).toString('hex')
|
||||
var sessionTokenHex = crypto.randomBytes(32).toString('hex')
|
||||
var b = x.bundle(keyFetchTokenHex, sessionTokenHex)
|
||||
var ub = x.unbundle(b)
|
||||
t.equal(ub.keyFetchTokenHex, keyFetchTokenHex)
|
||||
t.equal(ub.sessionTokenHex, sessionTokenHex)
|
||||
return x
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function (x) {
|
||||
return x.del()
|
||||
}
|
||||
)
|
||||
.done(end, end)
|
||||
}
|
||||
)
|
|
@ -261,7 +261,10 @@ var RToken = require('../models/token')(inherits, RBundle)
|
|||
var KeyFetchToken = require('../models/key_fetch_token')(inherits, KToken, dbs.store)
|
||||
var AccountResetToken = require('../models/account_reset_token')(inherits, RToken, crypto, dbs.store)
|
||||
var SessionToken = require('../models/session_token')(inherits, SToken, dbs.store)
|
||||
|
||||
var AuthToken = require('../models/auth_token')(inherits, SToken, dbs.store)
|
||||
var tokens = {
|
||||
AuthToken: AuthToken,
|
||||
KeyFetchToken: KeyFetchToken,
|
||||
AccountResetToken: AccountResetToken,
|
||||
SessionToken: SessionToken
|
||||
|
@ -277,8 +280,8 @@ FakeAccount.prototype.addSessionToken = function (t) {
|
|||
this.sessionTokenIds[t.id] = true
|
||||
return P(null)
|
||||
}
|
||||
FakeAccount.prototype.setResetToken = function (t) {
|
||||
this.resetTokenId = t.id
|
||||
FakeAccount.prototype.setAuthToken = function (t) {
|
||||
this.authTokenId = t.id
|
||||
return P(null)
|
||||
}
|
||||
|
||||
|
@ -334,72 +337,53 @@ test(
|
|||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'/session/auth/finish',
|
||||
function (t) {
|
||||
account = new FakeAccount()
|
||||
AuthBundle.login(sessionAuth.K, 'xxx')
|
||||
.done(
|
||||
function (authBundle) {
|
||||
var b = authBundle.bundle
|
||||
t.equal(b, sessionAuth.ciphertext + sessionAuth.hmac)
|
||||
t.end()
|
||||
},
|
||||
function (err) {
|
||||
t.fail(err)
|
||||
t.end()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
// test(
|
||||
// '/session/auth/finish',
|
||||
// function (t) {
|
||||
// account = new FakeAccount()
|
||||
// AuthBundle.login(sessionAuth.K, 'xxx')
|
||||
// .done(
|
||||
// function (authBundle) {
|
||||
// var b = authBundle.bundle
|
||||
// t.equal(b, sessionAuth.ciphertext + sessionAuth.hmac)
|
||||
// t.end()
|
||||
// },
|
||||
// function (err) {
|
||||
// t.fail(err)
|
||||
// t.end()
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// )
|
||||
|
||||
// test(
|
||||
// '/password/change/auth/finish',
|
||||
// function (t) {
|
||||
// account = new FakeAccount()
|
||||
// AuthBundle.passwordChange(sessionAuth.K, 'xxx')
|
||||
// .done(
|
||||
// function (changePasswordBundle) {
|
||||
// var b = changePasswordBundle.bundle
|
||||
// t.equal(b, passwordChange.ciphertext + passwordChange.hmac)
|
||||
// t.end()
|
||||
// },
|
||||
// function (err) {
|
||||
// t.fail(err)
|
||||
// t.end()
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// )
|
||||
|
||||
test(
|
||||
'/password/change/auth/finish',
|
||||
'login sets authToken on account',
|
||||
function (t) {
|
||||
account = new FakeAccount()
|
||||
AuthBundle.passwordChange(sessionAuth.K, 'xxx')
|
||||
.done(
|
||||
function (changePasswordBundle) {
|
||||
var b = changePasswordBundle.bundle
|
||||
t.equal(b, passwordChange.ciphertext + passwordChange.hmac)
|
||||
t.end()
|
||||
},
|
||||
function (err) {
|
||||
t.fail(err)
|
||||
t.end()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'login adds sessionToken to account',
|
||||
function (t) {
|
||||
account = new FakeAccount()
|
||||
t.equal(Object.keys(account.sessionTokenIds).length, 0)
|
||||
t.equal(!!account.authTokenId, false)
|
||||
AuthBundle.login(sessionAuth.K, 'xxx')
|
||||
.done(
|
||||
function () {
|
||||
t.equal(Object.keys(account.sessionTokenIds).length, 1)
|
||||
t.end()
|
||||
},
|
||||
function (err) {
|
||||
t.fail(err)
|
||||
t.end()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'changePassword sets resetToken on account',
|
||||
function (t) {
|
||||
account = new FakeAccount()
|
||||
t.equal(!!account.resetTokenId, false)
|
||||
AuthBundle.passwordChange(sessionAuth.K, 'xxx')
|
||||
.done(
|
||||
function () {
|
||||
t.equal(!!account.resetTokenId, true)
|
||||
t.equal(!!account.authTokenId, true)
|
||||
t.end()
|
||||
},
|
||||
function (err) {
|
||||
|
|
|
@ -22,6 +22,9 @@ var server = cp.spawn(
|
|||
}
|
||||
)
|
||||
|
||||
server.stdout.on('data', process.stdout.write.bind(process.stdout))
|
||||
server.stderr.on('data', process.stderr.write.bind(process.stderr))
|
||||
|
||||
function main() {
|
||||
test(
|
||||
'Create account flow',
|
||||
|
@ -57,7 +60,7 @@ function main() {
|
|||
},
|
||||
function (err) {
|
||||
server.kill('SIGINT')
|
||||
t.fail(err.message)
|
||||
t.fail(err.message || err.error)
|
||||
t.end()
|
||||
}
|
||||
)
|
||||
|
@ -78,6 +81,7 @@ function waitLoop() {
|
|||
.done(
|
||||
main,
|
||||
function (err) {
|
||||
console.log('waiting...')
|
||||
setTimeout(waitLoop, 100)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -41,14 +41,11 @@ Account.create(alice)
|
|||
test(
|
||||
'create login session works',
|
||||
function (t) {
|
||||
SrpSession
|
||||
.create('login', a)
|
||||
SrpSession.create(a)
|
||||
.done(
|
||||
function (s) {
|
||||
t.equal(s.uid, a.uid)
|
||||
t.equal(s.s, a.srp.salt)
|
||||
t.equal(s.type, 'login')
|
||||
|
||||
t.end()
|
||||
}
|
||||
)
|
||||
|
@ -60,8 +57,7 @@ Account.create(alice)
|
|||
function (t) {
|
||||
var session = null
|
||||
var K = null
|
||||
SrpSession
|
||||
.create('login', a)
|
||||
SrpSession.create(a)
|
||||
.then(
|
||||
function (s) {
|
||||
session = s
|
||||
|
|
Загрузка…
Ссылка в новой задаче