mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Let InitialInteractionHandler redirect requests
This commit is contained in:
@@ -9,19 +9,14 @@
|
||||
"allowedPathNames": [ "^/idp/?$" ],
|
||||
"handler": {
|
||||
"@type": "InitialInteractionHandler",
|
||||
"renderHandlerMap": [
|
||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||
"redirectMap": [
|
||||
{
|
||||
"InitialInteractionHandler:_renderHandlerMap_key": "consent",
|
||||
"InitialInteractionHandler:_renderHandlerMap_value": {
|
||||
"@type": "TemplateHandler",
|
||||
"templateEngine": {
|
||||
"@type": "EjsTemplateEngine",
|
||||
"template": "$PACKAGE_ROOT/templates/identity/email-password/confirm.html.ejs"
|
||||
}
|
||||
}
|
||||
"InitialInteractionHandler:_redirectMap_key": "consent",
|
||||
"InitialInteractionHandler:_redirectMap_value": "/idp/confirm"
|
||||
}
|
||||
],
|
||||
"renderHandlerMap_default": { "@id": "urn:solid-server:auth:password:LoginRenderHandler" }
|
||||
"redirectMap_default": "/idp/login"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,12 +4,22 @@
|
||||
{
|
||||
"comment": "Handles confirm requests",
|
||||
"@id": "urn:solid-server:auth:password:SessionInteractionHandler",
|
||||
"@type": "RouterHandler",
|
||||
"allowedMethods": [ "POST" ],
|
||||
"allowedPathNames": [ "^/idp/confirm/?$" ],
|
||||
"handler": {
|
||||
"@type": "IdpRouteController",
|
||||
"pathName": "^/idp/confirm/?$",
|
||||
"postHandler": {
|
||||
"@type": "SessionHttpHandler",
|
||||
"interactionCompleter": { "@id": "urn:solid-server:auth:password:InteractionCompleter" }
|
||||
},
|
||||
"renderHandler": { "@id": "urn:solid-server:auth:password:ConfirmRenderHandler" }
|
||||
},
|
||||
|
||||
{
|
||||
"comment": "Renders the confirmation page",
|
||||
"@id": "urn:solid-server:auth:password:ConfirmRenderHandler",
|
||||
"@type": "TemplateHandler",
|
||||
"templateEngine": {
|
||||
"@type": "EjsTemplateEngine",
|
||||
"template": "$PACKAGE_ROOT/templates/identity/email-password/confirm.html.ejs"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,47 +1,45 @@
|
||||
import urljoin from 'url-join';
|
||||
import { getLoggerFor } from '../../../logging/LogUtil';
|
||||
import type { InteractionHttpHandlerInput } from '../InteractionHttpHandler';
|
||||
import { InteractionHttpHandler } from '../InteractionHttpHandler';
|
||||
import type { IdpRenderHandler } from './IdpRenderHandler';
|
||||
|
||||
export interface RenderHandlerMap {
|
||||
[key: string]: IdpRenderHandler;
|
||||
default: IdpRenderHandler;
|
||||
export interface RedirectMap {
|
||||
[key: string]: string;
|
||||
default: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link InteractionHttpHandler} that redirects requests
|
||||
* to a specific {@link IdpRenderHandler} based on their prompt.
|
||||
* An {@link InteractionHttpHandler} that redirects requests based on their prompt.
|
||||
* A list of possible prompts can be found at https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
* In case there is no prompt or there is no match in the input map,
|
||||
* the `default` render handler will be used.
|
||||
* the `default` redirect will be used.
|
||||
*
|
||||
* Specifically, the prompt determines how the server should handle re-authentication and consent.
|
||||
*
|
||||
* Since this class specifically redirects to render handlers,
|
||||
* it is advised to wrap it in a {@link RouterHandler} that only allows GET requests.
|
||||
* Specifically, this is used to redirect the client to the correct way to login,
|
||||
* such as a login page, or a confirmation page if a login procedure already succeeded previously.
|
||||
*/
|
||||
export class InitialInteractionHandler extends InteractionHttpHandler {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly renderHandlerMap: RenderHandlerMap;
|
||||
private readonly baseUrl: string;
|
||||
private readonly redirectMap: RedirectMap;
|
||||
|
||||
public constructor(renderHandlerMap: RenderHandlerMap) {
|
||||
public constructor(baseUrl: string, redirectMap: RedirectMap) {
|
||||
super();
|
||||
this.renderHandlerMap = renderHandlerMap;
|
||||
this.baseUrl = baseUrl;
|
||||
this.redirectMap = redirectMap;
|
||||
}
|
||||
|
||||
public async handle({ request, response, provider }: InteractionHttpHandlerInput): Promise<void> {
|
||||
// Find the matching redirect in the map or take the default
|
||||
const interactionDetails = await provider.interactionDetails(request, response);
|
||||
const name = interactionDetails.prompt.name in this.renderHandlerMap ? interactionDetails.prompt.name : 'default';
|
||||
const name = interactionDetails.prompt.name in this.redirectMap ? interactionDetails.prompt.name : 'default';
|
||||
|
||||
this.logger.debug(`Calling ${name} render handler.`);
|
||||
// Create a valid redirect URL
|
||||
const location = urljoin(this.baseUrl, this.redirectMap[name]);
|
||||
this.logger.debug(`Redirecting ${name} prompt to ${location}.`);
|
||||
|
||||
await this.renderHandlerMap[name].handleSafe({
|
||||
response,
|
||||
contents: {
|
||||
errorMessage: '',
|
||||
prefilled: {},
|
||||
},
|
||||
});
|
||||
// Redirect to the result
|
||||
response.writeHead(302, { location });
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,12 @@ export class IdentityTestState {
|
||||
expect(nextUrl.startsWith(this.oidcIssuer)).toBeTruthy();
|
||||
|
||||
// Need to catch the redirect so we can copy the cookies
|
||||
const res = await this.fetchIdp(nextUrl);
|
||||
let res = await this.fetchIdp(nextUrl);
|
||||
expect(res.status).toBe(302);
|
||||
nextUrl = res.headers.get('location')!;
|
||||
|
||||
// Redirect from main page to specific page (login or confirmation)
|
||||
res = await this.fetchIdp(nextUrl);
|
||||
expect(res.status).toBe(302);
|
||||
nextUrl = res.headers.get('location')!;
|
||||
|
||||
|
||||
@@ -1,45 +1,43 @@
|
||||
import type { MockResponse } from 'node-mocks-http';
|
||||
import { createResponse } from 'node-mocks-http';
|
||||
import type { Provider } from 'oidc-provider';
|
||||
import type { RenderHandlerMap } from '../../../../../src/identity/interaction/util/InitialInteractionHandler';
|
||||
import type { RedirectMap } from '../../../../../src/identity/interaction/util/InitialInteractionHandler';
|
||||
import { InitialInteractionHandler } from '../../../../../src/identity/interaction/util/InitialInteractionHandler';
|
||||
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
|
||||
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
|
||||
|
||||
describe('An InitialInteractionHandler', (): void => {
|
||||
const baseUrl = 'http://test.com/';
|
||||
const request: HttpRequest = {} as any;
|
||||
const response: HttpResponse = {} as any;
|
||||
let provider: Provider;
|
||||
let response: MockResponse<any>;
|
||||
let provider: jest.Mocked<Provider>;
|
||||
// `Interaction` type is not exposed
|
||||
let details: any;
|
||||
let map: RenderHandlerMap;
|
||||
let map: RedirectMap;
|
||||
let handler: InitialInteractionHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
response = createResponse();
|
||||
|
||||
map = {
|
||||
default: { handleSafe: jest.fn() },
|
||||
test: { handleSafe: jest.fn() },
|
||||
} as any;
|
||||
default: '/idp/login',
|
||||
test: '/idp/test',
|
||||
};
|
||||
|
||||
details = { prompt: { name: 'test' }};
|
||||
provider = {
|
||||
interactionDetails: jest.fn().mockResolvedValue(details),
|
||||
} as any;
|
||||
|
||||
handler = new InitialInteractionHandler(map);
|
||||
handler = new InitialInteractionHandler(baseUrl, map);
|
||||
});
|
||||
|
||||
it('uses the named handler if it is found.', async(): Promise<void> => {
|
||||
await expect(handler.handle({ request, response, provider })).resolves.toBeUndefined();
|
||||
expect(provider.interactionDetails).toHaveBeenCalledTimes(1);
|
||||
expect(provider.interactionDetails).toHaveBeenLastCalledWith(request, response);
|
||||
expect(map.default.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(map.test.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(map.test.handleSafe).toHaveBeenLastCalledWith({
|
||||
response,
|
||||
contents: {
|
||||
errorMessage: '',
|
||||
prefilled: {},
|
||||
},
|
||||
});
|
||||
expect(response._isEndCalled()).toBe(true);
|
||||
expect(response.getHeader('location')).toBe('http://test.com/idp/test');
|
||||
expect(response.statusCode).toBe(302);
|
||||
});
|
||||
|
||||
it('uses the default handler if there is no match.', async(): Promise<void> => {
|
||||
@@ -47,14 +45,8 @@ describe('An InitialInteractionHandler', (): void => {
|
||||
await expect(handler.handle({ request, response, provider })).resolves.toBeUndefined();
|
||||
expect(provider.interactionDetails).toHaveBeenCalledTimes(1);
|
||||
expect(provider.interactionDetails).toHaveBeenLastCalledWith(request, response);
|
||||
expect(map.default.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(map.test.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(map.default.handleSafe).toHaveBeenLastCalledWith({
|
||||
response,
|
||||
contents: {
|
||||
errorMessage: '',
|
||||
prefilled: {},
|
||||
},
|
||||
});
|
||||
expect(response._isEndCalled()).toBe(true);
|
||||
expect(response.getHeader('location')).toBe('http://test.com/idp/login');
|
||||
expect(response.statusCode).toBe(302);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user