Skip to content

Commit

Permalink
fix: Make IDP routes independent of handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Feb 16, 2022
1 parent 1ed45c8 commit 1769b79
Show file tree
Hide file tree
Showing 17 changed files with 156 additions and 123 deletions.
16 changes: 8 additions & 8 deletions config/identity/handler/interaction/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@
"comment": "Converts redirect errors to location JSON responses.",
"@id": "urn:solid-server:auth:password:LocationInteractionHandler",
"@type": "LocationInteractionHandler",
"LocationInteractionHandler:_source" : { "@id": "urn:solid-server:auth:password:RouteInteractionHandler" }
"LocationInteractionHandler:_source" : { "@id": "urn:solid-server:auth:password:InteractionRouteHandler" }
},
{
"comment": "Handles every interaction based on their route.",
"@id": "urn:solid-server:auth:password:RouteInteractionHandler",
"@id": "urn:solid-server:auth:password:InteractionRouteHandler",
"@type": "WaterfallHandler",
"handlers": [
{
Expand All @@ -44,12 +44,12 @@
],
"@type": "UnsupportedAsyncHandler"
},
{ "@id": "urn:solid-server:auth:password:IndexRoute" },
{ "@id": "urn:solid-server:auth:password:PromptRoute" },
{ "@id": "urn:solid-server:auth:password:LoginRoute" },
{ "@id": "urn:solid-server:auth:password:ExistingLoginRoute" },
{ "@id": "urn:solid-server:auth:password:ForgotPasswordRoute" },
{ "@id": "urn:solid-server:auth:password:ResetPasswordRoute" }
{ "@id": "urn:solid-server:auth:password:IndexRouteHandler" },
{ "@id": "urn:solid-server:auth:password:PromptRouteHandler" },
{ "@id": "urn:solid-server:auth:password:LoginRouteHandler" },
{ "@id": "urn:solid-server:auth:password:ExistingLoginRouteHandler" },
{ "@id": "urn:solid-server:auth:password:ForgotPasswordRouteHandler" },
{ "@id": "urn:solid-server:auth:password:ResetPasswordRouteHandler" }
]
}
]
Expand Down
13 changes: 9 additions & 4 deletions config/identity/handler/interaction/routes/existing-login.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
"@graph": [
{
"comment": "Handles the interaction that occurs when a logged in user wants to authenticate with a new app.",
"@id": "urn:solid-server:auth:password:ExistingLoginRoute",
"@type": "RelativeInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/consent/",
"@id": "urn:solid-server:auth:password:ExistingLoginRouteHandler",
"@type":"InteractionRouteHandler",
"route": {
"@id": "urn:solid-server:auth:password:ExistingLoginRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/consent/"
},
"source": {
"@id": "urn:solid-server:auth:password:ExistingLoginHandler",
"@type": "ExistingLoginHandler",
"interactionCompleter": { "@type": "BaseInteractionCompleter" }
}
Expand Down
13 changes: 9 additions & 4 deletions config/identity/handler/interaction/routes/forgot-password.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
"@graph": [
{
"comment": "Handles the forgot password interaction",
"@id": "urn:solid-server:auth:password:ForgotPasswordRoute",
"@type": "RelativeInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/forgotpassword/",
"@id": "urn:solid-server:auth:password:ForgotPasswordRouteHandler",
"@type":"InteractionRouteHandler",
"route": {
"@id": "urn:solid-server:auth:password:ForgotPasswordRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/forgotpassword/"
},
"source": {
"@id": "urn:solid-server:auth:password:ForgotPasswordHandler",
"@type": "ForgotPasswordHandler",
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
"args_templateEngine": {
Expand Down
13 changes: 9 additions & 4 deletions config/identity/handler/interaction/routes/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
"@graph": [
{
"comment": "Root API entry. Returns an empty body so we can add controls pointing to other interaction routes.",
"@id": "urn:solid-server:auth:password:IndexRoute",
"@type": "RelativeInteractionRoute",
"base": { "@id": "urn:solid-server:default:variable:baseUrl" },
"relativePath": "/idp/",
"@id": "urn:solid-server:auth:password:IndexRouteHandler",
"@type": "InteractionRouteHandler",
"route": {
"@id": "urn:solid-server:auth:password:IndexRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:default:variable:baseUrl" },
"relativePath": "/idp/"
},
"source": {
"@id": "urn:solid-server:auth:password:IndexHandler",
"@type": "FixedInteractionHandler",
"response": {}
}
Expand Down
13 changes: 9 additions & 4 deletions config/identity/handler/interaction/routes/login.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
"@graph": [
{
"comment": "Handles the login interaction",
"@id": "urn:solid-server:auth:password:LoginRoute",
"@type": "RelativeInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/login/",
"@id": "urn:solid-server:auth:password:LoginRouteHandler",
"@type": "InteractionRouteHandler",
"route": {
"@id": "urn:solid-server:auth:password:LoginRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/login/"
},
"source": {
"@id": "urn:solid-server:auth:password:LoginHandler",
"@type": "LoginHandler",
"accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
"interactionCompleter": { "@type": "BaseInteractionCompleter" }
Expand Down
12 changes: 8 additions & 4 deletions config/identity/handler/interaction/routes/prompt.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
"@graph": [
{
"comment": "Handles OIDC redirects containing a prompt, such as login or consent.",
"@id": "urn:solid-server:auth:password:PromptRoute",
"@type": "RelativeInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/prompt/",
"@id": "urn:solid-server:auth:password:PromptRouteHandler",
"@type": "InteractionRouteHandler",
"route": {
"@id": "urn:solid-server:auth:password:PromptRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/prompt/"
},
"source": {
"@type": "PromptHandler",
"@id": "urn:solid-server:auth:password:PromptHandler",
Expand Down
13 changes: 9 additions & 4 deletions config/identity/handler/interaction/routes/reset-password.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
"@graph": [
{
"comment": "Handles the reset password interaction",
"@id": "urn:solid-server:auth:password:ResetPasswordRoute",
"@type": "RelativeInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/resetpassword/",
"@id": "urn:solid-server:auth:password:ResetPasswordRouteHandler",
"@type": "InteractionRouteHandler",
"route": {
"@id": "urn:solid-server:auth:password:ResetPasswordRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/resetpassword/"
},
"source": {
"@id": "urn:solid-server:auth:password:ResetPasswordHandler",
"@type": "ResetPasswordHandler",
"accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }
}
Expand Down
12 changes: 2 additions & 10 deletions config/identity/registration/enabled.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
],
"@graph": [
{
"@id": "urn:solid-server:auth:password:RouteInteractionHandler",
"@id": "urn:solid-server:auth:password:InteractionRouteHandler",
"WaterfallHandler:_handlers": [
{
"comment": [
Expand All @@ -14,7 +14,7 @@
],
"@type": "UnsupportedAsyncHandler"
},
{ "@id": "urn:solid-server:auth:password:RegistrationRoute" }
{ "@id": "urn:solid-server:auth:password:RegistrationRouteHandler" }
]
},
{
Expand All @@ -32,14 +32,6 @@
{
"HtmlViewHandler:_templates_key": "@css:templates/identity/email-password/register.html.ejs",
"HtmlViewHandler:_templates_value": { "@id": "urn:solid-server:auth:password:RegistrationRoute" }
},
{
"HtmlViewHandler:_templates_key": "@css:templates/identity/email-password/reset-password-response.html.ejs",
"HtmlViewHandler:_templates_value": {
"@type": "RelativeInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:ResetPasswordRoute" },
"relativePath": "/response/"
}
}
]
}
Expand Down
13 changes: 9 additions & 4 deletions config/identity/registration/route/registration.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
"@graph": [
{
"comment": "Handles the register interaction",
"@id": "urn:solid-server:auth:password:RegistrationRoute",
"@type": "RelativeInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/register/",
"@id": "urn:solid-server:auth:password:RegistrationRouteHandler",
"@type": "InteractionRouteHandler",
"route": {
"@id": "urn:solid-server:auth:password:RegistrationRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/register/"
},
"source": {
"@id": "urn:solid-server:auth:password:RegistrationHandler",
"@type": "RegistrationHandler",
"registrationManager": {
"@type": "RegistrationManager",
Expand Down
16 changes: 16 additions & 0 deletions src/identity/interaction/routing/AbsolutePathInteractionRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { InteractionRoute } from './InteractionRoute';

/**
* A route that returns the input string as path.
*/
export class AbsolutePathInteractionRoute implements InteractionRoute {
private readonly path: string;

public constructor(path: string) {
this.path = path;
}

public getPath(): string {
return this.path;
}
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,28 @@
import type { Representation } from '../../../http/representation/Representation';
import { NotFoundHttpError } from '../../../util/errors/NotFoundHttpError';
import { UnsupportedAsyncHandler } from '../../../util/handlers/UnsupportedAsyncHandler';
import { InteractionHandler } from '../InteractionHandler';
import type { InteractionHandlerInput } from '../InteractionHandler';
import { InteractionHandler } from '../InteractionHandler';
import type { InteractionRoute } from './InteractionRoute';

/**
* Default implementation of an InteractionHandler with an InteractionRoute.
* InteractionHandler that only accepts operations with an expected path.
*
* Rejects operations that target a different path,
* otherwise the input parameters get passed to the source handler.
*
* In case no source handler is provided it defaults to an {@link UnsupportedAsyncHandler}.
* This can be useful if you want an object with just the route.
* otherwise the input parameters are passed to the source handler.
*/
export class BasicInteractionRoute extends InteractionHandler implements InteractionRoute {
private readonly path: string;
export class InteractionRouteHandler extends InteractionHandler {
private readonly route: InteractionRoute;
private readonly source: InteractionHandler;

public constructor(path: string, source?: InteractionHandler) {
public constructor(route: InteractionRoute, source: InteractionHandler) {
super();
this.path = path;
this.source = source ?? new UnsupportedAsyncHandler('This route has no associated handler.');
}

public getPath(): string {
return this.path;
this.route = route;
this.source = source;
}

public async canHandle(input: InteractionHandlerInput): Promise<void> {
const { target } = input.operation;
const path = this.getPath();
const path = this.route.getPath();
if (target.path !== path) {
throw new NotFoundHttpError();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { joinUrl } from '../../../util/PathUtil';
import type { InteractionHandler } from '../InteractionHandler';
import { BasicInteractionRoute } from './BasicInteractionRoute';
import { AbsolutePathInteractionRoute } from './AbsolutePathInteractionRoute';
import type { InteractionRoute } from './InteractionRoute';

/**
* A route that is relative to another route.
* The relative path will be joined to the input base,
* which can either be an absolute URL or an InteractionRoute of which the path will be used.
* The source handler will be called for all operation requests
*/
export class RelativeInteractionRoute extends BasicInteractionRoute {
public constructor(base: InteractionRoute | string, relativePath: string, source?: InteractionHandler) {
export class RelativePathInteractionRoute extends AbsolutePathInteractionRoute {
public constructor(base: InteractionRoute | string, relativePath: string) {
const url = typeof base === 'string' ? base : base.getPath();
const path = joinUrl(url, relativePath);
super(path, source);
super(path);
}
}
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,10 @@ export * from './identity/interaction/email-password/util/RegistrationManager';
export * from './identity/interaction/email-password/EmailPasswordUtil';

// Identity/Interaction/Routing
export * from './identity/interaction/routing/BasicInteractionRoute';
export * from './identity/interaction/routing/AbsolutePathInteractionRoute';
export * from './identity/interaction/routing/InteractionRoute';
export * from './identity/interaction/routing/RelativeInteractionRoute';
export * from './identity/interaction/routing/InteractionRouteHandler';
export * from './identity/interaction/routing/RelativePathInteractionRoute';

// Identity/Interaction/Util
export * from './identity/interaction/util/BaseEmailSender';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {
AbsolutePathInteractionRoute,
} from '../../../../../src/identity/interaction/routing/AbsolutePathInteractionRoute';

describe('An AbsolutePathInteractionRoute', (): void => {
const path = 'http://example.com/idp/path/';
const route = new AbsolutePathInteractionRoute(path);

it('returns the given path.', async(): Promise<void> => {
expect(route.getPath()).toBe('http://example.com/idp/path/');
});
});
Original file line number Diff line number Diff line change
@@ -1,59 +1,53 @@
import type { Operation } from '../../../../../src/http/Operation';
import { BasicRepresentation } from '../../../../../src/http/representation/BasicRepresentation';
import type { Representation } from '../../../../../src/http/representation/Representation';
import type {
InteractionHandler,
} from '../../../../../src/identity/interaction/InteractionHandler';
import { BasicInteractionRoute } from '../../../../../src/identity/interaction/routing/BasicInteractionRoute';
import type { InteractionHandler } from '../../../../../src/identity/interaction/InteractionHandler';
import type { InteractionRoute } from '../../../../../src/identity/interaction/routing/InteractionRoute';
import { InteractionRouteHandler } from '../../../../../src/identity/interaction/routing/InteractionRouteHandler';
import { APPLICATION_JSON } from '../../../../../src/util/ContentTypes';
import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError';
import { createPostJsonOperation } from '../email-password/handler/Util';

describe('A BasicInteractionRoute', (): void => {
describe('An InteractionRouteHandler', (): void => {
const path = 'http://example.com/idp/path/';
let operation: Operation;
let representation: Representation;
let route: InteractionRoute;
let source: jest.Mocked<InteractionHandler>;
let route: BasicInteractionRoute;
let handler: InteractionRouteHandler;

beforeEach(async(): Promise<void> => {
operation = createPostJsonOperation({}, 'http://example.com/idp/path/');
operation = createPostJsonOperation({}, path);

representation = new BasicRepresentation(JSON.stringify({}), APPLICATION_JSON);

route = {
getPath: jest.fn().mockReturnValue(path),
};

source = {
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue(representation),
} as any;

route = new BasicInteractionRoute(path, source);
});

it('returns the given path.', async(): Promise<void> => {
expect(route.getPath()).toBe('http://example.com/idp/path/');
handler = new InteractionRouteHandler(route, source);
});

it('rejects other paths.', async(): Promise<void> => {
operation = createPostJsonOperation({}, 'http://example.com/idp/otherPath/');
await expect(route.canHandle({ operation })).rejects.toThrow(NotFoundHttpError);
await expect(handler.canHandle({ operation })).rejects.toThrow(NotFoundHttpError);
});

it('rejects input its source cannot handle.', async(): Promise<void> => {
source.canHandle.mockRejectedValueOnce(new Error('bad data'));
await expect(route.canHandle({ operation })).rejects.toThrow('bad data');
await expect(handler.canHandle({ operation })).rejects.toThrow('bad data');
});

it('can handle requests its source can handle.', async(): Promise<void> => {
await expect(route.canHandle({ operation })).resolves.toBeUndefined();
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
});

it('lets its source handle requests.', async(): Promise<void> => {
await expect(route.handle({ operation })).resolves.toBe(representation);
});

it('defaults to an UnsupportedAsyncHandler if no source is provided.', async(): Promise<void> => {
route = new BasicInteractionRoute(path);
await expect(route.canHandle({ operation })).rejects.toThrow('This route has no associated handler.');
await expect(route.handle({ operation })).rejects.toThrow('This route has no associated handler.');
await expect(handler.handle({ operation })).resolves.toBe(representation);
});
});
Loading

0 comments on commit 1769b79

Please sign in to comment.