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,467 @@
import fetch from 'cross-fetch';
import { parse, splitCookiesString } from 'set-cookie-parser';
import type { App } from '../../src/init/App';
import { APPLICATION_X_WWW_FORM_URLENCODED } from '../../src/util/ContentTypes';
import { joinUrl } from '../../src/util/PathUtil';
import { getPort } from '../util/Util';
import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config';
const port = getPort('Accounts');
const baseUrl = `http://localhost:${port}/`;
// Don't send actual e-mails
jest.mock('nodemailer');
describe('A server with account management', (): void => {
let app: App;
let sendMail: jest.Mock;
let cookie: string;
const email = 'test@example.com';
let password = 'secret';
const indexUrl = joinUrl(baseUrl, '.account/');
let controls: {
main: Record<'index' | 'logins', string>;
account: Record<'create' | 'account' | 'logout' | 'pod' | 'webId' | 'clientCredentials', string>;
password: Record<'login' | 'forgot' | 'create', string>;
};
let passwordResource: string;
let pod: string;
let webId: string;
beforeAll(async(): Promise<void> => {
// Needs to happen before Components.js instantiation
sendMail = jest.fn();
const nodemailer = jest.requireMock('nodemailer');
Object.assign(nodemailer, { createTransport: (): any => ({ sendMail }) });
const instances = await instantiateFromConfig(
'urn:solid-server:test:Instances',
getTestConfigPath('server-memory.json'),
getDefaultVariables(port, baseUrl),
) as Record<string, any>;
({ app } = instances);
await app.start();
controls = { main: {}, account: {}, login: {}, password: {}} as any;
});
afterAll(async(): Promise<void> => {
await app.stop();
});
it('can get the general index.', async(): Promise<void> => {
const res = await fetch(indexUrl);
expect(res.status).toBe(200);
const json = await res.json();
expect(json.controls.main.index).toBe(indexUrl);
expect(json.controls.main.logins).toBeDefined();
controls.main = json.controls.main;
expect(json.controls.account.create).toBeDefined();
controls.account = json.controls.account;
expect(json.controls.password.login).toBeDefined();
expect(json.controls.password.forgot).toBeDefined();
controls.password = json.controls.password;
expect(json.controls.html).toBeDefined();
expect(json.controls.html.main).toBeDefined();
expect(json.controls.html.password).toBeDefined();
});
it('can create an account.', async(): Promise<void> => {
const res = await fetch(controls.account.create, { method: 'POST' });
expect(res.status).toBe(200);
const json = await res.json();
expect(json.resource).toBeDefined();
expect(res.headers.get('set-cookie')).toBeDefined();
const cookies = parse(splitCookiesString(res.headers.get('set-cookie')!));
expect(cookies).toHaveLength(1);
cookie = `${cookies[0].name}=${cookies[0].value}`;
expect(json.cookie).toBe(cookies[0].value);
controls.account.account = json.resource;
});
it('can access the account using the cookie.', async(): Promise<void> => {
expect((await fetch(controls.account.account)).status).toBe(401);
const res = await fetch(controls.account.account, { headers: { cookie }});
expect(res.status).toBe(200);
const json = await res.json();
expect(json.controls.account.account).toEqual(controls.account.account);
expect(json.controls.account.logout).toBeDefined();
expect(json.controls.account.pod).toBeDefined();
expect(json.controls.account.webId).toBeDefined();
expect(json.controls.account.clientCredentials).toBeDefined();
controls.account = json.controls.account;
expect(json.controls.password.create).toBeDefined();
controls.password = json.controls.password;
expect(json.controls.html.account).toBeDefined();
});
it('can also access the account using the custom authorization header.', async(): Promise<void> => {
expect((await fetch(controls.account.account)).status).toBe(401);
const res = await fetch(controls.account.account, { headers:
{ authorization: `CSS-Account-Cookie ${cookie.split('=')[1]}` }});
expect(res.status).toBe(200);
const json = await res.json();
expect(json.controls.account.account).toEqual(controls.account.account);
});
it('can not create a pod since the account has no login.', async(): Promise<void> => {
const res = await fetch(controls.account.pod, {
method: 'POST',
headers: { cookie, 'content-type': 'application/json' },
body: JSON.stringify({ name: 'test' }),
});
expect(res.status).toBe(400);
expect((await res.json()).message).toBe('An account needs at least 1 login method.');
});
it('can add a password login to the account.', async(): Promise<void> => {
let res = await fetch(controls.password.create, {
method: 'POST',
headers: { cookie, 'content-type': 'application/json' },
body: JSON.stringify({
email,
password,
}),
});
expect(res.status).toBe(200);
const json = await res.json();
expect(json.resource).toBeDefined();
({ resource: passwordResource } = json);
// Verify if the content was added to the profile
res = await fetch(controls.account.account, { headers: { cookie }});
expect(res.status).toBe(200);
expect((await res.json()).logins.password).toEqual({ [email]: passwordResource });
});
it('can not delete its last login method.', async(): Promise<void> => {
const res = await fetch(passwordResource, { method: 'DELETE', headers: { cookie }});
expect(res.status).toBe(400);
expect((await res.json()).message).toBe('An account needs at least 1 login method.');
});
it('can not use the same email for a different account.', async(): Promise<void> => {
let res = await fetch(controls.account.create, { method: 'POST' });
const cookies = parse(splitCookiesString(res.headers.get('set-cookie')!));
const newCookie = `${cookies[0].name}=${cookies[0].value}`;
const { resource } = await res.json();
res = await fetch(resource, { headers: { cookie: newCookie }});
const oldAccount: { controls: typeof controls } = await res.json();
// This will fail because the email address is already used by a different account
res = await fetch(oldAccount.controls.password.create, {
method: 'POST',
headers: { cookie: newCookie, 'content-type': 'application/json' },
body: JSON.stringify({ email, password }),
});
expect(res.status).toBe(400);
// Make sure the account still has no login method
res = await fetch(resource, { headers: { cookie: newCookie }});
await expect(res.json()).resolves.toEqual(oldAccount);
});
it('can log out.', async(): Promise<void> => {
const res = await fetch(controls.account.logout, { method: 'POST', headers: { cookie }});
expect(res.status).toBe(200);
// Cookie doesn't work anymore
expect((await fetch(controls.account.account, { headers: { cookie }})).status).toBe(401);
});
it('can login again with email/password.', async(): Promise<void> => {
const res = await fetch(controls.password.login, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email, password }),
});
expect(res.status).toBe(200);
const cookies = parse(splitCookiesString(res.headers.get('set-cookie')!));
expect(cookies).toHaveLength(1);
cookie = `${cookies[0].name}=${cookies[0].value}`;
// Cookie is valid again
expect((await fetch(controls.account.account, { headers: { cookie }})).status).toBe(200);
});
it('can change the password.', async(): Promise<void> => {
let res = await fetch(passwordResource, {
method: 'POST',
headers: { cookie, 'content-type': 'application/json' },
body: JSON.stringify({
oldPassword: password,
newPassword: 'secret2',
}),
});
password = 'secret2';
expect(res.status).toBe(200);
// Check new password
res = await fetch(controls.password.login, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email, password }),
});
expect(res.status).toBe(200);
expect(res.headers.get('set-cookie')).toBeDefined();
});
it('can create a pod.', async(): Promise<void> => {
let res = await fetch(controls.account.pod, {
method: 'POST',
headers: { cookie, 'content-type': 'application/json' },
body: JSON.stringify({ name: 'test' }),
});
expect(res.status).toBe(200);
let json = await res.json();
expect(json.pod).toBeDefined();
expect(json.podResource).toBeDefined();
expect(json.webId).toBeDefined();
expect(json.webIdResource).toBeDefined();
({ pod, webId } = json);
// Verify if the content was added to the profile
res = await fetch(controls.account.account, { headers: { cookie }});
expect(res.status).toBe(200);
json = await res.json();
expect(json.pods[pod]).toBeDefined();
expect(json.webIds[webId]).toBeDefined();
});
it('does not store any data if creating a pod fails on the same account.', async(): Promise<void> => {
let res = await fetch(controls.account.account, { headers: { cookie }});
const oldAccount = await res.json();
res = await fetch(controls.account.pod, {
method: 'POST',
headers: { cookie, 'content-type': 'application/json' },
body: JSON.stringify({ name: 'test' }),
});
expect(res.status).toBe(400);
// Verify nothing was added
res = await fetch(controls.account.account, { headers: { cookie }});
await expect(res.json()).resolves.toEqual(oldAccount);
});
it('does not store any data if creating a pod fails on a different account.', async(): Promise<void> => {
// We have to create a new account here to try to create a pod with the same name.
// Otherwise the server will never try to write data
// since it would notice the account already has a pod with that name.
let res = await fetch(controls.account.create, { method: 'POST' });
const cookies = parse(splitCookiesString(res.headers.get('set-cookie')!));
const newCookie = `${cookies[0].name}=${cookies[0].value}`;
res = await fetch(indexUrl, { headers: { cookie: newCookie }});
const json: { controls: typeof controls } = await res.json();
res = await fetch(json.controls.password.create, {
method: 'POST',
headers: { cookie: newCookie, 'content-type': 'application/json' },
body: JSON.stringify({
email: 'differentMail@example.com',
password,
}),
});
expect(res.status).toBe(200);
res = await fetch(json.controls.account.account, { headers: { cookie: newCookie }});
const oldAccount = await res.json();
// This will fail because there already is a pod with this name
res = await fetch(json.controls.account.pod, {
method: 'POST',
headers: { cookie: newCookie, 'content-type': 'application/json' },
body: JSON.stringify({ name: 'test' }),
});
expect(res.status).toBe(400);
expect((await res.json()).message).toContain('Pod creation failed');
// Make sure there is no reference in the account data
res = await fetch(json.controls.account.account, { headers: { cookie: newCookie }});
await expect(res.json()).resolves.toEqual(oldAccount);
});
it('can remove the WebID link.', async(): Promise<void> => {
let res = await fetch(controls.account.account, { headers: { cookie }});
const webIdResource = (await res.json()).webIds[webId];
res = await fetch(webIdResource, { method: 'DELETE', headers: { cookie }});
expect(res.status).toBe(200);
res = await fetch(controls.account.account, { headers: { cookie }});
expect((await res.json()).webIds[webId]).toBeUndefined();
});
it('can link the WebID again.', async(): Promise<void> => {
let res = await fetch(controls.account.webId, {
method: 'POST',
headers: { cookie, 'content-type': 'application/json' },
body: JSON.stringify({ webId }),
});
expect(res.status).toBe(200);
let json = await res.json();
expect(json.resource).toBeDefined();
expect(json.oidcIssuer).toBe(baseUrl);
// Verify if the content was added to the profile
res = await fetch(controls.account.account, { headers: { cookie }});
expect(res.status).toBe(200);
json = await res.json();
expect(json.webIds[webId]).toBeDefined();
});
it('needs to prove ownership when linking a WebID outside of a pod.', async(): Promise<void> => {
const otherWebId = joinUrl(baseUrl, 'other#me');
// Create the WebID
let res = await fetch(otherWebId, {
method: 'PUT',
headers: { 'content-type': 'text/turtle' },
body: '',
});
expect(res.status).toBe(201);
// Try to link the WebID
res = await fetch(controls.account.webId, {
method: 'POST',
headers: { cookie, 'content-type': 'application/json' },
body: JSON.stringify({ webId: otherWebId }),
});
expect(res.status).toBe(400);
let json = await res.json();
expect(json.details?.quad).toBeDefined();
const { quad } = json.details;
// Update the WebID with the identifying quad
await fetch(otherWebId, {
method: 'PUT',
headers: { 'content-type': 'text/turtle' },
body: quad,
});
// Try to link the WebID again
res = await fetch(controls.account.webId, {
method: 'POST',
headers: { cookie, 'content-type': 'application/json' },
body: JSON.stringify({ webId: otherWebId }),
});
expect(res.status).toBe(200);
// Verify if the content was added to the profile
res = await fetch(controls.account.account, { headers: { cookie }});
expect(res.status).toBe(200);
json = await res.json();
// 2 linked WebIDs now
expect(json.webIds[webId]).toBeDefined();
expect(json.webIds[otherWebId]).toBeDefined();
});
it('can create a client credentials token.', async(): Promise<void> => {
let res = await fetch(controls.account.clientCredentials, {
method: 'POST',
headers: { cookie, 'content-type': 'application/json' },
body: JSON.stringify({ name: 'token', webId }),
});
expect(res.status).toBe(200);
const json = await res.json();
expect(json.id).toMatch(/^token/u);
expect(json.secret).toBeDefined();
expect(json.resource).toBeDefined();
const { id, resource, secret } = json;
// Verify if the content was added to the profile
res = await fetch(controls.account.account, { headers: { cookie }});
expect(res.status).toBe(200);
const { clientCredentials } = await res.json();
expect(clientCredentials[id]).toBe(resource);
// Request a token
const authString = `${encodeURIComponent(id)}:${encodeURIComponent(secret)}`;
res = await fetch(joinUrl(baseUrl, '.oidc/token'), {
method: 'POST',
headers: {
authorization: `Basic ${Buffer.from(authString).toString('base64')}`,
'content-type': APPLICATION_X_WWW_FORM_URLENCODED,
},
body: 'grant_type=client_credentials&scope=webid',
});
expect(res.status).toBe(200);
const { access_token: token } = await res.json();
expect(token).toBeDefined();
});
it('can remove registered WebIDs.', async(): Promise<void> => {
let res = await fetch(controls.account.account, { headers: { cookie }});
expect(res.status).toBe(200);
let json = await res.json();
res = await fetch(json.webIds[webId], { method: 'DELETE', headers: { cookie }});
expect(res.status).toBe(200);
// Make sure it's gone
res = await fetch(controls.account.account, { headers: { cookie }});
json = await res.json();
expect(json.webIds[webId]).toBeUndefined();
});
it('can remove credential tokens.', async(): Promise<void> => {
let res = await fetch(controls.account.account, { headers: { cookie }});
expect(res.status).toBe(200);
let json = await res.json();
const tokenUrl = Object.values(json.clientCredentials)[0] as string;
res = await fetch(tokenUrl, { method: 'DELETE', headers: { cookie }});
expect(res.status).toBe(200);
// Make sure it's gone
res = await fetch(controls.account.account, { headers: { cookie }});
json = await res.json();
expect(Object.keys(json.clientCredentials)).toHaveLength(0);
});
it('can reset a password if forgotten.', async(): Promise<void> => {
let res = await fetch(controls.password.forgot, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email }),
});
expect(res.status).toBe(200);
expect(sendMail).toHaveBeenCalledTimes(1);
// Parse reset URL out of mail
const mail = sendMail.mock.calls[0][0];
expect(mail.to).toBe(email);
const match = /(http:.*)$/u.exec(mail.text);
expect(match).toBeDefined();
const resetUrl = match![1];
res = await fetch(resetUrl);
const url = new URL(resetUrl);
const recordId = url.searchParams.get('rid');
expect(recordId).toBeDefined();
// Reset the password
password = 'resetSecret';
res = await fetch(resetUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ recordId, password }),
});
expect(res.status).toBe(200);
// Verify logging in with the new password works
res = await fetch(controls.password.login, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email, password }),
});
expect(res.status).toBe(200);
expect(res.headers.get('set-cookie')).toBeDefined();
});
});

