diff --git a/config/identity/handler/adapter-factory/wrapped-fetch.json b/config/identity/handler/adapter-factory/webid.json similarity index 79% rename from config/identity/handler/adapter-factory/wrapped-fetch.json rename to config/identity/handler/adapter-factory/webid.json index 18b9796f9b..381b021150 100644 --- a/config/identity/handler/adapter-factory/wrapped-fetch.json +++ b/config/identity/handler/adapter-factory/webid.json @@ -4,12 +4,13 @@ { "comment": "An adapter is responsible for storing all interaction metadata.", "@id": "urn:solid-server:default:IdpAdapterFactory", - "@type": "WrappedFetchAdapterFactory", + "@type": "WebIdAdapterFactory", "source": { "@type": "ExpiringAdapterFactory", "args_storageName": "/idp/oidc", "args_storage": { "@id": "urn:solid-server:default:ExpiringIdpStorage" } - } + }, + "converter": { "@id": "urn:solid-server:default:RepresentationConverter" } } ] } diff --git a/config/identity/handler/default.json b/config/identity/handler/default.json index 3aa96994c4..f47063605c 100644 --- a/config/identity/handler/default.json +++ b/config/identity/handler/default.json @@ -1,7 +1,7 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld", "import": [ - "files-scs:config/identity/handler/adapter-factory/wrapped-fetch.json", + "files-scs:config/identity/handler/adapter-factory/webid.json", "files-scs:config/identity/handler/interaction/handler.json", "files-scs:config/identity/handler/key-value/resource-store.json", "files-scs:config/identity/handler/provider-factory/identity.json" diff --git a/config/identity/handler/provider-factory/identity.json b/config/identity/handler/provider-factory/identity.json index ede7553f83..66c54e2e9a 100644 --- a/config/identity/handler/provider-factory/identity.json +++ b/config/identity/handler/provider-factory/identity.json @@ -37,6 +37,7 @@ "AccessToken": "jwt" }, "scopes": [ "openid", "profile", "offline_access" ], + "subjectTypes": [ "public", "pairwise" ], "ttl": { "AccessToken": 3600, "AuthorizationCode": 600, diff --git a/package.json b/package.json index 2f91dfec29..bf30623f41 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "bcrypt": "^5.0.1", "componentsjs": "^4.3.0", "cors": "^2.8.5", + "cross-fetch": "^3.1.4", "ejs": "^3.1.6", "end-of-stream": "^1.4.4", "escape-string-regexp": "^4.0.0", @@ -150,7 +151,6 @@ "@typescript-eslint/parser": "^4.28.1", "cheerio": "^1.0.0-rc.10", "componentsjs-generator": "^2.4.0", - "cross-fetch": "^3.1.4", "eslint": "^7.29.0", "eslint-config-es": "^3.20.3", "eslint-import-resolver-typescript": "^2.4.0", diff --git a/src/identity/storage/WebIdAdapterFactory.ts b/src/identity/storage/WebIdAdapterFactory.ts new file mode 100644 index 0000000000..b618e3d8f6 --- /dev/null +++ b/src/identity/storage/WebIdAdapterFactory.ts @@ -0,0 +1,146 @@ +import type { Response } from 'cross-fetch'; +import { fetch } from 'cross-fetch'; +import { Store } from 'n3'; +import type { Adapter, AdapterPayload } from 'oidc-provider'; +import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation'; +import { getLoggerFor } from '../../logging/LogUtil'; +import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter'; +import { INTERNAL_QUADS } from '../../util/ContentTypes'; +import { createErrorMessage } from '../../util/errors/ErrorUtil'; +import type { AdapterFactory } from './AdapterFactory'; + +/* eslint-disable @typescript-eslint/naming-convention */ + +/** + * 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, + * this class will do an HTTP GET request to that WebId. + * If a valid `solid:oidcRegistration` triple is found there, + * that data will be returned instead. + */ +export class WebIdAdapter implements Adapter { + protected readonly logger = getLoggerFor(this); + + private readonly name: string; + private readonly source: Adapter; + private readonly converter: RepresentationConverter; + + public constructor(name: string, source: Adapter, converter: RepresentationConverter) { + this.name = name; + this.source = source; + this.converter = converter; + } + + public async upsert(id: string, payload: AdapterPayload, expiresIn: number): Promise { + return this.source.upsert(id, payload, expiresIn); + } + + public async find(id: string): Promise { + let payload = await this.source.find(id); + + // No payload is stored for the given Client ID. + // Try to see if valid client metadata is found at the given Client ID. + // The oidc-provider library will check if the redirect_uri matches an entry in the list of redirect_uris, + // so no extra checks are needed from our side. + if (!payload && this.name === 'Client' && /^https?:\/\/.+/u.test(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)) { + throw new Error(`SSL is required for client_id authentication unless working locally.`); + } + const response = await fetch(id); + if (response.status !== 200) { + throw new Error(`Unable to access data at ${id}: ${await response.text()}`); + } + const data = await response.text(); + let json: any | undefined; + try { + json = JSON.parse(data); + // We can only parse as simple JSON if the @context is correct + if (json['@context'] !== 'https://www.w3.org/ns/solid/oidc-context.jsonld') { + throw new Error('Invalid context'); + } + } catch (error: unknown) { + json = undefined; + this.logger.debug(`Found unexpected client WebID for ${id}: ${createErrorMessage(error)}`); + } + + if (json) { + // Need to make sure the document is about the id + if (json.client_id !== id) { + throw new Error('The client registration `client_id` field must match the client WebID'); + } + payload = json; + } else { + // Since the WebID does not match the default JSON-LD we try to interpret it as RDF + payload = await this.parseRdfWebId(data, id, response); + } + + // `token_endpoint_auth_method: 'none'` prevents oidc-provider from requiring a client_secret + payload = { ...payload, token_endpoint_auth_method: 'none' }; + } + + // Will also be returned if no valid client data was found above + return payload; + } + + private async parseRdfWebId(data: string, id: string, response: Response): Promise { + const contentType = response.headers.get('content-type'); + if (!contentType) { + throw new Error(`No content-type received for client WebID ${id}`); + } + + // Try to convert to quads + const representation = new BasicRepresentation(data, contentType); + const preferences = { type: { [INTERNAL_QUADS]: 1 }}; + const converted = await this.converter.handleSafe({ representation, identifier: { path: id }, preferences }); + const quads = new Store(); + const importer = quads.import(converted.data); + await new Promise((resolve, reject): void => { + importer.on('end', resolve); + importer.on('error', reject); + }); + + // Find the valid redirect uris + const match = quads.getObjects(id, 'http://www.w3.org/ns/solid/oidc#redirect_uris', null); + + return { + client_id: id, + redirect_uris: match.map((node): string => node.value), + }; + } + + public async findByUserCode(userCode: string): Promise { + return this.source.findByUserCode(userCode); + } + + public async findByUid(uid: string): Promise { + return this.source.findByUid(uid); + } + + public async destroy(id: string): Promise { + return this.source.destroy(id); + } + + public async revokeByGrantId(grantId: string): Promise { + return this.source.revokeByGrantId(grantId); + } + + public async consume(id: string): Promise { + return this.source.consume(id); + } +} + +export class WebIdAdapterFactory implements AdapterFactory { + private readonly source: AdapterFactory; + private readonly converter: RepresentationConverter; + + public constructor(source: AdapterFactory, converter: RepresentationConverter) { + this.source = source; + this.converter = converter; + } + + public createStorageAdapter(name: string): Adapter { + return new WebIdAdapter(name, this.source.createStorageAdapter(name), this.converter); + } +} diff --git a/src/identity/storage/WrappedFetchAdapterFactory.ts b/src/identity/storage/WrappedFetchAdapterFactory.ts deleted file mode 100644 index 7b5d3fc07e..0000000000 --- a/src/identity/storage/WrappedFetchAdapterFactory.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { DataFactory } from 'n3'; -import type { Adapter, AdapterPayload } from 'oidc-provider'; -import type { Dataset, Quad } from 'rdf-js'; -import { getLoggerFor } from '../../logging/LogUtil'; -import { fetchDataset } from '../../util/FetchUtil'; -import { SOLID } from '../../util/Vocabularies'; -import type { AdapterFactory } from './AdapterFactory'; -import namedNode = DataFactory.namedNode; - -/** - * 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, - * this class will do an HTTP GET request to that WebId. - * If a valid `solid:oidcRegistration` triple is found there, - * that data will be returned instead. - */ -export class WrappedFetchAdapter implements Adapter { - protected readonly logger = getLoggerFor(this); - - private readonly source: Adapter; - private readonly name: string; - - public constructor(name: string, source: Adapter) { - this.source = source; - this.name = name; - } - - public async upsert(id: string, payload: AdapterPayload, expiresIn: number): Promise { - return this.source.upsert(id, payload, expiresIn); - } - - public async find(id: string): Promise { - const payload = await this.source.find(id); - - // No payload is stored for the given WebId. - // Try to see if a solid:oidcRegistration triple is stored at the WebId that can be used instead. - if (!payload && this.name === 'Client') { - this.logger.debug(`Looking for payload data at ${id}`); - let dataset: Dataset; - try { - dataset = await fetchDataset(id); - } catch { - this.logger.debug(`Looking for payload data failed at ${id}`); - return payload; - } - - // Get the OIDC Registration JSON - const rawRegistrationJsonQuads = dataset.match(namedNode(id), SOLID.terms.oidcRegistration); - - // Check all the registrations to see if any are valid. - for (const rawRegistrationJsonQuad of rawRegistrationJsonQuads) { - try { - return this.validateRegistrationQuad(rawRegistrationJsonQuad, id); - } catch { - // Keep looking for a valid quad - } - } - this.logger.debug(`No payload data was found at ${id}`); - } - - // Will also be returned if no valid registration data was found above - return payload; - } - - public async findByUserCode(userCode: string): Promise { - return this.source.findByUserCode(userCode); - } - - public async findByUid(uid: string): Promise { - return this.source.findByUid(uid); - } - - public async destroy(id: string): Promise { - return this.source.destroy(id); - } - - public async revokeByGrantId(grantId: string): Promise { - return this.source.revokeByGrantId(grantId); - } - - public async consume(id: string): Promise { - return this.source.consume(id); - } - - /** - * Validates if the quad object contains valid JSON with the required client_id. - * In case of success, the AdapterPayload will be returned, otherwise an error will be thrown. - */ - private validateRegistrationQuad(quad: Quad, id: string): AdapterPayload { - const rawRegistrationJson = quad.object.value; - let registrationJson; - try { - registrationJson = JSON.parse(rawRegistrationJson); - } catch { - throw new Error('Could not parse registration JSON'); - } - - // Ensure the registration JSON matches the client WebId - if (id !== registrationJson.client_id) { - throw new Error('The client registration `client_id` field must match the Client WebId'); - } - return { - ...registrationJson, - // Snake case is required for tokens - // eslint-disable-next-line @typescript-eslint/naming-convention - token_endpoint_auth_method: 'none', - }; - } -} - -export class WrappedFetchAdapterFactory implements AdapterFactory { - private readonly source: AdapterFactory; - - public constructor(source: AdapterFactory) { - this.source = source; - } - - public createStorageAdapter(name: string): Adapter { - return new WrappedFetchAdapter(name, this.source.createStorageAdapter(name)); - } -} diff --git a/src/index.ts b/src/index.ts index 30c5106497..aa95300a2c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -54,7 +54,7 @@ export * from './identity/ownership/TokenOwnershipValidator'; // Identity/Storage export * from './identity/storage/AdapterFactory'; export * from './identity/storage/ExpiringAdapterFactory'; -export * from './identity/storage/WrappedFetchAdapterFactory'; +export * from './identity/storage/WebIdAdapterFactory'; // Identity export * from './identity/IdentityProviderHttpHandler'; diff --git a/test/unit/identity/storage/WebIdAdapterFactory.test.ts b/test/unit/identity/storage/WebIdAdapterFactory.test.ts new file mode 100644 index 0000000000..3a70c6fd5a --- /dev/null +++ b/test/unit/identity/storage/WebIdAdapterFactory.test.ts @@ -0,0 +1,166 @@ +import fetch from 'cross-fetch'; +import type { Adapter } from 'oidc-provider'; +import type { AdapterFactory } from '../../../../src/identity/storage/AdapterFactory'; +import { WebIdAdapterFactory } from '../../../../src/identity/storage/WebIdAdapterFactory'; +import { RdfToQuadConverter } from '../../../../src/storage/conversion/RdfToQuadConverter'; + +jest.mock('cross-fetch'); + +/* eslint-disable @typescript-eslint/naming-convention */ +describe('A WebIdAdapterFactory', (): void => { + const fetchMock: jest.Mock = fetch as any; + const id = 'https://app.test.com/card#me'; + let data: string; + let json: any; + let rdf: string; + let source: Adapter; + let sourceFactory: AdapterFactory; + let adapter: Adapter; + const converter = new RdfToQuadConverter(); + let factory: WebIdAdapterFactory; + + beforeEach(async(): Promise => { + json = { + '@context': 'https://www.w3.org/ns/solid/oidc-context.jsonld', + + client_id: id, + client_name: 'Solid Application Name', + redirect_uris: [ 'http://test.com/' ], + scope: 'openid profile offline_access', + grant_types: [ 'refresh_token', 'authorization_code' ], + response_types: [ 'code' ], + default_max_age: 3600, + require_auth_time: true, + }; + rdf = `<${id}> .`; + + fetchMock.mockReturnValue({ text: (): any => data }); + + source = { + upsert: jest.fn(), + find: jest.fn(), + findByUserCode: jest.fn(), + findByUid: jest.fn(), + destroy: jest.fn(), + revokeByGrantId: jest.fn(), + consume: jest.fn(), + }; + + sourceFactory = { + createStorageAdapter: jest.fn().mockReturnValue(source), + }; + + factory = new WebIdAdapterFactory(sourceFactory, converter); + adapter = factory.createStorageAdapter('Client'); + }); + + it('passes the call to the source for upsert.', async(): Promise => { + await expect(adapter.upsert('id', 'payload' as any, 5)).resolves.toBeUndefined(); + expect(source.upsert).toHaveBeenCalledTimes(1); + expect(source.upsert).toHaveBeenLastCalledWith('id', 'payload' as any, 5); + }); + + it('passes the call to the source for findByUserCode.', async(): Promise => { + await expect(adapter.findByUserCode('userCode')).resolves.toBeUndefined(); + expect(source.findByUserCode).toHaveBeenCalledTimes(1); + expect(source.findByUserCode).toHaveBeenLastCalledWith('userCode'); + }); + + it('passes the call to the source for findByUid.', async(): Promise => { + await expect(adapter.findByUid('uid')).resolves.toBeUndefined(); + expect(source.findByUid).toHaveBeenCalledTimes(1); + expect(source.findByUid).toHaveBeenLastCalledWith('uid'); + }); + + it('passes the call to the source for destroy.', async(): Promise => { + await expect(adapter.destroy('id')).resolves.toBeUndefined(); + expect(source.destroy).toHaveBeenCalledTimes(1); + expect(source.destroy).toHaveBeenLastCalledWith('id'); + }); + + it('passes the call to the source for revokeByGrantId.', async(): Promise => { + await expect(adapter.revokeByGrantId('grantId')).resolves.toBeUndefined(); + expect(source.revokeByGrantId).toHaveBeenCalledTimes(1); + expect(source.revokeByGrantId).toHaveBeenLastCalledWith('grantId'); + }); + + it('passes the call to the source for consume.', async(): Promise => { + await expect(adapter.consume('id')).resolves.toBeUndefined(); + expect(source.consume).toHaveBeenCalledTimes(1); + expect(source.consume).toHaveBeenLastCalledWith('id'); + }); + + it('returns the source payload if there is one.', async(): Promise => { + (source.find as jest.Mock).mockResolvedValueOnce('payload!'); + await expect(adapter.find(id)).resolves.toBe('payload!'); + }); + + it('returns undefined if this is not a Client Adapter and there is no source payload.', async(): Promise => { + adapter = factory.createStorageAdapter('NotClient'); + await expect(adapter.find(id)).resolves.toBeUndefined(); + }); + + it('returns undefined if the client ID is not a URL.', async(): Promise => { + await expect(adapter.find('noUrl')).resolves.toBeUndefined(); + }); + + it('errors if the client ID is unsecure.', async(): Promise => { + await expect(adapter.find('http://unsecure')).rejects + .toThrow('SSL is required for client_id authentication unless working locally.'); + }); + + it('errors if the client ID requests does not respond with 200.', async(): Promise => { + fetchMock.mockResolvedValueOnce({ status: 400, text: (): string => 'error' }); + await expect(adapter.find(id)).rejects.toThrow(`Unable to access data at ${id}: error`); + }); + + it('can handle a valid JSON-LD response.', async(): Promise => { + fetchMock.mockResolvedValueOnce({ status: 200, text: (): string => JSON.stringify(json) }); + await expect(adapter.find(id)).resolves.toEqual({ + ...json, + token_endpoint_auth_method: 'none', + }); + }); + + it('errors if there is a client_id mismatch.', async(): Promise => { + json.client_id = 'someone else'; + fetchMock.mockResolvedValueOnce({ status: 200, text: (): string => JSON.stringify(json) }); + await expect(adapter.find(id)).rejects + .toThrow('The client registration `client_id` field must match the client WebID'); + }); + + it('can handle a valid RDF response.', async(): Promise => { + fetchMock.mockResolvedValueOnce( + { status: 200, text: (): string => rdf, headers: { get: (): any => 'text/turtle' }}, + ); + await expect(adapter.find(id)).resolves.toEqual({ + client_id: id, + redirect_uris: [ 'http://test.com' ], + token_endpoint_auth_method: 'none', + }); + }); + + it('falls back to RDF parsing if no valid context was found.', async(): Promise => { + json = { + '@id': 'https://app.test.com/card#me', + 'http://www.w3.org/ns/solid/oidc#redirect_uris': { '@id': 'http://test.com' }, + 'http://randomField': { '@value': 'this will not be there since RDF parsing only takes preset fields' }, + }; + fetchMock.mockResolvedValueOnce( + { status: 200, text: (): string => JSON.stringify(json), headers: { get: (): any => 'application/ld+json' }}, + ); + await expect(adapter.find(id)).resolves.toEqual({ + client_id: id, + redirect_uris: [ 'http://test.com' ], + token_endpoint_auth_method: 'none', + }); + }); + + it('errors if there is no content-type.', async(): Promise => { + fetchMock.mockResolvedValueOnce( + { status: 200, text: (): string => rdf, headers: { get: jest.fn() }}, + ); + await expect(adapter.find(id)).rejects + .toThrow(`No content-type received for client WebID ${id}`); + }); +}); diff --git a/test/unit/identity/storage/WrappedFetchAdapterFactory.test.ts b/test/unit/identity/storage/WrappedFetchAdapterFactory.test.ts deleted file mode 100644 index db5ce82110..0000000000 --- a/test/unit/identity/storage/WrappedFetchAdapterFactory.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { literal, namedNode, quad } from '@rdfjs/data-model'; -import fetch from '@rdfjs/fetch'; -import type { DatasetResponse } from '@rdfjs/fetch-lite'; -import type { Adapter } from 'oidc-provider'; -import type { Dataset, Quad, Term } from 'rdf-js'; -import type { AdapterFactory } from '../../../../src/identity/storage/AdapterFactory'; -import { WrappedFetchAdapterFactory } from '../../../../src/identity/storage/WrappedFetchAdapterFactory'; -import { SOLID } from '../../../../src/util/Vocabularies'; - -jest.mock('@rdfjs/fetch'); - -describe('A WrappedFetchAdapterFactory', (): void => { - const fetchMock: jest.Mock = fetch as any; - let triples: Quad[]; - const id = 'http://alice.test.com/card#me'; - let source: Adapter; - let sourceFactory: AdapterFactory; - let adapter: Adapter; - let factory: WrappedFetchAdapterFactory; - - beforeEach(async(): Promise => { - triples = []; - - const dataset: Dataset = { - match: (subject: Term, predicate: Term): Quad[] => triples.filter((triple): boolean => - triple.subject.equals(subject) && triple.predicate.equals(predicate)), - } as any; - - const rawResponse: DatasetResponse = { - dataset: async(): Promise => dataset, - } as any; - - fetchMock.mockReturnValue(rawResponse); - - source = { - upsert: jest.fn(), - find: jest.fn(), - findByUserCode: jest.fn(), - findByUid: jest.fn(), - destroy: jest.fn(), - revokeByGrantId: jest.fn(), - consume: jest.fn(), - }; - - sourceFactory = { - createStorageAdapter: jest.fn().mockReturnValue(source), - }; - - factory = new WrappedFetchAdapterFactory(sourceFactory); - adapter = factory.createStorageAdapter('Client'); - }); - - it('passes the call to the source for upsert.', async(): Promise => { - await expect(adapter.upsert('id', 'payload' as any, 5)).resolves.toBeUndefined(); - expect(source.upsert).toHaveBeenCalledTimes(1); - expect(source.upsert).toHaveBeenLastCalledWith('id', 'payload' as any, 5); - }); - - it('passes the call to the source for findByUserCode.', async(): Promise => { - await expect(adapter.findByUserCode('userCode')).resolves.toBeUndefined(); - expect(source.findByUserCode).toHaveBeenCalledTimes(1); - expect(source.findByUserCode).toHaveBeenLastCalledWith('userCode'); - }); - - it('passes the call to the source for findByUid.', async(): Promise => { - await expect(adapter.findByUid('uid')).resolves.toBeUndefined(); - expect(source.findByUid).toHaveBeenCalledTimes(1); - expect(source.findByUid).toHaveBeenLastCalledWith('uid'); - }); - - it('passes the call to the source for destroy.', async(): Promise => { - await expect(adapter.destroy('id')).resolves.toBeUndefined(); - expect(source.destroy).toHaveBeenCalledTimes(1); - expect(source.destroy).toHaveBeenLastCalledWith('id'); - }); - - it('passes the call to the source for revokeByGrantId.', async(): Promise => { - await expect(adapter.revokeByGrantId('grantId')).resolves.toBeUndefined(); - expect(source.revokeByGrantId).toHaveBeenCalledTimes(1); - expect(source.revokeByGrantId).toHaveBeenLastCalledWith('grantId'); - }); - - it('passes the call to the source for consume.', async(): Promise => { - await expect(adapter.consume('id')).resolves.toBeUndefined(); - expect(source.consume).toHaveBeenCalledTimes(1); - expect(source.consume).toHaveBeenLastCalledWith('id'); - }); - - it('returns the source find payload if there is one.', async(): Promise => { - (source.find as jest.Mock).mockResolvedValueOnce('payload!'); - await expect(adapter.find(id)).resolves.toBe('payload!'); - }); - - it('returns undefined if this is not a Client Adapter and there is no source payload.', async(): Promise => { - adapter = factory.createStorageAdapter('NotClient'); - await expect(adapter.find(id)).resolves.toBeUndefined(); - }); - - it('returns undefined if there was a problem accessing the id.', async(): Promise => { - fetchMock.mockRejectedValueOnce(new Error('bad data!')); - await expect(adapter.find(id)).resolves.toBeUndefined(); - }); - - it('returns undefined if there are no solid:oidcRegistration triples.', async(): Promise => { - triples = [ - quad(namedNode(id), namedNode('irrelevant'), literal('value')), - ]; - await expect(adapter.find(id)).resolves.toBeUndefined(); - }); - - it('returns undefined if there are no valid solid:oidcRegistration triples.', async(): Promise => { - triples = [ - quad(namedNode(id), namedNode('irrelevant'), literal('value')), - quad(namedNode(id), SOLID.terms.oidcRegistration, literal('}{')), - ]; - await expect(adapter.find(id)).resolves.toBeUndefined(); - }); - - it('returns undefined if there are no matching solid:oidcRegistration triples.', async(): Promise => { - triples = [ - quad(namedNode(id), namedNode('irrelevant'), literal('value')), - quad(namedNode(id), SOLID.terms.oidcRegistration, literal('}{')), - quad(namedNode(id), SOLID.terms.oidcRegistration, literal('{ "client_id": "invalid_id" }')), - ]; - await expect(adapter.find(id)).resolves.toBeUndefined(); - }); - - it('returns a new payload if there is a registration match.', async(): Promise => { - triples = [ - quad(namedNode(id), namedNode('irrelevant'), literal('value')), - quad(namedNode(id), SOLID.terms.oidcRegistration, literal('}{')), - quad(namedNode(id), SOLID.terms.oidcRegistration, literal('{ "client_id": "invalid_id" }')), - quad(namedNode(id), SOLID.terms.oidcRegistration, literal(`{ "client_id": "${id}" }`)), - ]; - - /* eslint-disable @typescript-eslint/naming-convention */ - await expect(adapter.find(id)).resolves.toEqual({ - client_id: id, - token_endpoint_auth_method: 'none', - }); - }); -});