Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions packages/uma/config/credentials/validators/pat.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
{
"@context": [
"https://linkedsoftwaredependencies.org/bundles/npm/@solidlab/uma/^0.0.0/components/context.jsonld"
"https://linkedsoftwaredependencies.org/bundles/npm/@solidlab/uma/^0.0.0/components/context.jsonld",
"https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^8.0.0/components/context.jsonld",
"https://linkedsoftwaredependencies.org/bundles/npm/asynchronous-handlers/^1.0.0/components/context.jsonld"
],
"@graph": [
{
"@id": "urn:uma:default:RequestValidator",
"@type": "PatRequestValidator",
"storage": { "@id": "urn:solid-server:default:ClientRegistrationStorage" }
"@type": "StatusWaterfallHandler",
"handlers": [
{
"@type": "PatRequestValidator",
"storage": { "@id": "urn:solid-server:default:ClientRegistrationStorage" }
},
{
"@type": "CredentialVerifierRequestValidator",
"credentialParser": { "@id": "urn:uma:default:CredentialParser" },
"verifier": { "@id": "urn:uma:default:Verifier" }
}
]
}
]
}
1 change: 1 addition & 0 deletions packages/uma/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export * from './util/http/server/JsonHttpErrorHandler';
export * from './util/http/server/JsonFormHttpHandler';
export * from './util/http/server/NodeHttpRequestResponseHandler';
export * from './util/http/server/RoutedHttpRequestHandler';
export * from './util/http/validate/CredentialVerifierRequestValidator';
export * from './util/http/validate/PatRequestValidator';
export * from './util/http/validate/RequestValidator';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { UnauthorizedHttpError } from '@solid/community-server';
import { WEBID } from '../../../credentials/Claims';
import { CredentialParser } from '../../../credentials/CredentialParser';
import { Verifier } from '../../../credentials/verify/Verifier';
import { RequestValidator, RequestValidatorInput, RequestValidatorOutput } from './RequestValidator';

/**
* Validates requests by verifying the Credential and extracting the owner.
*/
export class CredentialVerifierRequestValidator extends RequestValidator {
public constructor(
protected readonly credentialParser: CredentialParser,
protected readonly verifier: Verifier,
) {
super();
}

public async canHandle({ request }: RequestValidatorInput): Promise<void> {
await this.credentialParser.canHandle(request);
}

public async handle({ request }: RequestValidatorInput): Promise<RequestValidatorOutput> {
const result = await this.credentialParser.handle(request);
const claims = await this.verifier.verify(result);
if (!claims[WEBID] || claims[WEBID].length === 0) {
throw new UnauthorizedHttpError('Could not determine owner.');
}
return { owner: claims[WEBID][0] as string};
}
}
13 changes: 10 additions & 3 deletions packages/uma/src/util/http/validate/PatRequestValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,24 @@ export class PatRequestValidator extends RequestValidator {
this.storage = storage;
}

