mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Store account settings separately
Account settings are stored using the WebID as key. Reason for using the WebID is that this allows faster access to the settings in authenticated requests. A consequence of this is that passwords are now always required during registration, and that there can only be 1 account per WebID.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import assert from 'assert';
|
||||
import type { Operation } from '../../../../ldp/operations/Operation';
|
||||
import { getLoggerFor } from '../../../../logging/LogUtil';
|
||||
import { BadRequestHttpError } from '../../../../util/errors/BadRequestHttpError';
|
||||
import { readJsonStream } from '../../../../util/StreamUtil';
|
||||
import type { AccountStore } from '../storage/AccountStore';
|
||||
import { InteractionHandler } from './InteractionHandler';
|
||||
@@ -23,6 +24,11 @@ export class LoginHandler extends InteractionHandler {
|
||||
const { email, password, remember } = await this.parseInput(operation);
|
||||
// Try to log in, will error if email/password combination is invalid
|
||||
const webId = await this.accountStore.authenticate(email, password);
|
||||
const settings = await this.accountStore.getSettings(webId);
|
||||
if (!settings.useIdp) {
|
||||
// There is an account but is not used for identification with the IDP
|
||||
throw new BadRequestHttpError('This server is not an identity provider for this account.');
|
||||
}
|
||||
this.logger.debug(`Logging in user ${email}`);
|
||||
return {
|
||||
type: 'complete',
|
||||
|
||||
@@ -1,42 +1,67 @@
|
||||
/**
|
||||
* Options that can be set on an account.
|
||||
*/
|
||||
export interface AccountSettings {
|
||||
/**
|
||||
* If this account can be used to identify as the corresponding WebID in the IDP.
|
||||
*/
|
||||
useIdp: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage needed for the email-password interaction
|
||||
*/
|
||||
export interface AccountStore {
|
||||
/**
|
||||
* Authenticate if the username and password are correct and return the webId
|
||||
* if it is. Return an error if it is not.
|
||||
* @param email - the user's email
|
||||
* @param password - this user's password
|
||||
* @returns The user's WebId
|
||||
* Authenticate if the username and password are correct and return the WebID
|
||||
* if it is. Throw an error if it is not.
|
||||
* @param email - The user's email.
|
||||
* @param password - This user's password.
|
||||
* @returns The user's WebID.
|
||||
*/
|
||||
authenticate: (email: string, password: string) => Promise<string>;
|
||||
|
||||
/**
|
||||
* Creates a new account
|
||||
* @param email - the account email
|
||||
* @param webId - account webId
|
||||
* @param password - account password
|
||||
* Creates a new account.
|
||||
* @param email - Account email.
|
||||
* @param webId - Account WebID.
|
||||
* @param password - Account password.
|
||||
* @param settings - Specific settings for the account.
|
||||
*/
|
||||
create: (email: string, webId: string, password: string) => Promise<void>;
|
||||
create: (email: string, webId: string, password: string, settings: AccountSettings) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Verifies the account creation. This can be used with, for example, e-mail verification.
|
||||
* The account can only be used after it is verified.
|
||||
* In case verification is not required, this should be called immediately after the `create` call.
|
||||
* @param email - the account email
|
||||
* @param email - The account email.
|
||||
*/
|
||||
verify: (email: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Changes the password
|
||||
* @param email - the user's email
|
||||
* @param password - the user's password
|
||||
* Changes the password.
|
||||
* @param email - The user's email.
|
||||
* @param password - The user's password.
|
||||
*/
|
||||
changePassword: (email: string, password: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Delete the account
|
||||
* @param email - the user's email
|
||||
* Gets the settings associated with this account.
|
||||
* Errors if there is no matching account.
|
||||
* @param webId - The account WebID.
|
||||
*/
|
||||
getSettings: (webId: string) => Promise<AccountSettings>;
|
||||
|
||||
/**
|
||||
* Updates the settings associated with this account.
|
||||
* @param webId - The account WebID.
|
||||
* @param settings - New settings for the account.
|
||||
*/
|
||||
updateSettings: (webId: string, settings: AccountSettings) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Delete the account.
|
||||
* @param email - The user's email.
|
||||
*/
|
||||
deleteAccount: (email: string) => Promise<void>;
|
||||
|
||||
@@ -44,22 +69,22 @@ export interface AccountStore {
|
||||
* Creates a Forgot Password Confirmation Record. This will be to remember that
|
||||
* a user has made a request to reset a password. Throws an error if the email doesn't
|
||||
* exist
|
||||
* @param email - the user's email
|
||||
* @returns the record id. This should be included in the reset password link
|
||||
* @param email - The user's email.
|
||||
* @returns The record id. This should be included in the reset password link.
|
||||
*/
|
||||
generateForgotPasswordRecord: (email: string) => Promise<string>;
|
||||
|
||||
/**
|
||||
* Gets the email associated with the forgot password confirmation record or undefined
|
||||
* if it's not present
|
||||
* @param recordId - the record id retrieved from the link
|
||||
* @returns the user's email
|
||||
* @param recordId - The record id retrieved from the link.
|
||||
* @returns The user's email.
|
||||
*/
|
||||
getForgotPasswordRecord: (recordId: string) => Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Deletes the Forgot Password Confirmation Record
|
||||
* @param recordId - the record id of the forgot password confirmation record
|
||||
* @param recordId - The record id of the forgot password confirmation record.
|
||||
*/
|
||||
deleteForgotPasswordRecord: (recordId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from 'assert';
|
||||
import { hash, compare } from 'bcrypt';
|
||||
import { v4 } from 'uuid';
|
||||
import type { KeyValueStorage } from '../../../../storage/keyvalue/KeyValueStorage';
|
||||
import type { AccountStore } from './AccountStore';
|
||||
import type { AccountSettings, AccountStore } from './AccountStore';
|
||||
|
||||
/**
|
||||
* A payload to persist a user account
|
||||
@@ -23,7 +23,7 @@ export interface ForgotPasswordPayload {
|
||||
recordId: string;
|
||||
}
|
||||
|
||||
export type EmailPasswordData = AccountPayload | ForgotPasswordPayload;
|
||||
export type EmailPasswordData = AccountPayload | ForgotPasswordPayload | AccountSettings;
|
||||
|
||||
/**
|
||||
* A EmailPasswordStore that uses a KeyValueStorage
|
||||
@@ -52,58 +52,84 @@ export class BaseAccountStore implements AccountStore {
|
||||
return `forgot-password-resource-identifier/${encodeURIComponent(recordId)}`;
|
||||
}
|
||||
|
||||
/* eslint-disable lines-between-class-members */
|
||||
/**
|
||||
* Helper function that converts the given e-mail to an account identifier
|
||||
* and retrieves the account data from the internal storage.
|
||||
*
|
||||
* Will error if `checkExistence` is true and the account does not exist.
|
||||
*/
|
||||
private async getAccountPayload(email: string): Promise<{ key: string; account?: AccountPayload }> {
|
||||
private async getAccountPayload(email: string, checkExistence: true):
|
||||
Promise<{ key: string; account: AccountPayload }>;
|
||||
private async getAccountPayload(email: string, checkExistence: false):
|
||||
Promise<{ key: string; account?: AccountPayload }>;
|
||||
private async getAccountPayload(email: string, checkExistence: boolean):
|
||||
Promise<{ key: string; account?: AccountPayload }> {
|
||||
const key = this.getAccountResourceIdentifier(email);
|
||||
const account = await this.storage.get(key) as AccountPayload | undefined;
|
||||
assert(!checkExistence || account, 'Account does not exist');
|
||||
return { key, account };
|
||||
}
|
||||
/* eslint-enable lines-between-class-members */
|
||||
|
||||
public async authenticate(email: string, password: string): Promise<string> {
|
||||
const { account } = await this.getAccountPayload(email);
|
||||
assert(account, 'No account by that email');
|
||||
const { account } = await this.getAccountPayload(email, true);
|
||||
assert(account.verified, 'Account still needs to be verified');
|
||||
assert(await compare(password, account.password), 'Incorrect password');
|
||||
return account.webId;
|
||||
}
|
||||
|
||||
public async create(email: string, webId: string, password: string): Promise<void> {
|
||||
const { key, account } = await this.getAccountPayload(email);
|
||||
public async create(email: string, webId: string, password: string, settings: AccountSettings): Promise<void> {
|
||||
const { key, account } = await this.getAccountPayload(email, false);
|
||||
assert(!account, 'Account already exists');
|
||||
// Make sure there is no other account for this WebID
|
||||
const storedSettings = await this.storage.get(webId);
|
||||
assert(!storedSettings, 'There already is an account for this WebID');
|
||||
const payload: AccountPayload = {
|
||||
email,
|
||||
webId,
|
||||
password: await hash(password, this.saltRounds),
|
||||
verified: false,
|
||||
webId,
|
||||
};
|
||||
await this.storage.set(key, payload);
|
||||
await this.storage.set(webId, settings);
|
||||
}
|
||||
|
||||
public async verify(email: string): Promise<void> {
|
||||
const { key, account } = await this.getAccountPayload(email);
|
||||
assert(account, 'Account does not exist');
|
||||
const { key, account } = await this.getAccountPayload(email, true);
|
||||
account.verified = true;
|
||||
await this.storage.set(key, account);
|
||||
}
|
||||
|
||||
public async changePassword(email: string, password: string): Promise<void> {
|
||||
const { key, account } = await this.getAccountPayload(email);
|
||||
assert(account, 'Account does not exist');
|
||||
const { key, account } = await this.getAccountPayload(email, true);
|
||||
account.password = await hash(password, this.saltRounds);
|
||||
await this.storage.set(key, account);
|
||||
}
|
||||
|
||||
public async getSettings(webId: string): Promise<AccountSettings> {
|
||||
const settings = await this.storage.get(webId) as AccountSettings | undefined;
|
||||
assert(settings, 'Account does not exist');
|
||||
return settings;
|
||||
}
|
||||
|
||||
public async updateSettings(webId: string, settings: AccountSettings): Promise<void> {
|
||||
const oldSettings = await this.storage.get(webId);
|
||||
assert(oldSettings, 'Account does not exist');
|
||||
await this.storage.set(webId, settings);
|
||||
}
|
||||
|
||||
public async deleteAccount(email: string): Promise<void> {
|
||||
await this.storage.delete(this.getAccountResourceIdentifier(email));
|
||||
const { key, account } = await this.getAccountPayload(email, false);
|
||||
if (account) {
|
||||
await this.storage.delete(key);
|
||||
await this.storage.delete(account.webId);
|
||||
}
|
||||
}
|
||||
|
||||
public async generateForgotPasswordRecord(email: string): Promise<string> {
|
||||
const recordId = v4();
|
||||
const { account } = await this.getAccountPayload(email);
|
||||
assert(account, 'Account does not exist');
|
||||
await this.getAccountPayload(email, true);
|
||||
await this.storage.set(
|
||||
this.getForgotPasswordRecordResourceIdentifier(recordId),
|
||||
{ recordId, email },
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { PodSettings } from '../../../../pods/settings/PodSettings';
|
||||
import { joinUrl } from '../../../../util/PathUtil';
|
||||
import type { OwnershipValidator } from '../../../ownership/OwnershipValidator';
|
||||
import { assertPassword } from '../EmailPasswordUtil';
|
||||
import type { AccountStore } from '../storage/AccountStore';
|
||||
import type { AccountSettings, AccountStore } from '../storage/AccountStore';
|
||||
|
||||
export interface RegistrationManagerArgs {
|
||||
/**
|
||||
@@ -42,7 +42,7 @@ export interface RegistrationManagerArgs {
|
||||
export interface RegistrationParams {
|
||||
email: string;
|
||||
webId?: string;
|
||||
password?: string;
|
||||
password: string;
|
||||
podName?: string;
|
||||
template?: string;
|
||||
createWebId: boolean;
|
||||
@@ -123,8 +123,11 @@ export class RegistrationManager {
|
||||
const trimmedEmail = this.trimString(email);
|
||||
assert(trimmedEmail && emailRegex.test(trimmedEmail), 'Please enter a valid e-mail address.');
|
||||
|
||||
assertPassword(password, confirmPassword);
|
||||
|
||||
const validated: RegistrationParams = {
|
||||
email: trimmedEmail,
|
||||
password,
|
||||
register: Boolean(register) || Boolean(createWebId),
|
||||
createPod: Boolean(createPod) || Boolean(createWebId),
|
||||
createWebId: Boolean(createWebId),
|
||||
@@ -147,14 +150,6 @@ export class RegistrationManager {
|
||||
validated.podName = trimmedPodName;
|
||||
}
|
||||
|
||||
// Parse account
|
||||
if (validated.register) {
|
||||
const trimmedPassword = this.trimString(password);
|
||||
const trimmedConfirmPassword = this.trimString(confirmPassword);
|
||||
assertPassword(trimmedPassword, trimmedConfirmPassword);
|
||||
validated.password = trimmedPassword;
|
||||
}
|
||||
|
||||
// Parse template if there is one
|
||||
if (template) {
|
||||
validated.template = this.trimString(template);
|
||||
@@ -194,9 +189,10 @@ export class RegistrationManager {
|
||||
}
|
||||
|
||||
// Register the account
|
||||
if (input.register) {
|
||||
await this.accountStore.create(input.email, input.webId!, input.password!);
|
||||
}
|
||||
const settings: AccountSettings = {
|
||||
useIdp: input.register,
|
||||
};
|
||||
await this.accountStore.create(input.email, input.webId!, input.password, settings);
|
||||
|
||||
// Create the pod
|
||||
if (input.createPod) {
|
||||
@@ -216,20 +212,15 @@ export class RegistrationManager {
|
||||
// Only allow overwrite for root pods
|
||||
await this.podManager.createPod(podBaseUrl!, podSettings, allowRoot);
|
||||
} catch (error: unknown) {
|
||||
// In case pod creation errors we don't want to keep the account
|
||||
if (input.register) {
|
||||
await this.accountStore.deleteAccount(input.email);
|
||||
}
|
||||
await this.accountStore.deleteAccount(input.email);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the account
|
||||
if (input.register) {
|
||||
// This prevents there being a small timeframe where the account can be used before the pod creation is finished.
|
||||
// That timeframe could potentially be used by malicious users.
|
||||
await this.accountStore.verify(input.email);
|
||||
}
|
||||
// This prevents there being a small timeframe where the account can be used before the pod creation is finished.
|
||||
// That timeframe could potentially be used by malicious users.
|
||||
await this.accountStore.verify(input.email);
|
||||
|
||||
return {
|
||||
webId: input.webId,
|
||||
|
||||
@@ -107,13 +107,6 @@
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div id="noPasswordForm" class="hidden">
|
||||
<p>
|
||||
Since you will be using your existing WebID setup to access your pod,
|
||||
<br>
|
||||
you do <em>not</em> need to set a password.
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Assist the user with filling out the form by hiding irrelevant fields -->
|
||||
@@ -132,7 +125,7 @@
|
||||
'mainForm',
|
||||
'createWebIdOn', 'createWebIdOff', 'createWebIdForm', 'existingWebIdForm', 'webId',
|
||||
'createPod', 'createPodForm', 'rootPodOn', 'rootPodOff', 'podNameForm', 'podName',
|
||||
'register', 'passwordForm', 'noPasswordForm',
|
||||
'register', 'passwordForm',
|
||||
].forEach(registerElement);
|
||||
|
||||
// Conditions under which elements should be visible
|
||||
@@ -141,8 +134,6 @@
|
||||
existingWebIdForm: () => elements.createWebIdOff.checked,
|
||||
createPodForm: () => elements.createPod.checked,
|
||||
podNameForm: () => !elements.rootPodOn.checked,
|
||||
passwordForm: () => elements.createWebIdOn.checked || elements.register.checked,
|
||||
noPasswordForm: () => !isVisible('passwordForm'),
|
||||
};
|
||||
|
||||
// Ensures that the only relevant input fields are visible and enabled
|
||||
|
||||
@@ -23,7 +23,15 @@ const configs: [string, any][] = [
|
||||
// Tests are very similar to subdomain/pod tests. Would be nice if they can be combined
|
||||
describe.each(configs)('A dynamic pod server with template config %s', (template, { teardown }): void => {
|
||||
let app: App;
|
||||
const settings = { podName: 'alice', webId: 'http://test.com/#alice', email: 'alice@test.email', template, createPod: true };
|
||||
const settings = {
|
||||
podName: 'alice',
|
||||
webId: 'http://test.com/#alice',
|
||||
email: 'alice@test.email',
|
||||
password: 'password',
|
||||
confirmPassword: 'password',
|
||||
template,
|
||||
createPod: true,
|
||||
};
|
||||
const podUrl = `${baseUrl}${settings.podName}/`;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
@@ -109,10 +117,11 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
});
|
||||
|
||||
it('should not be able to create a pod with the same name.', async(): Promise<void> => {
|
||||
const newSettings = { ...settings, webId: 'http://test.com/#bob', email: 'bob@test.email' };
|
||||
const res = await fetch(`${baseUrl}idp/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
body: JSON.stringify(newSettings),
|
||||
});
|
||||
expect(res.status).toBe(409);
|
||||
await expect(res.text()).resolves.toContain(`There already is a pod at ${podUrl}`);
|
||||
|
||||
@@ -53,6 +53,7 @@ describe('A Solid server with IDP', (): void => {
|
||||
const oidcIssuer = baseUrl;
|
||||
const card = joinUrl(baseUrl, 'profile/card');
|
||||
const webId = `${card}#me`;
|
||||
const webId2 = `${card}#someoneElse`;
|
||||
const email = 'test@test.com';
|
||||
const password = 'password!';
|
||||
const password2 = 'password2!';
|
||||
@@ -241,25 +242,32 @@ describe('A Solid server with IDP', (): void => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('creating pods without registering', (): void => {
|
||||
describe('creating pods without registering with the IDP', (): void => {
|
||||
let formBody: string;
|
||||
let registrationTriple: string;
|
||||
const podName = 'myPod';
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
// We will need this twice
|
||||
formBody = stringify({ email, webId, podName, createPod: 'ok' });
|
||||
formBody = stringify({
|
||||
email: 'bob@test.email',
|
||||
webId: webId2,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
podName,
|
||||
createPod: 'ok',
|
||||
});
|
||||
});
|
||||
|
||||
it('sends the form once to receive the registration triple.', async(): Promise<void> => {
|
||||
const res = await postForm(`${baseUrl}idp/register`, formBody);
|
||||
expect(res.status).toBe(400);
|
||||
registrationTriple = extractRegistrationTriple(await res.text(), webId);
|
||||
registrationTriple = extractRegistrationTriple(await res.text(), webId2);
|
||||
});
|
||||
|
||||
it('updates the webId with the registration token.', async(): Promise<void> => {
|
||||
const patchBody = `INSERT DATA { ${registrationTriple} }`;
|
||||
const res = await fetch(webId, {
|
||||
const res = await fetch(webId2, {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/sparql-update' },
|
||||
body: patchBody,
|
||||
|
||||
@@ -28,7 +28,14 @@ const stores: [string, any][] = [
|
||||
// Simulating subdomains using the forwarded header so no DNS changes are required
|
||||
describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardown }): void => {
|
||||
let app: App;
|
||||
const settings = { podName: 'alice', webId: 'http://test.com/#alice', email: 'alice@test.email', createPod: true };
|
||||
const settings = {
|
||||
podName: 'alice',
|
||||
webId: 'http://test.com/#alice',
|
||||
email: 'alice@test.email',
|
||||
password: 'password',
|
||||
confirmPassword: 'password',
|
||||
createPod: true,
|
||||
};
|
||||
const podHost = `alice.localhost:${port}`;
|
||||
const podUrl = `http://${podHost}/`;
|
||||
|
||||
@@ -142,10 +149,11 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
|
||||
});
|
||||
|
||||
it('should not be able to create a pod with the same name.', async(): Promise<void> => {
|
||||
const newSettings = { ...settings, webId: 'http://test.com/#bob', email: 'bob@test.email' };
|
||||
const res = await fetch(`${baseUrl}idp/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
body: JSON.stringify(newSettings),
|
||||
});
|
||||
expect(res.status).toBe(409);
|
||||
await expect(res.text()).resolves.toContain(`There already is a resource at ${podUrl}`);
|
||||
|
||||
@@ -9,42 +9,44 @@ describe('A LoginHandler', (): void => {
|
||||
const webId = 'http://alice.test.com/card#me';
|
||||
const email = 'alice@test.email';
|
||||
let input: InteractionHandlerInput;
|
||||
let storageAdapter: AccountStore;
|
||||
let accountStore: jest.Mocked<AccountStore>;
|
||||
let handler: LoginHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
input = {} as any;
|
||||
|
||||
storageAdapter = {
|
||||
accountStore = {
|
||||
authenticate: jest.fn().mockResolvedValue(webId),
|
||||
getSettings: jest.fn().mockResolvedValue({ useIdp: true }),
|
||||
} as any;
|
||||
|
||||
handler = new LoginHandler(storageAdapter);
|
||||
handler = new LoginHandler(accountStore);
|
||||
});
|
||||
|
||||
it('errors on invalid emails.', async(): Promise<void> => {
|
||||
input.operation = createPostJsonOperation({});
|
||||
let prom = handler.handle(input);
|
||||
await expect(prom).rejects.toThrow('Email required');
|
||||
await expect(handler.handle(input)).rejects.toThrow('Email required');
|
||||
input.operation = createPostJsonOperation({ email: [ 'a', 'b' ]});
|
||||
prom = handler.handle(input);
|
||||
await expect(prom).rejects.toThrow('Email required');
|
||||
await expect(handler.handle(input)).rejects.toThrow('Email required');
|
||||
});
|
||||
|
||||
it('errors on invalid passwords.', async(): Promise<void> => {
|
||||
input.operation = createPostJsonOperation({ email });
|
||||
let prom = handler.handle(input);
|
||||
await expect(prom).rejects.toThrow('Password required');
|
||||
await expect(handler.handle(input)).rejects.toThrow('Password required');
|
||||
input.operation = createPostJsonOperation({ email, password: [ 'a', 'b' ]});
|
||||
prom = handler.handle(input);
|
||||
await expect(prom).rejects.toThrow('Password required');
|
||||
await expect(handler.handle(input)).rejects.toThrow('Password required');
|
||||
});
|
||||
|
||||
it('throws an error if there is a problem.', async(): Promise<void> => {
|
||||
input.operation = createPostJsonOperation({ email, password: 'password!' });
|
||||
(storageAdapter.authenticate as jest.Mock).mockRejectedValueOnce(new Error('auth failed!'));
|
||||
const prom = handler.handle(input);
|
||||
await expect(prom).rejects.toThrow('auth failed!');
|
||||
accountStore.authenticate.mockRejectedValueOnce(new Error('auth failed!'));
|
||||
await expect(handler.handle(input)).rejects.toThrow('auth failed!');
|
||||
});
|
||||
|
||||
it('throws an error if the account does not have the correct settings.', async(): Promise<void> => {
|
||||
input.operation = createPostJsonOperation({ email, password: 'password!' });
|
||||
accountStore.getSettings.mockResolvedValueOnce({ useIdp: false });
|
||||
await expect(handler.handle(input)).rejects.toThrow('This server is not an identity provider for this account.');
|
||||
});
|
||||
|
||||
it('returns an InteractionCompleteResult when done.', async(): Promise<void> => {
|
||||
@@ -53,7 +55,7 @@ describe('A LoginHandler', (): void => {
|
||||
type: 'complete',
|
||||
details: { webId, shouldRemember: false },
|
||||
});
|
||||
expect(storageAdapter.authenticate).toHaveBeenCalledTimes(1);
|
||||
expect(storageAdapter.authenticate).toHaveBeenLastCalledWith(email, 'password!');
|
||||
expect(accountStore.authenticate).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.authenticate).toHaveBeenLastCalledWith(email, 'password!');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AccountSettings } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
|
||||
import type {
|
||||
EmailPasswordData,
|
||||
} from '../../../../../../src/identity/interaction/email-password/storage/BaseAccountStore';
|
||||
@@ -11,6 +12,7 @@ describe('A BaseAccountStore', (): void => {
|
||||
const email = 'test@test.com';
|
||||
const webId = 'http://test.com/#webId';
|
||||
const password = 'password!';
|
||||
const settings: AccountSettings = { useIdp: true };
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
const map = new Map();
|
||||
@@ -24,20 +26,26 @@ describe('A BaseAccountStore', (): void => {
|
||||
});
|
||||
|
||||
it('can create accounts.', async(): Promise<void> => {
|
||||
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
|
||||
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('errors when creating a second account for an email.', async(): Promise<void> => {
|
||||
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
|
||||
await expect(store.create(email, 'diffId', 'diffPass')).rejects.toThrow('Account already exists');
|
||||
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
|
||||
await expect(store.create(email, webId, 'diffPass', settings)).rejects.toThrow('Account already exists');
|
||||
});
|
||||
|
||||
it('errors when creating a second account for a WebID.', async(): Promise<void> => {
|
||||
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
|
||||
await expect(store.create('bob@test.email', webId, 'diffPass', settings))
|
||||
.rejects.toThrow('There already is an account for this WebID');
|
||||
});
|
||||
|
||||
it('errors when authenticating a non-existent account.', async(): Promise<void> => {
|
||||
await expect(store.authenticate(email, password)).rejects.toThrow('No account by that email');
|
||||
await expect(store.authenticate(email, password)).rejects.toThrow('Account does not exist');
|
||||
});
|
||||
|
||||
it('errors when authenticating an unverified account.', async(): Promise<void> => {
|
||||
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
|
||||
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
|
||||
await expect(store.authenticate(email, 'wrongPassword')).rejects.toThrow('Account still needs to be verified');
|
||||
});
|
||||
|
||||
@@ -46,13 +54,13 @@ describe('A BaseAccountStore', (): void => {
|
||||
});
|
||||
|
||||
it('errors when authenticating with the wrong password.', async(): Promise<void> => {
|
||||
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
|
||||
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
|
||||
await expect(store.verify(email)).resolves.toBeUndefined();
|
||||
await expect(store.authenticate(email, 'wrongPassword')).rejects.toThrow('Incorrect password');
|
||||
});
|
||||
|
||||
it('can authenticate.', async(): Promise<void> => {
|
||||
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
|
||||
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
|
||||
await expect(store.verify(email)).resolves.toBeUndefined();
|
||||
await expect(store.authenticate(email, password)).resolves.toBe(webId);
|
||||
});
|
||||
@@ -63,16 +71,35 @@ describe('A BaseAccountStore', (): void => {
|
||||
|
||||
it('can change the password.', async(): Promise<void> => {
|
||||
const newPassword = 'newPassword!';
|
||||
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
|
||||
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
|
||||
await expect(store.verify(email)).resolves.toBeUndefined();
|
||||
await expect(store.changePassword(email, newPassword)).resolves.toBeUndefined();
|
||||
await expect(store.authenticate(email, newPassword)).resolves.toBe(webId);
|
||||
});
|
||||
|
||||
it('can get the settings.', async(): Promise<void> => {
|
||||
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
|
||||
await expect(store.verify(email)).resolves.toBeUndefined();
|
||||
await expect(store.getSettings(webId)).resolves.toBe(settings);
|
||||
});
|
||||
|
||||
it('can update the settings.', async(): Promise<void> => {
|
||||
const newSettings = { webId, useIdp: false };
|
||||
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
|
||||
await expect(store.verify(email)).resolves.toBeUndefined();
|
||||
await expect(store.updateSettings(webId, newSettings)).resolves.toBeUndefined();
|
||||
await expect(store.getSettings(webId)).resolves.toBe(newSettings);
|
||||
});
|
||||
|
||||
it('can delete an account.', async(): Promise<void> => {
|
||||
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
|
||||
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
|
||||
await expect(store.deleteAccount(email)).resolves.toBeUndefined();
|
||||
await expect(store.authenticate(email, password)).rejects.toThrow('Account does not exist');
|
||||
await expect(store.getSettings(webId)).rejects.toThrow('Account does not exist');
|
||||
});
|
||||
|
||||
it('does nothing when deleting non-existent accounts.', async(): Promise<void> => {
|
||||
await expect(store.deleteAccount(email)).resolves.toBeUndefined();
|
||||
await expect(store.authenticate(email, password)).rejects.toThrow('No account by that email');
|
||||
});
|
||||
|
||||
it('errors when forgetting the password of an account that does not exist.', async(): Promise<void> => {
|
||||
@@ -80,7 +107,7 @@ describe('A BaseAccountStore', (): void => {
|
||||
});
|
||||
|
||||
it('generates a recordId when a password was forgotten.', async(): Promise<void> => {
|
||||
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
|
||||
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
|
||||
const recordId = await store.generateForgotPasswordRecord(email);
|
||||
expect(typeof recordId).toBe('string');
|
||||
});
|
||||
@@ -90,13 +117,13 @@ describe('A BaseAccountStore', (): void => {
|
||||
});
|
||||
|
||||
it('returns the email matching the forgotten password record.', async(): Promise<void> => {
|
||||
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
|
||||
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
|
||||
const recordId = await store.generateForgotPasswordRecord(email);
|
||||
await expect(store.getForgotPasswordRecord(recordId)).resolves.toBe(email);
|
||||
});
|
||||
|
||||
it('can delete stored forgotten password records.', async(): Promise<void> => {
|
||||
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
|
||||
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
|
||||
const recordId = await store.generateForgotPasswordRecord(email);
|
||||
await expect(store.deleteForgotPasswordRecord(recordId)).resolves.toBeUndefined();
|
||||
await expect(store.getForgotPasswordRecord('unknownRecord')).resolves.toBeUndefined();
|
||||
|
||||
@@ -74,70 +74,72 @@ describe('A RegistrationManager', (): void => {
|
||||
expect((): any => manager.validateInput(input)).toThrow('Please enter a valid e-mail address.');
|
||||
});
|
||||
|
||||
it('errors on invalid passwords.', async(): Promise<void> => {
|
||||
const input: any = { email, webId, password, confirmPassword: 'bad' };
|
||||
expect((): any => manager.validateInput(input)).toThrow('Your password and confirmation did not match.');
|
||||
});
|
||||
|
||||
it('errors on missing passwords.', async(): Promise<void> => {
|
||||
const input: any = { email, webId };
|
||||
expect((): any => manager.validateInput(input)).toThrow('Please enter a password.');
|
||||
});
|
||||
|
||||
it('errors when setting rootPod to true when not allowed.', async(): Promise<void> => {
|
||||
const input = { email, createWebId, rootPod };
|
||||
const input = { email, password, confirmPassword, createWebId, rootPod };
|
||||
expect((): any => manager.validateInput(input)).toThrow('Creating a root pod is not supported.');
|
||||
});
|
||||
|
||||
it('errors when a required WebID is not valid.', async(): Promise<void> => {
|
||||
let input: any = { email, register, webId: undefined };
|
||||
let input: any = { email, password, confirmPassword, register, webId: undefined };
|
||||
expect((): any => manager.validateInput(input)).toThrow('Please enter a valid WebID.');
|
||||
|
||||
input = { email, register, webId: '' };
|
||||
input = { email, password, confirmPassword, register, webId: '' };
|
||||
expect((): any => manager.validateInput(input)).toThrow('Please enter a valid WebID.');
|
||||
});
|
||||
|
||||
it('errors on invalid passwords when registering.', async(): Promise<void> => {
|
||||
const input: any = { email, webId, password, confirmPassword: 'bad', register };
|
||||
expect((): any => manager.validateInput(input)).toThrow('Your password and confirmation did not match.');
|
||||
});
|
||||
|
||||
it('errors on invalid pod names when required.', async(): Promise<void> => {
|
||||
let input: any = { email, webId, createPod, podName: undefined };
|
||||
let input: any = { email, webId, password, confirmPassword, createPod, podName: undefined };
|
||||
expect((): any => manager.validateInput(input)).toThrow('Please specify a Pod name.');
|
||||
|
||||
input = { email, webId, createPod, podName: ' ' };
|
||||
input = { email, webId, password, confirmPassword, createPod, podName: ' ' };
|
||||
expect((): any => manager.validateInput(input)).toThrow('Please specify a Pod name.');
|
||||
|
||||
input = { email, webId, createWebId };
|
||||
input = { email, webId, password, confirmPassword, createWebId };
|
||||
expect((): any => manager.validateInput(input)).toThrow('Please specify a Pod name.');
|
||||
});
|
||||
|
||||
it('errors when trying to create a WebID without registering or creating a pod.', async(): Promise<void> => {
|
||||
let input: any = { email, podName, createWebId };
|
||||
expect((): any => manager.validateInput(input)).toThrow('Please enter a password.');
|
||||
|
||||
input = { email, podName, createWebId, createPod };
|
||||
expect((): any => manager.validateInput(input)).toThrow('Please enter a password.');
|
||||
|
||||
input = { email, podName, createWebId, createPod, register };
|
||||
expect((): any => manager.validateInput(input)).toThrow('Please enter a password.');
|
||||
});
|
||||
|
||||
it('errors when no option is chosen.', async(): Promise<void> => {
|
||||
const input = { email, webId };
|
||||
const input = { email, webId, password, confirmPassword };
|
||||
expect((): any => manager.validateInput(input)).toThrow('Please register for a WebID or create a Pod.');
|
||||
});
|
||||
|
||||
it('adds the template parameter if there is one.', async(): Promise<void> => {
|
||||
const input = { email, webId, podName, template: 'template', createPod };
|
||||
const input = { email, webId, password, confirmPassword, podName, template: 'template', createPod };
|
||||
expect(manager.validateInput(input)).toEqual({
|
||||
email, webId, podName, template: 'template', createWebId: false, register: false, createPod, rootPod: false,
|
||||
email,
|
||||
webId,
|
||||
password,
|
||||
podName,
|
||||
template: 'template',
|
||||
createWebId: false,
|
||||
register: false,
|
||||
createPod,
|
||||
rootPod: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not require a pod name when creating a root pod.', async(): Promise<void> => {
|
||||
const input = { email, webId, createPod, rootPod };
|
||||
const input = { email, password, confirmPassword, webId, createPod, rootPod };
|
||||
expect(manager.validateInput(input, true)).toEqual({
|
||||
email, webId, createWebId: false, register: false, createPod, rootPod,
|
||||
email, password, webId, createWebId: false, register: false, createPod, rootPod,
|
||||
});
|
||||
});
|
||||
|
||||
it('trims input parameters.', async(): Promise<void> => {
|
||||
it('trims non-password input parameters.', async(): Promise<void> => {
|
||||
let input: any = {
|
||||
email: ` ${email} `,
|
||||
password: ` ${password} `,
|
||||
confirmPassword: ` ${password} `,
|
||||
password: ' a ',
|
||||
confirmPassword: ' a ',
|
||||
podName: ` ${podName} `,
|
||||
template: ' template ',
|
||||
createWebId,
|
||||
@@ -145,19 +147,19 @@ describe('A RegistrationManager', (): void => {
|
||||
createPod,
|
||||
};
|
||||
expect(manager.validateInput(input)).toEqual({
|
||||
email, password, podName, template: 'template', createWebId, register, createPod, rootPod: false,
|
||||
email, password: ' a ', podName, template: 'template', createWebId, register, createPod, rootPod: false,
|
||||
});
|
||||
|
||||
input = { email, webId: ` ${webId} `, password, confirmPassword, register: true };
|
||||
input = { email, webId: ` ${webId} `, password: ' a ', confirmPassword: ' a ', register: true };
|
||||
expect(manager.validateInput(input)).toEqual({
|
||||
email, webId, password, createWebId: false, register, createPod: false, rootPod: false,
|
||||
email, webId, password: ' a ', createWebId: false, register, createPod: false, rootPod: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handling data', (): void => {
|
||||
it('can register a user.', async(): Promise<void> => {
|
||||
const params: any = { email, webId, password, confirmPassword, register, createPod: false, createWebId: false };
|
||||
const params: any = { email, webId, password, register, createPod: false, createWebId: false };
|
||||
await expect(manager.register(params)).resolves.toEqual({
|
||||
email,
|
||||
webId,
|
||||
@@ -170,7 +172,7 @@ describe('A RegistrationManager', (): void => {
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password);
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password, { useIdp: true });
|
||||
expect(accountStore.verify).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.verify).toHaveBeenLastCalledWith(email);
|
||||
|
||||
@@ -180,7 +182,7 @@ describe('A RegistrationManager', (): void => {
|
||||
});
|
||||
|
||||
it('can create a pod.', async(): Promise<void> => {
|
||||
const params: any = { email, webId, podName, createPod, createWebId: false, register: false };
|
||||
const params: any = { email, webId, password, podName, createPod, createWebId: false, register: false };
|
||||
await expect(manager.register(params)).resolves.toEqual({
|
||||
email,
|
||||
webId,
|
||||
@@ -197,9 +199,10 @@ describe('A RegistrationManager', (): void => {
|
||||
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
|
||||
expect(podManager.createPod).toHaveBeenCalledTimes(1);
|
||||
expect(podManager.createPod).toHaveBeenLastCalledWith({ path: `${baseUrl}${podName}/` }, podSettings, false);
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password, { useIdp: false });
|
||||
expect(accountStore.verify).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.verify).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.deleteAccount).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
@@ -219,7 +222,7 @@ describe('A RegistrationManager', (): void => {
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password);
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password, { useIdp: true });
|
||||
expect(identifierGenerator.generate).toHaveBeenCalledTimes(1);
|
||||
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
|
||||
expect(podManager.createPod).toHaveBeenCalledTimes(1);
|
||||
@@ -239,7 +242,7 @@ describe('A RegistrationManager', (): void => {
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password);
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password, { useIdp: true });
|
||||
expect(identifierGenerator.generate).toHaveBeenCalledTimes(1);
|
||||
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
|
||||
expect(podManager.createPod).toHaveBeenCalledTimes(1);
|
||||
@@ -250,23 +253,6 @@ describe('A RegistrationManager', (): void => {
|
||||
expect(accountStore.verify).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('does not try to delete an account on failure if there was no registration.', async(): Promise<void> => {
|
||||
const params: any = { email, webId, podName, createPod };
|
||||
(podManager.createPod as jest.Mock).mockRejectedValueOnce(new Error('pod error'));
|
||||
await expect(manager.register(params)).rejects.toThrow('pod error');
|
||||
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
||||
expect(identifierGenerator.generate).toHaveBeenCalledTimes(1);
|
||||
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
|
||||
expect(podManager.createPod).toHaveBeenCalledTimes(1);
|
||||
expect(podManager.createPod).toHaveBeenLastCalledWith({ path: `${baseUrl}${podName}/` }, podSettings, false);
|
||||
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.deleteAccount).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.verify).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('can create a WebID with an account and pod.', async(): Promise<void> => {
|
||||
const params: any = { email, password, confirmPassword, podName, createWebId, register, createPod };
|
||||
const generatedWebID = joinUrl(baseUrl, podName, webIdSuffix);
|
||||
@@ -286,7 +272,7 @@ describe('A RegistrationManager', (): void => {
|
||||
expect(identifierGenerator.generate).toHaveBeenCalledTimes(1);
|
||||
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, generatedWebID, password);
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, generatedWebID, password, { useIdp: true });
|
||||
expect(accountStore.verify).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.verify).toHaveBeenLastCalledWith(email);
|
||||
expect(podManager.createPod).toHaveBeenCalledTimes(1);
|
||||
@@ -297,7 +283,7 @@ describe('A RegistrationManager', (): void => {
|
||||
});
|
||||
|
||||
it('can create a root pod.', async(): Promise<void> => {
|
||||
const params: any = { email, webId, createPod, rootPod, createWebId: false, register: false };
|
||||
const params: any = { email, webId, password, createPod, rootPod, createWebId: false, register: false };
|
||||
podSettings.podBaseUrl = baseUrl;
|
||||
await expect(manager.register(params, true)).resolves.toEqual({
|
||||
email,
|
||||
@@ -313,10 +299,11 @@ describe('A RegistrationManager', (): void => {
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
||||
expect(podManager.createPod).toHaveBeenCalledTimes(1);
|
||||
expect(podManager.createPod).toHaveBeenLastCalledWith({ path: baseUrl }, podSettings, true);
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password, { useIdp: false });
|
||||
expect(accountStore.verify).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(identifierGenerator.generate).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.create).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.verify).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.deleteAccount).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user