mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Bearer token support
This commit is contained in:
parent
97e7e42fdc
commit
bdfd7cf902
@ -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
6
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
42
src/authentication/BearerWebIdExtractor.ts
Normal file
42
src/authentication/BearerWebIdExtractor.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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';
|
||||||
|
84
test/unit/authentication/BearerWebIdExtractor.test.ts
Normal file
84
test/unit/authentication/BearerWebIdExtractor.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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> => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user