feat: Pass optional Interaction to InteractionHandlers

This commit is contained in:
Joachim Van Herwegen
2021-08-03 15:57:11 +02:00
parent 66accacde8
commit d3de5f3114
14 changed files with 113 additions and 119 deletions

View File

@@ -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 : {};

View File

@@ -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 },
};
}
}

View File

@@ -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);

View File

@@ -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> {}

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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',