/
TokenOwnershipValidator.ts
96 lines (86 loc) · 3.33 KB
/
TokenOwnershipValidator.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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);
}
}