CommunitySolidServer/src/identity/ownership/TokenOwnershipValidator.ts
Thomas Dupont ce754c119f
fix: Add content-negotiation when fetching dataset from url
* Solution works but tests don't

* refactor(FetchUtil): use arrayifyStream

* refactor(FetchUtil): split fetchDataset into 2 separate functions

* style(FetchUtil): onelining instead of declaring new local var

* test: trying to mock rdfDereferencer

* refactor: promise can't have async function as arg

* test(FetchUtil): pass Quad array to mockDereference instead

* test: all tests should pass now and coverage is back to 100%

* style: comment typo

* chore: make package.json and package-lock.json compatible with main

* chore: fix package.json double entries

* chore: updated package.json to be alfabetical again

* refactor(AgentGroupAccessChecker): Remove converter from contructor and config

* refactor(TokenOwnerShipValidator): Remove converter from constructor and config

* refactor(FetchUtil): Return BadRequestHttpError instead of generic Error

* test(FetchUtil): return Response object instead of mocking fetch

* style: typos and newlines
2022-02-15 13:44:03 +01:00

95 lines
3.2 KiB
TypeScript

import type { Quad } from 'n3';
import { DataFactory } from 'n3';
import { v4 } from 'uuid';
import { getLoggerFor } from '../../logging/LogUtil';
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 storage: ExpiringStorage<string, string>;
private readonly expiration: number;
public constructor(storage: ExpiringStorage<string, string>, expiration = 30) {
super();
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);
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(' ');
const details = { quad: `<${webId}> <${SOLID.oidcIssuerRegistrationToken}> "${token}".` };
throw new BadRequestHttpError(errorMessage, { details });
}
}