mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
97 lines
3.3 KiB
TypeScript
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);
|
|
}
|
|
}
|