Repository: solid/community-server
Pull request #889 (2021-08-03 14:05:09)

feat: Pass optional Interaction to InteractionHandlers
No longer passes the Response object to the InteractionHandlers. Instead passes an optional Interaction object that will be defined if the OIDC library is waiting on us to finish up an interaction (specifically: we're trying to log a user in).

@RubenVerborgh That Interaction object can be used in the RegistrationHandler to solve #880 in case you want to that fixed before 1.0.0. Just need to check if it is defined or not.


Patch:
diff --git a/config/identity/handler/interaction/routes/session.json b/config/identity/handler/interaction/routes/session.json
index 8e16f17f0..8b2fe5f8a 100644
--- a/config/identity/handler/interaction/routes/session.json
+++ b/config/identity/handler/interaction/routes/session.json
@@ -8,10 +8,7 @@
       "route": "^/confirm/?$",
       "prompt": "consent",
       "viewTemplate": "@css:templates/identity/email-password/confirm.html.ejs",
-      "handler": {
-        "@type": "SessionHttpHandler",
-        "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }
-      }
+      "handler": { "@type": "SessionHttpHandler" }
     }
   ]
 }
diff --git a/src/identity/IdentityProviderHttpHandler.ts b/src/identity/IdentityProviderHttpHandler.ts
index afe34774c..deaeb6761 100644
--- a/src/identity/IdentityProviderHttpHandler.ts
+++ b/src/identity/IdentityProviderHttpHandler.ts
@@ -11,9 +11,11 @@ import { assertError, createErrorMessage } from '../util/errors/ErrorUtil';
 import { InternalServerError } from '../util/errors/InternalServerError';
 import { trimTrailingSlashes } from '../util/PathUtil';
 import type { ProviderFactory } from './configuration/ProviderFactory';
-import type { InteractionHandler,
-  InteractionHandlerResult } from './interaction/email-password/handler/InteractionHandler';
-
+import type {
+  Interaction,
+  InteractionHandler,
+  InteractionHandlerResult,
+} from './interaction/email-password/handler/InteractionHandler';
 import { IdpInteractionError } from './interaction/util/IdpInteractionError';
 import type { InteractionCompleter } from './interaction/util/InteractionCompleter';
 
@@ -118,33 +120,40 @@ export class IdentityProviderHttpHandler extends HttpHandler {
    * Finds the matching route and resolves the request.
    */
   private async handleRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
+    // This being defined means we're in an OIDC session
+    let oidcInteraction: Interaction | undefined;
+    try {
+      const provider = await this.providerFactory.getProvider();
+      oidcInteraction = await provider.interactionDetails(request, response);
+    } catch {
+      // Just a regular request
+    }
+
     // If our own interaction handler does not support the input, it is either invalid or a request for the OIDC library
-    const route = await this.findRoute(request, response);
+    const route = await this.findRoute(request, oidcInteraction);
     if (!route) {
       const provider = await this.providerFactory.getProvider();
       this.logger.debug(`Sending request to oidc-provider: ${request.url}`);
       return provider.callback(request, response);
     }
 
-    await this.resolveRoute(request, response, route);
+    await this.resolveRoute(request, response, route, oidcInteraction);
   }
 
   /**
    * Finds a route that supports the given request.
    */
