diff --git a/.eslintrc.js b/.eslintrc.js index 5a80fbcce..090bc1134 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,6 +41,7 @@ module.exports = { '@typescript-eslint/no-invalid-void-type': 'off', // Problems with optional parameters '@typescript-eslint/no-unnecessary-condition': 'off', + '@typescript-eslint/prefer-optional-chain': 'error', '@typescript-eslint/space-before-function-paren': [ 'error', 'never' ], '@typescript-eslint/unbound-method': 'off', '@typescript-eslint/unified-signatures': 'off', diff --git a/config/ldp/metadata-writer/default.json b/config/ldp/metadata-writer/default.json index 93a1be725..98f2d0e6c 100644 --- a/config/ldp/metadata-writer/default.json +++ b/config/ldp/metadata-writer/default.json @@ -4,7 +4,8 @@ "files-scs:config/ldp/metadata-writer/writers/constant.json", "files-scs:config/ldp/metadata-writer/writers/link-rel.json", "files-scs:config/ldp/metadata-writer/writers/mapped.json", - "files-scs:config/ldp/metadata-writer/writers/wac-allow.json" + "files-scs:config/ldp/metadata-writer/writers/wac-allow.json", + "files-scs:config/ldp/metadata-writer/writers/www-auth.json" ], "@graph": [ { @@ -15,7 +16,8 @@ { "@id": "urn:solid-server:default:MetadataWriter_Constant" }, { "@id": "urn:solid-server:default:MetadataWriter_Mapped" }, { "@id": "urn:solid-server:default:MetadataWriter_LinkRel" }, - { "@id": "urn:solid-server:default:MetadataWriter_WacAllow" } + { "@id": "urn:solid-server:default:MetadataWriter_WacAllow" }, + { "@id": "urn:solid-server:default:MetadataWriter_WwwAuth" } ] } ] diff --git a/config/ldp/metadata-writer/writers/www-auth.json b/config/ldp/metadata-writer/writers/www-auth.json new file mode 100644 index 000000000..af206aa04 --- /dev/null +++ b/config/ldp/metadata-writer/writers/www-auth.json @@ -0,0 +1,11 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", + "@graph": [ + { + "comment": "Adds the WWW-Authenticate header on 401 responses. The current auth value is required for the legacy solid-auth-client.", + "@id": "urn:solid-server:default:MetadataWriter_WwwAuth", + "@type": "WwwAuthMetadataWriter", + "auth": "Bearer scope=\"openid webid\"" + } + ] +} diff --git a/src/index.ts b/src/index.ts index 6084c0141..13408156f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -97,6 +97,7 @@ export * from './ldp/http/metadata/MetadataParser'; export * from './ldp/http/metadata/MetadataWriter'; export * from './ldp/http/metadata/SlugParser'; export * from './ldp/http/metadata/WacAllowMetadataWriter'; +export * from './ldp/http/metadata/WwwAuthMetadataWriter'; // LDP/HTTP/Response export * from './ldp/http/response/CreatedResponseDescription'; diff --git a/src/ldp/http/metadata/WwwAuthMetadataWriter.ts b/src/ldp/http/metadata/WwwAuthMetadataWriter.ts new file mode 100644 index 000000000..92aa630f9 --- /dev/null +++ b/src/ldp/http/metadata/WwwAuthMetadataWriter.ts @@ -0,0 +1,24 @@ +import type { HttpResponse } from '../../../server/HttpResponse'; +import { addHeader } from '../../../util/HeaderUtil'; +import { HTTP } from '../../../util/Vocabularies'; +import type { RepresentationMetadata } from '../../representation/RepresentationMetadata'; +import { MetadataWriter } from './MetadataWriter'; + +/** + * Adds the `WWW-Authenticate` header with the injected value in case the response status code is 401. + */ +export class WwwAuthMetadataWriter extends MetadataWriter { + private readonly auth: string; + + public constructor(auth: string) { + super(); + this.auth = auth; + } + + public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise { + const statusLiteral = input.metadata.get(HTTP.terms.statusCodeNumber); + if (statusLiteral?.value === '401') { + addHeader(input.response, 'WWW-Authenticate', this.auth); + } + } +} diff --git a/test/integration/LdpHandlerWithAuth.test.ts b/test/integration/LdpHandlerWithAuth.test.ts index 05c4eb1d1..b8570981c 100644 --- a/test/integration/LdpHandlerWithAuth.test.ts +++ b/test/integration/LdpHandlerWithAuth.test.ts @@ -188,4 +188,16 @@ describe.each(stores)('An LDP handler with auth using %s', (name, { storeConfig, // Close response await response.text(); }); + + it('returns the legacy WWW-Authenticate header on 401 requests.', async(): Promise => { + await aclHelper.setSimpleAcl(baseUrl, { + permissions: {}, + agentClass: 'agent', + accessTo: true, + }); + + const response = await fetch(`${baseUrl}.acl`); + expect(response.status).toBe(401); + expect(response.headers.get('www-authenticate')).toBe('Bearer scope="openid webid"'); + }); }); diff --git a/test/unit/ldp/http/metadata/WacAllowMetadataWriter.test.ts b/test/unit/ldp/http/metadata/WacAllowMetadataWriter.test.ts index 9788bdbf1..874b18fd4 100644 --- a/test/unit/ldp/http/metadata/WacAllowMetadataWriter.test.ts +++ b/test/unit/ldp/http/metadata/WacAllowMetadataWriter.test.ts @@ -4,7 +4,7 @@ import { RepresentationMetadata } from '../../../../../src/ldp/representation/Re import type { HttpResponse } from '../../../../../src/server/HttpResponse'; import { ACL, AUTH } from '../../../../../src/util/Vocabularies'; -describe('WacAllowMetadataWriter', (): void => { +describe('A WacAllowMetadataWriter', (): void => { const writer = new WacAllowMetadataWriter(); let response: HttpResponse; diff --git a/test/unit/ldp/http/metadata/WwwAuthMetadataWriter.test.ts b/test/unit/ldp/http/metadata/WwwAuthMetadataWriter.test.ts new file mode 100644 index 000000000..31af4f1e8 --- /dev/null +++ b/test/unit/ldp/http/metadata/WwwAuthMetadataWriter.test.ts @@ -0,0 +1,36 @@ +import { createResponse } from 'node-mocks-http'; +import { WwwAuthMetadataWriter } from '../../../../../src/ldp/http/metadata/WwwAuthMetadataWriter'; +import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata'; +import type { HttpResponse } from '../../../../../src/server/HttpResponse'; +import { HTTP } from '../../../../../src/util/Vocabularies'; + +describe('A WwwAuthMetadataWriter', (): void => { + const auth = 'Bearer scope="openid webid"'; + const writer = new WwwAuthMetadataWriter(auth); + let response: HttpResponse; + + beforeEach(async(): Promise => { + response = createResponse(); + }); + + it('adds no header if there is no relevant metadata.', async(): Promise => { + const metadata = new RepresentationMetadata(); + await expect(writer.handle({ response, metadata })).resolves.toBeUndefined(); + expect(response.getHeaders()).toEqual({ }); + }); + + it('adds no header if the status code is not 401.', async(): Promise => { + const metadata = new RepresentationMetadata({ [HTTP.statusCodeNumber]: '403' }); + await expect(writer.handle({ response, metadata })).resolves.toBeUndefined(); + expect(response.getHeaders()).toEqual({ }); + }); + + it('adds a WWW-Authenticate header if the status code is 401.', async(): Promise => { + const metadata = new RepresentationMetadata({ [HTTP.statusCodeNumber]: '401' }); + await expect(writer.handle({ response, metadata })).resolves.toBeUndefined(); + + expect(response.getHeaders()).toEqual({ + 'www-authenticate': auth, + }); + }); +});