View File

@@ -54,7 +54,7 @@ export function getDefaultVariables(port: number, baseUrl?: string): Record<stri
'urn:solid-server:default:variable:socket': null,
'urn:solid-server:default:variable:loggingLevel': 'off',
'urn:solid-server:default:variable:showStackTrace': true,
'urn:solid-server:default:variable:seededPodConfigJson': null,
'urn:solid-server:default:variable:seedConfig': null,
'urn:solid-server:default:variable:workers': 1,
};
}

View File

@@ -2,6 +2,8 @@ import { mkdirSync } from 'fs';
import fetch from 'cross-fetch';
import type { App } from '../../src/init/App';
import { joinFilePath } from '../../src/util/PathUtil';
import { register } from '../util/AccountUtil';
import type { User } from '../util/AccountUtil';
import { getPort } from '../util/Util';
import { getDefaultVariables, getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder } from './Config';
@@ -12,6 +14,7 @@ const podConfigJson = joinFilePath(rootFilePath, 'config-pod.json');
const configs: [string, any][] = [
[ 'memory.json', {
// Need to remove the config-pod.json
teardown: async(): Promise<void> => removeFolder(rootFilePath),
}],
[ 'filesystem.json', {
@@ -23,16 +26,18 @@ const configs: [string, any][] = [
// Tests are very similar to subdomain/pod tests. Would be nice if they can be combined
describe.each(configs)('A dynamic pod server with template config %s', (template, { teardown }): void => {
let app: App;
const settings = {
const user: User = {
podName: 'alice',
webId: 'http://test.com/#alice',
webId: 'http://example.com/#alice',
email: 'alice@test.email',
password: 'password',
confirmPassword: 'password',
template,
createPod: true,
settings: {
template,
},
};
const podUrl = `${baseUrl}${settings.podName}/`;
const podUrl = `${baseUrl}${user.podName}/`;
let controls: any;
let authorization: string;
beforeAll(async(): Promise<void> => {
const variables: Record<string, any> = {
@@ -61,13 +66,9 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
});
it('creates a pod with the given config.', async(): Promise<void> => {
const res = await fetch(`${baseUrl}idp/register/`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(settings),
});
expect(res.status).toBe(200);
await expect(res.text()).resolves.toContain(podUrl);
const result = await register(baseUrl, user);
({ controls, authorization } = result);
expect(result.pod).toBe(podUrl);
});
it('can fetch the created pod.', async(): Promise<void> => {
@@ -83,7 +84,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
it('should be able to read acl file with the correct credentials.', async(): Promise<void> => {
const res = await fetch(`${podUrl}.acl`, {
headers: {
authorization: `WebID ${settings.webId}`,
authorization: `WebID ${user.webId}`,
},
});
expect(res.status).toBe(200);
@@ -92,7 +93,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
it('should be able to write to the pod now as the owner.', async(): Promise<void> => {
let res = await fetch(`${podUrl}test`, {
headers: {
authorization: `WebID ${settings.webId}`,
authorization: `WebID ${user.webId}`,
},
});
expect(res.status).toBe(404);
@@ -100,7 +101,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
res = await fetch(`${podUrl}test`, {
method: 'PUT',
headers: {
authorization: `WebID ${settings.webId}`,
authorization: `WebID ${user.webId}`,
'content-type': 'text/plain',
},
body: 'this is new data!',
@@ -110,7 +111,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
res = await fetch(`${podUrl}test`, {
headers: {
authorization: `WebID ${settings.webId}`,
authorization: `WebID ${user.webId}`,
},
});
expect(res.status).toBe(200);
@@ -118,13 +119,12 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
});
it('should not be able to create a pod with the same name.', async(): Promise<void> => {
const newSettings = { ...settings, webId: 'http://test.com/#bob', email: 'bob@test.email' };
const res = await fetch(`${baseUrl}idp/register/`, {
const res = await fetch(controls.account.pod, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(newSettings),
headers: { authorization, 'content-type': 'application/json' },
body: JSON.stringify({ name: user.podName, settings: { template }}),
});
expect(res.status).toBe(409);
expect(res.status).toBe(400);
await expect(res.text()).resolves.toContain(`There already is a pod at ${podUrl}`);
});
});

View File

@@ -1,17 +1,15 @@
import { stringify } from 'querystring';
import { URL } from 'url';
import type { KeyPair } from '@inrupt/solid-client-authn-core';
import {
buildAuthenticatedFetch,
createDpopHeader,
generateDpopKeyPair,
} from '@inrupt/solid-client-authn-core';
import { load } from 'cheerio';
import type { Response } from 'cross-fetch';
import { fetch } from 'cross-fetch';
import { parse, splitCookiesString } from 'set-cookie-parser';
import type { App } from '../../src/init/App';
import { APPLICATION_JSON, APPLICATION_X_WWW_FORM_URLENCODED } from '../../src/util/ContentTypes';
import { APPLICATION_X_WWW_FORM_URLENCODED } from '../../src/util/ContentTypes';
import { joinUrl } from '../../src/util/PathUtil';
import { register } from '../util/AccountUtil';
import { getPort } from '../util/Util';
import { getDefaultVariables, getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder } from './Config';
import { IdentityTestState } from './IdentityTestState';
@@ -31,20 +29,9 @@ const stores: [string, any][] = [
}],
];
// Don't send actual e-mails
jest.mock('nodemailer');
// Prevent panva/node-openid-client from emitting DraftWarning
jest.spyOn(process, 'emitWarning').mockImplementation();
async function postForm(url: string, formBody: string): Promise<Response> {
return fetch(url, {
method: 'POST',
headers: { 'content-type': APPLICATION_X_WWW_FORM_URLENCODED },
body: formBody,
});
}
// No way around the cookies https://github.com/panva/node-oidc-provider/issues/552 .
// They will be simulated by storing the values and passing them along.
// This is why the redirects are handled manually.
@@ -54,23 +41,20 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
const redirectUrl = 'http://mockedredirect/';
const container = new URL('secret/', baseUrl).href;
const oidcIssuer = baseUrl;
const card = joinUrl(baseUrl, 'profile/card');
const webId = `${card}#me`;
const webId2 = `${card}#someoneElse`;
let webId3: string;
const email = 'test@test.com';
const email2 = 'bob@test.email';
const email3 = 'alice@test.email';
const indexUrl = joinUrl(baseUrl, '.account/');
let webId: string;
let webId2: string;
const email = 'test@example.com';
const email2 = 'otherMail@example.com';
const password = 'password!';
const password2 = 'password2!';
let sendMail: jest.Mock;
let controls: {
oidc: { webId: string; consent: string; forgetWebId: string; prompt: string };
main: { index: string };
account: { create: string; pod: string; logout: string };
password: { create: string; login: string };
};
beforeAll(async(): Promise<void> => {
// Needs to happen before Components.js instantiation
sendMail = jest.fn();
const nodemailer = jest.requireMock('nodemailer');
Object.assign(nodemailer, { createTransport: (): any => ({ sendMail }) });
const instances = await instantiateFromConfig(
'urn:solid-server:test:Instances',
getTestConfigPath(config),
@@ -82,13 +66,9 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
({ app } = instances);
await app.start();
// Create a simple webId
const webIdTurtle = `<${webId}> <http://www.w3.org/ns/solid/terms#oidcIssuer> <${baseUrl}> .`;
await fetch(card, {
method: 'PUT',
headers: { 'content-type': 'text/turtle' },
body: webIdTurtle,
});
// Create accounts
({ webId, controls } = await register(baseUrl, { email, password, podName: 'test' }));
({ webId: webId2 } = await register(baseUrl, { email: email2, password, podName: 'otherTest' }));
// Create container where only webId can write
const aclTurtle = `
@@ -114,43 +94,6 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
await app.stop();
});
describe('doing registration', (): void => {
let formBody: string;
let registrationTriple: string;
beforeAll(async(): Promise<void> => {
// We will need this twice
formBody = stringify({ email, webId, password, confirmPassword: password, register: 'ok' });
});
it('sends the form once to receive the registration triple.', async(): Promise<void> => {
const res = await postForm(`${baseUrl}idp/register/`, formBody);
expect(res.status).toBe(400);
const json = await res.json();
registrationTriple = json.details.quad;
});
it('updates the webId with the registration token.', async(): Promise<void> => {
const patchBody = `INSERT DATA { ${registrationTriple} }`;
const res = await fetch(webId, {
method: 'PATCH',
headers: { 'content-type': 'application/sparql-update' },
body: patchBody,
});
expect(res.status).toBe(205);
});
it('sends the form again to successfully register.', async(): Promise<void> => {
const res = await postForm(`${baseUrl}idp/register/`, formBody);
expect(res.status).toBe(200);
await expect(res.json()).resolves.toEqual(expect.objectContaining({
webId,
email,
oidcIssuer: baseUrl,
}));
});
});
describe('authenticating', (): void => {
let state: IdentityTestState;
@@ -162,16 +105,64 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
await state.session.logout();
});
it('initializes the session and logs in.', async(): Promise<void> => {
let url = await state.startSession();
it('initializes the session.', async(): Promise<void> => {
// This is the auth URL with all the relevant query parameters
let url = await state.initSession();
expect(url.startsWith(oidcIssuer)).toBeTruthy();
// Always redirect to our index page
url = await state.handleRedirect(url);
// Compare received URL with login URL in our controls
expect(controls.main.index).toBe(url);
// Add the OIDC controls to the object
const res = await state.fetchIdp(url);
expect(res.status).toBe(200);
url = await state.login(url, email, password);
await state.consent(url);
expect(state.session.info?.webId).toBe(webId);
controls = {
...(await res.json()).controls,
...controls,
};
});
it('can only access the container when using the logged in session.', async(): Promise<void> => {
it('logs in.', async(): Promise<void> => {
// Log in using email/password
const res = await state.fetchIdp(controls.password.login, 'POST', JSON.stringify({ email, password }));
// Redirect to WebID picker
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
});
it('sends a token for the chosen WebID.', async(): Promise<void> => {
// See the available WebIDs
let res = await state.fetchIdp(controls.oidc.webId);
expect(res.status).toBe(200);
const json = await res.json();
expect(json.webIds).toEqual([ webId ]);
// Pick the WebID
// Errors if the WebID is not registered to the account
res = await state.fetchIdp(controls.oidc.webId, 'POST', { webId: 'http://example.com/wrong' });
expect(res.status).toBe(400);
res = await state.fetchIdp(controls.oidc.webId, 'POST', { webId, remember: true });
// Redirect to the consent page
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
});
it('consents and redirects back to the client.', async(): Promise<void> => {
let res = await state.fetchIdp(controls.oidc.consent);
const json = await res.json();
expect(json.webId).toBe(webId);
expect(json.client.grant_types).toContain('authorization_code');
expect(json.client.grant_types).toContain('refresh_token');
res = await state.fetchIdp(controls.oidc.consent, 'POST');
// Redirect back to the client and verify login success
await state.handleIncomingRedirect(res, webId);
});
it('can only access the profile container when using the logged in session.', async(): Promise<void> => {
let res = await fetch(container);
expect(res.status).toBe(401);
@@ -185,16 +176,61 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
expect(res.status).toBe(401);
});
it('can log in again.', async(): Promise<void> => {
const url = await state.startSession();
it('immediately gets redirect to the consent page in the next session.', async(): Promise<void> => {
const url = await state.initSession();
await state.handleRedirect(url);
const res = await state.fetchIdp(url);
const res = await state.fetchIdp(controls.oidc.prompt);
expect(res.status).toBe(200);
await expect(res.json()).resolves.toEqual(expect.objectContaining({ prompt: 'consent' }));
});
it('can forget the stored WebID.', async(): Promise<void> => {
let res = await state.fetchIdp(controls.oidc.forgetWebId, 'POST');
expect(res.status).toBe(200);
// Will receive confirm screen here instead of login screen
await state.consent(url);
// We have to pick a WebID again
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
res = await state.fetchIdp(controls.oidc.webId, 'POST', { webId });
expect(state.session.info?.webId).toBe(webId);
// Redirect back to the consent page
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
});
it('can consent again.', async(): Promise<void> => {
let res = await state.fetchIdp(controls.oidc.consent, 'POST');
// Redirect back to the client and verify login success
await state.handleIncomingRedirect(res, webId);
// Verify by accessing the private container
res = await state.session.fetch(container);
expect(res.status).toBe(200);
});
it('can log out.', async(): Promise<void> => {
// Log out
let res = await state.fetchIdp(controls.account.logout, 'POST');
expect(res.status).toBe(200);
// Log out of the previous session and start a new one
await state.session.logout();
const url = await state.initSession();
await state.handleRedirect(url);
// Log in
res = await state.fetchIdp(controls.password.login, 'POST', { email: email2, password });
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
// Pick the new WebID
res = await state.fetchIdp(controls.oidc.webId, 'POST', { webId: webId2 });
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
// Consent again
res = await state.fetchIdp(controls.oidc.consent, 'POST');
// Redirect back to the client and verify login success
await state.handleIncomingRedirect(res, webId2);
});
});
@@ -248,20 +284,37 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
});
it('initializes the session and logs in.', async(): Promise<void> => {
let url = await state.startSession(clientId);
const res = await state.fetchIdp(url);
expect(res.status).toBe(200);
url = await state.login(url, email, password);
const url = await state.initSession(clientId);
// Redirect to our login page
await state.handleRedirect(url);
// Log in using email/password
let res = await state.fetchIdp(controls.password.login, 'POST', { email, password });
// Redirect to WebID picker
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
// Pick the WebID
res = await state.fetchIdp(controls.oidc.webId, 'POST', { webId, remember: true });
// Redirect to the consent page
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
// Verify the client information the server discovered
const consentRes = await state.fetchIdp(url, 'GET');
expect(consentRes.status).toBe(200);
const { client } = await consentRes.json();
expect(client.client_id).toBe(clientJson.client_id);
expect(client.client_name).toBe(clientJson.client_name);
res = await state.fetchIdp(controls.oidc.consent);
expect(res.status).toBe(200);
const json = await res.json();
expect(json.webId).toBe(webId);
expect(json.client.client_id).toBe(clientJson.client_id);
expect(json.client.client_name).toBe(clientJson.client_name);
expect(json.client.grant_types).toContain('authorization_code');
expect(json.client.grant_types).toContain('refresh_token');
await state.consent(url);
expect(state.session.info?.webId).toBe(webId);
res = await state.fetchIdp(controls.oidc.consent, 'POST');
// Redirect back to the client and verify login success
await state.handleIncomingRedirect(res, webId);
});
it('rejects requests in case the redirect URL is not accepted.', async(): Promise<void> => {
@@ -288,7 +341,6 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
});
describe('using client_credentials', (): void => {
const credentialsUrl = joinUrl(baseUrl, '/idp/credentials/');
const tokenUrl = joinUrl(baseUrl, '.oidc/token');
let dpopKey: KeyPair;
let id: string | undefined;
@@ -300,18 +352,26 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
});
it('can request a credentials token.', async(): Promise<void> => {
// Login and save cookie
const loginResponse = await fetch(controls.password.login, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const cookies = parse(splitCookiesString(loginResponse.headers.get('set-cookie')!));
const cookie = `${cookies[0].name}=${cookies[0].value}`;
// Request token
const accountJson = await (await fetch(indexUrl, { headers: { cookie }})).json();
const credentialsUrl = accountJson.controls.account.clientCredentials;
const res = await fetch(credentialsUrl, {
method: 'POST',
headers: {
'content-type': APPLICATION_JSON,
},
body: JSON.stringify({ email, password, name: 'token' }),
headers: { cookie, 'content-type': 'application/json' },
body: JSON.stringify({ name: 'token', webId }),
});
expect(res.status).toBe(200);
({ id, secret } = await res.json());
expect(typeof id).toBe('string');
expect(typeof secret).toBe('string');
expect(id).toMatch(/^token/u);
});
it('can request an access token using the credentials.', async(): Promise<void> => {
@@ -339,288 +399,6 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
res = await authFetch(container);
expect(res.status).toBe(200);
});
it('can see all credentials.', async(): Promise<void> => {
const res = await fetch(credentialsUrl, {
method: 'POST',
headers: {
'content-type': APPLICATION_JSON,
},
body: JSON.stringify({ email, password }),
});
expect(res.status).toBe(200);
await expect(res.json()).resolves.toEqual([ id ]);
});
it('can delete credentials.', async(): Promise<void> => {
let res = await fetch(credentialsUrl, {
method: 'POST',
headers: {
'content-type': APPLICATION_JSON,
},
body: JSON.stringify({ email, password, delete: id }),
});
expect(res.status).toBe(200);
// Client_credentials call should fail now
const dpopHeader = await createDpopHeader(tokenUrl, 'POST', dpopKey);
const authString = `${encodeURIComponent(id!)}:${encodeURIComponent(secret!)}`;
res = await fetch(tokenUrl, {
method: 'POST',
headers: {
authorization: `Basic ${Buffer.from(authString).toString('base64')}`,
'content-type': APPLICATION_X_WWW_FORM_URLENCODED,
dpop: dpopHeader,
},
body: 'grant_type=client_credentials&scope=webid',
});
expect(res.status).toBe(401);
});
});
describe('resetting password', (): void => {
let nextUrl: string;
it('sends the corresponding email address through the form to get a mail.', async(): Promise<void> => {
const res = await postForm(`${baseUrl}idp/forgotpassword/`, stringify({ email }));
expect(res.status).toBe(200);
const json = await res.json();
expect(json.email).toBe(email);
const mail = sendMail.mock.calls[0][0];
expect(mail.to).toBe(email);
const match = /(http:.*)$/u.exec(mail.text);
expect(match).toBeDefined();
nextUrl = match![1];
expect(nextUrl).toMatch(/\/resetpassword\/[^/]+$/u);
});
it('resets the password through the given link.', async(): Promise<void> => {
// Extract the submit URL from the reset password form
let res = await fetch(nextUrl);
expect(res.status).toBe(200);
const text = await res.text();
const relative = load(text)('form').attr('action');
// Reset password form has no action causing the current URL to be used
expect(relative).toBeUndefined();
// Extract recordId from URL since JS is used to add it
const recordId = /\?rid=([^/]+)$/u.exec(nextUrl)?.[1];
expect(typeof recordId).toBe('string');
// POST the new password to the same URL
const formData = stringify({ password: password2, confirmPassword: password2, recordId });
res = await fetch(nextUrl, {
method: 'POST',
headers: { 'content-type': APPLICATION_X_WWW_FORM_URLENCODED },
body: formData,
});
expect(res.status).toBe(200);
});
});
describe('logging in after password reset', (): void => {
let state: IdentityTestState;
let nextUrl: string;
beforeAll(async(): Promise<void> => {
state = new IdentityTestState(baseUrl, redirectUrl, oidcIssuer);
});
afterAll(async(): Promise<void> => {
await state.session.logout();
});
it('can not log in with the old password anymore.', async(): Promise<void> => {
const url = await state.startSession();
nextUrl = url;
let res = await state.fetchIdp(url);
expect(res.status).toBe(200);
const formData = stringify({ email, password });
res = await state.fetchIdp(url, 'POST', formData, APPLICATION_X_WWW_FORM_URLENCODED);
expect(res.status).toBe(500);
expect(await res.text()).toContain('Incorrect password');
});
it('can log in with the new password.', async(): Promise<void> => {
const url = await state.login(nextUrl, email, password2);
await state.consent(url);
expect(state.session.info?.webId).toBe(webId);
});
});
describe('creating pods without registering with the IDP', (): void => {
let formBody: string;
let registrationTriple: string;
const podName = 'myPod';
beforeAll(async(): Promise<void> => {
// We will need this twice
formBody = stringify({
email: email2,
webId: webId2,
password,
confirmPassword: password,
podName,
createPod: 'ok',
});
});
it('sends the form once to receive the registration triple.', async(): Promise<void> => {
const res = await postForm(`${baseUrl}idp/register/`, formBody);
expect(res.status).toBe(400);
const json = await res.json();
registrationTriple = json.details.quad;
});
it('updates the webId with the registration token.', async(): Promise<void> => {
const patchBody = `INSERT DATA { ${registrationTriple} }`;
const res = await fetch(webId2, {
method: 'PATCH',
headers: { 'content-type': 'application/sparql-update' },
body: patchBody,
});
expect(res.status).toBe(205);
});
it('sends the form again to successfully register.', async(): Promise<void> => {
const res = await postForm(`${baseUrl}idp/register/`, formBody);
expect(res.status).toBe(200);
await expect(res.json()).resolves.toEqual(expect.objectContaining({
email: email2,
webId: webId2,
podBaseUrl: `${baseUrl}${podName}/`,
}));
});
});
describe('creating a new WebID', (): void => {
const podName = 'alice';
let state: IdentityTestState;
const formBody = stringify({
email: email3, password, confirmPassword: password, podName, createWebId: 'ok', register: 'ok', createPod: 'ok',
});
afterAll(async(): Promise<void> => {
await state.session.logout();
});
it('sends the form to create the WebID and register.', async(): Promise<void> => {
const res = await postForm(`${baseUrl}idp/register/`, formBody);
expect(res.status).toBe(200);
const json = await res.json();
expect(json).toEqual(expect.objectContaining({
webId: expect.any(String),
email: email3,
oidcIssuer: baseUrl,
podBaseUrl: `${baseUrl}${podName}/`,
}));
webId3 = json.webId;
});
it('initializes the session and logs in.', async(): Promise<void> => {
state = new IdentityTestState(baseUrl, redirectUrl, oidcIssuer);
let url = await state.startSession();
const res = await state.fetchIdp(url);
expect(res.status).toBe(200);
url = await state.login(url, email3, password);
await state.consent(url);
expect(state.session.info?.webId).toBe(webId3);
});
it('can only write to the new profile when using the logged in session.', async(): Promise<void> => {
const patchOptions = {
method: 'PATCH',
headers: { 'content-type': 'application/sparql-update' },
body: `INSERT DATA { <> <http://www.w3.org/2000/01/rdf-schema#label> "A cool WebID." }`,
};
let res = await fetch(webId3, patchOptions);
expect(res.status).toBe(401);
res = await state.session.fetch(webId3, patchOptions);
expect(res.status).toBe(205);
});
it('always has control over data in the pod.', async(): Promise<void> => {
const podBaseUrl = `${baseUrl}${podName}/`;
const brokenAcl = '<#authorization> a <http://www.w3.org/ns/auth/acl#Authorization> .';
// Make the acl file unusable
let res = await state.session.fetch(`${podBaseUrl}.acl`, {
method: 'PUT',
headers: { 'content-type': 'text/turtle' },
body: brokenAcl,
});
expect(res.status).toBe(205);
// The owner is locked out of their own pod due to a faulty acl file
res = await state.session.fetch(podBaseUrl);
expect(res.status).toBe(403);
const fixedAcl = `@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
<#authorization>
a acl:Authorization;
acl:agentClass foaf:Agent;
acl:mode acl:Read;
acl:accessTo <./>.`;
// Owner can still update the acl
res = await state.session.fetch(`${podBaseUrl}.acl`, {
method: 'PUT',
headers: { 'content-type': 'text/turtle' },
body: fixedAcl,
});
expect(res.status).toBe(205);
// Access is possible again
res = await state.session.fetch(podBaseUrl);
expect(res.status).toBe(200);
});
});
describe('having multiple accounts', (): void => {
let state: IdentityTestState;
let url: string;
beforeAll(async(): Promise<void> => {
state = new IdentityTestState(baseUrl, redirectUrl, oidcIssuer);
});
afterAll(async(): Promise<void> => {
await state.session.logout();
});
it('initializes the session and logs in with the first account.', async(): Promise<void> => {
url = await state.startSession();
const res = await state.fetchIdp(url);
expect(res.status).toBe(200);
url = await state.login(url, email, password2);
await state.consent(url);
expect(state.session.info?.webId).toBe(webId);
});
it('can log out on the consent page.', async(): Promise<void> => {
await state.session.logout();
url = await state.startSession();
const res = await state.fetchIdp(url);
expect(res.status).toBe(200);
// Will receive confirm screen here instead of login screen
url = await state.logout(url);
});
it('can log in with a different account.', async(): Promise<void> => {
const res = await state.fetchIdp(url);
expect(res.status).toBe(200);
url = await state.login(url, email3, password);
await state.consent(url);
expect(state.session.info?.webId).toBe(webId3);
});
});
describe('setup', (): void => {

View File

@@ -1,12 +1,8 @@
import { stringify } from 'querystring';
import { URL } from 'url';
import { Session } from '@inrupt/solid-client-authn-node';
import { load } from 'cheerio';
import type { Response } from 'cross-fetch';
import { fetch } from 'cross-fetch';
import type { Cookie } from 'set-cookie-parser';
import { parse, splitCookiesString } from 'set-cookie-parser';
import { APPLICATION_X_WWW_FORM_URLENCODED } from '../../src/util/ContentTypes';
/* eslint-disable jest/no-standalone-expect */
/**
@@ -34,11 +30,17 @@ export class IdentityTestState {
* Performs a fetch call while keeping track of the stored cookies and preventing redirects.
* @param url - URL to call.
* @param method - Method to use.
* @param body - Body to send along.
* @param contentType - Content-Type of the body.
* @param body - Body to send along. If this is not a string it will be JSONified.
* @param contentType - Content-Type of the body. If not defined but there is a body, this will be set to JSON.
*/
public async fetchIdp(url: string, method = 'GET', body?: string, contentType?: string): Promise<Response> {
public async fetchIdp(url: string, method = 'GET', body?: string | unknown, contentType?: string): Promise<Response> {
const options = { method, headers: { cookie: this.cookie }, body, redirect: 'manual' } as any;
if (body && typeof body !== 'string') {
options.body = JSON.stringify(body);
}
if (body && !contentType) {
contentType = 'application/json';
}
if (contentType) {
options.headers['content-type'] = contentType;
}
@@ -58,24 +60,11 @@ export class IdentityTestState {
}
/**
* Uses the given jquery command to find a node in the given html body.
* The value from the given attribute field then gets extracted and combined with the base url.
* @param html - Body to parse.
* @param jquery - Query to run on the body.
* @param attr - Attribute to extract.
* Initializes the OIDC session for the given clientId.
* If undefined, dynamic registration will be used.
*/
public extractUrl(html: string, jquery: string, attr: string): string {
const url = load(html)(jquery).attr(attr);
expect(typeof url).toBe('string');
return new URL(url!, this.baseUrl).href;
}
/**
* Initializes an authentication session and stores the relevant cookies for later re-use.
* All te relevant links from the login page get extracted.
*/
public async startSession(clientId?: string): Promise<string> {
let nextUrl = '';
public async initSession(clientId?: string): Promise<string> {
let nextUrl: string;
await this.session.login({
redirectUrl: this.redirectUrl,
oidcIssuer: this.oidcIssuer,
@@ -84,67 +73,43 @@ export class IdentityTestState {
nextUrl = data;
},
});
expect(nextUrl.length > 0).toBeTruthy();
expect(nextUrl.startsWith(this.oidcIssuer)).toBeTruthy();
// Need to catch the redirect so we can copy the cookies
let res = await this.fetchIdp(nextUrl);
expect(res.status).toBe(303);
nextUrl = res.headers.get('location')!;
// Handle redirect
res = await this.fetchIdp(nextUrl);
expect(res.status).toBe(200);
// Need to send request to prompt API to get actual location
let json = await res.json();
res = await this.fetchIdp(json.controls.prompt);
json = await res.json();
nextUrl = json.location;
return nextUrl;
return nextUrl!;
}
/**
* Logs in by sending the corresponding email and password to the given form action.
* The URL should be extracted from the login page.
* Handles a URL that is expected to redirect and returns the target it would redirect to.
*/
public async login(url: string, email: string, password: string): Promise<string> {
const formData = stringify({ email, password });
let res = await this.fetchIdp(url, 'POST', formData, APPLICATION_X_WWW_FORM_URLENCODED);
expect(res.status).toBe(200);
const json = await res.json();
res = await this.fetchIdp(json.location);
public async handleRedirect(url: string): Promise<string> {
const res = await this.fetchIdp(url);
expect(res.status).toBe(303);
expect(res.headers.has('location')).toBe(true);
return res.headers.get('location')!;
}
/**
* Handles the consent screen at the given URL and the followup redirect back to the client.
* Handles a JSON redirect. That is a request that returns a 200,
* but has a `location` field in the JSON to indicate what it should redirect to.
* That URL is expected to be another redirect, and this returns what it would redirect to.
*/
public async consent(url: string): Promise<void> {
let res = await this.fetchIdp(url, 'POST', '', APPLICATION_X_WWW_FORM_URLENCODED);
public async handleLocationRedirect(res: Response): Promise<string> {
expect(res.status).toBe(200);
const json = await res.json();
// The OIDC redirect
expect(json.location).toBeDefined();
res = await this.fetchIdp(json.location);
expect(res.status).toBe(303);
const mockUrl = res.headers.get('location')!;
expect(mockUrl.startsWith(this.redirectUrl)).toBeTruthy();
return this.handleRedirect(json.location);
}
public async handleIncomingRedirect(res: Response, webId: string): Promise<void> {
// Redirect back to the client
const url = await this.handleLocationRedirect(res);
expect(url.startsWith(this.redirectUrl)).toBe(true);
// Workaround for https://github.com/inrupt/solid-client-authn-js/issues/2985
const strippedUrl = new URL(mockUrl);
const strippedUrl = new URL(url);
strippedUrl.searchParams.delete('iss');
const info = await this.session.handleIncomingRedirect(strippedUrl.href);
expect(info?.isLoggedIn).toBe(true);
}
public async logout(url: string): Promise<string> {
let res = await this.fetchIdp(url, 'POST', stringify({ logOut: true }), APPLICATION_X_WWW_FORM_URLENCODED);
expect(res.status).toBe(200);
const json = await res.json();
res = await this.fetchIdp(json.location);
expect(res.status).toBe(303);
return res.headers.get('location')!;
expect(info?.webId).toBe(webId);
}
}

View File

@@ -4,6 +4,7 @@ import type { Response } from 'cross-fetch';
import { ensureDir, pathExists, stat } from 'fs-extra';
import { joinUrl } from '../../src';
import type { App } from '../../src';
import { register } from '../util/AccountUtil';
import { getPort } from '../util/Util';
import { getDefaultVariables, getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder } from './Config';
@@ -24,23 +25,7 @@ async function performSimplePutWithLength(path: string, length: number): Promise
/** Registers two test pods on the server matching the 'baseUrl' */
async function registerTestPods(baseUrl: string, pods: string[]): Promise<void> {
for (const pod of pods) {
await fetch(`${baseUrl}idp/register/`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
createWebId: 'on',
webId: '',
register: 'on',
createPod: 'on',
podName: pod,
email: `${pod}@example.ai`,
password: 't',
confirmPassword: 't',
submit: '',
}),
});
await register(baseUrl, { podName: pod, email: `${pod}@example.ai`, password: 't' });
}
}

View File

@@ -1,9 +1,10 @@
import { fetch } from 'cross-fetch';
import type { App } from '../../src/init/App';
import { joinUrl } from '../../src/util/PathUtil';
import { register } from '../util/AccountUtil';
import type { User } from '../util/AccountUtil';
import { getPort } from '../util/Util';
import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config';
import { IdentityTestState } from './IdentityTestState';
const port = getPort('RestrictedIdentity');
const baseUrl = `http://localhost:${port}/`;
@@ -16,16 +17,13 @@ jest.spyOn(process, 'emitWarning').mockImplementation();
describe('A server with restricted IDP access', (): void => {
let app: App;
const settings = {
const user: User = {
podName: 'alice',
email: 'alice@test.email',
password: 'password',
confirmPassword: 'password',
createWebId: true,
register: true,
createPod: true,
};
const webId = joinUrl(baseUrl, 'alice/profile/card#me');
let controls: any;
beforeAll(async(): Promise<void> => {
const instances = await instantiateFromConfig(
@@ -45,22 +43,17 @@ describe('A server with restricted IDP access', (): void => {
let res = await fetch(joinUrl(baseUrl, '.well-known/.acl'));
expect(res.status).toBe(200);
res = await fetch(joinUrl(baseUrl, 'idp/.acl'));
res = await fetch(joinUrl(baseUrl, '.account/.acl'));
expect(res.status).toBe(200);
});
it('can create a pod.', async(): Promise<void> => {
const res = await fetch(`${baseUrl}idp/register/`, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify(settings),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.webId).toBe(webId);
const result = await register(baseUrl, user);
({ controls } = result);
expect(result.webId).toBe(webId);
});
it('can restrict registration access.', async(): Promise<void> => {
it('can restrict account creation.', async(): Promise<void> => {
// Only allow new WebID to register
const restrictedAcl = `@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
@@ -71,49 +64,33 @@ describe('A server with restricted IDP access', (): void => {
acl:mode acl:Read, acl:Write, acl:Control;
acl:accessTo <./>.`;
let res = await fetch(`${baseUrl}idp/register/.acl`, {
let res = await fetch(`${controls.account.create}.acl`, {
method: 'PUT',
headers: { 'content-type': 'text/turtle' },
body: restrictedAcl,
});
expect(res.status).toBe(201);
expect(res.headers.get('location')).toBe(`${baseUrl}idp/register/.acl`);
expect(res.headers.get('location')).toBe(`${controls.account.create}.acl`);
// Registration is now disabled
res = await fetch(`${baseUrl}idp/register/`);
res = await fetch(controls.account.create);
expect(res.status).toBe(401);
res = await fetch(`${baseUrl}idp/register/`, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify({ ...settings, email: 'bob@test.email', podName: 'bob' }),
});
res = await fetch(controls.account.create, { method: 'POST' });
expect(res.status).toBe(401);
});
it('can still access registration with the correct credentials.', async(): Promise<void> => {
// Logging into session
const state = new IdentityTestState(baseUrl, 'http://mockedredirect/', baseUrl);
let url = await state.startSession();
let res = await state.fetchIdp(url);
expect(res.status).toBe(200);
url = await state.login(url, settings.email, settings.password);
await state.consent(url);
expect(state.session.info?.webId).toBe(webId);
// Registration still works for this WebID
res = await state.session.fetch(`${baseUrl}idp/register/`);
expect(res.status).toBe(200);
res = await state.session.fetch(`${baseUrl}idp/register/`, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify({ ...settings, email: 'bob@test.email', podName: 'bob' }),
it('can still create accounts with the correct credentials.', async(): Promise<void> => {
// Account creation still works for the WebID
let res = await fetch(controls.account.create, {
headers: { authorization: `WebID ${webId}` },
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.webId).toBe(joinUrl(baseUrl, 'bob/profile/card#me'));
await state.session.logout();
res = await fetch(controls.account.create, {
method: 'POST',
headers: { authorization: `WebID ${webId}` },
});
expect(res.status).toBe(200);
});
});

View File

@@ -1,5 +1,5 @@
import fetch from 'cross-fetch';
import { outputJson } from 'fs-extra';
import { ensureFile, writeJson } from 'fs-extra';
import type { App } from '../../src/init/App';
import { joinFilePath, joinUrl } from '../../src/util/PathUtil';
import { getPort } from '../util/Util';
@@ -11,34 +11,50 @@ const baseUrl = `http://localhost:${port}/`;
const rootFilePath = getTestFolder('seeding-pods');
describe('A server with seeded pods', (): void => {
const seedingJson = joinFilePath(rootFilePath, 'pods.json');
const indexUrl = joinUrl(baseUrl, '.account/');
let app: App;
beforeAll(async(): Promise<void> => {
// Create seeding config
await outputJson(seedingJson, [
// Create the seed file
const seed = [
{
podName: 'alice',
email: 'alice@example.com',
password: 'alice-password',
email: 'test1@example.com',
password: 'password1',
pods: [
{ name: 'pod1' },
{ name: 'pod2' },
],
},
{
podName: 'bob',
email: 'bob@example.com',
password: 'bob-password',
register: false,
email: 'test2@example.com',
password: 'password2',
pods: [
{ name: 'pod3' },
// This will fail
{ name: 'pod2' },
],
},
]);
const variables = {
...getDefaultVariables(port, baseUrl),
'urn:solid-server:default:variable:seededPodConfigJson': seedingJson,
};
{
// This will all fail
email: 'test1@example.com',
password: 'password3',
pods: [
{ name: 'pod4' },
],
},
];
const path = joinFilePath(rootFilePath, 'seed.json');
await ensureFile(path);
await writeJson(path, seed);
// Start server with the seed config
const instances = await instantiateFromConfig(
'urn:solid-server:test:Instances',
getTestConfigPath('server-memory.json'),
variables,
{
...getDefaultVariables(port, baseUrl),
'urn:solid-server:default:variable:seedConfig': path,
},
) as Record<string, any>;
({ app } = instances);
await app.start();
@@ -49,10 +65,33 @@ describe('A server with seeded pods', (): void => {
await app.stop();
});
it('has created the requested pods.', async(): Promise<void> => {
let response = await fetch(joinUrl(baseUrl, 'alice/profile/card#me'));
expect(response.status).toBe(200);
response = await fetch(joinUrl(baseUrl, 'bob/profile/card#me'));
expect(response.status).toBe(200);
it('can seed accounts and pods.', async(): Promise<void> => {
// Get the controls
const res = await fetch(indexUrl);
expect(res.status).toBe(200);
const { controls } = await res.json();
// Verify that the pods exists
await expect(fetch(joinUrl(baseUrl, 'pod1/'))).resolves.toEqual(expect.objectContaining({ status: 200 }));
await expect(fetch(joinUrl(baseUrl, 'pod2/'))).resolves.toEqual(expect.objectContaining({ status: 200 }));
await expect(fetch(joinUrl(baseUrl, 'pod3/'))).resolves.toEqual(expect.objectContaining({ status: 200 }));
await expect(fetch(joinUrl(baseUrl, 'pod4/'))).resolves.toEqual(expect.objectContaining({ status: 404 }));
// Verify that we can log in with the accounts
await expect(fetch(controls.password.login, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email: 'test1@example.com', password: 'password1' }),
})).resolves.toEqual(expect.objectContaining({ status: 200 }));
await expect(fetch(controls.password.login, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email: 'test2@example.com', password: 'password2' }),
})).resolves.toEqual(expect.objectContaining({ status: 200 }));
await expect(fetch(controls.password.login, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email: 'test1@example.com', password: 'password3' }),
})).resolves.toEqual(expect.objectContaining({ status: 403 }));
});
});

View File

@@ -11,6 +11,8 @@ const baseUrl = `http://localhost:${port}/`;
// Some tests with real Requests/Responses until the mocking library has been removed from the tests
describe('A Solid server', (): void => {
const document = `${baseUrl}document`;
const container = `${baseUrl}container/`;
let app: App;
beforeAll(async(): Promise<void> => {
@@ -31,76 +33,70 @@ describe('A Solid server', (): void => {
await app.stop();
});
it('can PUT to containers.', async(): Promise<void> => {
const res = await fetch(container, {
method: 'PUT',
headers: {
'content-type': 'text/turtle',
},
body: '<a:b> <a:b> <a:b>.',
});
expect(res.status).toBe(201);
expect(res.headers.get('location')).toBe(container);
});
it('can PUT to documents.', async(): Promise<void> => {
const res = await fetch(document, {
method: 'PUT',
headers: {
'content-type': 'text/turtle',
},
body: '<a:b> <a:b> <a:b>.',
});
expect(res.status).toBe(201);
expect(res.headers.get('location')).toBe(document);
});
it('can do a successful HEAD request to a container.', async(): Promise<void> => {
const res = await fetch(baseUrl, { method: 'HEAD' });
const res = await fetch(container, { method: 'HEAD' });
expect(res.status).toBe(200);
});
it('can do a successful HEAD request to a container without accept headers.', async(): Promise<void> => {
const res = await fetch(baseUrl, { method: 'HEAD', headers: { accept: '' }});
const res = await fetch(container, { method: 'HEAD', headers: { accept: '' }});
expect(res.status).toBe(200);
});
it('can do a successful HEAD request to a document.', async(): Promise<void> => {
const url = `${baseUrl}.acl`;
const res = await fetch(url, { method: 'HEAD' });
const res = await fetch(document, { method: 'HEAD' });
expect(res.status).toBe(200);
});
it('can do a successful HEAD request to a document without accept headers.', async(): Promise<void> => {
const url = `${baseUrl}.acl`;
const res = await fetch(url, { method: 'HEAD', headers: { accept: '' }});
const res = await fetch(document, { method: 'HEAD', headers: { accept: '' }});
expect(res.status).toBe(200);
});
it('can do a successful GET request to a container.', async(): Promise<void> => {
const res = await fetch(baseUrl);
const res = await fetch(container);
expect(res.status).toBe(200);
});
it('can do a successful GET request to a container without accept headers.', async(): Promise<void> => {
const res = await fetch(baseUrl, { headers: { accept: '' }});
const res = await fetch(container, { headers: { accept: '' }});
expect(res.status).toBe(200);
});
it('can do a successful GET request to a document.', async(): Promise<void> => {
const url = `${baseUrl}.acl`;
const res = await fetch(url);
const res = await fetch(document);
expect(res.status).toBe(200);
});
it('can do a successful GET request to a document without accept headers.', async(): Promise<void> => {
const url = `${baseUrl}.acl`;
const res = await fetch(url, { headers: { accept: '' }});
const res = await fetch(document, { headers: { accept: '' }});
expect(res.status).toBe(200);
});
it('can PUT to containers.', async(): Promise<void> => {
const url = `${baseUrl}containerPUT/`;
const res = await fetch(url, {
method: 'PUT',
headers: {
'content-type': 'text/turtle',
},
body: '<a:b> <a:b> <a:b>.',
});
expect(res.status).toBe(201);
expect(res.headers.get('location')).toBe(url);
});
it('can PUT to resources.', async(): Promise<void> => {
const url = `${baseUrl}resourcePUT`;
const res = await fetch(url, {
method: 'PUT',
headers: {
'content-type': 'text/turtle',
},
body: '<a:b> <a:b> <a:b>.',
});
expect(res.status).toBe(201);
expect(res.headers.get('location')).toBe(url);
});
it('can handle PUT errors.', async(): Promise<void> => {
// There was a specific case where the following request caused the connection to close instead of error
const res = await fetch(baseUrl, {
@@ -116,7 +112,7 @@ describe('A Solid server', (): void => {
});
it('can POST to create a container.', async(): Promise<void> => {
const res = await fetch(baseUrl, {
const res = await fetch(container, {
method: 'POST',
headers: {
'content-type': 'text/turtle',
@@ -126,58 +122,41 @@ describe('A Solid server', (): void => {
body: '<a:b> <a:b> <a:b>.',
});
expect(res.status).toBe(201);
expect(res.headers.get('location')).toBe(`${baseUrl}containerPOST/`);
expect(res.headers.get('location')).toBe(`${container}containerPOST/`);
});
it('can POST to create a document.', async(): Promise<void> => {
const res = await fetch(baseUrl, {
const res = await fetch(container, {
method: 'POST',
headers: {
'content-type': 'text/turtle',
slug: 'resourcePOST',
slug: 'documentPOST',
},
body: '<a:b> <a:b> <a:b>.',
});
expect(res.status).toBe(201);
expect(res.headers.get('location')).toBe(`${baseUrl}resourcePOST`);
expect(res.headers.get('location')).toBe(`${container}documentPOST`);
});
it('can DELETE containers.', async(): Promise<void> => {
const url = `${baseUrl}containerDELETE/`;
await fetch(url, {
method: 'PUT',
headers: {
'content-type': 'text/turtle',
},
body: '<a:b> <a:b> <a:b>.',
});
const res = await fetch(url, { method: 'DELETE' });
const res = await fetch(`${container}containerPOST/`, { method: 'DELETE' });
expect(res.status).toBe(205);
});
it('can DELETE documents.', async(): Promise<void> => {
const url = `${baseUrl}resourceDELETE`;
await fetch(url, {
method: 'PUT',
headers: {
'content-type': 'text/turtle',
},
body: '<a:b> <a:b> <a:b>.',
});
const res = await fetch(url, { method: 'DELETE' });
const res = await fetch(`${container}documentPOST`, { method: 'DELETE' });
expect(res.status).toBe(205);
});
it('can PATCH documents.', async(): Promise<void> => {
const url = `${baseUrl}resourcePATCH`;
await fetch(url, {
await fetch(document, {
method: 'PUT',
headers: {
'content-type': 'text/turtle',
},
body: '<a:b> <a:b> <a:b>.',
});
const res = await fetch(url, {
const res = await fetch(document, {
method: 'PATCH',
headers: {
'content-type': 'application/sparql-update',
@@ -188,14 +167,13 @@ describe('A Solid server', (): void => {
});
it('can not PATCH containers.', async(): Promise<void> => {
const url = `${baseUrl}containerPATCH/`;
await fetch(url, {
await fetch(container, {
method: 'PUT',
headers: {
'content-type': 'text/turtle',
},
});
const res = await fetch(url, {
const res = await fetch(container, {
method: 'PATCH',
headers: {
'content-type': 'application/sparql-update',
@@ -206,15 +184,7 @@ describe('A Solid server', (): void => {
});
it('can PATCH metadata resources.', async(): Promise<void> => {
const url = `${baseUrl}resourcePATCH`;
await fetch(url, {
method: 'PUT',
headers: {
'content-type': 'text/turtle',
},
body: '<a:b> <a:b> <a:b>.',
});
const res = await fetch(`${url}.meta`, {
const res = await fetch(`${document}.meta`, {
method: 'PATCH',
headers: {
'content-type': 'application/sparql-update',

View File

@@ -1,5 +1,7 @@
import fetch from 'cross-fetch';
import type { App } from '../../src/init/App';
import { register } from '../util/AccountUtil';
import type { User } from '../util/AccountUtil';
import { getPort } from '../util/Util';
import {
getDefaultVariables,
@@ -28,16 +30,16 @@ const stores: [string, any][] = [
// Simulating subdomains using the forwarded header so no DNS changes are required
describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardown }): void => {
let app: App;
const settings = {
podName: 'alice',
webId: 'http://test.com/#alice',
email: 'alice@test.email',
const user: User = {
email: 'alice@example.com',
password: 'password',
confirmPassword: 'password',
createPod: true,
webId: 'http://example.com/#alice',
podName: 'alice',
};
const podHost = `alice.localhost:${port}`;
const podUrl = `http://${podHost}/`;
let authorization: string;
let controls: any;
beforeAll(async(): Promise<void> => {
const variables: Record<string, any> = {
@@ -74,7 +76,7 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
let res = await fetch(`${baseUrl}alice`, {
method: 'PUT',
headers: {
authorization: `WebID ${settings.webId}`,
authorization: `WebID ${user.webId}`,
'content-type': 'text/plain',
},
body: 'this is new data!',
@@ -90,13 +92,9 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
describe('handling pods', (): void => {
it('creates pods in a subdomain.', async(): Promise<void> => {
const res = await fetch(`${baseUrl}idp/register/`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(settings),
});
expect(res.status).toBe(200);
await expect(res.text()).resolves.toContain(podUrl);
const result = await register(baseUrl, user);
({ controls, authorization } = result);
expect(result.pod).toBe(podUrl);
});
it('can fetch the created pod in a subdomain.', async(): Promise<void> => {
@@ -113,7 +111,7 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
const res = await fetch(`${baseUrl}.acl`, {
headers: {
forwarded: `host=${podHost}`,
authorization: `WebID ${settings.webId}`,
authorization: `WebID ${user.webId}`,
},
});
expect(res.status).toBe(200);
@@ -123,7 +121,7 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
let res = await fetch(`${baseUrl}alice`, {
headers: {
forwarded: `host=${podHost}`,
authorization: `WebID ${settings.webId}`,
authorization: `WebID ${user.webId}`,
},
});
expect(res.status).toBe(404);
@@ -132,7 +130,7 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
method: 'PUT',
headers: {
forwarded: `host=${podHost}`,
authorization: `WebID ${settings.webId}`,
authorization: `WebID ${user.webId}`,
'content-type': 'text/plain',
},
body: 'this is new data!',
@@ -143,7 +141,7 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
res = await fetch(`${baseUrl}alice`, {
headers: {
forwarded: `host=${podHost}`,
authorization: `WebID ${settings.webId}`,
authorization: `WebID ${user.webId}`,
},
});
expect(res.status).toBe(200);
@@ -151,13 +149,12 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
});
it('should not be able to create a pod with the same name.', async(): Promise<void> => {
const newSettings = { ...settings, webId: 'http://test.com/#bob', email: 'bob@test.email' };
const res = await fetch(`${baseUrl}idp/register/`, {
const res = await fetch(controls.account.pod, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(newSettings),
headers: { authorization, 'content-type': 'application/json' },
body: JSON.stringify({ name: user.podName }),
});
expect(res.status).toBe(409);
expect(res.status).toBe(400);
await expect(res.text()).resolves.toContain(`There already is a resource at ${podUrl}`);
});
});

View File

@@ -11,9 +11,9 @@
"css:config/identity/access/public.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/no-accounts.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/disabled.json",
"css:config/ldp/authentication/debug-auth-header.json",
"css:config/ldp/authorization/acp.json",
"css:config/ldp/handler/default.json",

View File

@@ -11,9 +11,9 @@
"css:config/identity/access/public.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/no-accounts.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/disabled.json",
"css:config/ldp/authentication/debug-auth-header.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",

View File

@@ -11,9 +11,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",

View File

@@ -11,9 +11,9 @@
"css:config/identity/access/public.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/no-accounts.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/disabled.json",
"css:config/ldp/authentication/debug-auth-header.json",
"css:config/ldp/handler/default.json",

View File

@@ -11,9 +11,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/allow-all.json",
"css:config/ldp/handler/default.json",

View File

@@ -11,9 +11,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/allow-all.json",
"css:config/ldp/handler/default.json",

View File

@@ -11,10 +11,10 @@
"css:config/identity/access/restricted.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authentication/debug-auth-header.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
"css:config/ldp/metadata-parser/default.json",

View File

@@ -11,9 +11,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/default.json",
"css:config/identity/ownership/unsafe-no-check.json",
"css:config/identity/pod/dynamic.json",
"css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/debug-auth-header.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
@@ -29,7 +29,7 @@
"css:config/util/index/default.json",
"css:config/util/logging/winston.json",
"css:config/util/representation-conversion/default.json",
"css:config/util/resource-locker/memory.json",
"css:config/util/resource-locker/debug-void.json",
"css:config/util/variables/default.json"
],
"@graph": [

View File

@@ -11,9 +11,9 @@
"css:config/identity/access/public.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",

View File

@@ -11,9 +11,9 @@
"css:config/identity/access/public.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",

View File

@@ -11,9 +11,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",

View File

@@ -8,12 +8,12 @@
"css:config/http/notifications/disabled.json",
"css:config/http/server-factory/http.json",
"css:config/http/static/default.json",
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/no-accounts.json",
"css:config/identity/ownership/unsafe-no-check.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/disabled.json",
"css:config/ldp/authentication/debug-auth-header.json",
"css:config/ldp/authorization/allow-all.json",
"css:config/ldp/handler/default.json",
@@ -30,10 +30,7 @@
"css:config/util/logging/winston.json",
"css:config/util/representation-conversion/default.json",
"css:config/util/resource-locker/redis.json",
"css:config/util/variables/default.json",
"css:config/identity/handler/account-store/default.json",
"css:config/identity/ownership/unsafe-no-check.json"
"css:config/util/variables/default.json"
],
"@graph": [
{

View File

@@ -11,9 +11,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/default.json",
"css:config/identity/ownership/unsafe-no-check.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/debug-auth-header.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",

View File

@@ -8,12 +8,12 @@
"css:config/http/notifications/websockets.json",
"css:config/http/server-factory/http.json",
"css:config/http/static/default.json",
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/no-accounts.json",
"css:config/identity/ownership/unsafe-no-check.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/disabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/allow-all.json",
"css:config/ldp/handler/default.json",
@@ -30,9 +30,7 @@
"css:config/util/logging/winston.json",
"css:config/util/representation-conversion/default.json",
"css:config/util/resource-locker/memory.json",
"css:config/util/variables/default.json",
"css:config/identity/handler/account-store/default.json"
"css:config/util/variables/default.json"
],
"@graph": [
]

View File

@@ -11,9 +11,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/no-accounts.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/disabled.json",
"css:config/ldp/authentication/debug-auth-header.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",

View File

@@ -11,9 +11,9 @@
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
"css:config/identity/interaction/no-accounts.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/disabled.json",
"css:config/ldp/authentication/debug-auth-header.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",