feat: Bearer token support

This commit is contained in:
Matthieu Bosquet 2020-12-28 16:14:47 +00:00 committed by Ruben Verborgh
parent 97e7e42fdc
commit bdfd7cf902
8 changed files with 142 additions and 11 deletions

View File

@ -11,6 +11,9 @@
"@id": "urn:solid-server:default:TargetExtractor" "@id": "urn:solid-server:default:TargetExtractor"
} }
}, },
{
"@type": "BearerWebIdExtractor"
},
{ {
"@type": "EmptyCredentialsExtractor" "@type": "EmptyCredentialsExtractor"
} }

6
package-lock.json generated
View File

@ -9413,9 +9413,9 @@
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
}, },
"ts-dpop": { "ts-dpop": {
"version": "0.3.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/ts-dpop/-/ts-dpop-0.3.0.tgz", "resolved": "https://registry.npmjs.org/ts-dpop/-/ts-dpop-0.4.0.tgz",
"integrity": "sha512-5UzXARerh1kh8iYusP0IWL5NanosuDG+ORJpYAGi2ZXwItHrLhJOiECB8RoPeW73jOhJQgV2wSgjeGRv4pCIyQ==", "integrity": "sha512-xqd8mknCupOjQj+YqYjwE22XxeEfReyXBEhBgP0J7hC6JAZFnqBUvk0cWcYMnTqTwLIiGgjT4QtA94spUz2vXQ==",
"requires": { "requires": {
"cross-fetch": "^3.0.6", "cross-fetch": "^3.0.6",
"jose": "^3.5.0", "jose": "^3.5.0",

View File

@ -101,7 +101,7 @@
"sparqlalgebrajs": "^2.3.1", "sparqlalgebrajs": "^2.3.1",
"sparqljs": "^3.1.2", "sparqljs": "^3.1.2",
"streamify-array": "^1.0.1", "streamify-array": "^1.0.1",
"ts-dpop": "^0.3.0", "ts-dpop": "^0.4.0",
"uuid": "^8.3.0", "uuid": "^8.3.0",
"winston": "^3.3.3", "winston": "^3.3.3",
"winston-transport": "^4.4.0", "winston-transport": "^4.4.0",

View File

@ -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<void> {
const { authorization } = headers;
if (!authorization || !authorization.startsWith('Bearer ')) {
throw new NotImplementedHttpError('No Bearer Authorization header specified.');
}
}
public async handle(request: HttpRequest): Promise<Credentials> {
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);
}
}
}

View File

@ -1,4 +1,4 @@
import type { RequestMethod, SolidTokenVerifierFunction } from 'ts-dpop'; import type { SolidTokenVerifierFunction, RequestMethod } from 'ts-dpop';
import { createSolidTokenVerifier } from 'ts-dpop'; import { createSolidTokenVerifier } from 'ts-dpop';
import type { TargetExtractor } from '../ldp/http/TargetExtractor'; import type { TargetExtractor } from '../ldp/http/TargetExtractor';
import { getLoggerFor } from '../logging/LogUtil'; import { getLoggerFor } from '../logging/LogUtil';
@ -9,7 +9,7 @@ import type { Credentials } from './Credentials';
import { CredentialsExtractor } from './CredentialsExtractor'; 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 { export class DPoPWebIdExtractor extends CredentialsExtractor {
protected readonly logger = getLoggerFor(this); protected readonly logger = getLoggerFor(this);
@ -39,11 +39,12 @@ export class DPoPWebIdExtractor extends CredentialsExtractor {
try { try {
const { webid: webId } = await this.verify( const { webid: webId } = await this.verify(
authorization as string, authorization as string,
dpop as string, {
method as RequestMethod, header: dpop as string,
resource.path, method: method as RequestMethod,
url: resource.path,
},
); );
this.logger.info(`Verified WebID via DPoP-bound access token: ${webId}`); this.logger.info(`Verified WebID via DPoP-bound access token: ${webId}`);
return { webId }; return { webId };
} catch (error: unknown) { } catch (error: unknown) {

View File

@ -1,4 +1,5 @@
// Authentication // Authentication
export * from './authentication/BearerWebIdExtractor';
export * from './authentication/Credentials'; export * from './authentication/Credentials';
export * from './authentication/CredentialsExtractor'; export * from './authentication/CredentialsExtractor';
export * from './authentication/DPoPWebIdExtractor'; export * from './authentication/DPoPWebIdExtractor';

View File

@ -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<any>;
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<void> => {
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<void> => {
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<void> => {
await webIdExtractor.handleSafe(request);
expect(solidTokenVerifier).toHaveBeenCalledTimes(1);
expect(solidTokenVerifier).toHaveBeenCalledWith('Bearer token-1234');
});
it('returns the extracted WebID.', async(): Promise<void> => {
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<void> => {
const result = webIdExtractor.handleSafe(request);
await expect(result).rejects.toThrow(BadRequestHttpError);
await expect(result).rejects.toThrow('Error verifying WebID via Bearer access token: invalid');
});
});
});

View File

@ -80,7 +80,7 @@ describe('A DPoPWebIdExtractor', (): void => {
it('calls the DPoP verifier with the correct parameters.', async(): Promise<void> => { it('calls the DPoP verifier with the correct parameters.', async(): Promise<void> => {
await webIdExtractor.handleSafe(request); await webIdExtractor.handleSafe(request);
expect(solidTokenVerifier).toHaveBeenCalledTimes(1); 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<void> => { it('returns the extracted WebID.', async(): Promise<void> => {