-  private async findRoute(request: HttpRequest, response: HttpResponse): Promise<InteractionRoute | undefined> {
+  private async findRoute(request: HttpRequest, oidcInteraction?: Interaction): Promise<InteractionRoute | undefined> {
     if (!request.url || !request.url.startsWith(this.idpPath)) {
       // This is either an invalid request or a call to the .well-known configuration
       return;
     }
-    const url = request.url.slice(this.idpPath.length);
-    let route = this.getRouteMatch(url);
+    const pathName = request.url.slice(this.idpPath.length);
+    let route = this.getRouteMatch(pathName);
 
     // In case the request targets the IDP entry point the prompt determines where to go
-    if (!route && (url === '/' || url === '')) {
-      const provider = await this.providerFactory.getProvider();
-      const interactionDetails = await provider.interactionDetails(request, response);
-      route = this.getPromptMatch(interactionDetails.prompt.name);
+    if (!route && oidcInteraction && trimTrailingSlashes(pathName).length === 0) {
+      route = this.getPromptMatch(oidcInteraction.prompt.name);
     }
     return route;
   }
@@ -155,7 +164,8 @@ export class IdentityProviderHttpHandler extends HttpHandler {
    *
    * GET requests go to the templateHandler, POST requests to the specific InteractionHandler of the route.
    */
-  private async resolveRoute(request: HttpRequest, response: HttpResponse, route: InteractionRoute): Promise<void> {
+  private async resolveRoute(request: HttpRequest, response: HttpResponse, route: InteractionRoute,
+    oidcInteraction?: Interaction): Promise<void> {
     if (request.method === 'GET') {
       // .ejs templates errors on undefined variables
       return await this.handleTemplateResponse(response, route.viewTemplate, { errorMessage: '', prefilled: {}});
@@ -164,7 +174,7 @@ export class IdentityProviderHttpHandler extends HttpHandler {
     if (request.method === 'POST') {
       let result: InteractionHandlerResult;
       try {
-        result = await route.handler.handleSafe({ request, response });
+        result = await route.handler.handleSafe({ request, oidcInteraction });
       } catch (error: unknown) {
         // Render error in the view
         const prefilled = IdpInteractionError.isInstance(error) ? error.prefilled : {};
diff --git a/src/identity/interaction/SessionHttpHandler.ts b/src/identity/interaction/SessionHttpHandler.ts
index f74ec00c8..7980ea2ee 100644
--- a/src/identity/interaction/SessionHttpHandler.ts
+++ b/src/identity/interaction/SessionHttpHandler.ts
@@ -1,29 +1,18 @@
-import type { HttpHandlerInput } from '../../server/HttpHandler';
 import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
-import type { ProviderFactory } from '../configuration/ProviderFactory';
 import { InteractionHandler } from './email-password/handler/InteractionHandler';
-import type { InteractionCompleteResult } from './email-password/handler/InteractionHandler';
+import type { InteractionCompleteResult, InteractionHandlerInput } from './email-password/handler/InteractionHandler';
 
 /**
  * Simple InteractionHttpHandler that sends the session accountId to the InteractionCompleter as webId.
  */
 export class SessionHttpHandler extends InteractionHandler {
-  private readonly providerFactory: ProviderFactory;
-
-  public constructor(providerFactory: ProviderFactory) {
-    super();
-    this.providerFactory = providerFactory;
-  }
-
-  public async handle(input: HttpHandlerInput): Promise<InteractionCompleteResult> {
-    const provider = await this.providerFactory.getProvider();
-    const details = await provider.interactionDetails(input.request, input.response);
-    if (!details.session || !details.session.accountId) {
-      throw new NotImplementedHttpError('Only confirm actions with a session and accountId are supported');
+  public async handle({ oidcInteraction }: InteractionHandlerInput): Promise<InteractionCompleteResult> {
+    if (!oidcInteraction?.session) {
+      throw new NotImplementedHttpError('Only interactions with a valid session are supported.');
     }
     return {
       type: 'complete',
-      details: { webId: details.session.accountId },
+      details: { webId: oidcInteraction.session.accountId },
     };
   }
 }
diff --git a/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts b/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts
index c02e87886..211e8800f 100644
--- a/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts
+++ b/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts
@@ -1,7 +1,6 @@
 import assert from 'assert';
 import urljoin from 'url-join';
 import { getLoggerFor } from '../../../../logging/LogUtil';
-import type { HttpHandlerInput } from '../../../../server/HttpHandler';
 import { ensureTrailingSlash } from '../../../../util/PathUtil';
 import type { TemplateEngine } from '../../../../util/templates/TemplateEngine';
 import type { EmailSender } from '../../util/EmailSender';
@@ -9,7 +8,7 @@ import { getFormDataRequestBody } from '../../util/FormDataUtil';
 import { throwIdpInteractionError } from '../EmailPasswordUtil';
 import type { AccountStore } from '../storage/AccountStore';
 import { InteractionHandler } from './InteractionHandler';
-import type { InteractionResponseResult } from './InteractionHandler';
+import type { InteractionResponseResult, InteractionHandlerInput } from './InteractionHandler';
 
 export interface ForgotPasswordHandlerArgs {
   accountStore: AccountStore;
@@ -40,10 +39,10 @@ export class ForgotPasswordHandler extends InteractionHandler {
     this.emailSender = args.emailSender;
   }
 
-  public async handle(input: HttpHandlerInput): Promise<InteractionResponseResult<{ email: string }>> {
+  public async handle({ request }: InteractionHandlerInput): Promise<InteractionResponseResult<{ email: string }>> {
     try {
       // Validate incoming data
-      const { email } = await getFormDataRequestBody(input.request);
+      const { email } = await getFormDataRequestBody(request);
       assert(typeof email === 'string' && email.length > 0, 'Email required');
 
       await this.resetPassword(email);
diff --git a/src/identity/interaction/email-password/handler/InteractionHandler.ts b/src/identity/interaction/email-password/handler/InteractionHandler.ts
index bc747b0cf..58a7cb2c8 100644
--- a/src/identity/interaction/email-password/handler/InteractionHandler.ts
+++ b/src/identity/interaction/email-password/handler/InteractionHandler.ts
@@ -1,7 +1,23 @@
-import type { HttpHandlerInput } from '../../../../server/HttpHandler';
+import type { KoaContextWithOIDC } from 'oidc-provider';
+import type { HttpRequest } from '../../../../server/HttpRequest';
 import { AsyncHandler } from '../../../../util/handlers/AsyncHandler';
 import type { InteractionCompleterParams } from '../../util/InteractionCompleter';
 
+// OIDC library does not directly export the Interaction type
+export type Interaction = KoaContextWithOIDC['oidc']['entities']['Interaction'];
+
+export interface InteractionHandlerInput {
+  /**
+   * The request being made.
+   */
+  request: HttpRequest;
+  /**
+   * Will be defined if the OIDC library expects us to resolve an interaction it can't handle itself,
+   * such as logging a user in.
+   */
+  oidcInteraction?: Interaction;
+}
+
 export type InteractionHandlerResult = InteractionResponseResult | InteractionCompleteResult;
 
 export interface InteractionResponseResult<T = NodeJS.Dict<any>> {
@@ -17,4 +33,4 @@ export interface InteractionCompleteResult {
 /**
  * Handler used for IDP interactions.
  */
-export abstract class InteractionHandler extends AsyncHandler<HttpHandlerInput, InteractionHandlerResult> {}
+export abstract class InteractionHandler extends AsyncHandler<InteractionHandlerInput, InteractionHandlerResult> {}
diff --git a/src/identity/interaction/email-password/handler/LoginHandler.ts b/src/identity/interaction/email-password/handler/LoginHandler.ts
index 579542c07..a5289cb09 100644
--- a/src/identity/interaction/email-password/handler/LoginHandler.ts
+++ b/src/identity/interaction/email-password/handler/LoginHandler.ts
@@ -1,12 +1,11 @@
 import assert from 'assert';
 import { getLoggerFor } from '../../../../logging/LogUtil';
-import type { HttpHandlerInput } from '../../../../server/HttpHandler';
 import type { HttpRequest } from '../../../../server/HttpRequest';
 import { getFormDataRequestBody } from '../../util/FormDataUtil';
 import { throwIdpInteractionError } from '../EmailPasswordUtil';
 import type { AccountStore } from '../storage/AccountStore';
 import { InteractionHandler } from './InteractionHandler';
-import type { InteractionCompleteResult } from './InteractionHandler';
+import type { InteractionCompleteResult, InteractionHandlerInput } from './InteractionHandler';
 
 /**
  * Handles the submission of the Login Form and logs the user in.
@@ -21,8 +20,8 @@ export class LoginHandler extends InteractionHandler {
     this.accountStore = accountStore;
   }
 
-  public async handle(input: HttpHandlerInput): Promise<InteractionCompleteResult> {
-    const { email, password, remember } = await this.parseInput(input.request);
+  public async handle({ request }: InteractionHandlerInput): Promise<InteractionCompleteResult> {
+    const { email, password, remember } = await this.parseInput(request);
     try {
       // Try to log in, will error if email/password combination is invalid
       const webId = await this.accountStore.authenticate(email, password);
diff --git a/src/identity/interaction/email-password/handler/RegistrationHandler.ts b/src/identity/interaction/email-password/handler/RegistrationHandler.ts
index ded5dda54..16b78de21 100644
--- a/src/identity/interaction/email-password/handler/RegistrationHandler.ts
+++ b/src/identity/interaction/email-password/handler/RegistrationHandler.ts
@@ -5,13 +5,12 @@ import { getLoggerFor } from '../../../../logging/LogUtil';
 import type { IdentifierGenerator } from '../../../../pods/generate/IdentifierGenerator';
 import type { PodManager } from '../../../../pods/PodManager';
 import type { PodSettings } from '../../../../pods/settings/PodSettings';
-import type { HttpHandlerInput } from '../../../../server/HttpHandler';
 import type { HttpRequest } from '../../../../server/HttpRequest';
 import type { OwnershipValidator } from '../../../ownership/OwnershipValidator';
 import { getFormDataRequestBody } from '../../util/FormDataUtil';
 import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil';
 import type { AccountStore } from '../storage/AccountStore';
-import type { InteractionResponseResult } from './InteractionHandler';
+import type { InteractionResponseResult, InteractionHandlerInput } from './InteractionHandler';
 import { InteractionHandler } from './InteractionHandler';
 
 const emailRegex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/u;
@@ -103,7 +102,7 @@ export class RegistrationHandler extends InteractionHandler {
     this.podManager = args.podManager;
   }
 
-  public async handle({ request }: HttpHandlerInput): Promise<InteractionResponseResult<RegistrationResponse>> {
+  public async handle({ request }: InteractionHandlerInput): Promise<InteractionResponseResult<RegistrationResponse>> {
     const result = await this.parseInput(request);
 
     try {
diff --git a/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts b/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts
index e47a88c40..647149d88 100644
--- a/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts
+++ b/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts
@@ -1,10 +1,9 @@
 import assert from 'assert';
 import { getLoggerFor } from '../../../../logging/LogUtil';
-import type { HttpHandlerInput } from '../../../../server/HttpHandler';
 import { getFormDataRequestBody } from '../../util/FormDataUtil';
 import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil';
 import type { AccountStore } from '../storage/AccountStore';
-import type { InteractionResponseResult } from './InteractionHandler';
+import type { InteractionResponseResult, InteractionHandlerInput } from './InteractionHandler';
 import { InteractionHandler } from './InteractionHandler';
 
 /**
@@ -21,12 +20,12 @@ export class ResetPasswordHandler extends InteractionHandler {
     this.accountStore = accountStore;
   }
 
-  public async handle(input: HttpHandlerInput): Promise<InteractionResponseResult> {
+  public async handle({ request }: InteractionHandlerInput): Promise<InteractionResponseResult> {
     try {
       // Extract record ID from request URL
-      const recordId = /\/([^/]+)$/u.exec(input.request.url!)?.[1];
+      const recordId = /\/([^/]+)$/u.exec(request.url!)?.[1];
       // Validate input data
-      const { password, confirmPassword } = await getFormDataRequestBody(input.request);
+      const { password, confirmPassword } = await getFormDataRequestBody(request);
       assert(
         typeof recordId === 'string' && recordId.length > 0,
         'Invalid request. Open the link from your email again',


Test patch:
diff --git a/test/unit/identity/IdentityProviderHttpHandler.test.ts b/test/unit/identity/IdentityProviderHttpHandler.test.ts
index 8722ad077..eec635f49 100644
--- a/test/unit/identity/IdentityProviderHttpHandler.test.ts
+++ b/test/unit/identity/IdentityProviderHttpHandler.test.ts
@@ -93,7 +93,7 @@ describe('An IdentityProviderHttpHandler', (): void => {
     request.method = 'POST';
     await expect(handler.handle({ request, response })).resolves.toBeUndefined();
     expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1);
-    expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request, response });
+    expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request });
     expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1);
     expect(templateHandler.handleSafe).toHaveBeenLastCalledWith(
       { response, templateFile: routes.response.responseTemplate, contents: { key: 'val' }},
@@ -106,7 +106,7 @@ describe('An IdentityProviderHttpHandler', (): void => {
     (routes.response.handler as jest.Mocked<InteractionHandler>).handleSafe.mockResolvedValueOnce({ type: 'response' });
     await expect(handler.handle({ request, response })).resolves.toBeUndefined();
     expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1);
-    expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request, response });
+    expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request });
     expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1);
     expect(templateHandler.handleSafe).toHaveBeenLastCalledWith(
       { response, templateFile: routes.response.responseTemplate, contents: {}},
@@ -118,7 +118,7 @@ describe('An IdentityProviderHttpHandler', (): void => {
     request.method = 'POST';
     await expect(handler.handle({ request, response })).resolves.toBeUndefined();
     expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(1);
-    expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ request, response });
+    expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ request });
     expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(1);
     expect(interactionCompleter.handleSafe).toHaveBeenLastCalledWith({ request, response, webId: 'webId' });
   });
@@ -126,18 +126,22 @@ describe('An IdentityProviderHttpHandler', (): void => {
   it('matches paths based on prompt for requests to the root IDP.', async(): Promise<void> => {
     request.url = '/idp';
     request.method = 'POST';
-    provider.interactionDetails.mockResolvedValueOnce({ prompt: { name: 'other' }} as any);
+    const oidcInteraction = { prompt: { name: 'other' }};
+    provider.interactionDetails.mockResolvedValueOnce(oidcInteraction as any);
     await expect(handler.handle({ request, response })).resolves.toBeUndefined();
     expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(0);
     expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(1);
+    expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ request, oidcInteraction });
   });
 
   it('uses the default route for requests to the root IDP without (matching) prompt.', async(): Promise<void> => {
     request.url = '/idp';
     request.method = 'POST';
-    provider.interactionDetails.mockResolvedValueOnce({ prompt: { name: 'notSupported' }} as any);
+    const oidcInteraction = { prompt: { name: 'notSupported' }};
+    provider.interactionDetails.mockResolvedValueOnce(oidcInteraction as any);
     await expect(handler.handle({ request, response })).resolves.toBeUndefined();
     expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1);
+    expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request, oidcInteraction });
     expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(0);
   });
 
diff --git a/test/unit/identity/interaction/SessionHttpHandler.test.ts b/test/unit/identity/interaction/SessionHttpHandler.test.ts
index fe3311a68..41b300205 100644
--- a/test/unit/identity/interaction/SessionHttpHandler.test.ts
+++ b/test/unit/identity/interaction/SessionHttpHandler.test.ts
@@ -1,45 +1,31 @@
-import type { Provider } from 'oidc-provider';
-import type { ProviderFactory } from '../../../../src/identity/configuration/ProviderFactory';
+import type { Interaction } from '../../../../src/identity/interaction/email-password/handler/InteractionHandler';
 import { SessionHttpHandler } from '../../../../src/identity/interaction/SessionHttpHandler';
 import type { HttpRequest } from '../../../../src/server/HttpRequest';
-import type { HttpResponse } from '../../../../src/server/HttpResponse';
 import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
 
 describe('A SessionHttpHandler', (): void => {
   const request: HttpRequest = {} as any;
-  const response: HttpResponse = {} as any;
   const webId = 'http://test.com/id#me';
-  let details: any = {};
-  let provider: Provider;
+  let oidcInteraction: Interaction;
   let handler: SessionHttpHandler;
 
   beforeEach(async(): Promise<void> => {
-    details = { session: { accountId: webId }};
-    provider = {
-      interactionDetails: jest.fn().mockResolvedValue(details),
-    } as any;
+    oidcInteraction = { session: { accountId: webId }} as any;
 
-    const factory: ProviderFactory = {
-      getProvider: jest.fn().mockResolvedValue(provider),
-    };
-
-    handler = new SessionHttpHandler(factory);
+    handler = new SessionHttpHandler();
   });
 
-  it('requires a session and accountId.', async(): Promise<void> => {
-    details.session = undefined;
-    await expect(handler.handle({ request, response })).rejects.toThrow(NotImplementedHttpError);
+  it('requires a defined oidcInteraction with a session.', async(): Promise<void> => {
+    oidcInteraction!.session = undefined;
+    await expect(handler.handle({ request, oidcInteraction })).rejects.toThrow(NotImplementedHttpError);
 
-    details.session = { accountId: undefined };
-    await expect(handler.handle({ request, response })).rejects.toThrow(NotImplementedHttpError);
+    await expect(handler.handle({ request })).rejects.toThrow(NotImplementedHttpError);
   });
 
-  it('calls the oidc completer with the webId in the session.', async(): Promise<void> => {
-    await expect(handler.handle({ request, response })).resolves.toEqual({
+  it('returns an InteractionCompleteResult when done.', async(): Promise<void> => {
+    await expect(handler.handle({ request, oidcInteraction })).resolves.toEqual({
       details: { webId },
       type: 'complete',
     });
-    expect(provider.interactionDetails).toHaveBeenCalledTimes(1);
-    expect(provider.interactionDetails).toHaveBeenLastCalledWith(request, response);
   });
 });
diff --git a/test/unit/identity/interaction/email-password/handler/ForgotPasswordHandler.test.ts b/test/unit/identity/interaction/email-password/handler/ForgotPasswordHandler.test.ts
index 4cac62f59..e9df67902 100644
--- a/test/unit/identity/interaction/email-password/handler/ForgotPasswordHandler.test.ts
+++ b/test/unit/identity/interaction/email-password/handler/ForgotPasswordHandler.test.ts
@@ -4,13 +4,11 @@ import {
 import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
 import type { EmailSender } from '../../../../../../src/identity/interaction/util/EmailSender';
 import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
-import type { HttpResponse } from '../../../../../../src/server/HttpResponse';
 import type { TemplateEngine } from '../../../../../../src/util/templates/TemplateEngine';
 import { createPostFormRequest } from './Util';
 
 describe('A ForgotPasswordHandler', (): void => {
   let request: HttpRequest;
-  const response: HttpResponse = {} as any;
   const email = 'test@test.email';
   const recordId = '123456';
   const html = `<a href="/base/idp/resetpassword/${recordId}">Reset Password</a>`;
@@ -47,20 +45,20 @@ describe('A ForgotPasswordHandler', (): void => {
 
   it('errors on non-string emails.', async(): Promise<void> => {
     request = createPostFormRequest({});
-    await expect(handler.handle({ request, response })).rejects.toThrow('Email required');
+    await expect(handler.handle({ request })).rejects.toThrow('Email required');
     request = createPostFormRequest({ email: [ 'email', 'email2' ]});
-    await expect(handler.handle({ request, response })).rejects.toThrow('Email required');
+    await expect(handler.handle({ request })).rejects.toThrow('Email required');
   });
 
   it('does not send a mail if a ForgotPassword record could not be generated.', async(): Promise<void> => {
     (accountStore.generateForgotPasswordRecord as jest.Mock).mockRejectedValueOnce('error');
-    await expect(handler.handle({ request, response })).resolves
+    await expect(handler.handle({ request })).resolves
       .toEqual({ type: 'response', details: { email }});
     expect(emailSender.handleSafe).toHaveBeenCalledTimes(0);
   });
 
   it('sends a mail if a ForgotPassword record could be generated.', async(): Promise<void> => {
-    await expect(handler.handle({ request, response })).resolves
+    await expect(handler.handle({ request })).resolves
       .toEqual({ type: 'response', details: { email }});
     expect(emailSender.handleSafe).toHaveBeenCalledTimes(1);
     expect(emailSender.handleSafe).toHaveBeenLastCalledWith({
diff --git a/test/unit/identity/interaction/email-password/handler/LoginHandler.test.ts b/test/unit/identity/interaction/email-password/handler/LoginHandler.test.ts
index f4ab906ed..91bbb6ab4 100644
--- a/test/unit/identity/interaction/email-password/handler/LoginHandler.test.ts
+++ b/test/unit/identity/interaction/email-password/handler/LoginHandler.test.ts
@@ -1,12 +1,14 @@
+import type {
+  InteractionHandlerInput,
+} from '../../../../../../src/identity/interaction/email-password/handler/InteractionHandler';
 import { LoginHandler } from '../../../../../../src/identity/interaction/email-password/handler/LoginHandler';
 import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
-import type { HttpHandlerInput } from '../../../../../../src/server/HttpHandler';
 import { createPostFormRequest } from './Util';
 
 describe('A LoginHandler', (): void => {
   const webId = 'http://alice.test.com/card#me';
   const email = 'alice@test.email';
-  let input: HttpHandlerInput;
+  let input: InteractionHandlerInput;
   let storageAdapter: AccountStore;
   let handler: LoginHandler;
 
diff --git a/test/unit/identity/interaction/email-password/handler/RegistrationHandler.test.ts b/test/unit/identity/interaction/email-password/handler/RegistrationHandler.test.ts
index 8524aae3d..71a6511d7 100644
--- a/test/unit/identity/interaction/email-password/handler/RegistrationHandler.test.ts
+++ b/test/unit/identity/interaction/email-password/handler/RegistrationHandler.test.ts
@@ -10,7 +10,6 @@ import type { IdentifierGenerator } from '../../../../../../src/pods/generate/Id
 import type { PodManager } from '../../../../../../src/pods/PodManager';
 import type { PodSettings } from '../../../../../../src/pods/settings/PodSettings';
 import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
-import type { HttpResponse } from '../../../../../../src/server/HttpResponse';
 import { createPostFormRequest } from './Util';
 
 describe('A RegistrationHandler', (): void => {
@@ -27,7 +26,6 @@ describe('A RegistrationHandler', (): void => {
   const createPod = 'true';
 
   let request: HttpRequest;
-  const response: HttpResponse = {} as any;
 
   const baseUrl = 'http://test.com/';
   const webIdSuffix = '/profile/card';
@@ -72,71 +70,71 @@ describe('A RegistrationHandler', (): void => {
   describe('validating data', (): void => {
     it('rejects array inputs.', async(): Promise<void> => {
       request = createPostFormRequest({ mydata: [ 'a', 'b' ]});
-      await expect(handler.handle({ request, response }))
+      await expect(handler.handle({ request }))
         .rejects.toThrow('Unexpected multiple values for mydata.');
     });
 
     it('errors on invalid emails.', async(): Promise<void> => {
       request = createPostFormRequest({ email: undefined });
-      await expect(handler.handle({ request, response }))
+      await expect(handler.handle({ request }))
         .rejects.toThrow('Please enter a valid e-mail address.');
 
       request = createPostFormRequest({ email: '' });
-      await expect(handler.handle({ request, response }))
+      await expect(handler.handle({ request }))
         .rejects.toThrow('Please enter a valid e-mail address.');
 
       request = createPostFormRequest({ email: 'invalidEmail' });
-      await expect(handler.handle({ request, response }))
+      await expect(handler.handle({ request }))
         .rejects.toThrow('Please enter a valid e-mail address.');
     });
 
     it('errors when a required WebID is not valid.', async(): Promise<void> => {
       request = createPostFormRequest({ email, register, webId: undefined });
-      await expect(handler.handle({ request, response }))
+      await expect(handler.handle({ request }))
         .rejects.toThrow('Please enter a valid WebID.');
 
       request = createPostFormRequest({ email, register, webId: '' });
-      await expect(handler.handle({ request, response }))
+      await expect(handler.handle({ request }))
         .rejects.toThrow('Please enter a valid WebID.');
     });
 
     it('errors on invalid passwords when registering.', async(): Promise<void> => {
       request = createPostFormRequest({ email, webId, password, confirmPassword: 'bad', register });
-      await expect(handler.handle({ request, response }))
+      await expect(handler.handle({ request }))
         .rejects.toThrow('Your password and confirmation did not match.');
     });
 
     it('errors on invalid pod names when required.', async(): Promise<void> => {
       request = createPostFormRequest({ email, webId, createPod, podName: undefined });
-      await expect(handler.handle({ request, response }))
+      await expect(handler.handle({ request }))
         .rejects.toThrow('Please specify a Pod name.');
 
       request = createPostFormRequest({ email, webId, createPod, podName: ' ' });
-      await expect(handler.handle({ request, response }))
+      await expect(handler.handle({ request }))
         .rejects.toThrow('Please specify a Pod name.');
 
       request = createPostFormRequest({ email, webId, createWebId });
-      await expect(handler.handle({ request, response }))
+      await expect(handler.handle({ request }))
         .rejects.toThrow('Please specify a Pod name.');
     });
 
     it('errors when trying to create a WebID without registering or creating a pod.', async(): Promise<void> => {
       request = createPostFormRequest({ email, podName, createWebId });
-      await expect(handler.handle({ request, response }))
+      await expect(handler.handle({ request }))
         .rejects.toThrow('Please enter a password.');
 
       request = createPostFormRequest({ email, podName, createWebId, createPod });
-      await expect(handler.handle({ request, response }))
+      await expect(handler.handle({ request }))
         .rejects.toThrow('Please enter a password.');
 
       request = createPostFormRequest({ email, podName, createWebId, createPod, register });
-      await expect(handler.handle({ request, response }))
+      await expect(handler.handle({ request }))
         .rejects.toThrow('Please enter a password.');
     });
 
     it('errors when no option is chosen.', async(): Promise<void> => {
       request = createPostFormRequest({ email, webId });
-      await expect(handler.handle({ request, response }))
+      await expect(handler.handle({ request }))
         .rejects.toThrow('Please register for a WebID or create a Pod.');
     });
   });
@@ -144,7 +142,7 @@ describe('A RegistrationHandler', (): void => {
   describe('handling data', (): void => {
     it('can register a user.', async(): Promise<void> => {
       request = createPostFormRequest({ email, webId, password, confirmPassword, register });
-      await expect(handler.handle({ request, response })).resolves.toEqual({
+      await expect(handler.handle({ request })).resolves.toEqual({
         details: {
           email,
           webId,
@@ -171,7 +169,7 @@ describe('A RegistrationHandler', (): void => {
     it('can create a pod.', async(): Promise<void> => {
       const params = { email, webId, podName, createPod };
       request = createPostFormRequest(params);
-      await expect(handler.handle({ request, response })).resolves.toEqual({
+      await expect(handler.handle({ request })).resolves.toEqual({
         details: {
           email,
           webId,
@@ -200,7 +198,7 @@ describe('A RegistrationHandler', (): void => {
       const params = { email, webId, password, confirmPassword, podName, register, createPod };
       podSettings.oidcIssuer = baseUrl;
       request = createPostFormRequest(params);
-      await expect(handler.handle({ request, response })).resolves.toEqual({
+      await expect(handler.handle({ request })).resolves.toEqual({
         details: {
           email,
           webId,
@@ -232,7 +230,7 @@ describe('A RegistrationHandler', (): void => {
       podSettings.oidcIssuer = baseUrl;
       request = createPostFormRequest(params);
       (podManager.createPod as jest.Mock).mockRejectedValueOnce(new Error('pod error'));
-      await expect(handler.handle({ request, response })).rejects.toThrow('pod error');
+      await expect(handler.handle({ request })).rejects.toThrow('pod error');
 
       expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
       expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
@@ -255,7 +253,7 @@ describe('A RegistrationHandler', (): void => {
       podSettings.oidcIssuer = baseUrl;
 
       request = createPostFormRequest(params);
-      await expect(handler.handle({ request, response })).resolves.toEqual({
+      await expect(handler.handle({ request })).resolves.toEqual({
         details: {
           email,
           webId: generatedWebID,
@@ -285,7 +283,7 @@ describe('A RegistrationHandler', (): void => {
       const params = { email, webId, podName, createPod };
       request = createPostFormRequest(params);
       (podManager.createPod as jest.Mock).mockRejectedValueOnce(new Error('pod error'));
-      const prom = handler.handle({ request, response });
+      const prom = handler.handle({ request });
       await expect(prom).rejects.toThrow('pod error');
       await expect(prom).rejects.toThrow(IdpInteractionError);
       // Using the cleaned input for prefilled
diff --git a/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts b/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts
index c6bedcd42..bd7204a1f 100644
--- a/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts
+++ b/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts
@@ -3,12 +3,10 @@ import {
 } from '../../../../../../src/identity/interaction/email-password/handler/ResetPasswordHandler';
 import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
 import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
-import type { HttpResponse } from '../../../../../../src/server/HttpResponse';
 import { createPostFormRequest } from './Util';
 
 describe('A ResetPasswordHandler', (): void => {
   let request: HttpRequest;
-  const response: HttpResponse = {} as any;
   const recordId = '123456';
   const url = `/resetURL/${recordId}`;
   const email = 'alice@test.email';
@@ -28,27 +26,27 @@ describe('A ResetPasswordHandler', (): void => {
   it('errors for non-string recordIds.', async(): Promise<void> => {
     const errorMessage = 'Invalid request. Open the link from your email again';
     request = createPostFormRequest({});
-    await expect(handler.handle({ request, response })).rejects.toThrow(errorMessage);
+    await expect(handler.handle({ request })).rejects.toThrow(errorMessage);
     request = createPostFormRequest({}, '');
-    await expect(handler.handle({ request, response })).rejects.toThrow(errorMessage);
+    await expect(handler.handle({ request })).rejects.toThrow(errorMessage);
   });
 
   it('errors for invalid passwords.', async(): Promise<void> => {
     const errorMessage = 'Your password and confirmation did not match.';
     request = createPostFormRequest({ password: 'password!', confirmPassword: 'otherPassword!' }, url);
-    await expect(handler.handle({ request, response })).rejects.toThrow(errorMessage);
+    await expect(handler.handle({ request })).rejects.toThrow(errorMessage);
   });
 
   it('errors for invalid emails.', async(): Promise<void> => {
     const errorMessage = 'This reset password link is no longer valid.';
     request = createPostFormRequest({ password: 'password!', confirmPassword: 'password!' }, url);
     (accountStore.getForgotPasswordRecord as jest.Mock).mockResolvedValueOnce(undefined);
-    await expect(handler.handle({ request, response })).rejects.toThrow(errorMessage);
+    await expect(handler.handle({ request })).rejects.toThrow(errorMessage);
   });
 
   it('renders a message on success.', async(): Promise<void> => {
     request = createPostFormRequest({ password: 'password!', confirmPassword: 'password!' }, url);
-    await expect(handler.handle({ request, response })).resolves.toEqual({ type: 'response' });
+    await expect(handler.handle({ request })).resolves.toEqual({ type: 'response' });
     expect(accountStore.getForgotPasswordRecord).toHaveBeenCalledTimes(1);
     expect(accountStore.getForgotPasswordRecord).toHaveBeenLastCalledWith(recordId);
     expect(accountStore.deleteForgotPasswordRecord).toHaveBeenCalledTimes(1);
@@ -61,6 +59,6 @@ describe('A ResetPasswordHandler', (): void => {
     const errorMessage = 'Unknown error: not native';
     request = createPostFormRequest({ password: 'password!', confirmPassword: 'password!' }, url);
     (accountStore.getForgotPasswordRecord as jest.Mock).mockRejectedValueOnce('not native');
-    await expect(handler.handle({ request, response })).rejects.toThrow(errorMessage);
+    await expect(handler.handle({ request })).rejects.toThrow(errorMessage);
   });
 });