feat: Full rework of account management

Complete rewrite of the account management and related systems.
Makes the architecture more modular,
allowing for easier extensions and configurations.
This commit is contained in:
Joachim Van Herwegen
2022-03-16 10:12:13 +01:00
parent ade977bb4f
commit a47f5236ef
366 changed files with 12345 additions and 5111 deletions

View File

@@ -0,0 +1,43 @@
import { DataFactory } from 'n3';
import type { NamedNode } from 'rdf-js';
import type { HttpRequest } from '../../../server/HttpRequest';
import { matchesAuthorizationScheme } from '../../../util/HeaderUtil';
import { SOLID_META } from '../../../util/Vocabularies';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import { MetadataParser } from './MetadataParser';
import namedNode = DataFactory.namedNode;
/**
* Parses specific authorization schemes and stores their value as metadata.
* The keys of the input `authMap` should be the schemes,
* and the values the corresponding predicate that should be used to store the value in the metadata.
* The scheme will be sliced off the value, after which it is used as the object in the metadata triple.
*
* This should be used for custom authorization schemes,
* for things like OIDC tokens a {@link CredentialsExtractor} should be used.
*/
export class AuthorizationParser extends MetadataParser {
private readonly authMap: Record<string, NamedNode>;
public constructor(authMap: Record<string, string>) {
super();
this.authMap = Object.fromEntries(
Object.entries(authMap).map(([ scheme, uri ]): [string, NamedNode] => [ scheme, namedNode(uri) ]),
);
}
public async handle(input: { request: HttpRequest; metadata: RepresentationMetadata }): Promise<void> {
const authHeader = input.request.headers.authorization;
if (!authHeader) {
return;
}
for (const [ scheme, uri ] of Object.entries(this.authMap)) {
if (matchesAuthorizationScheme(scheme, authHeader)) {
// This metadata should not be stored
input.metadata.add(uri, authHeader.slice(scheme.length + 1), SOLID_META.ResponseMetadata);
// There can only be 1 match
return;
}
}
}
}

View File

@@ -0,0 +1,36 @@
import { parse } from 'cookie';
import { DataFactory } from 'n3';
import type { NamedNode } from 'rdf-js';
import type { HttpRequest } from '../../../server/HttpRequest';
import { SOLID_META } from '../../../util/Vocabularies';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import { MetadataParser } from './MetadataParser';
import namedNode = DataFactory.namedNode;
/**
* Parses the cookie header and stores their values as metadata.
* The keys of the input `cookieMap` should be the cookie names,
* and the values the corresponding predicate that should be used to store the value in the metadata.
* The values of the cookies will be used as objects in the generated triples
*/
export class CookieParser extends MetadataParser {
private readonly cookieMap: Record<string, NamedNode>;
public constructor(cookieMap: Record<string, string>) {
super();
this.cookieMap = Object.fromEntries(
Object.entries(cookieMap).map(([ name, uri ]): [string, NamedNode] => [ name, namedNode(uri) ]),
);
}
public async handle(input: { request: HttpRequest; metadata: RepresentationMetadata }): Promise<void> {
const cookies = parse(input.request.headers.cookie ?? '');
for (const [ name, uri ] of Object.entries(this.cookieMap)) {
const value = cookies[name];
if (value) {
// This metadata should not be stored
input.metadata.add(uri, value, SOLID_META.ResponseMetadata);
}
}
}
}

View File

@@ -0,0 +1,50 @@
import { serialize } from 'cookie';
import type { NamedNode } from 'n3';
import { DataFactory } from 'n3';
import type { HttpResponse } from '../../../server/HttpResponse';
import { addHeader } from '../../../util/HeaderUtil';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import { MetadataWriter } from './MetadataWriter';
/**
* Generates the necessary `Set-Cookie` header if a cookie value is detected in the metadata.
* The keys of the input `cookieMap` should be the URIs of the predicates
* used in the metadata when the object is a cookie value.
* The value of the map are objects that contain the name of the cookie,
* and the URI that is used to store the expiration date in the metadata, if any.
* If no expiration date is found in the metadata, none will be set for the cookie,
* causing it to be a session cookie.
*/
export class CookieMetadataWriter extends MetadataWriter {
private readonly cookieMap: Map<NamedNode, { name: string; expirationUri?: NamedNode }>;
public constructor(cookieMap: Record<string, { name: string; expirationUri?: string }>) {
super();
this.cookieMap = new Map<NamedNode, { name: string; expirationUri?: NamedNode }>(Object.entries(cookieMap)
.map(([ uri, { name, expirationUri }]): [ NamedNode, { name: string; expirationUri?: NamedNode } ] =>
[
DataFactory.namedNode(uri),
{
name,
expirationUri: expirationUri ? DataFactory.namedNode(expirationUri) : undefined,
},
]));
}
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
const { response, metadata } = input;
for (const [ uri, { name, expirationUri }] of this.cookieMap.entries()) {
const value = metadata.get(uri)?.value;
if (value) {
const expiration = expirationUri && metadata.get(expirationUri)?.value;
const expires = typeof expiration === 'string' ? new Date(expiration) : undefined;
// Not setting secure flag since not all tools realize those cookies are also valid for http://localhost.
// Not setting the httpOnly flag as that would prevent JS API access.
// SameSite: Lax makes it so the cookie gets sent if the origin is the server,
// or if the browser navigates there from another site.
// Setting the path to `/` so it applies to the entire server.
addHeader(response, 'Set-Cookie', serialize(name, value, { path: '/', sameSite: 'lax', expires }));
}
}
}
}