From bdfd7cf902afb0cab45b26c62cb0bae18fbcc1ee Mon Sep 17 00:00:00 2001 From: Matthieu Bosquet Date: Mon, 28 Dec 2020 16:14:47 +0000 Subject: [PATCH] feat: Bearer token support --- config/presets/ldp/credentials-extractor.json | 3 + package-lock.json | 6 +- package.json | 2 +- src/authentication/BearerWebIdExtractor.ts | 42 ++++++++++ src/authentication/DPoPWebIdExtractor.ts | 13 +-- src/index.ts | 1 + .../BearerWebIdExtractor.test.ts | 84 +++++++++++++++++++ .../authentication/DPoPWebIdExtractor.test.ts | 2 +- 8 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 src/authentication/BearerWebIdExtractor.ts create mode 100644 test/unit/authentication/BearerWebIdExtractor.test.ts diff --git a/config/presets/ldp/credentials-extractor.json b/config/presets/ldp/credentials-extractor.json index 6d6b91f3f..d9abee40c 100644 --- a/config/presets/ldp/credentials-extractor.json +++ b/config/presets/ldp/credentials-extractor.json @@ -11,6 +11,9 @@ "@id": "urn:solid-server:default:TargetExtractor" } }, + { + "@type": "BearerWebIdExtractor" + }, { "@type": "EmptyCredentialsExtractor" } diff --git a/package-lock.json b/package-lock.json index 5223b080e..a8b1659f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9413,9 +9413,9 @@ "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, "ts-dpop": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/ts-dpop/-/ts-dpop-0.3.0.tgz", - "integrity": "sha512-5UzXARerh1kh8iYusP0IWL5NanosuDG+ORJpYAGi2ZXwItHrLhJOiECB8RoPeW73jOhJQgV2wSgjeGRv4pCIyQ==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/ts-dpop/-/ts-dpop-0.4.0.tgz", + "integrity": "sha512-xqd8mknCupOjQj+YqYjwE22XxeEfReyXBEhBgP0J7hC6JAZFnqBUvk0cWcYMnTqTwLIiGgjT4QtA94spUz2vXQ==", "requires": { "cross-fetch": "^3.0.6", "jose": "^3.5.0", diff --git a/package.json b/package.json index d7fc7b915..c75b58baf 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "sparqlalgebrajs": "^2.3.1", "sparqljs": "^3.1.2", "streamify-array": "^1.0.1", - "ts-dpop": "^0.3.0", + "ts-dpop": "^0.4.0", "uuid": "^8.3.0", "winston": "^3.3.3", "winston-transport": "^4.4.0", diff --git a/src/authentication/BearerWebIdExtractor.ts b/src/authentication/BearerWebIdExtractor.ts new file mode 100644 index 000000000..02253948c --- /dev/null +++ b/src/authentication/BearerWebIdExtractor.ts @@ -0,0 +1,42 @@ +import type { SolidTokenVerifierFunction } from 'ts-dpop'; +import { createSolidTokenVerifier } from 'ts-dpop'; +import { getLoggerFor } from '../logging/LogUtil'; +import type { HttpRequest } from '../server/HttpRequest'; +import { BadRequestHttpError } from '../util/errors/BadRequestHttpError'; +import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError'; +import type { Credentials } from './Credentials'; +import { CredentialsExtractor } from './CredentialsExtractor'; + +/** + * Credentials extractor that extracts a WebID from a Bearer access token. + */ +export class BearerWebIdExtractor extends CredentialsExtractor { + protected readonly logger = getLoggerFor(this); + private readonly verify: SolidTokenVerifierFunction; + + public constructor() { + super(); + this.verify = createSolidTokenVerifier(); + } + + public async canHandle({ headers }: HttpRequest): Promise { + const { authorization } = headers; + if (!authorization || !authorization.startsWith('Bearer ')) { + throw new NotImplementedHttpError('No Bearer Authorization header specified.'); + } + } + + public async handle(request: HttpRequest): Promise { + const { headers: { authorization }} = request; + + try { + const { webid: webId } = await this.verify(authorization as string); + this.logger.info(`Verified WebID via Bearer access token: ${webId}`); + return { webId }; + } catch (error: unknown) { + const message = `Error verifying WebID via Bearer access token: ${(error as Error).message}`; + this.logger.warn(message); + throw new BadRequestHttpError(message); + } + } +} diff --git a/src/authentication/DPoPWebIdExtractor.ts b/src/authentication/DPoPWebIdExtractor.ts index 95b11fd98..d00fab1dd 100644 --- a/src/authentication/DPoPWebIdExtractor.ts +++ b/src/authentication/DPoPWebIdExtractor.ts @@ -1,4 +1,4 @@ -import type { RequestMethod, SolidTokenVerifierFunction } from 'ts-dpop'; +import type { SolidTokenVerifierFunction, RequestMethod } from 'ts-dpop'; import { createSolidTokenVerifier } from 'ts-dpop'; import type { TargetExtractor } from '../ldp/http/TargetExtractor'; import { getLoggerFor } from '../logging/LogUtil'; @@ -9,7 +9,7 @@ import type { Credentials } from './Credentials'; import { CredentialsExtractor } from './CredentialsExtractor'; /** - * Credentials extractor which extracts a WebID from a DPoP token. + * Credentials extractor that extracts a WebID from a DPoP-bound access token. */ export class DPoPWebIdExtractor extends CredentialsExtractor { protected readonly logger = getLoggerFor(this); @@ -39,11 +39,12 @@ export class DPoPWebIdExtractor extends CredentialsExtractor { try { const { webid: webId } = await this.verify( authorization as string, - dpop as string, - method as RequestMethod, - resource.path, + { + header: dpop as string, + method: method as RequestMethod, + url: resource.path, + }, ); - this.logger.info(`Verified WebID via DPoP-bound access token: ${webId}`); return { webId }; } catch (error: unknown) { diff --git a/src/index.ts b/src/index.ts index 4d9ee40f5..39f02dc73 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ // Authentication +export * from './authentication/BearerWebIdExtractor'; export * from './authentication/Credentials'; export * from './authentication/CredentialsExtractor'; export * from './authentication/DPoPWebIdExtractor'; diff --git a/test/unit/authentication/BearerWebIdExtractor.test.ts b/test/unit/authentication/BearerWebIdExtractor.test.ts new file mode 100644 index 000000000..3c5c02cb0 --- /dev/null +++ b/test/unit/authentication/BearerWebIdExtractor.test.ts @@ -0,0 +1,84 @@ +import { createSolidTokenVerifier } from 'ts-dpop'; +import { BearerWebIdExtractor } from '../../../src/authentication/BearerWebIdExtractor'; +import type { HttpRequest } from '../../../src/server/HttpRequest'; +import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError'; +import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError'; + +const solidTokenVerifier = createSolidTokenVerifier() as jest.MockedFunction; + +describe('A BearerWebIdExtractor', (): void => { + const webIdExtractor = new BearerWebIdExtractor(); + + afterEach((): void => { + jest.clearAllMocks(); + }); + + describe('on a request without Authorization header', (): void => { + const request = { + method: 'GET', + headers: { }, + } as any as HttpRequest; + + it('throws an error.', async(): Promise => { + const result = webIdExtractor.handleSafe(request); + await expect(result).rejects.toThrow(NotImplementedHttpError); + await expect(result).rejects.toThrow('No Bearer Authorization header specified.'); + }); + }); + + describe('on a request with an Authorization header that does not start with Bearer', (): void => { + const request = { + method: 'GET', + headers: { + authorization: 'Other token-1234', + }, + } as any as HttpRequest; + + it('throws an error.', async(): Promise => { + const result = webIdExtractor.handleSafe(request); + await expect(result).rejects.toThrow(NotImplementedHttpError); + await expect(result).rejects.toThrow('No Bearer Authorization header specified.'); + }); + }); + + describe('on a request with Authorization', (): void => { + const request = { + method: 'GET', + headers: { + authorization: 'Bearer token-1234', + }, + } as any as HttpRequest; + + it('calls the Bearer verifier with the correct parameters.', async(): Promise => { + await webIdExtractor.handleSafe(request); + expect(solidTokenVerifier).toHaveBeenCalledTimes(1); + expect(solidTokenVerifier).toHaveBeenCalledWith('Bearer token-1234'); + }); + + it('returns the extracted WebID.', async(): Promise => { + const result = webIdExtractor.handleSafe(request); + await expect(result).resolves.toEqual({ webId: 'http://alice.example/card#me' }); + }); + }); + + describe('when verification throws an error', (): void => { + const request = { + method: 'GET', + headers: { + authorization: 'Bearer token-1234', + }, + } as any as HttpRequest; + + beforeEach((): void => { + solidTokenVerifier.mockImplementationOnce((): void => { + throw new Error('invalid'); + }); + }); + + it('throws an error.', async(): Promise => { + const result = webIdExtractor.handleSafe(request); + await expect(result).rejects.toThrow(BadRequestHttpError); + await expect(result).rejects.toThrow('Error verifying WebID via Bearer access token: invalid'); + }); + }); +}); diff --git a/test/unit/authentication/DPoPWebIdExtractor.test.ts b/test/unit/authentication/DPoPWebIdExtractor.test.ts index 57f286f5c..8a703b9eb 100644 --- a/test/unit/authentication/DPoPWebIdExtractor.test.ts +++ b/test/unit/authentication/DPoPWebIdExtractor.test.ts @@ -80,7 +80,7 @@ describe('A DPoPWebIdExtractor', (): void => { it('calls the DPoP verifier with the correct parameters.', async(): Promise => { await webIdExtractor.handleSafe(request); expect(solidTokenVerifier).toHaveBeenCalledTimes(1); - expect(solidTokenVerifier).toHaveBeenCalledWith('DPoP token-1234', 'token-5678', 'GET', 'http://example.org/foo/bar'); + expect(solidTokenVerifier).toHaveBeenCalledWith('DPoP token-1234', { header: 'token-5678', method: 'GET', url: 'http://example.org/foo/bar' }); }); it('returns the extracted WebID.', async(): Promise => {