fix: Make IDP routes independent of handlers

This commit is contained in:
Joachim Van Herwegen 2022-02-14 12:02:03 +01:00
parent 1ed45c8903
commit 1769b799df
17 changed files with 156 additions and 123 deletions

View File

@ -30,11 +30,11 @@
"comment": "Converts redirect errors to location JSON responses.", "comment": "Converts redirect errors to location JSON responses.",
"@id": "urn:solid-server:auth:password:LocationInteractionHandler", "@id": "urn:solid-server:auth:password:LocationInteractionHandler",
"@type": "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.", "comment": "Handles every interaction based on their route.",
"@id": "urn:solid-server:auth:password:RouteInteractionHandler", "@id": "urn:solid-server:auth:password:InteractionRouteHandler",
"@type": "WaterfallHandler", "@type": "WaterfallHandler",
"handlers": [ "handlers": [
{ {
@ -44,12 +44,12 @@
], ],
"@type": "UnsupportedAsyncHandler" "@type": "UnsupportedAsyncHandler"
}, },
{ "@id": "urn:solid-server:auth:password:IndexRoute" }, { "@id": "urn:solid-server:auth:password:IndexRouteHandler" },
{ "@id": "urn:solid-server:auth:password:PromptRoute" }, { "@id": "urn:solid-server:auth:password:PromptRouteHandler" },
{ "@id": "urn:solid-server:auth:password:LoginRoute" }, { "@id": "urn:solid-server:auth:password:LoginRouteHandler" },
{ "@id": "urn:solid-server:auth:password:ExistingLoginRoute" }, { "@id": "urn:solid-server:auth:password:ExistingLoginRouteHandler" },
{ "@id": "urn:solid-server:auth:password:ForgotPasswordRoute" }, { "@id": "urn:solid-server:auth:password:ForgotPasswordRouteHandler" },
{ "@id": "urn:solid-server:auth:password:ResetPasswordRoute" } { "@id": "urn:solid-server:auth:password:ResetPasswordRouteHandler" }
] ]
} }
] ]

View File

