Skip to content

Commit 97f3119

Browse files
committed
Changed tokenService to allow for domains of tokens
And added some tests
1 parent 6bfafe8 commit 97f3119

File tree

4 files changed

+172
-22
lines changed

4 files changed

+172
-22
lines changed

lib/models/account-manager.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,40 @@ class AccountManager {
418418
* @return {string} Generated token
419419
*/
420420
generateResetToken (userAccount) {
421-
return this.tokenService.generate({ webId: userAccount.webId })
421+
return this.tokenService.generate('reset-password', { webId: userAccount.webId })
422+
}
423+
424+
/**
425+
* Generates an expiring one-time-use token for password reset purposes
426+
* (the user's Web ID is saved in the token service).
427+
*
428+
* @param userAccount {UserAccount}
429+
*
430+
* @return {string} Generated token
431+
*/
432+
generateDeleteToken (userAccount) {
433+
return this.tokenService.generate('delete-account', { webId: userAccount.webId })
434+
}
435+
436+
/**
437+
* Validates that a token exists and is not expired, and returns the saved
438+
* token contents, or throws an error if invalid.
439+
* Does not consume / clear the token.
440+
*
441+
* @param token {string}
442+
*
443+
* @throws {Error} If missing or invalid token
444+
*
445+
* @return {Object|false} Saved token data object if verified, false otherwise
446+
*/
447+
validateDeleteToken (token) {
448+
let tokenValue = this.tokenService.verify('delete-account', token)
449+
450+
if (!tokenValue) {
451+
throw new Error('Invalid or expired delete account token')
452+
}
453+
454+
return tokenValue
422455
}
423456

424457
/**
@@ -433,7 +466,7 @@ class AccountManager {
433466
* @return {Object|false} Saved token data object if verified, false otherwise
434467
*/
435468
validateResetToken (token) {
436-
let tokenValue = this.tokenService.verify(token)
469+
let tokenValue = this.tokenService.verify('reset-password', token)
437470

438471
if (!tokenValue) {
439472
throw new Error('Invalid or expired reset token')
@@ -515,7 +548,7 @@ class AccountManager {
515548
sendDeleteAccountEmail (userAccount) {
516549
return Promise.resolve()
517550
.then(() => this.verifyEmailDependencies(userAccount))
518-
.then(() => this.generateResetToken(userAccount))
551+
.then(() => this.generateDeleteToken(userAccount))
519552
.then(resetToken => {
520553
const deleteUrl = this.getAccountDeleteUrl(resetToken)
521554

lib/models/token-service.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,26 @@ class TokenService {
77
this.tokens = {}
88
}
99

10-
generate (data = {}) {
10+
generate (domain, data = {}) {
1111
const token = ulid()
12+
this.tokens[domain] = this.tokens[domain] || {}
1213

1314
const value = {
1415
exp: new Date(Date.now() + 20 * 60 * 1000)
1516
}
16-
17-
this.tokens[token] = Object.assign({}, value, data)
17+
this.tokens[domain][token] = Object.assign({}, value, data)
1818

1919
return token
2020
}
2121

22-
verify (token) {
22+
verify (domain, token) {
2323
const now = new Date()
2424

25-
let tokenValue = this.tokens[token]
25+
if (!this.tokens[domain]) {
26+
throw new Error(`Invalid domain for tokens: ${domain}`)
27+
}
28+
29+
let tokenValue = this.tokens[domain][token]
2630

2731
if (tokenValue && now < tokenValue.exp) {
2832
return tokenValue
@@ -31,8 +35,12 @@ class TokenService {
3135
}
3236
}
3337

34-
remove (token) {
35-
delete this.tokens[token]
38+
remove (domain, token) {
39+
if (!this.tokens[domain]) {
40+
throw new Error(`Invalid domain for tokens: ${domain}`)
41+
}
42+
43+
delete this.tokens[domain][token]
3644
}
3745
}
3846

test/unit/account-manager-test.js

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,27 @@ describe('AccountManager', () => {
390390
})
391391
})
392392

393+
describe('generateDeleteToken()', () => {
394+
it('should generate and store an expiring delete token', () => {
395+
let tokenService = new TokenService()
396+
let options = { host, tokenService }
397+
398+
let accountManager = AccountManager.from(options)
399+
400+
let aliceWebId = 'https://alice.example.com/#me'
401+
let userAccount = {
402+
webId: aliceWebId
403+
}
404+
405+
let token = accountManager.generateDeleteToken(userAccount)
406+
407+
let tokenValue = accountManager.tokenService.verify('delete-account', token)
408+
409+
expect(tokenValue.webId).to.equal(aliceWebId)
410+
expect(tokenValue).to.have.property('exp')
411+
})
412+
})
413+
393414
describe('generateResetToken()', () => {
394415
it('should generate and store an expiring reset token', () => {
395416
let tokenService = new TokenService()
@@ -404,7 +425,7 @@ describe('AccountManager', () => {
404425

405426
let token = accountManager.generateResetToken(userAccount)
406427

407-
let tokenValue = accountManager.tokenService.verify(token)
428+
let tokenValue = accountManager.tokenService.verify('reset-password', token)
408429

409430
expect(tokenValue.webId).to.equal(aliceWebId)
410431
expect(tokenValue).to.have.property('exp')
@@ -484,4 +505,75 @@ describe('AccountManager', () => {
484505
})
485506
})
486507
})
508+
509+
describe('sendDeleteAccountEmail()', () => {
510+
it('should compose and send a delete account email', () => {
511+
let deleteToken = '1234'
512+
let tokenService = {
513+
generate: sinon.stub().returns(deleteToken)
514+
}
515+
516+
let emailService = {
517+
sendWithTemplate: sinon.stub().resolves()
518+
}
519+
520+
let aliceWebId = 'https://alice.example.com/#me'
521+
let userAccount = {
522+
webId: aliceWebId,
523+
email: 'alice@example.com'
524+
}
525+
526+
let options = { host, tokenService, emailService }
527+
let accountManager = AccountManager.from(options)
528+
529+
accountManager.getAccountDeleteUrl = sinon.stub().returns('delete account url')
530+
531+
let expectedEmailData = {
532+
to: 'alice@example.com',
533+
webId: aliceWebId,
534+
deleteUrl: 'delete account url'
535+
}
536+
537+
return accountManager.sendDeleteAccountEmail(userAccount)
538+
.then(() => {
539+
expect(accountManager.getAccountDeleteUrl)
540+
.to.have.been.calledWith(deleteToken)
541+
expect(emailService.sendWithTemplate)
542+
.to.have.been.calledWith('delete-account', expectedEmailData)
543+
})
544+
})
545+
546+
it('should reject if no email service is set up', done => {
547+
let aliceWebId = 'https://alice.example.com/#me'
548+
let userAccount = {
549+
webId: aliceWebId,
550+
email: 'alice@example.com'
551+
}
552+
let options = { host }
553+
let accountManager = AccountManager.from(options)
554+
555+
accountManager.sendDeleteAccountEmail(userAccount)
556+
.catch(error => {
557+
expect(error.message).to.equal('Email service is not set up')
558+
done()
559+
})
560+
})
561+
562+
it('should reject if no user email is provided', done => {
563+
let aliceWebId = 'https://alice.example.com/#me'
564+
let userAccount = {
565+
webId: aliceWebId
566+
}
567+
let emailService = {}
568+
let options = { host, emailService }
569+
570+
let accountManager = AccountManager.from(options)
571+
572+
accountManager.sendDeleteAccountEmail(userAccount)
573+
.catch(error => {
574+
expect(error.message).to.equal('Account recovery email has not been provided')
575+
done()
576+
})
577+
})
578+
})
487579
})

test/unit/token-service-test.js

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ describe('TokenService', () => {
2121
it('should generate a new token and return a token key', () => {
2222
let service = new TokenService()
2323

24-
let token = service.generate()
25-
let value = service.tokens[token]
24+
let token = service.generate('test')
25+
let value = service.tokens.test[token]
2626

2727
expect(token).to.exist()
2828
expect(value).to.have.property('exp')
@@ -33,39 +33,56 @@ describe('TokenService', () => {
3333
it('should return false for expired tokens', () => {
3434
let service = new TokenService()
3535

36-
let token = service.generate()
36+
let token = service.generate('foo')
3737

38-
service.tokens[token].exp = new Date(Date.now() - 1000)
38+
service.tokens.foo[token].exp = new Date(Date.now() - 1000)
3939

40-
expect(service.verify(token)).to.be.false()
40+
expect(service.verify('foo', token)).to.be.false()
4141
})
4242

4343
it('should return false for non-existent tokens', () => {
4444
let service = new TokenService()
4545

46+
service.generate('foo') // to have generated the domain
4647
let token = 'invalid token 123'
4748

48-
expect(service.verify(token)).to.be.false()
49+
expect(service.verify('foo', token)).to.be.false()
4950
})
5051

5152
it('should return the token value if token not expired', () => {
5253
let service = new TokenService()
5354

54-
let token = service.generate()
55+
let token = service.generate('foo')
5556

56-
expect(service.verify(token)).to.be.ok()
57+
expect(service.verify('foo', token)).to.be.ok()
58+
})
59+
60+
it('should throw error if invalid domain', () => {
61+
let service = new TokenService()
62+
63+
let token = service.generate('foo')
64+
65+
expect(() => service.verify('bar', token)).to.throw()
5766
})
5867
})
5968

6069
describe('remove()', () => {
6170
it('should remove a generated token from the service', () => {
6271
let service = new TokenService()
6372

64-
let token = service.generate()
73+
let token = service.generate('bar')
74+
75+
service.remove('bar', token)
76+
77+
expect(service.tokens.bar[token]).to.not.exist()
78+
})
79+
80+
it('should throw an error if invalid domain', () => {
81+
let service = new TokenService()
6582

66-
service.remove(token)
83+
let token = service.generate('foo')
6784

68-
expect(service.tokens[token]).to.not.exist()
85+
expect(() => service.remove('bar', token)).to.throw()
6986
})
7087
})
7188
})

0 commit comments

Comments
 (0)