diff --git a/config/ldp/authentication/debug-auth-header.json b/config/ldp/authentication/debug-auth-header.json index b7a6cd387..553143c4a 100644 --- a/config/ldp/authentication/debug-auth-header.json +++ b/config/ldp/authentication/debug-auth-header.json @@ -7,8 +7,8 @@ "Supports authentication headers such as `Authentication: WebID http://test.com/card#me`" ], "@id": "urn:solid-server:default:CredentialsExtractor", - "@type": "WaterfallHandler", - "handlers": [ + "@type": "UnionCredentialsExtractor", + "extractors": [ { "@type": "UnsecureWebIdExtractor" }, { "@type": "EmptyCredentialsExtractor" } ] diff --git a/config/ldp/authentication/debug-test-agent.json b/config/ldp/authentication/debug-test-agent.json index f7a79b33a..798e78b52 100644 --- a/config/ldp/authentication/debug-test-agent.json +++ b/config/ldp/authentication/debug-test-agent.json @@ -7,8 +7,14 @@ "This extractor always sets the credentials to the fixed value." ], "@id": "urn:solid-server:default:CredentialsExtractor", - "@type": "UnsecureConstantCredentialsExtractor", - "agent": "http://test.com/card#me" + "@type": "UnionCredentialsExtractor", + "extractors": [ + { + "@type": "UnsecureConstantCredentialsExtractor", + "agent": "http://test.com/card#me" + }, + { "@type": "EmptyCredentialsExtractor" } + ] } ] } diff --git a/config/ldp/authentication/dpop-bearer.json b/config/ldp/authentication/dpop-bearer.json index 8a1182c9b..c7df44cdc 100644 --- a/config/ldp/authentication/dpop-bearer.json +++ b/config/ldp/authentication/dpop-bearer.json @@ -4,15 +4,20 @@ { "comment": "Supports DPoP and Bearer access tokens, or no credentials.", "@id": "urn:solid-server:default:CredentialsExtractor", - "@type": "WaterfallHandler", - "handlers": [ + "@type": "UnionCredentialsExtractor", + "extractors": [ { - "@type": "DPoPWebIdExtractor", - "originalUrlExtractor": { - "@type": "OriginalUrlExtractor" - } + "@type": "WaterfallHandler", + "handlers": [ + { + "@type": "DPoPWebIdExtractor", + "originalUrlExtractor": { + "@type": "OriginalUrlExtractor" + } + }, + { "@type": "BearerWebIdExtractor" } + ] }, - { "@type": "BearerWebIdExtractor" }, { "@type": "EmptyCredentialsExtractor" } ] } diff --git a/src/authentication/UnionCredentialsExtractor.ts b/src/authentication/UnionCredentialsExtractor.ts new file mode 100644 index 000000000..02fc14121 --- /dev/null +++ b/src/authentication/UnionCredentialsExtractor.ts @@ -0,0 +1,27 @@ +import { UnionHandler } from '../util/handlers/UnionHandler'; +import type { CredentialGroup, Credential, CredentialSet } from './Credentials'; + +import type { CredentialsExtractor } from './CredentialsExtractor'; + +/** + * Combines the results of several CredentialsExtractors into one. + * If multiple of these extractors return a value for the same key, + * the last result will be used. + */ +export class UnionCredentialsExtractor extends UnionHandler { + public constructor(extractors: CredentialsExtractor[]) { + super(extractors); + } + + public async combine(results: CredentialSet[]): Promise { + // Combine all the results into a single object + return results.reduce((result, credential): CredentialSet => { + for (const [ key, value ] of Object.entries(credential) as [ CredentialGroup, Credential ][]) { + if (value) { + result[key] = value; + } + } + return result; + }, {}); + } +} diff --git a/src/index.ts b/src/index.ts index 568acde88..424a1bc64 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export * from './authentication/Credentials'; export * from './authentication/CredentialsExtractor'; export * from './authentication/DPoPWebIdExtractor'; export * from './authentication/EmptyCredentialsExtractor'; +export * from './authentication/UnionCredentialsExtractor'; export * from './authentication/UnsecureConstantCredentialsExtractor'; export * from './authentication/UnsecureWebIdExtractor'; diff --git a/src/util/handlers/HandlerUtil.ts b/src/util/handlers/HandlerUtil.ts index 3be746a0c..efb61ea52 100644 --- a/src/util/handlers/HandlerUtil.ts +++ b/src/util/handlers/HandlerUtil.ts @@ -74,7 +74,7 @@ Promise[]> { await handler.canHandle(input); return handler; })); - const matches = results.filter((result): boolean => result.status === 'fulfilled') + const matches = results.filter(({ status }): boolean => status === 'fulfilled') .map((result): AsyncHandler => (result as PromiseFulfilledResult>).value); diff --git a/test/unit/authentication/UnionCredentialsExtractor.test.ts b/test/unit/authentication/UnionCredentialsExtractor.test.ts new file mode 100644 index 000000000..30f180c74 --- /dev/null +++ b/test/unit/authentication/UnionCredentialsExtractor.test.ts @@ -0,0 +1,46 @@ +import { CredentialGroup } from '../../../src/authentication/Credentials'; +import type { Credentials } from '../../../src/authentication/Credentials'; +import type { CredentialsExtractor } from '../../../src/authentication/CredentialsExtractor'; +import { UnionCredentialsExtractor } from '../../../src/authentication/UnionCredentialsExtractor'; +import type { HttpRequest } from '../../../src/server/HttpRequest'; + +describe('A UnionCredentialsExtractor', (): void => { + const agent: Credentials = { [CredentialGroup.agent]: { webId: 'http://test.com/#me' }}; + const everyone: Credentials = { [CredentialGroup.public]: {}}; + const request: HttpRequest = {} as any; + let extractors: jest.Mocked[]; + let extractor: UnionCredentialsExtractor; + + beforeEach(async(): Promise => { + extractors = [ + { + canHandle: jest.fn(), + handle: jest.fn().mockResolvedValue(agent), + } as any, + { + canHandle: jest.fn(), + handle: jest.fn().mockResolvedValue(everyone), + } as any, + ]; + + extractor = new UnionCredentialsExtractor(extractors); + }); + + it('combines the results of the extractors.', async(): Promise => { + await expect(extractor.handle(request)).resolves.toEqual({ + [CredentialGroup.agent]: agent.agent, + [CredentialGroup.public]: {}, + }); + }); + + it('ignores undefined values.', async(): Promise => { + extractors[1].handle.mockResolvedValueOnce({ + [CredentialGroup.public]: {}, + [CredentialGroup.agent]: undefined, + }); + await expect(extractor.handle(request)).resolves.toEqual({ + [CredentialGroup.agent]: agent.agent, + [CredentialGroup.public]: {}, + }); + }); +});