mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
174 lines
6.9 KiB
TypeScript
174 lines
6.9 KiB
TypeScript
import type { Provider } from 'oidc-provider';
|
|
import type { ProviderFactory } from '../../../../src/identity/configuration/ProviderFactory';
|
|
import { ConsentHandler } from '../../../../src/identity/interaction/ConsentHandler';
|
|
import type { Interaction } from '../../../../src/identity/interaction/InteractionHandler';
|
|
import { FoundHttpError } from '../../../../src/util/errors/FoundHttpError';
|
|
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
|
import { readJsonStream } from '../../../../src/util/StreamUtil';
|
|
import { createPostJsonOperation } from './email-password/handler/Util';
|
|
|
|
const newGrantId = 'newGrantId';
|
|
class DummyGrant {
|
|
public accountId: string;
|
|
public clientId: string;
|
|
|
|
public readonly scopes: string[] = [];
|
|
public claims: string[] = [];
|
|
public readonly rejectedScopes: string[] = [];
|
|
public readonly resourceScopes: Record<string, string> = {};
|
|
|
|
public constructor(props: { accountId: string; clientId: string }) {
|
|
this.accountId = props.accountId;
|
|
this.clientId = props.clientId;
|
|
}
|
|
|
|
public rejectOIDCScope(scope: string): void {
|
|
this.rejectedScopes.push(scope);
|
|
}
|
|
|
|
public addOIDCScope(scope: string): void {
|
|
this.scopes.push(scope);
|
|
}
|
|
|
|
public addOIDCClaims(claims: string[]): void {
|
|
this.claims = claims;
|
|
}
|
|
|
|
public addResourceScope(resource: string, scope: string): void {
|
|
this.resourceScopes[resource] = scope;
|
|
}
|
|
|
|
public async save(): Promise<string> {
|
|
return newGrantId;
|
|
}
|
|
}
|
|
|
|
describe('A ConsentHandler', (): void => {
|
|
const accountId = 'http://example.com/id#me';
|
|
const clientId = 'clientId';
|
|
const clientMetadata = {
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
client_id: 'clientId',
|
|
};
|
|
let grantFn: jest.Mock<DummyGrant> & { find: jest.Mock<DummyGrant> };
|
|
let knownGrant: DummyGrant;
|
|
let oidcInteraction: Interaction;
|
|
let provider: jest.Mocked<Provider>;
|
|
let providerFactory: jest.Mocked<ProviderFactory>;
|
|
let handler: ConsentHandler;
|
|
|
|
beforeEach(async(): Promise<void> => {
|
|
oidcInteraction = {
|
|
session: { accountId },
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
params: { client_id: clientId },
|
|
prompt: { details: {}},
|
|
save: jest.fn(),
|
|
} as any;
|
|
|
|
knownGrant = new DummyGrant({ accountId, clientId });
|
|
|
|
grantFn = jest.fn((props): DummyGrant => new DummyGrant(props)) as any;
|
|
grantFn.find = jest.fn((grantId: string): any => grantId ? knownGrant : undefined);
|
|
provider = {
|
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
Grant: grantFn,
|
|
Client: {
|
|
find: (id: string): any => (id ? { metadata: jest.fn().mockReturnValue(clientMetadata) } : undefined),
|
|
},
|
|
/* eslint-enable @typescript-eslint/naming-convention */
|
|
} as any;
|
|
|
|
providerFactory = {
|
|
getProvider: jest.fn().mockResolvedValue(provider),
|
|
};
|
|
|
|
handler = new ConsentHandler(providerFactory);
|
|
});
|
|
|
|
it('errors if no oidcInteraction is defined on POST requests.', async(): Promise<void> => {
|
|
const error = expect.objectContaining({
|
|
statusCode: 400,
|
|
message: 'This action can only be performed as part of an OIDC authentication flow.',
|
|
errorCode: 'E0002',
|
|
});
|
|
await expect(handler.canHandle({ operation: createPostJsonOperation({}) })).rejects.toThrow(error);
|
|
|
|
await expect(handler.canHandle({ operation: createPostJsonOperation({}), oidcInteraction }))
|
|
.resolves.toBeUndefined();
|
|
});
|
|
|
|
it('returns the client metadata on a GET request.', async(): Promise<void> => {
|
|
const operation = { method: 'GET', target: { path: 'http://example.com/foo' }} as any;
|
|
const representation = await handler.handle({ operation, oidcInteraction });
|
|
await expect(readJsonStream(representation.data)).resolves.toEqual({
|
|
client: {
|
|
...clientMetadata,
|
|
'@context': 'https://www.w3.org/ns/solid/oidc-context.jsonld',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('returns an empty object if no client was found.', async(): Promise<void> => {
|
|
delete oidcInteraction.params.client_id;
|
|
const operation = { method: 'GET', target: { path: 'http://example.com/foo' }} as any;
|
|
const representation = await handler.handle({ operation, oidcInteraction });
|
|
await expect(readJsonStream(representation.data)).resolves.toEqual({
|
|
client: {
|
|
'@context': 'https://www.w3.org/ns/solid/oidc-context.jsonld',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('requires an oidcInteraction with a defined session.', async(): Promise<void> => {
|
|
oidcInteraction.session = undefined;
|
|
await expect(handler.handle({ operation: createPostJsonOperation({}), oidcInteraction }))
|
|
.rejects.toThrow(NotImplementedHttpError);
|
|
});
|
|
|
|
it('throws a redirect error.', async(): Promise<void> => {
|
|
const operation = createPostJsonOperation({});
|
|
await expect(handler.handle({ operation, oidcInteraction })).rejects.toThrow(FoundHttpError);
|
|
});
|
|
|
|
it('stores the requested scopes and claims in the grant.', async(): Promise<void> => {
|
|
oidcInteraction.prompt.details = {
|
|
missingOIDCScope: [ 'scope1', 'scope2' ],
|
|
missingOIDCClaims: [ 'claim1', 'claim2' ],
|
|
missingResourceScopes: { resource: [ 'scope1', 'scope2' ]},
|
|
};
|
|
|
|
const operation = createPostJsonOperation({ remember: true });
|
|
await expect(handler.handle({ operation, oidcInteraction })).rejects.toThrow(FoundHttpError);
|
|
expect(grantFn.mock.results).toHaveLength(1);
|
|
expect(grantFn.mock.results[0].value.scopes).toEqual([ 'scope1 scope2' ]);
|
|
expect(grantFn.mock.results[0].value.claims).toEqual([ 'claim1', 'claim2' ]);
|
|
expect(grantFn.mock.results[0].value.resourceScopes).toEqual({ resource: 'scope1 scope2' });
|
|
expect(grantFn.mock.results[0].value.rejectedScopes).toEqual([]);
|
|
});
|
|
|
|
it('creates a new Grant when needed.', async(): Promise<void> => {
|
|
const operation = createPostJsonOperation({});
|
|
await expect(handler.handle({ operation, oidcInteraction })).rejects.toThrow(FoundHttpError);
|
|
expect(grantFn).toHaveBeenCalledTimes(1);
|
|
expect(grantFn).toHaveBeenLastCalledWith({ accountId, clientId });
|
|
expect(grantFn.find).toHaveBeenCalledTimes(0);
|
|
});
|
|
|
|
it('reuses existing Grant objects.', async(): Promise<void> => {
|
|
const operation = createPostJsonOperation({});
|
|
oidcInteraction.grantId = '123456';
|
|
await expect(handler.handle({ operation, oidcInteraction })).rejects.toThrow(FoundHttpError);
|
|
expect(grantFn).toHaveBeenCalledTimes(0);
|
|
expect(grantFn.find).toHaveBeenCalledTimes(1);
|
|
expect(grantFn.find).toHaveBeenLastCalledWith('123456');
|
|
});
|
|
|
|
it('rejectes offline_access as scope if a user does not want to be remembered.', async(): Promise<void> => {
|
|
const operation = createPostJsonOperation({});
|
|
await expect(handler.handle({ operation, oidcInteraction })).rejects.toThrow(FoundHttpError);
|
|
expect(grantFn.mock.results).toHaveLength(1);
|
|
expect(grantFn.mock.results[0].value.rejectedScopes).toEqual([ 'offline_access' ]);
|
|
});
|
|
});
|