mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Update OIDC provider dependency to v7
The biggest resulting change is that the consent page always appears after logging in. Some minor fixes to be closer to the spec are included together with some minor structural refactors.
This commit is contained in:
@@ -138,10 +138,11 @@ describe('A Solid server with IDP', (): void => {
|
||||
});
|
||||
|
||||
it('initializes the session and logs in.', async(): Promise<void> => {
|
||||
const url = await state.startSession();
|
||||
let url = await state.startSession();
|
||||
const res = await state.fetchIdp(url);
|
||||
expect(res.status).toBe(200);
|
||||
await state.login(url, email, password);
|
||||
url = await state.login(url, email, password);
|
||||
await state.consent(url);
|
||||
expect(state.session.info?.webId).toBe(webId);
|
||||
});
|
||||
|
||||
@@ -162,16 +163,12 @@ describe('A Solid server with IDP', (): void => {
|
||||
it('can log in again.', async(): Promise<void> => {
|
||||
const url = await state.startSession();
|
||||
|
||||
let res = await state.fetchIdp(url);
|
||||
const res = await state.fetchIdp(url);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Will receive confirm screen here instead of login screen
|
||||
res = await state.fetchIdp(url, 'POST', '', APPLICATION_X_WWW_FORM_URLENCODED);
|
||||
const json = await res.json();
|
||||
const nextUrl = json.location;
|
||||
expect(typeof nextUrl).toBe('string');
|
||||
await state.consent(url);
|
||||
|
||||
await state.handleLoginRedirect(nextUrl);
|
||||
expect(state.session.info?.webId).toBe(webId);
|
||||
});
|
||||
});
|
||||
@@ -223,10 +220,11 @@ describe('A Solid server with IDP', (): void => {
|
||||
});
|
||||
|
||||
it('initializes the session and logs in.', async(): Promise<void> => {
|
||||
const url = await state.startSession(clientId);
|
||||
let url = await state.startSession(clientId);
|
||||
const res = await state.fetchIdp(url);
|
||||
expect(res.status).toBe(200);
|
||||
await state.login(url, email, password);
|
||||
url = await state.login(url, email, password);
|
||||
await state.consent(url);
|
||||
expect(state.session.info?.webId).toBe(webId);
|
||||
});
|
||||
|
||||
@@ -318,7 +316,8 @@ describe('A Solid server with IDP', (): void => {
|
||||
});
|
||||
|
||||
it('can log in with the new password.', async(): Promise<void> => {
|
||||
await state.login(nextUrl, email, password2);
|
||||
const url = await state.login(nextUrl, email, password2);
|
||||
await state.consent(url);
|
||||
expect(state.session.info?.webId).toBe(webId);
|
||||
});
|
||||
});
|
||||
@@ -397,10 +396,11 @@ describe('A Solid server with IDP', (): void => {
|
||||
|
||||
it('initializes the session and logs in.', async(): Promise<void> => {
|
||||
state = new IdentityTestState(baseUrl, redirectUrl, oidcIssuer);
|
||||
const url = await state.startSession();
|
||||
let url = await state.startSession();
|
||||
const res = await state.fetchIdp(url);
|
||||
expect(res.status).toBe(200);
|
||||
await state.login(url, newMail, password);
|
||||
url = await state.login(url, newMail, password);
|
||||
await state.consent(url);
|
||||
expect(state.session.info?.webId).toBe(newWebId);
|
||||
});
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export class IdentityTestState {
|
||||
|
||||
// Need to catch the redirect so we can copy the cookies
|
||||
let res = await this.fetchIdp(nextUrl);
|
||||
expect(res.status).toBe(302);
|
||||
expect(res.status).toBe(303);
|
||||
nextUrl = res.headers.get('location')!;
|
||||
|
||||
// Handle redirect
|
||||
@@ -109,22 +109,26 @@ export class IdentityTestState {
|
||||
* Logs in by sending the corresponding email and password to the given form action.
|
||||
* The URL should be extracted from the login page.
|
||||
*/
|
||||
public async login(url: string, email: string, password: string): Promise<void> {
|
||||
public async login(url: string, email: string, password: string): Promise<string> {
|
||||
const formData = stringify({ email, password });
|
||||
const res = await this.fetchIdp(url, 'POST', formData, APPLICATION_X_WWW_FORM_URLENCODED);
|
||||
let res = await this.fetchIdp(url, 'POST', formData, APPLICATION_X_WWW_FORM_URLENCODED);
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
const nextUrl = json.location;
|
||||
|
||||
return this.handleLoginRedirect(nextUrl);
|
||||
res = await this.fetchIdp(json.location);
|
||||
expect(res.status).toBe(303);
|
||||
return res.headers.get('location')!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the redirect that happens after logging in.
|
||||
* Handles the consent screen at the given URL and the followup redirect back to the client.
|
||||
*/
|
||||
public async handleLoginRedirect(url: string): Promise<void> {
|
||||
const res = await this.fetchIdp(url);
|
||||
expect(res.status).toBe(302);
|
||||
public async consent(url: string): Promise<void> {
|
||||
let res = await this.fetchIdp(url, 'POST', '', APPLICATION_X_WWW_FORM_URLENCODED);
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
|
||||
res = await this.fetchIdp(json.location);
|
||||
expect(res.status).toBe(303);
|
||||
const mockUrl = res.headers.get('location')!;
|
||||
expect(mockUrl.startsWith(this.redirectUrl)).toBeTruthy();
|
||||
|
||||
|
||||
@@ -94,10 +94,11 @@ describe('A server with restricted IDP access', (): void => {
|
||||
it('can still access registration with the correct credentials.', async(): Promise<void> => {
|
||||
// Logging into session
|
||||
const state = new IdentityTestState(baseUrl, 'http://mockedredirect/', baseUrl);
|
||||
const url = await state.startSession();
|
||||
let url = await state.startSession();
|
||||
let res = await state.fetchIdp(url);
|
||||
expect(res.status).toBe(200);
|
||||
await state.login(url, settings.email, settings.password);
|
||||
url = await state.login(url, settings.email, settings.password);
|
||||
await state.consent(url);
|
||||
expect(state.session.info?.webId).toBe(webId);
|
||||
|
||||
// Registration still works for this WebID
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('An OidcHttpHandler', (): void => {
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
provider = {
|
||||
callback: jest.fn(),
|
||||
callback: jest.fn().mockReturnValue(jest.fn()),
|
||||
} as any;
|
||||
|
||||
providerFactory = {
|
||||
@@ -26,6 +26,7 @@ describe('An OidcHttpHandler', (): void => {
|
||||
it('sends all requests to the OIDC library.', async(): Promise<void> => {
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
expect(provider.callback).toHaveBeenCalledTimes(1);
|
||||
expect(provider.callback).toHaveBeenLastCalledWith(request, response);
|
||||
expect(provider.callback.mock.results[0].value).toHaveBeenCalledTimes(1);
|
||||
expect(provider.callback.mock.results[0].value).toHaveBeenLastCalledWith(request, response);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ jest.mock('oidc-provider', (): any => ({
|
||||
|
||||
const routes = {
|
||||
authorization: '/foo/oidc/auth',
|
||||
check_session: '/foo/oidc/session/check',
|
||||
backchannel_authentication: '/foo/oidc/backchannel',
|
||||
code_verification: '/foo/oidc/device',
|
||||
device_authorization: '/foo/oidc/device/auth',
|
||||
end_session: '/foo/oidc/session/end',
|
||||
@@ -100,23 +100,32 @@ describe('An IdentityProviderFactory', (): void => {
|
||||
expect(adapterFactory.createStorageAdapter).toHaveBeenLastCalledWith('test!');
|
||||
|
||||
expect(config.cookies?.keys).toEqual([ expect.any(String) ]);
|
||||
expect(config.jwks).toEqual({ keys: [ expect.objectContaining({ kty: 'RSA' }) ]});
|
||||
expect(config.jwks).toEqual({ keys: [ expect.objectContaining({ alg: 'ES256' }) ]});
|
||||
expect(config.routes).toEqual(routes);
|
||||
expect(config.pkce?.methods).toEqual([ 'S256' ]);
|
||||
expect((config.pkce!.required as any)()).toBe(true);
|
||||
expect(config.clientDefaults?.id_token_signed_response_alg).toBe('ES256');
|
||||
|
||||
await expect((config.interactions?.url as any)(ctx, oidcInteraction)).resolves.toBe(redirectUrl);
|
||||
expect((config.audiences as any)(null, null, {}, 'access_token')).toBe('solid');
|
||||
expect((config.audiences as any)(null, null, { clientId: 'clientId' }, 'client_credentials')).toBe('clientId');
|
||||
|
||||
const findResult = await config.findAccount?.({ oidc: { client: { clientId: 'clientId' }}} as any, webId);
|
||||
let findResult = await config.findAccount?.({ oidc: { client: { clientId: 'clientId' }}} as any, webId);
|
||||
expect(findResult?.accountId).toBe(webId);
|
||||
await expect((findResult?.claims as any)()).resolves.toEqual({ sub: webId, webid: webId, azp: 'clientId' });
|
||||
findResult = await config.findAccount?.({ oidc: {}} as any, webId);
|
||||
await expect((findResult?.claims as any)()).resolves.toEqual({ sub: webId, webid: webId });
|
||||
|
||||
expect((config.extraAccessTokenClaims as any)({}, {})).toEqual({});
|
||||
expect((config.extraAccessTokenClaims as any)({}, { kind: 'AccessToken', accountId: webId, clientId: 'clientId' }))
|
||||
.toEqual({
|
||||
webid: webId,
|
||||
client_id: 'clientId',
|
||||
});
|
||||
expect((config.extraTokenClaims as any)({}, {})).toEqual({});
|
||||
expect((config.extraTokenClaims as any)({}, { kind: 'AccessToken', accountId: webId, clientId: 'clientId' }))
|
||||
.toEqual({ webid: webId });
|
||||
|
||||
expect(config.features?.resourceIndicators?.enabled).toBe(true);
|
||||
expect((config.features?.resourceIndicators?.defaultResource as any)()).toBe('http://example.com/');
|
||||
expect((config.features?.resourceIndicators?.getResourceServerInfo as any)()).toEqual({
|
||||
scope: '',
|
||||
audience: 'solid',
|
||||
accessTokenFormat: 'jwt',
|
||||
jwt: { sign: { alg: 'ES256' }},
|
||||
});
|
||||
|
||||
// Test the renderError function
|
||||
const response = { } as HttpResponse;
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import type { Operation } from '../../../../src/http/Operation';
|
||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||
import { CompletingInteractionHandler } from '../../../../src/identity/interaction/CompletingInteractionHandler';
|
||||
import type {
|
||||
Interaction,
|
||||
InteractionHandlerInput,
|
||||
} from '../../../../src/identity/interaction/InteractionHandler';
|
||||
import type {
|
||||
InteractionCompleter,
|
||||
InteractionCompleterInput,
|
||||
} from '../../../../src/identity/interaction/util/InteractionCompleter';
|
||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||
|
||||
const webId = 'http://alice.test.com/card#me';
|
||||
class DummyCompletingInteractionHandler extends CompletingInteractionHandler {
|
||||
public constructor(interactionCompleter: InteractionCompleter) {
|
||||
super({}, interactionCompleter);
|
||||
}
|
||||
|
||||
public async getCompletionParameters(input: Required<InteractionHandlerInput>): Promise<InteractionCompleterInput> {
|
||||
return { webId, oidcInteraction: input.oidcInteraction };
|
||||
}
|
||||
}
|
||||
|
||||
describe('A CompletingInteractionHandler', (): void => {
|
||||
const oidcInteraction: Interaction = {} as any;
|
||||
const location = 'http://test.com/redirect';
|
||||
let operation: Operation;
|
||||
let interactionCompleter: jest.Mocked<InteractionCompleter>;
|
||||
let handler: DummyCompletingInteractionHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
const representation = new BasicRepresentation('', 'application/json');
|
||||
operation = {
|
||||
method: 'POST',
|
||||
body: representation,
|
||||
} as any;
|
||||
|
||||
interactionCompleter = {
|
||||
handleSafe: jest.fn().mockResolvedValue(location),
|
||||
} as any;
|
||||
|
||||
handler = new DummyCompletingInteractionHandler(interactionCompleter);
|
||||
});
|
||||
|
||||
it('calls the parent JSON canHandle check.', async(): Promise<void> => {
|
||||
operation.body.metadata.contentType = 'application/x-www-form-urlencoded';
|
||||
await expect(handler.canHandle({ operation, oidcInteraction } as any)).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
|
||||
it('can handle GET requests without interaction.', async(): Promise<void> => {
|
||||
operation.method = 'GET';
|
||||
await expect(handler.canHandle({ operation } as any)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
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 })).rejects.toThrow(error);
|
||||
|
||||
await expect(handler.canHandle({ operation, oidcInteraction })).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws a redirect error with the completer location.', async(): Promise<void> => {
|
||||
const error = expect.objectContaining({
|
||||
statusCode: 302,
|
||||
location,
|
||||
});
|
||||
await expect(handler.handle({ operation, oidcInteraction })).rejects.toThrow(error);
|
||||
expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(interactionCompleter.handleSafe).toHaveBeenLastCalledWith({ oidcInteraction, webId });
|
||||
});
|
||||
});
|
||||
142
test/unit/identity/interaction/ConsentHandler.test.ts
Normal file
142
test/unit/identity/interaction/ConsentHandler.test.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
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 { 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';
|
||||
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-next-line @typescript-eslint/naming-convention
|
||||
Grant: grantFn,
|
||||
} 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('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' ]);
|
||||
});
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
import { ExistingLoginHandler } from '../../../../src/identity/interaction/ExistingLoginHandler';
|
||||
import type { Interaction } from '../../../../src/identity/interaction/InteractionHandler';
|
||||
import type {
|
||||
InteractionCompleter,
|
||||
} from '../../../../src/identity/interaction/util/InteractionCompleter';
|
||||
import { FoundHttpError } from '../../../../src/util/errors/FoundHttpError';
|
||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||
import { createPostJsonOperation } from './email-password/handler/Util';
|
||||
|
||||
describe('An ExistingLoginHandler', (): void => {
|
||||
const webId = 'http://test.com/id#me';
|
||||
let oidcInteraction: Interaction;
|
||||
let interactionCompleter: jest.Mocked<InteractionCompleter>;
|
||||
let handler: ExistingLoginHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
oidcInteraction = { session: { accountId: webId }} as any;
|
||||
|
||||
interactionCompleter = {
|
||||
handleSafe: jest.fn().mockResolvedValue('http://test.com/redirect'),
|
||||
} as any;
|
||||
|
||||
handler = new ExistingLoginHandler(interactionCompleter);
|
||||
});
|
||||
|
||||
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('returns the correct completion parameters.', async(): Promise<void> => {
|
||||
const operation = createPostJsonOperation({ remember: true });
|
||||
await expect(handler.handle({ operation, oidcInteraction })).rejects.toThrow(FoundHttpError);
|
||||
expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(interactionCompleter.handleSafe).toHaveBeenLastCalledWith({ oidcInteraction, webId, shouldRemember: true });
|
||||
});
|
||||
});
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
ForgotPasswordHandler,
|
||||
} from '../../../../../../src/identity/interaction/email-password/handler/ForgotPasswordHandler';
|
||||
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
|
||||
import type { EmailSender } from '../../../../../../src/identity/interaction/email-password/util/EmailSender';
|
||||
import type { InteractionRoute } from '../../../../../../src/identity/interaction/routing/InteractionRoute';
|
||||
import type { EmailSender } from '../../../../../../src/identity/interaction/util/EmailSender';
|
||||
import { readJsonStream } from '../../../../../../src/util/StreamUtil';
|
||||
import type { TemplateEngine } from '../../../../../../src/util/templates/TemplateEngine';
|
||||
import { createPostJsonOperation } from './Util';
|
||||
|
||||
@@ -4,22 +4,23 @@ import type {
|
||||
Interaction,
|
||||
InteractionHandlerInput,
|
||||
} from '../../../../../../src/identity/interaction/InteractionHandler';
|
||||
import type {
|
||||
InteractionCompleter,
|
||||
} from '../../../../../../src/identity/interaction/util/InteractionCompleter';
|
||||
import { FoundHttpError } from '../../../../../../src/util/errors/FoundHttpError';
|
||||
import { createPostJsonOperation } from './Util';
|
||||
|
||||
describe('A LoginHandler', (): void => {
|
||||
const webId = 'http://alice.test.com/card#me';
|
||||
const email = 'alice@test.email';
|
||||
const oidcInteraction: Interaction = {} as any;
|
||||
let oidcInteraction: jest.Mocked<Interaction>;
|
||||
let input: Required<InteractionHandlerInput>;
|
||||
let accountStore: jest.Mocked<AccountStore>;
|
||||
let interactionCompleter: jest.Mocked<InteractionCompleter>;
|
||||
let handler: LoginHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
oidcInteraction = {
|
||||
exp: 123456,
|
||||
save: jest.fn(),
|
||||
} as any;
|
||||
|
||||
input = { oidcInteraction } as any;
|
||||
|
||||
accountStore = {
|
||||
@@ -27,11 +28,18 @@ describe('A LoginHandler', (): void => {
|
||||
getSettings: jest.fn().mockResolvedValue({ useIdp: true }),
|
||||
} as any;
|
||||
|
||||
interactionCompleter = {
|
||||
handleSafe: jest.fn().mockResolvedValue('http://test.com/redirect'),
|
||||
} as any;
|
||||
handler = new LoginHandler(accountStore);
|
||||
});
|
||||
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);
|
||||
|
||||
handler = new LoginHandler(accountStore, interactionCompleter);
|
||||
await expect(handler.canHandle({ operation: createPostJsonOperation({}), oidcInteraction }))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('errors on invalid emails.', async(): Promise<void> => {
|
||||
@@ -61,13 +69,13 @@ describe('A LoginHandler', (): void => {
|
||||
.rejects.toThrow('This server is not an identity provider for this account.');
|
||||
});
|
||||
|
||||
it('returns the correct completion parameters.', async(): Promise<void> => {
|
||||
it('returns the generated redirect URL.', async(): Promise<void> => {
|
||||
input.operation = createPostJsonOperation({ email, password: 'password!' });
|
||||
await expect(handler.handle(input)).rejects.toThrow(FoundHttpError);
|
||||
|
||||
expect(accountStore.authenticate).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.authenticate).toHaveBeenLastCalledWith(email, 'password!');
|
||||
expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(interactionCompleter.handleSafe).toHaveBeenLastCalledWith({ oidcInteraction, webId, shouldRemember: false });
|
||||
expect(oidcInteraction.save).toHaveBeenCalledTimes(1);
|
||||
expect(oidcInteraction.result).toEqual({ login: { accountId: webId, remember: false }});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { EmailSenderArgs } from '../../../../../src/identity/interaction/util/BaseEmailSender';
|
||||
import { BaseEmailSender } from '../../../../../src/identity/interaction/util/BaseEmailSender';
|
||||
import type { EmailArgs } from '../../../../../src/identity/interaction/util/EmailSender';
|
||||
import type { EmailSenderArgs } from '../../../../../../src/identity/interaction/email-password/util/BaseEmailSender';
|
||||
import { BaseEmailSender } from '../../../../../../src/identity/interaction/email-password/util/BaseEmailSender';
|
||||
import type { EmailArgs } from '../../../../../../src/identity/interaction/email-password/util/EmailSender';
|
||||
jest.mock('nodemailer');
|
||||
|
||||
describe('A BaseEmailSender', (): void => {
|
||||
@@ -1,56 +0,0 @@
|
||||
import type { Interaction } from '../../../../../src/identity/interaction/InteractionHandler';
|
||||
import { BaseInteractionCompleter } from '../../../../../src/identity/interaction/util/BaseInteractionCompleter';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('A BaseInteractionCompleter', (): void => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const webId = 'http://alice.test.com/#me';
|
||||
let oidcInteraction: jest.Mocked<Interaction>;
|
||||
let completer: BaseInteractionCompleter;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
oidcInteraction = {
|
||||
lastSubmission: {},
|
||||
exp: now + 500,
|
||||
returnTo: 'http://test.com/redirect',
|
||||
save: jest.fn(),
|
||||
} as any;
|
||||
|
||||
completer = new BaseInteractionCompleter();
|
||||
});
|
||||
|
||||
it('stores the correct data in the interaction.', async(): Promise<void> => {
|
||||
await expect(completer.handle({ oidcInteraction, webId, shouldRemember: true }))
|
||||
.resolves.toBe(oidcInteraction.returnTo);
|
||||
expect(oidcInteraction.result).toEqual({
|
||||
login: {
|
||||
account: webId,
|
||||
remember: true,
|
||||
ts: now,
|
||||
},
|
||||
consent: {
|
||||
rejectedScopes: [],
|
||||
},
|
||||
});
|
||||
expect(oidcInteraction.save).toHaveBeenCalledTimes(1);
|
||||
expect(oidcInteraction.save).toHaveBeenLastCalledWith(500);
|
||||
});
|
||||
|
||||
it('rejects offline access if shouldRemember is false.', async(): Promise<void> => {
|
||||
await expect(completer.handle({ oidcInteraction, webId, shouldRemember: false }))
|
||||
.resolves.toBe(oidcInteraction.returnTo);
|
||||
expect(oidcInteraction.result).toEqual({
|
||||
login: {
|
||||
account: webId,
|
||||
remember: false,
|
||||
ts: now,
|
||||
},
|
||||
consent: {
|
||||
rejectedScopes: [ 'offline_access' ],
|
||||
},
|
||||
});
|
||||
expect(oidcInteraction.save).toHaveBeenCalledTimes(1);
|
||||
expect(oidcInteraction.save).toHaveBeenLastCalledWith(500);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user