CommunitySolidServer/src/identity/ownership/TokenOwnershipValidator.ts
2021-08-23 16:39:25 +02:00

97 lines
3.3 KiB
TypeScript

import type { Quad } from 'n3';
import { DataFactory } from 'n3';
import { v4 } from 'uuid';
import { getLoggerFor } from '../../logging/LogUtil';
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
import type { ExpiringStorage } from '../../storage/keyvalue/ExpiringStorage';
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
import { fetchDataset } from '../../util/FetchUtil';
import { SOLID } from '../../util/Vocabularies';
import { OwnershipValidator } from './OwnershipValidator';
const { literal, namedNode, quad } = DataFactory;
/**
* Validates ownership of a WebId by seeing if a specific triple can be added.
* `expiration` parameter is how long the token should be valid in minutes.
*/
export class TokenOwnershipValidator extends OwnershipValidator {
protected readonly logger = getLoggerFor(this);
private readonly converter: RepresentationConverter;
private readonly storage: ExpiringStorage<string, string>;
private readonly expiration: number;
public constructor(converter: RepresentationConverter, storage: ExpiringStorage<string, string>, expiration = 30) {
super();
this.converter = converter;
this.storage = storage;
// Convert minutes to milliseconds
this.expiration = expiration * 60 * 1000;
}
public async handle({ webId }: { webId: string }): Promise<void> {
const key = this.getTokenKey(webId);
let token = await this.storage.get(key);
// No reason to fetch the WebId if we don't have a token yet
if (!token) {
token = this.generateToken();
await this.storage.set(key, token, this.expiration);
this.throwError(webId, token);
}
// Verify if the token can be found in the WebId
if (!await this.hasToken(webId, token)) {
this.throwError(webId, token);
}
this.logger.debug(`Verified ownership of ${webId}`);
await this.storage.delete(key);
}
/**
* Creates a key to use with the token storage.
*/
private getTokenKey(webId: string): string {
return `ownershipToken${webId}`;
}
/**
* Generates a random verification token;
*/
private generateToken(): string {
return v4();
}
/**
* Fetches data from the WebID to determine if the token is present.
*/
private async hasToken(webId: string, token: string): Promise<boolean> {
const representation = await fetchDataset(webId, this.converter);
const expectedQuad = quad(namedNode(webId), SOLID.terms.oidcIssuerRegistrationToken, literal(token));
for await (const data of representation.data) {
const triple = data as Quad;
if (triple.equals(expectedQuad)) {
representation.data.destroy();
return true;
}
}
return false;
}
/**
* Throws an error containing the description of which triple is needed for verification.
*/
private throwError(webId: string, token: string): never {
this.logger.debug(`No verification token found for ${webId}`);
const errorMessage = [
'Verification token not found.',
'Please add the RDF triple',
`<${webId}> <${SOLID.oidcIssuerRegistrationToken}> "${token}".`,
`to the WebID document at ${webId.replace(/#.*/u, '')}`,
'to prove it belongs to you.',
'You can remove this triple again after validation.',
].join(' ');
throw new BadRequestHttpError(errorMessage);
}
}