@ -3,11 +3,16 @@
"@graph": [ "@graph": [
{ {
"comment": "Handles the interaction that occurs when a logged in user wants to authenticate with a new app.", "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", "@id": "urn:solid-server:auth:password:ExistingLoginRouteHandler",
"@type": "RelativeInteractionRoute", "@type":"InteractionRouteHandler",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" }, "route": {
"relativePath": "/consent/", "@id": "urn:solid-server:auth:password:ExistingLoginRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/consent/"
},
"source": { "source": {
"@id": "urn:solid-server:auth:password:ExistingLoginHandler",
"@type": "ExistingLoginHandler", "@type": "ExistingLoginHandler",
"interactionCompleter": { "@type": "BaseInteractionCompleter" } "interactionCompleter": { "@type": "BaseInteractionCompleter" }
} }

View File

@ -3,11 +3,16 @@
"@graph": [ "@graph": [
{ {
"comment": "Handles the forgot password interaction", "comment": "Handles the forgot password interaction",
"@id": "urn:solid-server:auth:password:ForgotPasswordRoute", "@id": "urn:solid-server:auth:password:ForgotPasswordRouteHandler",
"@type": "RelativeInteractionRoute", "@type":"InteractionRouteHandler",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" }, "route": {
"relativePath": "/forgotpassword/", "@id": "urn:solid-server:auth:password:ForgotPasswordRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/forgotpassword/"
},
"source": { "source": {
"@id": "urn:solid-server:auth:password:ForgotPasswordHandler",
"@type": "ForgotPasswordHandler", "@type": "ForgotPasswordHandler",
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }, "args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
"args_templateEngine": { "args_templateEngine": {

View File

@ -3,11 +3,16 @@
"@graph": [ "@graph": [
{ {
"comment": "Root API entry. Returns an empty body so we can add controls pointing to other interaction routes.", "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", "@id": "urn:solid-server:auth:password:IndexRouteHandler",
"@type": "RelativeInteractionRoute", "@type": "InteractionRouteHandler",
"base": { "@id": "urn:solid-server:default:variable:baseUrl" }, "route": {
"relativePath": "/idp/", "@id": "urn:solid-server:auth:password:IndexRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:default:variable:baseUrl" },
"relativePath": "/idp/"
},
"source": { "source": {
"@id": "urn:solid-server:auth:password:IndexHandler",
"@type": "FixedInteractionHandler", "@type": "FixedInteractionHandler",
"response": {} "response": {}
} }

View File

@ -3,11 +3,16 @@
"@graph": [ "@graph": [
{ {
"comment": "Handles the login interaction", "comment": "Handles the login interaction",
"@id": "urn:solid-server:auth:password:LoginRoute", "@id": "urn:solid-server:auth:password:LoginRouteHandler",
"@type": "RelativeInteractionRoute", "@type": "InteractionRouteHandler",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" }, "route": {
"relativePath": "/login/", "@id": "urn:solid-server:auth:password:LoginRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/login/"
},
"source": { "source": {
"@id": "urn:solid-server:auth:password:LoginHandler",
"@type": "LoginHandler", "@type": "LoginHandler",
"accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }, "accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
"interactionCompleter": { "@type": "BaseInteractionCompleter" } "interactionCompleter": { "@type": "BaseInteractionCompleter" }

View File

@ -3,10 +3,14 @@
"@graph": [ "@graph": [
{ {
"comment": "Handles OIDC redirects containing a prompt, such as login or consent.", "comment": "Handles OIDC redirects containing a prompt, such as login or consent.",
"@id": "urn:solid-server:auth:password:PromptRoute", "@id": "urn:solid-server:auth:password:PromptRouteHandler",
"@type": "RelativeInteractionRoute", "@type": "InteractionRouteHandler",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" }, "route": {
"relativePath": "/prompt/", "@id": "urn:solid-server:auth:password:PromptRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/prompt/"
},
"source": { "source": {
"@type": "PromptHandler", "@type": "PromptHandler",
"@id": "urn:solid-server:auth:password:PromptHandler", "@id": "urn:solid-server:auth:password:PromptHandler",

View File

@ -3,11 +3,16 @@
"@graph": [ "@graph": [
{ {
"comment": "Handles the reset password interaction", "comment": "Handles the reset password interaction",
"@id": "urn:solid-server:auth:password:ResetPasswordRoute", "@id": "urn:solid-server:auth:password:ResetPasswordRouteHandler",
"@type": "RelativeInteractionRoute", "@type": "InteractionRouteHandler",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" }, "route": {
"relativePath": "/resetpassword/", "@id": "urn:solid-server:auth:password:ResetPasswordRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/resetpassword/"
},
"source": { "source": {
"@id": "urn:solid-server:auth:password:ResetPasswordHandler",
"@type": "ResetPasswordHandler", "@type": "ResetPasswordHandler",
"accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" } "accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }
} }

View File

@ -5,7 +5,7 @@
], ],
"@graph": [ "@graph": [
{ {
"@id": "urn:solid-server:auth:password:RouteInteractionHandler", "@id": "urn:solid-server:auth:password:InteractionRouteHandler",
"WaterfallHandler:_handlers": [ "WaterfallHandler:_handlers": [
{ {
"comment": [ "comment": [
@ -14,7 +14,7 @@
], ],
"@type": "UnsupportedAsyncHandler" "@type": "UnsupportedAsyncHandler"
}, },
{ "@id": "urn:solid-server:auth:password:RegistrationRoute" } { "@id": "urn:solid-server:auth:password:RegistrationRouteHandler" }
] ]
}, },
{ {
@ -32,14 +32,6 @@
{ {
"HtmlViewHandler:_templates_key": "@css:templates/identity/email-password/register.html.ejs", "HtmlViewHandler:_templates_key": "@css:templates/identity/email-password/register.html.ejs",
"HtmlViewHandler:_templates_value": { "@id": "urn:solid-server:auth:password:RegistrationRoute" } "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/"
}
} }
] ]
} }

View File

@ -3,11 +3,16 @@
"@graph": [ "@graph": [
{ {
"comment": "Handles the register interaction", "comment": "Handles the register interaction",
"@id": "urn:solid-server:auth:password:RegistrationRoute", "@id": "urn:solid-server:auth:password:RegistrationRouteHandler",
"@type": "RelativeInteractionRoute", "@type": "InteractionRouteHandler",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" }, "route": {
"relativePath": "/register/", "@id": "urn:solid-server:auth:password:RegistrationRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:auth:password:IndexRoute" },
"relativePath": "/register/"
},
"source": { "source": {
"@id": "urn:solid-server:auth:password:RegistrationHandler",
"@type": "RegistrationHandler", "@type": "RegistrationHandler",
"registrationManager": { "registrationManager": {
"@type": "RegistrationManager", "@type": "RegistrationManager",

View File

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

View File

@ -1,36 +1,28 @@
import type { Representation } from '../../../http/representation/Representation'; import type { Representation } from '../../../http/representation/Representation';
import { NotFoundHttpError } from '../../../util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../../util/errors/NotFoundHttpError';
import { UnsupportedAsyncHandler } from '../../../util/handlers/UnsupportedAsyncHandler';
import { InteractionHandler } from '../InteractionHandler';
import type { InteractionHandlerInput } from '../InteractionHandler'; import type { InteractionHandlerInput } from '../InteractionHandler';
import { InteractionHandler } from '../InteractionHandler';
import type { InteractionRoute } from './InteractionRoute'; 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, * Rejects operations that target a different path,
* otherwise the input parameters get passed to the source handler. * otherwise the input parameters are 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.
*/ */
export class BasicInteractionRoute extends InteractionHandler implements InteractionRoute { export class InteractionRouteHandler extends InteractionHandler {
private readonly path: string; private readonly route: InteractionRoute;
private readonly source: InteractionHandler; private readonly source: InteractionHandler;
public constructor(path: string, source?: InteractionHandler) { public constructor(route: InteractionRoute, source: InteractionHandler) {
super(); super();
this.path = path; this.route = route;
this.source = source ?? new UnsupportedAsyncHandler('This route has no associated handler.'); this.source = source;
}
public getPath(): string {
return this.path;
} }
public async canHandle(input: InteractionHandlerInput): Promise<void> { public async canHandle(input: InteractionHandlerInput): Promise<void> {
const { target } = input.operation; const { target } = input.operation;
const path = this.getPath(); const path = this.route.getPath();
if (target.path !== path) { if (target.path !== path) {
throw new NotFoundHttpError(); throw new NotFoundHttpError();
} }

View File

@ -1,18 +1,16 @@
import { joinUrl } from '../../../util/PathUtil'; import { joinUrl } from '../../../util/PathUtil';
import type { InteractionHandler } from '../InteractionHandler'; import { AbsolutePathInteractionRoute } from './AbsolutePathInteractionRoute';
import { BasicInteractionRoute } from './BasicInteractionRoute';
import type { InteractionRoute } from './InteractionRoute'; import type { InteractionRoute } from './InteractionRoute';
/** /**
* A route that is relative to another route. * A route that is relative to another route.
* The relative path will be joined to the input base, * 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. * 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 { export class RelativePathInteractionRoute extends AbsolutePathInteractionRoute {
public constructor(base: InteractionRoute | string, relativePath: string, source?: InteractionHandler) { public constructor(base: InteractionRoute | string, relativePath: string) {
const url = typeof base === 'string' ? base : base.getPath(); const url = typeof base === 'string' ? base : base.getPath();
const path = joinUrl(url, relativePath); const path = joinUrl(url, relativePath);
super(path, source); super(path);
} }
} }

View File

@ -145,9 +145,10 @@ export * from './identity/interaction/email-password/util/RegistrationManager';
export * from './identity/interaction/email-password/EmailPasswordUtil'; export * from './identity/interaction/email-password/EmailPasswordUtil';
// Identity/Interaction/Routing // 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/InteractionRoute';
export * from './identity/interaction/routing/RelativeInteractionRoute'; export * from './identity/interaction/routing/InteractionRouteHandler';
export * from './identity/interaction/routing/RelativePathInteractionRoute';
// Identity/Interaction/Util // Identity/Interaction/Util
export * from './identity/interaction/util/BaseEmailSender'; export * from './identity/interaction/util/BaseEmailSender';

View File

@ -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/');
});
});

View File

@ -1,59 +1,53 @@
import type { Operation } from '../../../../../src/http/Operation'; import type { Operation } from '../../../../../src/http/Operation';
import { BasicRepresentation } from '../../../../../src/http/representation/BasicRepresentation'; import { BasicRepresentation } from '../../../../../src/http/representation/BasicRepresentation';
import type { Representation } from '../../../../../src/http/representation/Representation'; import type { Representation } from '../../../../../src/http/representation/Representation';
import type { import type { InteractionHandler } from '../../../../../src/identity/interaction/InteractionHandler';
InteractionHandler, import type { InteractionRoute } from '../../../../../src/identity/interaction/routing/InteractionRoute';
} from '../../../../../src/identity/interaction/InteractionHandler'; import { InteractionRouteHandler } from '../../../../../src/identity/interaction/routing/InteractionRouteHandler';
import { BasicInteractionRoute } from '../../../../../src/identity/interaction/routing/BasicInteractionRoute';
import { APPLICATION_JSON } from '../../../../../src/util/ContentTypes'; import { APPLICATION_JSON } from '../../../../../src/util/ContentTypes';
import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError';
import { createPostJsonOperation } from '../email-password/handler/Util'; import { createPostJsonOperation } from '../email-password/handler/Util';
describe('A BasicInteractionRoute', (): void => { describe('An InteractionRouteHandler', (): void => {
const path = 'http://example.com/idp/path/'; const path = 'http://example.com/idp/path/';
let operation: Operation; let operation: Operation;
let representation: Representation; let representation: Representation;
let route: InteractionRoute;
let source: jest.Mocked<InteractionHandler>; let source: jest.Mocked<InteractionHandler>;
let route: BasicInteractionRoute; let handler: InteractionRouteHandler;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
operation = createPostJsonOperation({}, 'http://example.com/idp/path/'); operation = createPostJsonOperation({}, path);
representation = new BasicRepresentation(JSON.stringify({}), APPLICATION_JSON); representation = new BasicRepresentation(JSON.stringify({}), APPLICATION_JSON);
route = {
getPath: jest.fn().mockReturnValue(path),
};
source = { source = {
canHandle: jest.fn(), canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue(representation), handle: jest.fn().mockResolvedValue(representation),
} as any; } as any;
route = new BasicInteractionRoute(path, source); handler = new InteractionRouteHandler(route, source);
});
it('returns the given path.', async(): Promise<void> => {
expect(route.getPath()).toBe('http://example.com/idp/path/');
}); });
it('rejects other paths.', async(): Promise<void> => { it('rejects other paths.', async(): Promise<void> => {
operation = createPostJsonOperation({}, 'http://example.com/idp/otherPath/'); 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> => { it('rejects input its source cannot handle.', async(): Promise<void> => {
source.canHandle.mockRejectedValueOnce(new Error('bad data')); 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> => { 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> => { it('lets its source handle requests.', async(): Promise<void> => {
await expect(route.handle({ operation })).resolves.toBe(representation); await expect(handler.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.');
}); });
}); });

View File

@ -1,30 +0,0 @@
import type {
InteractionHandler,
} from '../../../../../src/identity/interaction/InteractionHandler';
import type { InteractionRoute } from '../../../../../src/identity/interaction/routing/InteractionRoute';
import { RelativeInteractionRoute } from '../../../../../src/identity/interaction/routing/RelativeInteractionRoute';
describe('A RelativeInteractionRoute', (): void => {
const relativePath = '/relative/';
let route: jest.Mocked<InteractionRoute>;
let source: jest.Mocked<InteractionHandler>;
let relativeRoute: RelativeInteractionRoute;
beforeEach(async(): Promise<void> => {
route = {
getPath: jest.fn().mockReturnValue('http://example.com/'),
} as any;
source = {
canHandle: jest.fn(),
} as any;
});
it('returns the joined path.', async(): Promise<void> => {
relativeRoute = new RelativeInteractionRoute(route, relativePath, source);
expect(relativeRoute.getPath()).toBe('http://example.com/relative/');
relativeRoute = new RelativeInteractionRoute('http://example.com/', relativePath, source);
expect(relativeRoute.getPath()).toBe('http://example.com/relative/');
});
});

View File

@ -0,0 +1,24 @@
import type { InteractionRoute } from '../../../../../src/identity/interaction/routing/InteractionRoute';
import {
RelativePathInteractionRoute,
} from '../../../../../src/identity/interaction/routing/RelativePathInteractionRoute';
describe('A RelativePathInteractionRoute', (): void => {
const relativePath = '/relative/';
let route: jest.Mocked<InteractionRoute>;
let relativeRoute: RelativePathInteractionRoute;
beforeEach(async(): Promise<void> => {
route = {
getPath: jest.fn().mockReturnValue('http://example.com/'),
};
});
it('returns the joined path.', async(): Promise<void> => {
relativeRoute = new RelativePathInteractionRoute(route, relativePath);
expect(relativeRoute.getPath()).toBe('http://example.com/relative/');
relativeRoute = new RelativePathInteractionRoute('http://example.com/test/', relativePath);
expect(relativeRoute.getPath()).toBe('http://example.com/test/relative/');
});
});