public async handle({ request }: RequestValidatorInput): Promise<RequestValidatorOutput> {
public async canHandle({ request }: RequestValidatorInput): Promise<void> {
const { authorization } = request.headers;
if (!authorization || !/^Bearer /ui.test(authorization)) {
throw new UnauthorizedHttpError('No Bearer Authorization header specified.');
}

const token = authorization?.replace(/^Bearer/, '')?.trimStart();
const patEntries = await this.storage.find(PAT_STORAGE_TYPE, { pat: token });
if (patEntries.length === 0) {
const patIds = await this.storage.findIds(PAT_STORAGE_TYPE, { pat: token });
if (patIds.length === 0) {
throw new ForbiddenHttpError('Unknown PAT.');
}
}

public async handle({ request }: RequestValidatorInput): Promise<RequestValidatorOutput> {
const { authorization } = request.headers;

const token = authorization?.replace(/^Bearer/, '')?.trimStart();
const patEntries = await this.storage.find(PAT_STORAGE_TYPE, { pat: token });
if (patEntries[0].expiration < Date.now()) {
throw new ForbiddenHttpError('Expired PAT.');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { BadRequestHttpError } from '@solid/community-server';
import { Mocked } from 'vitest';
import { WEBID } from '../../../../../src/credentials/Claims';
import { ClaimSet } from '../../../../../src/credentials/ClaimSet';
import { CredentialParser } from '../../../../../src/credentials/CredentialParser';
import { Verifier } from '../../../../../src/credentials/verify/Verifier';
import {
CredentialVerifierRequestValidator
} from '../../../../../src/util/http/validate/CredentialVerifierRequestValidator';
import { RequestValidatorInput } from '../../../../../src/util/http/validate/RequestValidator';

describe('CredentialVerifierRequestValidator', (): void => {
const webid = 'https://example.com/profile/card#me';
let input: RequestValidatorInput;
let claims: ClaimSet;
let tokenResponse: { token: string, format: string };

let credentialParser: Mocked<CredentialParser>;
let verifier: Mocked<Verifier>;
let validator: CredentialVerifierRequestValidator;

beforeEach(async(): Promise<void> => {
input = {
request: {
url: new URL('http://example.com/foo'),
method: 'GET',
headers: { authorization: 'Bearer token' },
}
};

tokenResponse = {
token: 'token',
format: 'format',
};

claims = {
[WEBID]: [ webid ],
};

credentialParser = {
canHandle: vi.fn(),
handle: vi.fn().mockResolvedValue(tokenResponse),
} satisfies Partial<CredentialParser> as any;

verifier = {
verify: vi.fn().mockResolvedValue(claims),
};

validator = new CredentialVerifierRequestValidator(credentialParser, verifier);
});

it('can handle requests the verifier can handle.', async(): Promise<void> => {
await expect(validator.canHandle(input)).resolves.toBeUndefined();
expect(credentialParser.canHandle).toHaveBeenLastCalledWith(input.request);

credentialParser.canHandle.mockRejectedValueOnce(new BadRequestHttpError('bad data'));
await expect(validator.canHandle(input)).rejects.toThrow('bad data');
});

it('returns the WEBID claim as owner.', async(): Promise<void> => {
await expect(validator.handle(input)).resolves.toEqual({ owner: webid });
expect(credentialParser.handle).toHaveBeenLastCalledWith(input.request);
expect(verifier.verify).toHaveBeenLastCalledWith(tokenResponse);
});

it('errors if there is no WEBID claim.', async(): Promise<void> => {
claims = {};
verifier.verify.mockResolvedValueOnce(claims);
await expect(validator.handle(input)).rejects.toThrow('Could not determine owner.');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,31 +30,31 @@ describe('PatRequestValidator', (): void => {

storage = {
find: vi.fn().mockResolvedValue([{ expiration: Date.now() + 5000, registration: registrationId }]),
findIds: vi.fn().mockResolvedValue([ registrationId ]),
get: vi.fn().mockResolvedValue({ userId }),
} as any;

validator = new PatRequestValidator(storage as any);
});

it('returns the stored user as owner.', async(): Promise<void> => {
await expect(validator.handle({ request })).resolves.toEqual({ owner: userId });
expect(storage.find).toHaveBeenLastCalledWith(PAT_STORAGE_TYPE, { pat });
expect(storage.get).toHaveBeenLastCalledWith(CLIENT_REGISTRATION_STORAGE_TYPE, registrationId);
it('can only handle Bearer tokens.', async(): Promise<void> => {
request.headers.authorization = 'Basic 1234';
await expect(validator.canHandle({ request })).rejects.toThrow('No Bearer Authorization header specified.');
});

it('errors on non-Bearer tokens.', async(): Promise<void> => {
request.headers.authorization = 'Basic 1234';
await expect(validator.handle({ request })).rejects.toThrow('No Bearer Authorization header specified.');
it('can only handle known tokens.', async(): Promise<void> => {
storage.findIds.mockResolvedValueOnce([]);
await expect(validator.canHandle({ request })).rejects.toThrow('Unknown PAT.');
});

it('errors on non-Bearer tokens.', async(): Promise<void> => {
request.headers.authorization = 'Basic 1234';
await expect(validator.handle({ request })).rejects.toThrow('No Bearer Authorization header specified.');
it('can handle known Bearer tokens.', async(): Promise<void> => {
await expect(validator.canHandle({ request })).resolves.toBeUndefined();
});

it('errors if no matched token was found.', async(): Promise<void> => {
storage.find.mockResolvedValueOnce([]);
await expect(validator.handle({ request })).rejects.toThrow('Unknown PAT.');
it('returns the stored user as owner.', async(): Promise<void> => {
await expect(validator.handle({ request })).resolves.toEqual({ owner: userId });
expect(storage.find).toHaveBeenLastCalledWith(PAT_STORAGE_TYPE, { pat });
expect(storage.get).toHaveBeenLastCalledWith(CLIENT_REGISTRATION_STORAGE_TYPE, registrationId);
});

it('errors if the PAT is expired.', async(): Promise<void> => {
Expand Down
Loading