feat: Combine pod creation with IDP registration

This commit is contained in:
Joachim Van Herwegen
2021-05-28 16:38:35 +02:00
parent 9bb42ddf0d
commit 4d7d939dc4
18 changed files with 700 additions and 255 deletions

View File

@@ -1,83 +1,241 @@
import assert from 'assert';
import urljoin from 'url-join';
import type { ResourceIdentifier } from '../../../../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../../../../logging/LogUtil';
import type { IdentifierGenerator } from '../../../../pods/generate/IdentifierGenerator';
import type { PodManager } from '../../../../pods/PodManager';
import type { HttpHandlerInput } from '../../../../server/HttpHandler';
import { HttpHandler } from '../../../../server/HttpHandler';
import type { HttpRequest } from '../../../../server/HttpRequest';
import type { InteractionHttpHandlerInput } from '../../InteractionHttpHandler';
import { InteractionHttpHandler } from '../../InteractionHttpHandler';
import type { RenderHandler } from '../../../../server/util/RenderHandler';
import type { OwnershipValidator } from '../../../ownership/OwnershipValidator';
import { getFormDataRequestBody } from '../../util/FormDataUtil';
import type { InteractionCompleter } from '../../util/InteractionCompleter';
import type { OwnershipValidator } from '../../util/OwnershipValidator';
import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil';
import type { AccountStore } from '../storage/AccountStore';
const emailRegex = /^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/u;
interface RegistrationHandlerArgs {
export interface RegistrationHandlerArgs {
/**
* Used to set the `oidcIssuer` value of newly registered pods.
*/
baseUrl: string;
/**
* Appended to the generated pod identifier to create the corresponding WebID.
*/
webIdSuffix: string;
/**
* Generates identifiers for new pods.
*/
identifierGenerator: IdentifierGenerator;
/**
* Verifies the user is the owner of the WebID they provide.
*/
ownershipValidator: OwnershipValidator;
/**
* Stores all the registered account information.
*/
accountStore: AccountStore;
interactionCompleter: InteractionCompleter;
/**
* Creates the new pods.
*/
podManager: PodManager;
/**
* Renders the response when registration is successful.
*/
responseHandler: RenderHandler<NodeJS.Dict<any>>;
}
// Results when parsing the input form data
type ParseResult = {
/**
* All the parameters that will be parsed from a request.
* `data` contains all the raw values to potentially be used by pod templates.
*/
interface ParseResult {
email: string;
password: string;
webId: string;
remember: boolean;
};
password?: string;
podName?: string;
webId?: string;
createWebId: boolean;
register: boolean;
createPod: boolean;
data: NodeJS.Dict<string>;
}
/**
* Handles the submission of the registration form.
* Creates the user and logs them in if successful.
* This class handles the 3 potential steps of the registration process:
* 1. Generating a new WebID.
* 2. Registering a WebID with the IDP.
* 3. Creating a new pod for a given WebID.
*
* All of these steps are optional and will be determined based on the input parameters of a request,
* with the following considerations:
* * At least one option needs to be chosen.
* * In case a new WebID needs to be created, the other 2 steps are obligatory.
* * Ownership will be verified when the WebID is provided.
* * When registering and creating a pod, the base URL will be used as oidcIssuer value.
*/
export class RegistrationHandler extends InteractionHttpHandler {
export class RegistrationHandler extends HttpHandler {
protected readonly logger = getLoggerFor(this);
private readonly baseUrl: string;
private readonly webIdSuffix: string;
private readonly identifierGenerator: IdentifierGenerator;
private readonly ownershipValidator: OwnershipValidator;
private readonly accountStore: AccountStore;
private readonly interactionCompleter: InteractionCompleter;
private readonly podManager: PodManager;
private readonly responseHandler: RenderHandler<NodeJS.Dict<any>>;
public constructor(args: RegistrationHandlerArgs) {
super();
this.baseUrl = args.baseUrl;
this.webIdSuffix = args.webIdSuffix;
this.identifierGenerator = args.identifierGenerator;
this.ownershipValidator = args.ownershipValidator;
this.accountStore = args.accountStore;
this.interactionCompleter = args.interactionCompleter;
this.podManager = args.podManager;
this.responseHandler = args.responseHandler;
}
public async handle(input: InteractionHttpHandlerInput): Promise<void> {
const { email, webId, password, remember } = await this.parseInput(input.request);
public async handle({ request, response }: HttpHandlerInput): Promise<void> {
const result = await this.parseInput(request);
try {
// Check if WebId contains required triples and register new account if successful
await this.ownershipValidator.handleSafe({ webId });
await this.accountStore.create(email, webId, password);
await this.interactionCompleter.handleSafe({
...input,
webId,
shouldRemember: Boolean(remember),
});
this.logger.debug(`Registering agent ${email} with WebId ${webId}`);
} catch (err: unknown) {
throwIdpInteractionError(err, { email, webId });
const props = await this.register(result);
await this.responseHandler.handleSafe({ response, props });
} catch (error: unknown) {
throwIdpInteractionError(error, result.data as Record<string, string>);
}
}
/**
* Parses and validates the input form data.
* Will throw an {@link IdpInteractionError} in case something is wrong.
* All relevant data that was correct up to that point will be prefilled.
* Does the full registration and pod creation process,
* with the steps chosen by the values in the `ParseResult`.
*/
private async register(result: ParseResult): Promise<NodeJS.Dict<any>> {
// This is only used when createWebId and/or createPod are true
let podBaseUrl: ResourceIdentifier | undefined;
// Create or verify the WebID
if (result.createWebId) {
podBaseUrl = this.identifierGenerator.generate(result.podName!);
result.webId = urljoin(podBaseUrl.path, this.webIdSuffix);
} else {
await this.ownershipValidator.handleSafe({ webId: result.webId! });
}
// Register the account
if (result.register) {
await this.accountStore.create(result.email, result.webId!, result.password!);
// Add relevant data for the templates
result.data.oidcIssuer = this.baseUrl;
}
// Create the pod
if (result.createPod) {
podBaseUrl = podBaseUrl ?? this.identifierGenerator.generate(result.podName!);
try {
await this.podManager.createPod(podBaseUrl, { ...result.data, webId: result.webId! });
} catch (error: unknown) {
// In case pod creation errors we don't want to keep the account
if (result.register) {
await this.accountStore.deleteAccount(result.email);
}
throw error;
}
}
// Verify the account
if (result.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(result.email);
}
return {
webId: result.webId,
email: result.email,
oidcIssuer: this.baseUrl,
podBaseUrl: podBaseUrl?.path,
createWebId: result.createWebId,
register: result.register,
createPod: result.createPod,
};
}
/**
* Parses the input request into a `ParseResult`.
*/
private async parseInput(request: HttpRequest): Promise<ParseResult> {
const prefilled: Record<string, string> = {};
const parsed = await getFormDataRequestBody(request);
let prefilled: Record<string, string> = {};
try {
const { email, webId, password, confirmPassword, remember } = await getFormDataRequestBody(request);
assert(typeof email === 'string' && email.length > 0, 'Email required');
assert(emailRegex.test(email), 'Invalid email');
prefilled.email = email;
assert(typeof webId === 'string' && webId.length > 0, 'WebId required');
prefilled.webId = webId;
assertPassword(password, confirmPassword);
return { email, password, webId, remember: Boolean(remember) };
for (const key of Object.keys(parsed)) {
if (Array.isArray(parsed[key])) {
throw new Error(`Multiple values found for key ${key}`);
}
}
prefilled = parsed as Record<string, string>;
return this.validateInput(prefilled);
} catch (err: unknown) {
throwIdpInteractionError(err, prefilled);
}
}
/**
* Converts the raw input date into a `ParseResult`.
* Verifies that all the data combinations make sense.
*/
private validateInput(parsed: NodeJS.Dict<string>): ParseResult {
const { email, password, confirmPassword, podName, webId } = parsed;
assert(typeof email === 'string' && email.length > 0 && emailRegex.test(email),
'A valid e-mail address is required');
const result: ParseResult = {
email,
createWebId: Boolean(parsed.createWebId),
register: Boolean(parsed.register),
createPod: Boolean(parsed.createPod),
data: parsed,
};
const validWebId = typeof webId === 'string' && webId.length > 0;
if (result.createWebId) {
if (validWebId) {
throw new Error('A WebID should only be provided when no new one is being created');
}
} else {
if (!validWebId) {
throw new Error('A WebID is required if no new one is being created');
}
result.webId = webId;
}
if (result.register) {
assertPassword(password, confirmPassword);
result.password = password;
} else if (typeof password === 'string' && password.length > 0) {
throw new Error('A password should only be provided when registering');
}
if (result.createWebId || result.createPod) {
assert(typeof podName === 'string' && podName.length > 0,
'A pod name is required when creating a pod and/or WebID');
result.podName = podName;
} else if (typeof podName === 'string' && podName.length > 0) {
throw new Error('A pod name should only be provided when creating a pod and/or WebID');
}
if (result.createWebId && !(result.register && result.createPod)) {
throw new Error('Creating a WebID is only possible when also registering and creating a pod');
}
if (!result.createWebId && !result.register && !result.createPod) {
throw new Error('At least one option needs to be chosen');
}
return result;
}
}