feat: Move OIDC library behaviour to separate path

This commit is contained in:
Joachim Van Herwegen
2021-11-09 11:55:49 +01:00
parent 11192ed4df
commit 520e4fe42f
16 changed files with 121 additions and 47 deletions

View File

@@ -10,6 +10,7 @@ import { OperationHttpHandler } from '../server/OperationHttpHandler';
import type { RepresentationConverter } from '../storage/conversion/RepresentationConverter';
import { APPLICATION_JSON } from '../util/ContentTypes';
import { BadRequestHttpError } from '../util/errors/BadRequestHttpError';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { joinUrl, trimTrailingSlashes } from '../util/PathUtil';
import { addTemplateMetadata, cloneRepresentation } from '../util/ResourceUtil';
import { readJsonStream } from '../util/StreamUtil';
@@ -77,8 +78,6 @@ export class IdentityProviderHttpHandler extends OperationHttpHandler {
private readonly controls: Record<string, string>;
public constructor(args: IdentityProviderHttpHandlerArgs) {
// It is important that the RequestParser does not read out the Request body stream.
// Otherwise we can't pass it anymore to the OIDC library when needed.
super();
// Trimming trailing slashes so the relative URL starts with a slash after slicing this off
this.baseUrl = trimTrailingSlashes(joinUrl(args.baseUrl, args.idpPath));
@@ -97,29 +96,19 @@ export class IdentityProviderHttpHandler extends OperationHttpHandler {
/**
* Finds the matching route and resolves the operation.
*/
public async handle({ operation, request, response }: OperationHttpHandlerInput):
Promise<ResponseDescription | undefined> {
public async handle({ operation, request, response }: OperationHttpHandlerInput): Promise<ResponseDescription> {
// This being defined means we're in an OIDC session
let oidcInteraction: Interaction | undefined;
try {
const provider = await this.providerFactory.getProvider();
// This being defined means we're in an OIDC session
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(operation, oidcInteraction);
if (!route) {
const provider = await this.providerFactory.getProvider();
this.logger.debug(`Sending request to oidc-provider: ${request.url}`);
// Even though the typings do not indicate this, this is a Promise that needs to be awaited.
// Otherwise the `BaseHttpServerFactory` will write a 404 before the OIDC library could handle the response.
// eslint-disable-next-line @typescript-eslint/await-thenable
await provider.callback(request, response);
return;
throw new NotFoundHttpError();
}
// Cloning input data so it can be sent back in case of errors
@@ -149,7 +138,7 @@ export class IdentityProviderHttpHandler extends OperationHttpHandler {
*/
private async findRoute(operation: Operation, oidcInteraction?: Interaction): Promise<InteractionRoute | undefined> {
if (!operation.target.path.startsWith(this.baseUrl)) {
// This is either an invalid request or a call to the .well-known configuration
// This is an invalid request
return;
}
const pathName = operation.target.path.slice(this.baseUrl.length);

View File

@@ -0,0 +1,27 @@
import { getLoggerFor } from '../logging/LogUtil';
import type { HttpHandlerInput } from '../server/HttpHandler';
import { HttpHandler } from '../server/HttpHandler';
import type { ProviderFactory } from './configuration/ProviderFactory';
/**
* HTTP handler that redirects all requests to the OIDC library.
*/
export class OidcHttpHandler extends HttpHandler {
protected readonly logger = getLoggerFor(this);
private readonly providerFactory: ProviderFactory;
public constructor(providerFactory: ProviderFactory) {
super();
this.providerFactory = providerFactory;
}
public async handle({ request, response }: HttpHandlerInput): Promise<void> {
const provider = await this.providerFactory.getProvider();
this.logger.debug(`Sending request to oidc-provider: ${request.url}`);
// Even though the typings do not indicate this, this is a Promise that needs to be awaited.
// Otherwise the `BaseHttpServerFactory` will write a 404 before the OIDC library could handle the response.
// eslint-disable-next-line @typescript-eslint/await-thenable
await provider.callback(request, response);
}
}

View File

@@ -30,7 +30,11 @@ export interface IdentityProviderFactoryArgs {
*/
baseUrl: string;
/**
* Path of the IDP component in the server.
* Path for all requests targeting the OIDC library.
*/
oidcPath: string;
/**
* The entry point for the custom IDP handlers of the server.
* Should start with a slash.
*/
idpPath: string;
@@ -62,6 +66,7 @@ export class IdentityProviderFactory implements ProviderFactory {
private readonly config: Configuration;
private readonly adapterFactory!: AdapterFactory;
private readonly baseUrl!: string;
private readonly oidcPath!: string;
private readonly idpPath!: string;
private readonly storage!: KeyValueStorage<string, unknown>;
private readonly errorHandler!: ErrorHandler;
@@ -107,6 +112,7 @@ export class IdentityProviderFactory implements ProviderFactory {
// Allow provider to interpret reverse proxy headers
const provider = new Provider(this.baseUrl, config);
provider.proxy = true;
return provider;
}
@@ -210,11 +216,11 @@ export class IdentityProviderFactory implements ProviderFactory {
/**
* Creates the route string as required by the `oidc-provider` library.
* In case base URL is `http://test.com/foo/`, `idpPath` is `/idp` and `relative` is `device/auth`,
* In case base URL is `http://test.com/foo/`, `oidcPath` is `/idp` and `relative` is `device/auth`,
* this would result in `/foo/idp/device/auth`.
*/
private createRoute(relative: string): string {
return new URL(joinUrl(this.baseUrl, this.idpPath, relative)).pathname;
return new URL(joinUrl(this.baseUrl, this.oidcPath, relative)).pathname;
}
/**

View File

@@ -165,6 +165,7 @@ export * from './identity/storage/WebIdAdapterFactory';
// Identity
export * from './identity/IdentityProviderHttpHandler';
export * from './identity/OidcHttpHandler';
// Init/Final
export * from './init/final/Finalizable';

View File

@@ -58,7 +58,7 @@ export class AuthorizingHttpHandler extends OperationHttpHandler {
this.operationHandler = args.operationHandler;
}
public async handle(input: OperationHttpHandlerInput): Promise<ResponseDescription | undefined> {
public async handle(input: OperationHttpHandlerInput): Promise<ResponseDescription> {
const { request, operation } = input;
const credentials: CredentialSet = await this.credentialsExtractor.handleSafe(request);
this.logger.verbose(`Extracted credentials: ${JSON.stringify(credentials)}`);

View File

@@ -9,8 +9,6 @@ export interface OperationHttpHandlerInput extends HttpHandlerInput {
/**
* An HTTP handler that makes use of an already parsed Operation.
* Can either return a ResponseDescription to be resolved by the calling class,
* or undefined if this class handles the response itself.
*/
export abstract class OperationHttpHandler
extends AsyncHandler<OperationHttpHandlerInput, ResponseDescription | undefined> {}
extends AsyncHandler<OperationHttpHandlerInput, ResponseDescription> {}