refactor: Rename WebIdAdapterFactory to ClientIdAdapterFactory

This commit is contained in:
Joachim Van Herwegen 2023-10-11 13:01:36 +02:00
parent 7a44581406
commit 607c04ff28
6 changed files with 35 additions and 33 deletions

View File

@ -85,6 +85,7 @@ These changes are relevant if you wrote custom modules for the server that depen
- All classes related to setup have been removed. - All classes related to setup have been removed.
- The `StaticAssetHandler` has bene updated to support the new functionality. - The `StaticAssetHandler` has bene updated to support the new functionality.
- `SeededPodInitializer` has been renamed to `SeededAccountInitializer`. - `SeededPodInitializer` has been renamed to `SeededAccountInitializer`.
- `WebIdAdapterFactory` has been renamed to `ClientIdAdapterFactory`.
## v6.1.0 ## v6.1.0

View File

@ -8,7 +8,7 @@
"webIdStore": { "@id": "urn:solid-server:default:WebIdStore" }, "webIdStore": { "@id": "urn:solid-server:default:WebIdStore" },
"clientCredentialsStore": { "@id": "urn:solid-server:default:ClientCredentialsStore" }, "clientCredentialsStore": { "@id": "urn:solid-server:default:ClientCredentialsStore" },
"source": { "source": {
"@type": "WebIdAdapterFactory", "@type": "ClientIdAdapterFactory",
"converter": { "@id": "urn:solid-server:default:RepresentationConverter" }, "converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
"source": { "source": {
"@type": "ExpiringAdapterFactory", "@type": "ExpiringAdapterFactory",

View File

@ -1,7 +1,7 @@
{ {
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld", "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [ "import": [
"css:config/identity/handler/adapter-factory/webid.json", "css:config/identity/handler/adapter-factory/default.json",
"css:config/identity/handler/jwks/default.json", "css:config/identity/handler/jwks/default.json",
"css:config/identity/handler/provider-factory/identity.json", "css:config/identity/handler/provider-factory/identity.json",
"css:config/identity/handler/storage/default.json" "css:config/identity/handler/storage/default.json"

View File

@ -11,16 +11,15 @@ import { OIDC } from '../../util/Vocabularies';
import type { AdapterFactory } from './AdapterFactory'; import type { AdapterFactory } from './AdapterFactory';
import { PassthroughAdapter, PassthroughAdapterFactory } from './PassthroughAdapterFactory'; import { PassthroughAdapter, PassthroughAdapterFactory } from './PassthroughAdapterFactory';
/* eslint-disable @typescript-eslint/naming-convention */
/** /**
* This {@link Adapter} redirects the `find` call to its source adapter. * This {@link Adapter} redirects the `find` call to its source adapter.
* In case no client data was found in the source for the given WebId, * In case no client data was found in the source for the given Client ID,
* this class will do an HTTP GET request to that WebId. * this class will do an HTTP GET request to that Client ID.
* If a valid `solid:oidcRegistration` triple is found there, * If the result is a valid Client ID document, that will be returned instead.
* that data will be returned instead. *
* See https://solidproject.org/TR/2022/oidc-20220328#clientids-document.
*/ */
export class WebIdAdapter extends PassthroughAdapter { export class ClientIdAdapter extends PassthroughAdapter {
protected readonly logger = getLoggerFor(this); protected readonly logger = getLoggerFor(this);
private readonly converter: RepresentationConverter; private readonly converter: RepresentationConverter;
@ -39,7 +38,6 @@ export class WebIdAdapter extends PassthroughAdapter {
// so no extra checks are needed from our side. // so no extra checks are needed from our side.
if (!payload && this.name === 'Client' && hasScheme(id, 'http', 'https')) { if (!payload && this.name === 'Client' && hasScheme(id, 'http', 'https')) {
this.logger.debug(`Looking for payload data at ${id}`); this.logger.debug(`Looking for payload data at ${id}`);
// All checks based on https://solid.github.io/authentication-panel/solid-oidc/#clientids-webid
if (!/^https:|^http:\/\/localhost(?::\d+)?(?:\/|$)/u.test(id)) { if (!/^https:|^http:\/\/localhost(?::\d+)?(?:\/|$)/u.test(id)) {
throw new Error(`SSL is required for client_id authentication unless working locally.`); throw new Error(`SSL is required for client_id authentication unless working locally.`);
} }
@ -58,21 +56,22 @@ export class WebIdAdapter extends PassthroughAdapter {
} }
} catch (error: unknown) { } catch (error: unknown) {
json = undefined; json = undefined;
this.logger.debug(`Found unexpected client WebID for ${id}: ${createErrorMessage(error)}`); this.logger.debug(`Found unexpected client ID for ${id}: ${createErrorMessage(error)}`);
} }
if (json) { if (json) {
// Need to make sure the document is about the id // Need to make sure the document is about the id
if (json.client_id !== id) { if (json.client_id !== id) {
throw new Error('The client registration `client_id` field must match the client WebID'); throw new Error('The client registration `client_id` field must match the client ID');
} }
payload = json; payload = json;
} else { } else {
// Since the WebID does not match the default JSON-LD we try to interpret it as RDF // Since the client ID does not match the default JSON-LD we try to interpret it as RDF
payload = await this.parseRdfWebId(data, id, response); payload = await this.parseRdfClientId(data, id, response);
} }
// `token_endpoint_auth_method: 'none'` prevents oidc-provider from requiring a client_secret // `token_endpoint_auth_method: 'none'` prevents oidc-provider from requiring a client_secret
// eslint-disable-next-line @typescript-eslint/naming-convention
payload = { ...payload, token_endpoint_auth_method: 'none' }; payload = { ...payload, token_endpoint_auth_method: 'none' };
} }
@ -81,12 +80,12 @@ export class WebIdAdapter extends PassthroughAdapter {
} }
/** /**
* Parses RDF data found at a client WebID. * Parses RDF data found at a Client ID.
* @param data - Raw data from the WebID. * @param data - Raw data from the Client ID.
* @param id - The actual WebID. * @param id - The actual Client ID.
* @param response - Response object from the request. * @param response - Response object from the request.
*/ */
private async parseRdfWebId(data: string, id: string, response: Response): Promise<AdapterPayload> { private async parseRdfClientId(data: string, id: string, response: Response): Promise<AdapterPayload> {
const representation = await responseToDataset(response, this.converter, data); const representation = await responseToDataset(response, this.converter, data);
// Find the valid redirect URIs // Find the valid redirect URIs
@ -98,14 +97,16 @@ export class WebIdAdapter extends PassthroughAdapter {
} }
} }
/* eslint-disable @typescript-eslint/naming-convention */
return { return {
client_id: id, client_id: id,
redirect_uris: redirectUris, redirect_uris: redirectUris,
}; };
/* eslint-enable @typescript-eslint/naming-convention */
} }
} }
export class WebIdAdapterFactory extends PassthroughAdapterFactory { export class ClientIdAdapterFactory extends PassthroughAdapterFactory {
private readonly converter: RepresentationConverter; private readonly converter: RepresentationConverter;
public constructor(source: AdapterFactory, converter: RepresentationConverter) { public constructor(source: AdapterFactory, converter: RepresentationConverter) {
@ -114,6 +115,6 @@ export class WebIdAdapterFactory extends PassthroughAdapterFactory {
} }
public createStorageAdapter(name: string): Adapter { public createStorageAdapter(name: string): Adapter {
return new WebIdAdapter(name, this.source.createStorageAdapter(name), this.converter); return new ClientIdAdapter(name, this.source.createStorageAdapter(name), this.converter);
} }
} }

View File

@ -253,9 +253,9 @@ export * from './identity/ownership/TokenOwnershipValidator';
// Identity/Storage // Identity/Storage
export * from './identity/storage/AdapterFactory'; export * from './identity/storage/AdapterFactory';
export * from './identity/storage/ClientIdAdapterFactory';
export * from './identity/storage/ExpiringAdapterFactory'; export * from './identity/storage/ExpiringAdapterFactory';
export * from './identity/storage/PassthroughAdapterFactory'; export * from './identity/storage/PassthroughAdapterFactory';
export * from './identity/storage/WebIdAdapterFactory';
// Identity // Identity
export * from './identity/AccountInitializer'; export * from './identity/AccountInitializer';

View File

@ -1,22 +1,22 @@
import fetch from 'cross-fetch'; import fetch from 'cross-fetch';
import type { AdapterFactory } from '../../../../src/identity/storage/AdapterFactory'; import type { AdapterFactory } from '../../../../src/identity/storage/AdapterFactory';
import { WebIdAdapterFactory } from '../../../../src/identity/storage/WebIdAdapterFactory'; import { ClientIdAdapterFactory } from '../../../../src/identity/storage/ClientIdAdapterFactory';
import { RdfToQuadConverter } from '../../../../src/storage/conversion/RdfToQuadConverter'; import { RdfToQuadConverter } from '../../../../src/storage/conversion/RdfToQuadConverter';
import type { Adapter } from '../../../../templates/types/oidc-provider'; import type { Adapter } from '../../../../templates/types/oidc-provider';
jest.mock('cross-fetch'); jest.mock('cross-fetch');
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
describe('A WebIdAdapterFactory', (): void => { describe('A ClientIdAdapterFactory', (): void => {
const fetchMock: jest.Mock = fetch as any; const fetchMock: jest.Mock = fetch as any;
const id = 'https://app.test.com/card#me'; const id = 'https://app.example.com/card#me';
let json: any; let json: any;
let rdf: string; let rdf: string;
let source: Adapter; let source: Adapter;
let sourceFactory: AdapterFactory; let sourceFactory: AdapterFactory;
let adapter: Adapter; let adapter: Adapter;
const converter = new RdfToQuadConverter(); const converter = new RdfToQuadConverter();
let factory: WebIdAdapterFactory; let factory: ClientIdAdapterFactory;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
json = { json = {
@ -24,14 +24,14 @@ describe('A WebIdAdapterFactory', (): void => {
client_id: id, client_id: id,
client_name: 'Solid Application Name', client_name: 'Solid Application Name',
redirect_uris: [ 'http://test.com/' ], redirect_uris: [ 'http://example.com/' ],
scope: 'openid profile offline_access', scope: 'openid profile offline_access',
grant_types: [ 'refresh_token', 'authorization_code' ], grant_types: [ 'refresh_token', 'authorization_code' ],
response_types: [ 'code' ], response_types: [ 'code' ],
default_max_age: 3600, default_max_age: 3600,
require_auth_time: true, require_auth_time: true,
}; };
rdf = `<${id}> <http://www.w3.org/ns/solid/oidc#redirect_uris> <http://test.com>.`; rdf = `<${id}> <http://www.w3.org/ns/solid/oidc#redirect_uris> <http://example.com>.`;
fetchMock.mockImplementation((url: string): any => ({ text: (): any => '', url, status: 200 })); fetchMock.mockImplementation((url: string): any => ({ text: (): any => '', url, status: 200 }));
@ -49,7 +49,7 @@ describe('A WebIdAdapterFactory', (): void => {
createStorageAdapter: jest.fn().mockReturnValue(source), createStorageAdapter: jest.fn().mockReturnValue(source),
}; };
factory = new WebIdAdapterFactory(sourceFactory, converter); factory = new ClientIdAdapterFactory(sourceFactory, converter);
adapter = factory.createStorageAdapter('Client'); adapter = factory.createStorageAdapter('Client');
}); });
@ -98,7 +98,7 @@ describe('A WebIdAdapterFactory', (): void => {
json.client_id = 'someone else'; json.client_id = 'someone else';
fetchMock.mockResolvedValueOnce({ url: id, status: 200, text: (): string => JSON.stringify(json) }); fetchMock.mockResolvedValueOnce({ url: id, status: 200, text: (): string => JSON.stringify(json) });
await expect(adapter.find(id)).rejects await expect(adapter.find(id)).rejects
.toThrow('The client registration `client_id` field must match the client WebID'); .toThrow('The client registration `client_id` field must match the client ID');
}); });
it('can handle a valid RDF response.', async(): Promise<void> => { it('can handle a valid RDF response.', async(): Promise<void> => {
@ -107,15 +107,15 @@ describe('A WebIdAdapterFactory', (): void => {
); );
await expect(adapter.find(id)).resolves.toEqual({ await expect(adapter.find(id)).resolves.toEqual({
client_id: id, client_id: id,
redirect_uris: [ 'http://test.com' ], redirect_uris: [ 'http://example.com' ],
token_endpoint_auth_method: 'none', token_endpoint_auth_method: 'none',
}); });
}); });
it('falls back to RDF parsing if no valid context was found.', async(): Promise<void> => { it('falls back to RDF parsing if no valid context was found.', async(): Promise<void> => {
json = { json = {
'@id': 'https://app.test.com/card#me', '@id': 'https://app.example.com/card#me',
'http://www.w3.org/ns/solid/oidc#redirect_uris': { '@id': 'http://test.com' }, 'http://www.w3.org/ns/solid/oidc#redirect_uris': { '@id': 'http://example.com' },
'http://randomField': { '@value': 'this will not be there since RDF parsing only takes preset fields' }, 'http://randomField': { '@value': 'this will not be there since RDF parsing only takes preset fields' },
}; };
fetchMock.mockResolvedValueOnce( fetchMock.mockResolvedValueOnce(
@ -126,7 +126,7 @@ describe('A WebIdAdapterFactory', (): void => {
); );
await expect(adapter.find(id)).resolves.toEqual({ await expect(adapter.find(id)).resolves.toEqual({
client_id: id, client_id: id,
redirect_uris: [ 'http://test.com' ], redirect_uris: [ 'http://example.com' ],
token_endpoint_auth_method: 'none', token_endpoint_auth_method: 'none',
}); });
}); });