diff --git a/config/identity/handler/account-store/default.json b/config/identity/handler/account-store/default.json index 896d9ebc4e..71cad4ca3a 100644 --- a/config/identity/handler/account-store/default.json +++ b/config/identity/handler/account-store/default.json @@ -8,7 +8,21 @@ "saltRounds": 10, "storage": { "@id": "urn:solid-server:default:AccountStorage" + }, + "forgotPasswordStorage": { + "@id": "urn:solid-server:default:ExpiringForgotPasswordStorage" } + }, + { + "comment": "Stores expiring data. This class has a `finalize` function that needs to be called after stopping the server.", + "@id": "urn:solid-server:default:ExpiringForgotPasswordStorage", + "@type": "WrappedExpiringStorage", + "source": { "@id": "urn:solid-server:default:ForgotPasswordStorage" } + }, + { + "comment": "Makes sure the expiring storage cleanup timer is stopped when the application needs to stop.", + "@id": "urn:solid-server:default:Finalizer", + "ParallelFinalizer:_finalizers": [ { "@id": "urn:solid-server:default:ExpiringForgotPasswordStorage" } ] } ] } diff --git a/config/storage/key-value/memory.json b/config/storage/key-value/memory.json index 41d766b385..19228f9af4 100644 --- a/config/storage/key-value/memory.json +++ b/config/storage/key-value/memory.json @@ -33,6 +33,11 @@ "comment": "Storage used by setup components.", "@id": "urn:solid-server:default:SetupStorage", "@type": "MemoryMapStorage" + }, + { + "comment": "Storage used for ForgotPassword records", + "@id": "urn:solid-server:default:ForgotPasswordStorage", + "@type":"MemoryMapStorage" } ] } diff --git a/config/storage/key-value/resource-store.json b/config/storage/key-value/resource-store.json index 23d2d9aa39..74f90d67f6 100644 --- a/config/storage/key-value/resource-store.json +++ b/config/storage/key-value/resource-store.json @@ -47,6 +47,14 @@ "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, "container": "/.internal/accounts/" }, + { + "comment": "Storage used for ForgotPassword records", + "@id": "urn:solid-server:default:ForgotPasswordStorage", + "@type":"JsonResourceStorage", + "source": { "@id": "urn:solid-server:default:ResourceStore" }, + "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, + "container": "/.internal/forgot-password/" + }, { "comment": "Storage used by setup components.", "@id": "urn:solid-server:default:SetupStorage", diff --git a/src/identity/interaction/email-password/storage/BaseAccountStore.ts b/src/identity/interaction/email-password/storage/BaseAccountStore.ts index 4cfc04d0bf..2d990f86e9 100644 --- a/src/identity/interaction/email-password/storage/BaseAccountStore.ts +++ b/src/identity/interaction/email-password/storage/BaseAccountStore.ts @@ -1,6 +1,7 @@ import assert from 'assert'; import { hash, compare } from 'bcrypt'; import { v4 } from 'uuid'; +import type { ExpiringStorage } from '../../../../storage/keyvalue/ExpiringStorage'; import type { KeyValueStorage } from '../../../../storage/keyvalue/KeyValueStorage'; import type { AccountSettings, AccountStore } from './AccountStore'; @@ -26,15 +27,25 @@ export interface ForgotPasswordPayload { export type EmailPasswordData = AccountPayload | ForgotPasswordPayload | AccountSettings; /** - * A EmailPasswordStore that uses a KeyValueStorage - * to persist its information. + * A EmailPasswordStore that uses a KeyValueStorage to persist its information and an + * ExpiringStorage to persist ForgotPassword records. + * + * `forgotPasswordExpiration` parameter is how long the ForgotPassword record should be + * stored in minutes. *(defaults to 15 minutes)* */ export class BaseAccountStore implements AccountStore { private readonly storage: KeyValueStorage; + private readonly forgotPasswordStorage: ExpiringStorage; private readonly saltRounds: number; + private readonly forgotPasswordExpiration: number; - public constructor(storage: KeyValueStorage, saltRounds: number) { + public constructor(storage: KeyValueStorage, + forgotPasswordStorage: ExpiringStorage, + saltRounds: number, + forgotPasswordExpiration = 15) { this.storage = storage; + this.forgotPasswordStorage = forgotPasswordStorage; + this.forgotPasswordExpiration = forgotPasswordExpiration * 60 * 1000; this.saltRounds = saltRounds; } @@ -130,20 +141,21 @@ export class BaseAccountStore implements AccountStore { public async generateForgotPasswordRecord(email: string): Promise { const recordId = v4(); await this.getAccountPayload(email, true); - await this.storage.set( + await this.forgotPasswordStorage.set( this.getForgotPasswordRecordResourceIdentifier(recordId), { recordId, email }, + this.forgotPasswordExpiration, ); return recordId; } public async getForgotPasswordRecord(recordId: string): Promise { const identifier = this.getForgotPasswordRecordResourceIdentifier(recordId); - const forgotPasswordRecord = await this.storage.get(identifier) as ForgotPasswordPayload | undefined; + const forgotPasswordRecord = await this.forgotPasswordStorage.get(identifier) as ForgotPasswordPayload | undefined; return forgotPasswordRecord?.email; } public async deleteForgotPasswordRecord(recordId: string): Promise { - await this.storage.delete(this.getForgotPasswordRecordResourceIdentifier(recordId)); + await this.forgotPasswordStorage.delete(this.getForgotPasswordRecordResourceIdentifier(recordId)); } } diff --git a/test/unit/identity/interaction/email-password/storage/BaseAccountStore.test.ts b/test/unit/identity/interaction/email-password/storage/BaseAccountStore.test.ts index e640b6b53a..e4cb7c122b 100644 --- a/test/unit/identity/interaction/email-password/storage/BaseAccountStore.test.ts +++ b/test/unit/identity/interaction/email-password/storage/BaseAccountStore.test.ts @@ -3,10 +3,12 @@ import type { EmailPasswordData, } from '../../../../../../src/identity/interaction/email-password/storage/BaseAccountStore'; import { BaseAccountStore } from '../../../../../../src/identity/interaction/email-password/storage/BaseAccountStore'; +import type { ExpiringStorage } from '../../../../../../src/storage/keyvalue/ExpiringStorage'; import type { KeyValueStorage } from '../../../../../../src/storage/keyvalue/KeyValueStorage'; describe('A BaseAccountStore', (): void => { let storage: KeyValueStorage; + let forgotPasswordStorage: ExpiringStorage; const saltRounds = 11; let store: BaseAccountStore; const email = 'test@test.com'; @@ -22,7 +24,13 @@ describe('A BaseAccountStore', (): void => { delete: jest.fn((id: string): any => map.delete(id)), } as any; - store = new BaseAccountStore(storage, saltRounds); + forgotPasswordStorage = { + get: jest.fn((id: string): any => map.get(id)), + set: jest.fn((id: string, value: any): any => map.set(id, value)), + delete: jest.fn((id: string): any => map.delete(id)), + } as any; + + store = new BaseAccountStore(storage, forgotPasswordStorage, saltRounds); }); it('can create accounts.', async(): Promise => {